git.lucas.co / hou-control
git clone https://git.lucas.co/hou-control.git

commit918d3db36362125c8aeddd894dbe8e804004e585
parent7752dd7381
authorLucas Galante <[email protected]>
date2026-04-21 12:14
hc: update network editor defaults and session behavior

 .gitignore                           |  1 +
 AGENTS.md                            | 30 +++++++++++++++++++++++++++
 GEMINI.md                            | 39 ++++++++++++++++++++++++++++++++++++
 MainMenuCommon.xml                   | 12 +++++------
 config/NodeGraphDark.inc             |  2 +-
 config/hc_colors.inc                 |  2 +-
 desktop/hc_attached.desk             |  2 +-
 hc_settings.json                     |  2 +-
 python3.11libs/hc/hcnetworkeditor.py |  2 ++
 python3.11libs/hc/hcnode.py          | 14 ++++++-------
 python3.11libs/hc/hcsession.py       | 21 ++++++++++---------
 python3.11libs/nodegraphhooks.py     | 12 ++++++-----
 12 files changed, 108 insertions(+), 31 deletions(-)

diff --git a/.gitignore b/.gitignore
index df24ba9..80aa499 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
 .DS_Store
 *.DS_Store
 *otls/backup
+.codex
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..f3ecc32
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,30 @@
+# Repository Guidelines
+
+## Project Structure & Module Organization
+
+`hou-control` is a SideFX Houdini package, not a standalone app. Core Python code lives in `python3.11libs/hc/`, where wrapper classes such as `HCSession`, `HCPane`, and `HCNetworkEditor` extend Houdini behavior without subclassing `hou.*` types. Houdini startup hooks live in `python3.11libs/uiready.py` and `python3.11libs/nodegraphhooks.py`. Session scripts are in `scripts/` (`123.py`, `456.py`, `OnCreated.py`). UI and package assets are organized under `config/`, `desktop/`, `radialmenu/`, `toolbar/`, `presets/`, `viewer_states/`, `vex/`, `help/`, and `otls/`.
+
+## Build, Test, and Development Commands
+
+There is no build system for this repository; Houdini loads it at runtime through `houdini-studio-utils.json`.
+
+- `python mcpserver/houdini_mcp.py`: starts the local MCP bridge for driving a running Houdini session.
+- `python -m py_compile python3.11libs/hc/*.py python3.11libs/*.py scripts/*.py`: quick syntax check outside Houdini.
+- In Houdini, use the `hc` menu actions `Reload Hotkeys`, `Reload Colors`, and `Reload Keycam` to validate targeted changes.
+- For full reloads in Houdini, use `hou.ui.reloadPackage(...)` via `HCSession.reloadHC()`.
+
+## Coding Style & Naming Conventions
+
+Follow the existing Python style: 4-space indentation, `snake_case` for functions/methods, `CapWords` for wrapper classes, and concise docstrings only where behavior is not obvious. Keep filesystem paths dynamic by reading `hou.getenv("HC_PATH")`; do not hardcode machine-specific paths. New commands should be implemented on the appropriate wrapper in `python3.11libs/hc/` and then registered in `hcmaps.py` if they must appear in the HC panel.
+
+## Testing Guidelines
+
+There is no committed automated test suite yet. Validate changes by reloading the affected subsystem inside Houdini and exercising the relevant pane, viewer state, menu, or hotkey. For hotkey work, update `hc_hotkeys.json` and confirm conflict handling through `HCBindings`. If you add nontrivial Python logic, run the `py_compile` check before manual Houdini verification.
+
+## Commit & Pull Request Guidelines
+
+Recent history favors short, scoped subjects such as `hc: fix phantom zoom when pressing Ctrl in Network Editor` or `UI: make locked camera pill and mask colors paler`. Use a lowercase area prefix when possible (`hc:`, `keycam:`, `UI:`) followed by an imperative summary. PRs should explain user-visible behavior, list the Houdini contexts exercised, and include screenshots or short clips for UI, node graph, or viewer-state changes.
+
+## Configuration & Runtime Notes
+
+This package runs inside Houdini's embedded Python 3.11. Many features depend on Houdini filename conventions and package loading, so preserve names such as `uiready.py`, `123.py`, and `456.py`. Treat `otls/` and Houdini config files as runtime assets and verify them in an actual Houdini session before merging.
diff --git a/GEMINI.md b/GEMINI.md
new file mode 100644
index 0000000..80ff637
--- /dev/null
+++ b/GEMINI.md
@@ -0,0 +1,39 @@
+# Project Overview: hou-control
+
+`hou-control` is a specialized Houdini environment and toolset designed to enhance the user interface, automate session management, and provide custom modeling and simulation operators. It bridges Houdini with external AI agents via a Model Context Protocol (MCP) server.
+
+## Architecture & Technology Stack
+
+- **Houdini Python API (hou):** Core integration for UI manipulation and node graph control.
+- **PySide6:** Used for custom Qt-based UI elements (HC Status, Floating Layouts).
+- **RPyC:** Enables remote procedure calls between the external MCP server and the internal Houdini session.
+- **FastMCP:** Powers the Model Context Protocol server for AI-driven interaction.
+- **Operator Sets:** 
+  - **GEM (General Export Methods):** Manufacturing and export-focused HDAs.
+  - **Developer (Shapeshifter):** Simulation and surface development tools.
+
+## Key Components
+
+- **`python3.11libs/hc/`**: Core logic for session management, hotkey binding, and UI overlays.
+- **`mcpserver/`**: Contains the `houdini_mcp.py` server which allows external agents to evaluate code in Houdini.
+- **`viewer_states/keycam.py`**: A custom viewer state for keyboard-driven camera navigation using the official `PluginHotkeyDefinitions` system.
+- **`config/`**: Contains UI theme files, custom cursors, and node shape definitions.
+
+## Building and Running
+
+Houdini automatically loads the package if the directory is added to the `HOUDINI_PATH`.
+
+- **Start Houdini with HC:** Ensure `uiready.py` is in your Houdini search path; it initializes the session and starts the RPyC server.
+- **Start MCP Server:**
+  ```bash
+  source mcpserver/.venv/bin/activate
+  python mcpserver/houdini_mcp.py
+  ```
+- **Tests:** Currently, verification is performed manually within the Houdini session or via RPyC evaluation.
+
+## Development Conventions
+
+- **Surgical Updates:** (MANDATORY) Never remove or refactor unrelated code. Maintain all existing parameters, menu items, and logic.
+- **Hotkey Management:** Use the official `hou.PluginHotkeyDefinitions` for viewer states to ensure proper context scoping.
+- **Node Tagging:** Use `hc_custom_color: "1"` user data to identify nodes that should be affected by global style updates.
+- **Communication:** Use the RPyC server (port 18811) for speculative research and debugging within the live environment.
diff --git a/MainMenuCommon.xml b/MainMenuCommon.xml
index b4e81f3..ae99919 100755
--- a/MainMenuCommon.xml
+++ b/MainMenuCommon.xml
@@ -45,14 +45,14 @@
         <!-- <label>Reload Package</label> -->
         <!-- <scriptCode><![CDATA[from hc import HCSession; HCSession().reloadStudioUtils()]]></scriptCode> -->
       <!-- </scriptItem> -->
+<scriptItem id="h.hc_session_reloadkeycam">
+  <label>Reload Keycam</label>
+  <scriptCode><![CDATA[from hc import HCSession; HCSession().reloadKeycam()]]></scriptCode>
+</scriptItem>
 
-      <scriptItem id="h.hc_session_reloadkeycam">
-        <label>Reload Keycam</label>
-        <scriptCode><![CDATA[from hc import HCSession; HCSession().reloadKeycam()]]></scriptCode>
-      </scriptItem>
+<separatorItem />
 
-      <scriptItem id="h.hc_session_rotatesplit">
-        <label>Rotate Split</label>
+<scriptItem id="h.hc_session_rotatesplit">        <label>Rotate Split</label>
         <scriptCode><![CDATA[from hc import HCSession; HCSession().rotateSplit()]]></scriptCode>
       </scriptItem>
 
diff --git a/config/NodeGraphDark.inc b/config/NodeGraphDark.inc
index 68a82ad..daa1f36 100644
--- a/config/NodeGraphDark.inc
+++ b/config/NodeGraphDark.inc
@@ -10,7 +10,7 @@ GraphOverviewBackground:          grey(0.10)
 GraphOverviewViewRegion:          hl_high
 GraphOverviewBorder:              hl_high
 GraphPromptText:                  grey(0.8)
-GraphGridLines:                   grey(0.95)

+GraphGridLines:                   grey(0.18)

 GraphGridPoints:                  border
 
 //
diff --git a/config/hc_colors.inc b/config/hc_colors.inc
index 3977bc0..a29f6cd 100644
--- a/config/hc_colors.inc
+++ b/config/hc_colors.inc
@@ -34,7 +34,7 @@
 #define button_high          #202828
 #define button_low           bg_high
 
-#define hl_high              #609090

+#define hl_high              #c0a080

 #define hl_low               #205050
 #define hl_menu              #556666
 
diff --git a/desktop/hc_attached.desk b/desktop/hc_attached.desk
index 9a3bbae..dd6676b 100644
--- a/desktop/hc_attached.desk
+++ b/desktop/hc_attached.desk
@@ -43,7 +43,7 @@ scenegraphtree -S 0.25 -m 2 -p "/collections /lights" -c  -i  -o  -x 1 -s 1 -M 0
 geospreadsheet -c 0 -l horizontal -s 0.5 -m none -f 0 panetab3
 scenegraphtree -S 0.25 -m 2 -p "/collections /lights" -c  -i  -o  -x 1 -s 1 -M 0 -C primtype,childcount,kind,populated,loaded,activation,visibility,solo,selectability  panetab3
 parmeditor -T 0.25 -w 0 panetab4
-networkeditor -p '{\n	"backgroundimagebrightness":"1.0",\n	"gridmode":"1",\n	"showcopsmenu":"1",\n	"taskgraphcollapsemode":"Off",\n	"showtasksmenu":"1",\n	"useworkitemcolorattribute":"0",\n	"typesidebarsplit":"0.25",\n	"showmenu":"0",\n	"gridxstep":"2.0",\n	"treesplit":"0.25",\n	"showparmdialogmax":"1",\n	"perfstatname":"Time",\n	"palettemode":"0",\n	"taskgraphsortdirection":"0",\n	"showchilddep":"1",\n	"backgroundimageediting":"0",\n	"showtaskgraphperf":"0",\n	"showgrouplist":"0",\n	"showperfstats":"1",\n	"showparmdialog":"0",\n	"overviewmode":"2",\n	"showdep":"0",\n	"palettecolors":"",\n	"showspareinputdep":"1",\n	"workitemcolorattribute":"",\n	"palettesize":"345 345",\n	"showtypesidebar":"0",\n	"showtree":"0",\n	"overviewbounds":"0.000000 0.000000 435.000000 358.200000",\n	"taskgraphperfstatname":"Cook Time",\n	"gridystep":"1.0",\n	"taskgraphsortcriteria":"Automatic",\n	"workitemattrtocollapseby":"",\n	"grouplistsplit":"0.75"\n}\n' panetab6
+networkeditor -p '{\n	"backgroundimagebrightness":"1.0",\n	"gridmode":"0",\n	"gridsnapping":"1",\n	"dosnapping":"1",\n	"showgrid":"0",\n	"showcopsmenu":"1",\n	"taskgraphcollapsemode":"Off",\n	"showtasksmenu":"1",\n	"useworkitemcolorattribute":"0",\n	"typesidebarsplit":"0.25",\n	"showmenu":"0",\n	"gridxstep":"2.0",\n	"treesplit":"0.25",\n	"showparmdialogmax":"1",\n	"perfstatname":"Time",\n	"palettemode":"0",\n	"taskgraphsortdirection":"0",\n	"showchilddep":"1",\n	"backgroundimageediting":"0",\n	"showtaskgraphperf":"0",\n	"showgrouplist":"0",\n	"showperfstats":"1",\n	"showparmdialog":"0",\n	"overviewmode":"2",\n	"showdep":"0",\n	"palettecolors":"",\n	"showspareinputdep":"1",\n	"workitemcolorattribute":"",\n	"palettesize":"345 345",\n	"showtypesidebar":"0",\n	"showtree":"0",\n	"overviewbounds":"0.000000 0.000000 435.000000 358.200000",\n	"taskgraphperfstatname":"Cook Time",\n	"gridystep":"1.0",\n	"taskgraphsortcriteria":"Automatic",\n	"workitemattrtocollapseby":"",\n	"grouplistsplit":"0.75"\n}\n' panetab6
 netcolumns -c display,xray,select,origin,capture,bone -n OBJ panetab6
 netcolumns -c display,render,template,selectabletemplate,highlight,unload,bypass,lock -n SOP panetab6
 netcolumns -c delscript,comment -n CHOPNET panetab6
diff --git a/hc_settings.json b/hc_settings.json
index 9bd7a20..aa46f05 100644
--- a/hc_settings.json
+++ b/hc_settings.json
@@ -2,7 +2,7 @@
     "desktop_mode": "detached",
     "keycam": {
         "guides": {
-            "axis_size": 0.2,
+            "axis_size": 0.05,
             "bbox": 0,
             "cam_axis": 0,
             "cam_geo": 1,
diff --git a/python3.11libs/hc/hcnetworkeditor.py b/python3.11libs/hc/hcnetworkeditor.py
index be293c7..2bc4128 100644
--- a/python3.11libs/hc/hcnetworkeditor.py
+++ b/python3.11libs/hc/hcnetworkeditor.py
@@ -18,6 +18,8 @@ class HCNetworkEditor(HCPathTab):
 
     def initialize(self):
         self.setMenuOpen(0)
+        # Show the grid by default in hc-spawned network editors.
+        self.hou_tab.setPref('gridmode', '2')
 
 
     """ Navigation """
diff --git a/python3.11libs/hc/hcnode.py b/python3.11libs/hc/hcnode.py
index 699cb99..4fb38d8 100644
--- a/python3.11libs/hc/hcnode.py
+++ b/python3.11libs/hc/hcnode.py
@@ -19,7 +19,7 @@ class HCNode:
 
     def selectedChildren(self):
         nodes = []
-        for node in self.hou_node.selectedChildren():
+        for hou_node in self.hou_node.selectedChildren():
             nodes.append(self.__class__(hou_node))
         return nodes
 
@@ -31,7 +31,7 @@ class HCNode:
 
     def displayNode(self):
         hou_display_node = self.hou_node.displayNode()
-        if display_node:
+        if hou_display_node:
             return self.__class__(hou_display_node)
         else:
             return None
@@ -42,12 +42,12 @@ class HCNode:
 
         while stack:
             hc_node = stack.pop()
-            if hc_node.node.isSubNetwork():
+            if hc_node.hou_node.isSubNetwork():
                 child_cat = hc_node.childCat()
-                if child_cat == 'Object' and hc_node.node.isDisplayFlagSet():
-                    stack.extend(node.children())
-                elif child_cat == 'Sop' and hc_node.node.isDisplayFlagSet():
-                    display_node = node.displayNode()
+                if child_cat == 'Object' and hc_node.hou_node.isDisplayFlagSet():
+                    stack.extend(hc_node.children())
+                elif child_cat == 'Sop' and hc_node.hou_node.isDisplayFlagSet():
+                    display_node = hc_node.displayNode()
                     if display_node:
                         visible_nodes.append(display_node)
 
diff --git a/python3.11libs/hc/hcsession.py b/python3.11libs/hc/hcsession.py
index acdb197..fac2e88 100644
--- a/python3.11libs/hc/hcsession.py
+++ b/python3.11libs/hc/hcsession.py
@@ -276,11 +276,14 @@ class HCSession:
         import subprocess
         import sys
         
-        # Save current state so 123.py knows what to open if we don't pass it explicitly,
-        # but we will pass it explicitly to be safe.
+        # Save current state
         current_hip = hou.hipFile.path()
-        if not hou.hipFile.isUntitled():
-            hou.hipFile.save()
+        # Houdini usually names untitled files "untitled.hip" (or .hiplc / .hipnc)
+        if "untitled.hip" not in current_hip.lower():
+            try:
+                hou.hipFile.save()
+            except hou.Error:
+                pass # Continue with restart even if save fails
         
         # Get the path to the houdini executable
         # On Linux, this is usually in $HFS/bin/houdini
@@ -291,12 +294,13 @@ class HCSession:
             executable = "houdini" # Fallback to path
 
         # Launch new process
-        # We pass the current hip file as an argument so it opens directly
+        # start_new_session=True detaches the new process from the current one
         args = [executable, current_hip]
-        subprocess.Popen(args, close_fds=True)
+        subprocess.Popen(args, close_fds=True, start_new_session=True)
         
-        # Exit current instance
-        hou.exit()
+        # Force an immediate low-level exit to bypass any hangs
+        import os
+        os._exit(0)
 
     def save(self):
         hou.hipFile.save()
@@ -486,4 +490,3 @@ class HCSession:
         for pane in panes:
             pane.showTabs(not visible)
 
-
diff --git a/python3.11libs/nodegraphhooks.py b/python3.11libs/nodegraphhooks.py
index 432cffb..4065ee6 100755
--- a/python3.11libs/nodegraphhooks.py
+++ b/python3.11libs/nodegraphhooks.py
@@ -7,11 +7,13 @@ from hc import HCNetworkEditor
 def createEventHandler(uievent, pending_actions):
 
     if isinstance(uievent, MouseEvent):
-        if uievent.modifierstate.ctrl and uievent.wheelvalue != 0:
-            network_editor = HCNetworkEditor(uievent.editor)
-            pivot = uievent.editor.posFromScreen(uievent.mousepos)
-            network_editor.zoom('in' if uievent.wheelvalue > 0 else 'out', amount=0.03, pivot=pivot)
-            return None, True
+        # Ctrl+scroll zoom disabled
+        return None, False
+
+    if isinstance(uievent, KeyboardEvent):
+        # Keyboard events are primarily handled via hc_hotkeys.json and 
+        # NetworkViewMenu.xml, but this hook remains for future 
+        # context-sensitive overrides.
         return None, False
 
     return None, False