git.lucas.co / hou-control
SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git

commit5b26e8cd76325d0ad6599a9bc1eb6df0da7744c1
parentb3c72b3eb0
authorLucas Galante <[email protected]>
date2026-09-10 14:54
hc: fix dead panel commands, settings handling and node graph redraw cost

Several HC Panel commands never worked. SelectionDialog.execute() called
each command bare, so Qt swallowed every exception and a failure looked
identical to a no-op; it now prints a traceback and reports the error to
the status bar. Behind that: Close Other Tabs compared against a
nonexistent self.tab, Show Shelf and Floating Parameter Editor called
methods HCSession does not have, Toggle Update Mode called a dict as a
function on a stringified function object, and Toggle Group List passed a
bound method to `not` so it only ever hid the list.

Replace Node was destructive. hou.Node has no setBypass(), so the call
raised after the replacement had been created and rewired but before the
original was destroyed, leaving both nodes in the graph.

HCSettings.prefs() never merged DEFAULTS, so a key missing from
hc_settings.json reached callers as None -- updateNodeColors() runs at
startup and on every hip load and would have failed before hc finished
loading. It now deep-merges, and caches on mtime: nodegraphhooks reads
settings on every UI event, and each overlay update re-read and re-parsed
the file several times over. updateCurrentNodeOverlay() also compares a
signature of what it would draw and skips the background image rebuild and
redraw when nothing visible changed.

OnCreated.py hardcoded a 1.0 grid, so every new node landed off-grid under
any non-default grid_*_step; it now snaps to the same grid as
HCNetworkEditor. setNodeShapes and setNodeColors read the settings instead
of hardcoded values. 123.py and 456.py no longer assume hou.ui exists, so
hython and batch runs stop erroring during startup.

Smaller fixes: allViewports() iterated a bound method; zoomLevel() divided
a value by itself and so was always 1.0, making translateView pan a fixed
distance at any zoom; HCGeo.centroid read a nonexistent attribute;
nextTab/prevTab indexed by tab name and always landed on the first of two
same-type tabs.

Dropped hcradial.py, hcvisualizers.py and hcconfigloader.py -- all
non-functional and referenced by nothing -- along with the broken legacy
layout helpers in hclayout.py. The radialmenu/ files Houdini loads by path
are unaffected.

tools/check.py is a headless smoke check -- run it with
`$HFS/bin/hython3.13 tools/check.py`. It binds every HCMaps entry, so a
command whose method was renamed or deleted fails loudly instead of
silently, and covers the settings merge, the replaceNode rewire and the
node grid snap. It cannot reach anything needing hou.ui or a live pane.

config/hcnetcursor_assets/ was untracked, so a fresh clone had no
hcnetcursor_*.png for the network editor cursor to draw itself with.

Also carries in-progress work that was already in the tree: keycam, HCCam,
HCGuides, the colour schemes, desktop layouts and hotkey bindings.

Co-Authored-By: Claude Opus 5 <[email protected]>

 AGENTS.md                                       |  30 -
 CLAUDE.md                                       |  13 +-
 MainMenuCommon.xml                              | 105 ++-
 NetworkViewMenu.xml                             | 179 ++++-
 config/NodeGraphDark.inc                        |   2 +-
 config/UIDark.hcs                               | 395 +++++-----
 config/hc_colors.inc                            | 206 +++---
 config/hcnetcursor_assets/hcnetcursor_10x1.png  | Bin 0 -> 1108 bytes
 config/hcnetcursor_assets/hcnetcursor_10x10.png | Bin 0 -> 9794 bytes
 config/hcnetcursor_assets/hcnetcursor_10x11.png | Bin 0 -> 10709 bytes
 config/hcnetcursor_assets/hcnetcursor_10x12.png | Bin 0 -> 11671 bytes
 config/hcnetcursor_assets/hcnetcursor_10x2.png  | Bin 0 -> 2083 bytes
 config/hcnetcursor_assets/hcnetcursor_10x3.png  | Bin 0 -> 3058 bytes
 config/hcnetcursor_assets/hcnetcursor_10x4.png  | Bin 0 -> 4021 bytes
 config/hcnetcursor_assets/hcnetcursor_10x5.png  | Bin 0 -> 4984 bytes
 config/hcnetcursor_assets/hcnetcursor_10x6.png  | Bin 0 -> 5944 bytes
 config/hcnetcursor_assets/hcnetcursor_10x7.png  | Bin 0 -> 6907 bytes
 config/hcnetcursor_assets/hcnetcursor_10x8.png  | Bin 0 -> 7869 bytes
 config/hcnetcursor_assets/hcnetcursor_10x9.png  | Bin 0 -> 8832 bytes
 config/hcnetcursor_assets/hcnetcursor_11x1.png  | Bin 0 -> 1221 bytes
 config/hcnetcursor_assets/hcnetcursor_11x10.png | Bin 0 -> 10879 bytes
 config/hcnetcursor_assets/hcnetcursor_11x11.png | Bin 0 -> 11954 bytes
 config/hcnetcursor_assets/hcnetcursor_11x12.png | Bin 0 -> 13029 bytes
 config/hcnetcursor_assets/hcnetcursor_11x2.png  | Bin 0 -> 2308 bytes
 config/hcnetcursor_assets/hcnetcursor_11x3.png  | Bin 0 -> 3396 bytes
 config/hcnetcursor_assets/hcnetcursor_11x4.png  | Bin 0 -> 4475 bytes
 config/hcnetcursor_assets/hcnetcursor_11x5.png  | Bin 0 -> 5547 bytes
 config/hcnetcursor_assets/hcnetcursor_11x6.png  | Bin 0 -> 6622 bytes
 config/hcnetcursor_assets/hcnetcursor_11x7.png  | Bin 0 -> 7697 bytes
 config/hcnetcursor_assets/hcnetcursor_11x8.png  | Bin 0 -> 8772 bytes
 config/hcnetcursor_assets/hcnetcursor_11x9.png  | Bin 0 -> 9847 bytes
 config/hcnetcursor_assets/hcnetcursor_12x1.png  | Bin 0 -> 1311 bytes
 config/hcnetcursor_assets/hcnetcursor_12x10.png | Bin 0 -> 11763 bytes
 config/hcnetcursor_assets/hcnetcursor_12x11.png | Bin 0 -> 12926 bytes
 config/hcnetcursor_assets/hcnetcursor_12x12.png | Bin 0 -> 14088 bytes
 config/hcnetcursor_assets/hcnetcursor_12x2.png  | Bin 0 -> 2486 bytes
 config/hcnetcursor_assets/hcnetcursor_12x3.png  | Bin 0 -> 3661 bytes
 config/hcnetcursor_assets/hcnetcursor_12x4.png  | Bin 0 -> 4832 bytes
 config/hcnetcursor_assets/hcnetcursor_12x5.png  | Bin 0 -> 5989 bytes
 config/hcnetcursor_assets/hcnetcursor_12x6.png  | Bin 0 -> 7152 bytes
 config/hcnetcursor_assets/hcnetcursor_12x7.png  | Bin 0 -> 8314 bytes
 config/hcnetcursor_assets/hcnetcursor_12x8.png  | Bin 0 -> 9477 bytes
 config/hcnetcursor_assets/hcnetcursor_12x9.png  | Bin 0 -> 10601 bytes
 config/hcnetcursor_assets/hcnetcursor_1x1.png   | Bin 0 -> 307 bytes
 config/hcnetcursor_assets/hcnetcursor_1x10.png  | Bin 0 -> 2171 bytes
 config/hcnetcursor_assets/hcnetcursor_1x11.png  | Bin 0 -> 2380 bytes
 config/hcnetcursor_assets/hcnetcursor_1x12.png  | Bin 0 -> 2585 bytes
 config/hcnetcursor_assets/hcnetcursor_1x2.png   | Bin 0 -> 513 bytes
 config/hcnetcursor_assets/hcnetcursor_1x3.png   | Bin 0 -> 721 bytes
 config/hcnetcursor_assets/hcnetcursor_1x4.png   | Bin 0 -> 928 bytes
 config/hcnetcursor_assets/hcnetcursor_1x5.png   | Bin 0 -> 1136 bytes
 config/hcnetcursor_assets/hcnetcursor_1x6.png   | Bin 0 -> 1342 bytes
 config/hcnetcursor_assets/hcnetcursor_1x7.png   | Bin 0 -> 1551 bytes
 config/hcnetcursor_assets/hcnetcursor_1x8.png   | Bin 0 -> 1756 bytes
 config/hcnetcursor_assets/hcnetcursor_1x9.png   | Bin 0 -> 1965 bytes
 config/hcnetcursor_assets/hcnetcursor_2x1.png   | Bin 0 -> 409 bytes
 config/hcnetcursor_assets/hcnetcursor_2x10.png  | Bin 0 -> 3110 bytes
 config/hcnetcursor_assets/hcnetcursor_2x11.png  | Bin 0 -> 3407 bytes
 config/hcnetcursor_assets/hcnetcursor_2x12.png  | Bin 0 -> 3709 bytes
 config/hcnetcursor_assets/hcnetcursor_2x2.png   | Bin 0 -> 708 bytes
 config/hcnetcursor_assets/hcnetcursor_2x3.png   | Bin 0 -> 1009 bytes
 config/hcnetcursor_assets/hcnetcursor_2x4.png   | Bin 0 -> 1310 bytes
 config/hcnetcursor_assets/hcnetcursor_2x5.png   | Bin 0 -> 1608 bytes
 config/hcnetcursor_assets/hcnetcursor_2x6.png   | Bin 0 -> 1908 bytes
 config/hcnetcursor_assets/hcnetcursor_2x7.png   | Bin 0 -> 2209 bytes
 config/hcnetcursor_assets/hcnetcursor_2x8.png   | Bin 0 -> 2508 bytes
 config/hcnetcursor_assets/hcnetcursor_2x9.png   | Bin 0 -> 2808 bytes
 config/hcnetcursor_assets/hcnetcursor_3x1.png   | Bin 0 -> 508 bytes
 config/hcnetcursor_assets/hcnetcursor_3x10.png  | Bin 0 -> 4036 bytes
 config/hcnetcursor_assets/hcnetcursor_3x11.png  | Bin 0 -> 4428 bytes
 config/hcnetcursor_assets/hcnetcursor_3x12.png  | Bin 0 -> 4818 bytes
 config/hcnetcursor_assets/hcnetcursor_3x2.png   | Bin 0 -> 898 bytes
 config/hcnetcursor_assets/hcnetcursor_3x3.png   | Bin 0 -> 1293 bytes
 config/hcnetcursor_assets/hcnetcursor_3x4.png   | Bin 0 -> 1683 bytes
 config/hcnetcursor_assets/hcnetcursor_3x5.png   | Bin 0 -> 2076 bytes
 config/hcnetcursor_assets/hcnetcursor_3x6.png   | Bin 0 -> 2467 bytes
 config/hcnetcursor_assets/hcnetcursor_3x7.png   | Bin 0 -> 2860 bytes
 config/hcnetcursor_assets/hcnetcursor_3x8.png   | Bin 0 -> 3252 bytes
 config/hcnetcursor_assets/hcnetcursor_3x9.png   | Bin 0 -> 3644 bytes
 config/hcnetcursor_assets/hcnetcursor_4x1.png   | Bin 0 -> 591 bytes
 config/hcnetcursor_assets/hcnetcursor_4x10.png  | Bin 0 -> 4808 bytes
 config/hcnetcursor_assets/hcnetcursor_4x11.png  | Bin 0 -> 5277 bytes
 config/hcnetcursor_assets/hcnetcursor_4x12.png  | Bin 0 -> 5743 bytes
 config/hcnetcursor_assets/hcnetcursor_4x2.png   | Bin 0 -> 1063 bytes
 config/hcnetcursor_assets/hcnetcursor_4x3.png   | Bin 0 -> 1530 bytes
 config/hcnetcursor_assets/hcnetcursor_4x4.png   | Bin 0 -> 1998 bytes
 config/hcnetcursor_assets/hcnetcursor_4x5.png   | Bin 0 -> 2467 bytes
 config/hcnetcursor_assets/hcnetcursor_4x6.png   | Bin 0 -> 2935 bytes
 config/hcnetcursor_assets/hcnetcursor_4x7.png   | Bin 0 -> 3403 bytes
 config/hcnetcursor_assets/hcnetcursor_4x8.png   | Bin 0 -> 3871 bytes
 config/hcnetcursor_assets/hcnetcursor_4x9.png   | Bin 0 -> 4341 bytes
 config/hcnetcursor_assets/hcnetcursor_5x1.png   | Bin 0 -> 668 bytes
 config/hcnetcursor_assets/hcnetcursor_5x10.png  | Bin 0 -> 5597 bytes
 config/hcnetcursor_assets/hcnetcursor_5x11.png  | Bin 0 -> 6153 bytes
 config/hcnetcursor_assets/hcnetcursor_5x12.png  | Bin 0 -> 6697 bytes
 config/hcnetcursor_assets/hcnetcursor_5x2.png   | Bin 0 -> 1224 bytes
 config/hcnetcursor_assets/hcnetcursor_5x3.png   | Bin 0 -> 1766 bytes
 config/hcnetcursor_assets/hcnetcursor_5x4.png   | Bin 0 -> 2319 bytes
 config/hcnetcursor_assets/hcnetcursor_5x5.png   | Bin 0 -> 2859 bytes
 config/hcnetcursor_assets/hcnetcursor_5x6.png   | Bin 0 -> 3412 bytes
 config/hcnetcursor_assets/hcnetcursor_5x7.png   | Bin 0 -> 3949 bytes
 config/hcnetcursor_assets/hcnetcursor_5x8.png   | Bin 0 -> 4505 bytes
 config/hcnetcursor_assets/hcnetcursor_5x9.png   | Bin 0 -> 5048 bytes
 config/hcnetcursor_assets/hcnetcursor_6x1.png   | Bin 0 -> 823 bytes
 config/hcnetcursor_assets/hcnetcursor_6x10.png  | Bin 0 -> 7138 bytes
 config/hcnetcursor_assets/hcnetcursor_6x11.png  | Bin 0 -> 7838 bytes
 config/hcnetcursor_assets/hcnetcursor_6x12.png  | Bin 0 -> 8538 bytes
 config/hcnetcursor_assets/hcnetcursor_6x2.png   | Bin 0 -> 1533 bytes
 config/hcnetcursor_assets/hcnetcursor_6x3.png   | Bin 0 -> 2233 bytes
 config/hcnetcursor_assets/hcnetcursor_6x4.png   | Bin 0 -> 2933 bytes
 config/hcnetcursor_assets/hcnetcursor_6x5.png   | Bin 0 -> 3633 bytes
 config/hcnetcursor_assets/hcnetcursor_6x6.png   | Bin 0 -> 4333 bytes
 config/hcnetcursor_assets/hcnetcursor_6x7.png   | Bin 0 -> 5033 bytes
 config/hcnetcursor_assets/hcnetcursor_6x8.png   | Bin 0 -> 5733 bytes
 config/hcnetcursor_assets/hcnetcursor_6x9.png   | Bin 0 -> 6438 bytes
 config/hcnetcursor_assets/hcnetcursor_7x1.png   | Bin 0 -> 902 bytes
 config/hcnetcursor_assets/hcnetcursor_7x10.png  | Bin 0 -> 7895 bytes
 config/hcnetcursor_assets/hcnetcursor_7x11.png  | Bin 0 -> 8670 bytes
 config/hcnetcursor_assets/hcnetcursor_7x12.png  | Bin 0 -> 9445 bytes
 config/hcnetcursor_assets/hcnetcursor_7x2.png   | Bin 0 -> 1690 bytes
 config/hcnetcursor_assets/hcnetcursor_7x3.png   | Bin 0 -> 2466 bytes
 config/hcnetcursor_assets/hcnetcursor_7x4.png   | Bin 0 -> 3241 bytes
 config/hcnetcursor_assets/hcnetcursor_7x5.png   | Bin 0 -> 4016 bytes
 config/hcnetcursor_assets/hcnetcursor_7x6.png   | Bin 0 -> 4791 bytes
 config/hcnetcursor_assets/hcnetcursor_7x7.png   | Bin 0 -> 5566 bytes
 config/hcnetcursor_assets/hcnetcursor_7x8.png   | Bin 0 -> 6345 bytes
 config/hcnetcursor_assets/hcnetcursor_7x9.png   | Bin 0 -> 7120 bytes
 config/hcnetcursor_assets/hcnetcursor_8x1.png   | Bin 0 -> 983 bytes
 config/hcnetcursor_assets/hcnetcursor_8x10.png  | Bin 0 -> 8652 bytes
 config/hcnetcursor_assets/hcnetcursor_8x11.png  | Bin 0 -> 9502 bytes
 config/hcnetcursor_assets/hcnetcursor_8x12.png  | Bin 0 -> 10352 bytes
 config/hcnetcursor_assets/hcnetcursor_8x2.png   | Bin 0 -> 1845 bytes
 config/hcnetcursor_assets/hcnetcursor_8x3.png   | Bin 0 -> 2701 bytes
 config/hcnetcursor_assets/hcnetcursor_8x4.png   | Bin 0 -> 3551 bytes
 config/hcnetcursor_assets/hcnetcursor_8x5.png   | Bin 0 -> 4401 bytes
 config/hcnetcursor_assets/hcnetcursor_8x6.png   | Bin 0 -> 5251 bytes
 config/hcnetcursor_assets/hcnetcursor_8x7.png   | Bin 0 -> 6102 bytes
 config/hcnetcursor_assets/hcnetcursor_8x8.png   | Bin 0 -> 6952 bytes
 config/hcnetcursor_assets/hcnetcursor_8x9.png   | Bin 0 -> 7802 bytes
 config/hcnetcursor_assets/hcnetcursor_9x1.png   | Bin 0 -> 1060 bytes
 config/hcnetcursor_assets/hcnetcursor_9x10.png  | Bin 0 -> 9405 bytes
 config/hcnetcursor_assets/hcnetcursor_9x11.png  | Bin 0 -> 10330 bytes
 config/hcnetcursor_assets/hcnetcursor_9x12.png  | Bin 0 -> 11197 bytes
 config/hcnetcursor_assets/hcnetcursor_9x2.png   | Bin 0 -> 1997 bytes
 config/hcnetcursor_assets/hcnetcursor_9x3.png   | Bin 0 -> 2932 bytes
 config/hcnetcursor_assets/hcnetcursor_9x4.png   | Bin 0 -> 3857 bytes
 config/hcnetcursor_assets/hcnetcursor_9x5.png   | Bin 0 -> 4782 bytes
 config/hcnetcursor_assets/hcnetcursor_9x6.png   | Bin 0 -> 5705 bytes
 config/hcnetcursor_assets/hcnetcursor_9x7.png   | Bin 0 -> 6630 bytes
 config/hcnetcursor_assets/hcnetcursor_9x8.png   | Bin 0 -> 7555 bytes
 config/hcnetcursor_assets/hcnetcursor_9x9.png   | Bin 0 -> 8480 bytes
 desktop/hc_attached.desk                        |   4 +-
 desktop/hc_detached.desk                        |  40 +-
 hc_hotkeys.json                                 | 166 +++--
 hc_settings.json                                |  20 +-
 mcpserver/houdini_mcp.py                        |  99 ---
 mcpserver/requirements.txt                      |   2 -
 notes/GEM production notes.md                   |  18 -
 notes/GEM.md                                    |  24 -
 notes/hou-developer.md                          | 237 ------
 notes/moldtekPartitionViewerState               |  21 -
 notes/network_editor_setttings_reference        | 148 ----
 notes/sop_verbs                                 | 345 ---------
 notes/systems.md                                |  27 -
 otls/sop_lsgalante.im_soft_transform.1.0.hdalc  | Bin 6570 -> 6897 bytes
 python3.13libs/hc/__init__.py                   |   4 -
 python3.13libs/hc/hcbindings.py                 |   7 +-
 python3.13libs/hc/hccam.py                      | 102 ++-
 python3.13libs/hc/hcconfigloader.py             |   6 -
 python3.13libs/hc/hcdefaultcam.py               |   6 +-
 python3.13libs/hc/hcdrawmode.py                 |  26 +
 python3.13libs/hc/hcframing.py                  |  16 +
 python3.13libs/hc/hcgeo.py                      |   2 +-
 python3.13libs/hc/hcgeometryutils.py            |  13 +
 python3.13libs/hc/hcguides.py                   | 115 ++-
 python3.13libs/hc/hclayout.py                   | 147 ++--
 python3.13libs/hc/hcmaps.py                     |  18 +-
 python3.13libs/hc/hcmenuutils.py                |   1 +
 python3.13libs/hc/hcnetworkeditor.py            | 928 ++++++++++++++++++++----
 python3.13libs/hc/hcnode.py                     |  23 +-
 python3.13libs/hc/hcpane.py                     |  89 ++-
 python3.13libs/hc/hcparametertab.py             |  62 ++
 python3.13libs/hc/hcpathtab.py                  |  12 +-
 python3.13libs/hc/hcradial.py                   | 186 -----
 python3.13libs/hc/hcsceneviewer.py              |  85 ++-
 python3.13libs/hc/hcsession.py                  | 360 +++++++--
 python3.13libs/hc/hcsettings.py                 | 186 ++++-
 python3.13libs/hc/hctab.py                      | 112 ++-
 python3.13libs/hc/hcviewport.py                 |   8 +-
 python3.13libs/hc/hcvisibility.py               |  21 +
 python3.13libs/hc/hcvisualizers.py              | 133 ----
 python3.13libs/hc/hcwidgets.py                  |  70 +-
 python3.13libs/nodegraphhooks.py                |  97 ++-
 python3.13libs/uiready.py                       |  76 +-
 scripts/123.py                                  |   5 +-
 scripts/OnCreated.py                            |  29 +-
 shortcomings.md                                 |  60 ++
 tools/check.py                                  | 198 +++++
 viewer_states/keycam.py                         | 232 +++++-
 199 files changed, 3288 insertions(+), 2228 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
deleted file mode 100644
index f3ecc32..0000000
--- a/AGENTS.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# 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/CLAUDE.md b/CLAUDE.md
index ecb7cba..5e9bc01 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,7 +4,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
 
 ## Overview
 
-`hou-control` (hc) is a SideFX Houdini customization package — not a standalone application. There is no build, no test suite, and no package manager. Code here is loaded by Houdini at runtime via the `hou-control.json` package manifest (which adds this directory to `HOUDINI_PATH`) and the `HC_PATH` env var, which the code reads with `hou.getenv("HC_PATH")` to locate config/scripts on disk.
+`hou-control` (hc) is a SideFX Houdini customization package — not a standalone application. There is no build and no package manager, and the only automated check is `tools/check.py` (below). Code here is loaded by Houdini at runtime via the `hou-control.json` package manifest (which adds this directory to `HOUDINI_PATH`) and the `HC_PATH` env var, which the code reads with `hou.getenv("HC_PATH")` to locate config/scripts on disk.
+
+`tools/check.py` is the only automated check: run it headlessly with
+`$HFS/bin/hython3.13 tools/check.py` from the repo root. It imports `hc`, binds
+every `HCMaps` entry (catching commands whose method was renamed or deleted),
+and exercises the settings merge, the `replaceNode` rewire and the node grid
+snap. It cannot cover anything needing `hou.ui` or a live pane — that still has
+to be driven by hand in Houdini.
 
 Everything runs inside Houdini's embedded Python 3.13. To exercise changes, reload from within Houdini:
 - From the `hc` main menu: **Reload Hotkeys**, **Reload Colors**, **Reload Keycam**.
@@ -38,7 +45,7 @@ These are recognized by Houdini's startup/event system by filename convention:
 - `123.py` / `456.py` are Houdini's magic filenames: `123.py` runs when Houdini starts without a `.hip`, `456.py` runs after any `.hip` load. Currently used for "open last file" tracking via `$HOUDINI_USER_PREF_DIR/st_data/state.json`.
 - `OnCreated.py` — node OnCreated event script.
 - `hc_hotkeys.json` (at repo root) — source of truth for keybindings, loaded by `HCBindings`. Conflicts with existing bindings are detected and cleared automatically.
-- `settings.json` — runtime settings for `keycam` viewer state and node graph defaults.
+- `hc_settings.json` (at repo root) — runtime settings for the `keycam` viewer state and node graph defaults, read through `HCSettings`, which overlays the file on `HCSettings.DEFAULTS` so a missing key never reaches a caller.
 
 ### Viewer states (`viewer_states/`)
 
@@ -67,6 +74,6 @@ These are reloaded via `HCSession.reloadColorSchemes()` (`hou.ui.reloadColorSche
 - `.type()` methods return string discriminators (`'HCNetworkEditor'`, `'HCSceneViewer'`, `'HCParameterTab'`, `'HCPathTab'`) — this is the idiomatic way to branch on tab kind in this codebase, not `isinstance`. When branching, remember that `'HCParameterTab'` is a sibling of `'HCPathTab'` (Parm tabs return the former, DetailsView returns the latter) — code that wants both should check `tab.type() in ('HCPathTab', 'HCParameterTab')`.
 - Toggle-style methods often use a small string map (e.g. `{'0': '1', '1': '0'}`) because Houdini prefs are stored as strings.
 - Paths to bundled files are built from `hou.getenv("HC_PATH")` — don't hardcode absolute paths.
-- **`HCWidgets.SelectionDialog.execute()` has no try/except**: any exception in the callable stored in `self.list_dict` is silently swallowed — the user just sees "nothing happens." When debugging a new HC Panel command that doesn't work, connect via `hrun` and test the callable directly, or add a temporary try/except with `traceback.print_exc()` in `execute()` to surface the error.
+- **`HCWidgets.SelectionDialog.execute()` surfaces command errors**: it wraps the callable from `self.list_dict` in a try/except that prints a traceback and writes the exception to the status bar. A HC Panel command that fails now says so — check the console. (Before this it swallowed everything and the user just saw "nothing happens.")
 - **`HCSession.reloadHC()` destroys HC-owned Qt widgets** (`hc_status_overlay`, `hc_status_dialog`) and must re-create any that should persist (e.g. status bar). Forgetting to restore them causes them to disappear until Houdini restarts.
 - **PySide6 widget API mismatches**: `setOpenExternalLinks()` belongs to `QTextBrowser`, not `QTextEdit`. Using the wrong base class causes a silent `AttributeError` inside SelectionDialog callables.
diff --git a/MainMenuCommon.xml b/MainMenuCommon.xml
index ae99919..a81f444 100755
--- a/MainMenuCommon.xml
+++ b/MainMenuCommon.xml
@@ -21,11 +21,6 @@
         <scriptCode><![CDATA[from hc import HCSession; HCSession().colorEditor()]]></scriptCode>
       </scriptItem>
 
-      <scriptItem id="h.hc_session_keycam">
-        <label>Keycam</label>
-        <scriptCode><![CDATA[from hc import HCSession; HCSession().keycam()]]></scriptCode>
-      </scriptItem>
-
       <scriptItem id="h.hc_session_reloadcolors">
         <label>Reload Viewport Colors</label>
         <scriptCode><![CDATA[from hc import HCSession; HCSession().reloadViewportColors()]]></scriptCode>
@@ -45,10 +40,11 @@
         <!-- <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_reloadkeycam">
+        <label>Reload Keycam</label>
+        <scriptCode><![CDATA[from hc import HCSession; HCSession().reloadKeycam()]]></scriptCode>
+      </scriptItem>
 
 <separatorItem />
 
@@ -101,11 +97,21 @@
         <scriptCode><![CDATA[from hc import HCSession; HCSession().toggleMainMenu()]]></scriptCode>
       </scriptItem>
 
+      <scriptItem id="h.hc_session_togglestatusbar">
+        <label>Toggle Status Bar</label>
+        <scriptCode><![CDATA[from hc import HCSession; HCSession().toggleStatusBar()]]></scriptCode>
+      </scriptItem>
+
       <!-- <scriptItem id="h.hc_session_updatemainmenubar"> -->
         <!-- <label>Update Main Menu Bar</label> -->
         <!-- <scriptCode><![CDATA[from hc import HCSession; HCSession().updateMainMenuBar()]]></scriptCode> -->
       <!-- </scriptItem> -->
 
+      <scriptItem id="h.hc_session_toggle_spreadsheet">
+        <label>Toggle Spreadsheet</label>
+        <scriptCode><![CDATA[from hc import HCSession; HCSession().toggleSpreadsheet()]]></scriptCode>
+      </scriptItem>
+
       <scriptItem id="h.hc_panel">
         <label>HC Panel</label>
         <scriptCode><![CDATA[from hc import HCSession; HCSession().hcPanel(); ]]></scriptCode>
@@ -117,12 +123,93 @@
         <label>Scene Viewer</label>
       </titleItem>
 
+      <scriptItem id="h.hc_session_keycam">
+        <label>Keycam</label>
+        <scriptCode><![CDATA[from hc import HCSession; HCSession().keycam()]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="h.pane.gview.hc_toggle_group_list">
+        <label>Toggle Group List</label>
+        <scriptCode><![CDATA[
+from hc import HCSession
+tab = HCSession().currentTab()
+if tab is not None and tab.type() == 'HCSceneViewer':
+    tab.toggleGroupList()
+        ]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="h.pane.gview.hc_frame_selection">
+        <label>Frame Selected</label>
+        <scriptCode><![CDATA[
+from hc import HCSession
+tab = HCSession().currentTab()
+if tab is not None and tab.type() == 'HCSceneViewer':
+    tab.hou_tab.curViewport().frameSelected()
+        ]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="h.pane.gview.hc_toggle_point_numbers">
+        <label>Toggle Point Numbers</label>
+        <scriptCode><![CDATA[
+from hc import HCSession
+tab = HCSession().currentTab()
+if tab is not None and tab.type() == 'HCSceneViewer':
+    tab.togglePointNumbers()
+        ]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="h.pane.gview.hc_toggle_prim_numbers">
+        <label>Toggle Prim Numbers</label>
+        <scriptCode><![CDATA[
+from hc import HCSession
+tab = HCSession().currentTab()
+if tab is not None and tab.type() == 'HCSceneViewer':
+    tab.togglePrimNumbers()
+        ]]></scriptCode>
+      </scriptItem>
+
+      <separatorItem />
+
+      <titleItem>
+        <label>Parameter Editor</label>
+      </titleItem>
+
+      <scriptItem id="h.pane.parms.hc_focus_next_parameter">
+        <label>Focus Next Parameter</label>
+        <scriptCode><![CDATA[
+from hc import HCSession
+tab = HCSession().currentTab()
+if tab is not None and tab.type() == 'HCParameterTab':
+    tab.focusNextParameter()
+        ]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="h.pane.parms.hc_focus_prev_parameter">
+        <label>Focus Previous Parameter</label>
+        <scriptCode><![CDATA[
+from hc import HCSession
+tab = HCSession().currentTab()
+if tab is not None and tab.type() == 'HCParameterTab':
+    tab.focusPreviousParameter()
+        ]]></scriptCode>
+      </scriptItem>
+
       <separatorItem />
 
       <titleItem>
         <label>Network Editor</label>
       </titleItem>
 
+      <scriptItem id="h.pane.wsheet.hc_replace_node">
+        <label>Replace Node</label>
+        <scriptCode><![CDATA[
+from hc import HCSession
+tab = HCSession().currentTab()
+if tab is not None and tab.type() == 'HCNetworkEditor':
+    tab.replaceNode()
+        ]]></scriptCode>
+      </scriptItem>
+
       <separatorItem />
 
     </subMenu>
diff --git a/NetworkViewMenu.xml b/NetworkViewMenu.xml
index 3dc96a7..076c83b 100644
--- a/NetworkViewMenu.xml
+++ b/NetworkViewMenu.xml
@@ -13,7 +13,7 @@
         <label>Navigate Up</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).navigateDirection('up')
+HCNetworkEditor(kwargs['editor']).run('Navigate Up', 'navigateDirection', 'up')
 ]]></scriptCode>
       </scriptItem>
 
@@ -21,7 +21,7 @@ HCNetworkEditor(kwargs['editor']).navigateDirection('up')
         <label>Navigate Down</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).navigateDirection('down')
+HCNetworkEditor(kwargs['editor']).run('Navigate Down', 'navigateDirection', 'down')
 ]]></scriptCode>
       </scriptItem>
 
@@ -29,7 +29,7 @@ HCNetworkEditor(kwargs['editor']).navigateDirection('down')
         <label>Navigate Left</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).navigateDirection('left')
+HCNetworkEditor(kwargs['editor']).run('Navigate Left', 'navigateDirection', 'left')
 ]]></scriptCode>
       </scriptItem>
 
@@ -37,7 +37,7 @@ HCNetworkEditor(kwargs['editor']).navigateDirection('left')
         <label>Navigate Right</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).navigateDirection('right')
+HCNetworkEditor(kwargs['editor']).run('Navigate Right', 'navigateDirection', 'right')
 ]]></scriptCode>
       </scriptItem>
 
@@ -51,7 +51,7 @@ HCNetworkEditor(kwargs['editor']).navigateDirection('right')
         <label>Extend Selection Up</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).addToSelection('up')
+HCNetworkEditor(kwargs['editor']).run('Extend Selection Up', 'addToSelection', 'up')
 ]]></scriptCode>
       </scriptItem>
 
@@ -59,7 +59,7 @@ HCNetworkEditor(kwargs['editor']).addToSelection('up')
         <label>Extend Selection Down</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).addToSelection('down')
+HCNetworkEditor(kwargs['editor']).run('Extend Selection Down', 'addToSelection', 'down')
 ]]></scriptCode>
       </scriptItem>
 
@@ -67,7 +67,7 @@ HCNetworkEditor(kwargs['editor']).addToSelection('down')
         <label>Extend Selection Left</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).addToSelection('left')
+HCNetworkEditor(kwargs['editor']).run('Extend Selection Left', 'addToSelection', 'left')
 ]]></scriptCode>
       </scriptItem>
 
@@ -75,7 +75,7 @@ HCNetworkEditor(kwargs['editor']).addToSelection('left')
         <label>Extend Selection Right</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).addToSelection('right')
+HCNetworkEditor(kwargs['editor']).run('Extend Selection Right', 'addToSelection', 'right')
 ]]></scriptCode>
       </scriptItem>
 
@@ -89,7 +89,7 @@ HCNetworkEditor(kwargs['editor']).addToSelection('right')
         <label>Move Up</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).translateSelectedNodes('up')
+HCNetworkEditor(kwargs['editor']).run('Move Up', 'translateSelectedNodes', 'up')
 ]]></scriptCode>
       </scriptItem>
 
@@ -97,7 +97,7 @@ HCNetworkEditor(kwargs['editor']).translateSelectedNodes('up')
         <label>Move Down</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).translateSelectedNodes('down')
+HCNetworkEditor(kwargs['editor']).run('Move Down', 'translateSelectedNodes', 'down')
 ]]></scriptCode>
       </scriptItem>
 
@@ -105,7 +105,7 @@ HCNetworkEditor(kwargs['editor']).translateSelectedNodes('down')
         <label>Move Left</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).translateSelectedNodes('left')
+HCNetworkEditor(kwargs['editor']).run('Move Left', 'translateSelectedNodes', 'left')
 ]]></scriptCode>
       </scriptItem>
 
@@ -113,12 +113,86 @@ HCNetworkEditor(kwargs['editor']).translateSelectedNodes('left')
         <label>Move Right</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).translateSelectedNodes('right')
+HCNetworkEditor(kwargs['editor']).run('Move Right', 'translateSelectedNodes', 'right')
 ]]></scriptCode>
       </scriptItem>
 
       <separatorItem />
 
+      <titleItem>
+        <label>Move Hierarchy</label>
+      </titleItem>
+
+      <scriptItem id="pane.wsheet.hc_move_descendants_up">
+        <label>Move Descendants Up</label>
+        <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).run('Move Descendants Up', 'translateSelectedNodes', 'up', 'descendants')
+]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="pane.wsheet.hc_move_descendants_down">
+        <label>Move Descendants Down</label>
+        <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).run('Move Descendants Down', 'translateSelectedNodes', 'down', 'descendants')
+]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="pane.wsheet.hc_move_descendants_left">
+        <label>Move Descendants Left</label>
+        <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).run('Move Descendants Left', 'translateSelectedNodes', 'left', 'descendants')
+]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="pane.wsheet.hc_move_descendants_right">
+        <label>Move Descendants Right</label>
+        <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).run('Move Descendants Right', 'translateSelectedNodes', 'right', 'descendants')
+]]></scriptCode>
+      </scriptItem>
+
+      <separatorItem />
+
+      <scriptItem id="pane.wsheet.hc_move_ancestors_up">
+        <label>Move Ancestors Up</label>
+        <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).run('Move Ancestors Up', 'translateSelectedNodes', 'up', 'ancestors')
+]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="pane.wsheet.hc_move_ancestors_down">
+        <label>Move Ancestors Down</label>
+        <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).run('Move Ancestors Down', 'translateSelectedNodes', 'down', 'ancestors')
+]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="pane.wsheet.hc_move_ancestors_left">
+        <label>Move Ancestors Left</label>
+        <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).run('Move Ancestors Left', 'translateSelectedNodes', 'left', 'ancestors')
+]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="pane.wsheet.hc_move_ancestors_right">
+        <label>Move Ancestors Right</label>
+        <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).run('Move Ancestors Right', 'translateSelectedNodes', 'right', 'ancestors')
+]]></scriptCode>
+      </scriptItem>
+
+      <separatorItem />
+
+      <separatorItem />
+
       <titleItem>
         <label>View</label>
       </titleItem>
@@ -127,7 +201,7 @@ HCNetworkEditor(kwargs['editor']).translateSelectedNodes('right')
         <label>Pan View Up</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).translateView('up')
+HCNetworkEditor(kwargs['editor']).run('Pan View Up', 'translateView', 'up')
 ]]></scriptCode>
       </scriptItem>
 
@@ -135,7 +209,7 @@ HCNetworkEditor(kwargs['editor']).translateView('up')
         <label>Pan View Down</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).translateView('down')
+HCNetworkEditor(kwargs['editor']).run('Pan View Down', 'translateView', 'down')
 ]]></scriptCode>
       </scriptItem>
 
@@ -143,7 +217,7 @@ HCNetworkEditor(kwargs['editor']).translateView('down')
         <label>Pan View Left</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).translateView('left')
+HCNetworkEditor(kwargs['editor']).run('Pan View Left', 'translateView', 'left')
 ]]></scriptCode>
       </scriptItem>
 
@@ -151,7 +225,7 @@ HCNetworkEditor(kwargs['editor']).translateView('left')
         <label>Pan View Right</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).translateView('right')
+HCNetworkEditor(kwargs['editor']).run('Pan View Right', 'translateView', 'right')
 ]]></scriptCode>
       </scriptItem>
 
@@ -159,7 +233,7 @@ HCNetworkEditor(kwargs['editor']).translateView('right')
         <label>Zoom In</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).zoom('in')
+HCNetworkEditor(kwargs['editor']).run('Zoom In', 'zoom', 'in')
 ]]></scriptCode>
       </scriptItem>
 
@@ -167,7 +241,7 @@ HCNetworkEditor(kwargs['editor']).zoom('in')
         <label>Zoom Out</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).zoom('out')
+HCNetworkEditor(kwargs['editor']).run('Zoom Out', 'zoom', 'out')
 ]]></scriptCode>
       </scriptItem>
 
@@ -175,7 +249,23 @@ HCNetworkEditor(kwargs['editor']).zoom('out')
         <label>Frame All</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).frameAll()
+HCNetworkEditor(kwargs['editor']).run('Frame All', 'frameAll')
+]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="pane.wsheet.hc_frame_cursor">
+        <label>Frame HC Cursor</label>
+        <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).run('Frame HC Cursor', 'frameHcnetcursor')
+]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="pane.wsheet.hc_frame_component">
+        <label>Frame Component</label>
+        <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).run('Frame Component', 'frameCurrentComponent')
 ]]></scriptCode>
       </scriptItem>
 
@@ -183,7 +273,26 @@ HCNetworkEditor(kwargs['editor']).frameAll()
         <label>Toggle Grid</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).toggleGridMode()
+HCNetworkEditor(kwargs['editor']).run('Toggle Grid', 'toggleGridMode')
+]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="pane.wsheet.hc_reset_cursor_size">
+        <label>Reset Cursor Size</label>
+        <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).run('Reset Cursor Size', 'resetHcnetcursorSize')
+]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="pane.wsheet.hc_zz">
+        <label>Center on Cursor (zz)</label>
+        <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+from hc.hcleader import chord
+ed = HCNetworkEditor(kwargs['editor'])
+if chord(ed.hou_tab, ed.centerOnCursor):
+    ed.hou_tab.flashMessage(None, 'Center on Cursor', 1.0)
 ]]></scriptCode>
       </scriptItem>
 
@@ -193,11 +302,19 @@ HCNetworkEditor(kwargs['editor']).toggleGridMode()
         <label>Selection</label>
       </titleItem>
 
+      <scriptItem id="pane.wsheet.hc_paste">
+        <label>Paste at Cursor</label>
+        <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).run('Paste at Cursor', 'pasteAtCursor')
+]]></scriptCode>
+      </scriptItem>
+
       <scriptItem id="pane.wsheet.hc_select_all">
         <label>Select All</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).selectAllNodes()
+HCNetworkEditor(kwargs['editor']).run('Select All', 'selectAllNodes')
 ]]></scriptCode>
       </scriptItem>
 
@@ -205,7 +322,15 @@ HCNetworkEditor(kwargs['editor']).selectAllNodes()
         <label>Deselect All</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).deselectAllNodes()
+HCNetworkEditor(kwargs['editor']).run('Deselect All', 'deselectAllNodes')
+]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="pane.wsheet.hc_recook_selection">
+        <label>Recook Selection</label>
+        <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).run('Recook Selection', 'recookSelection')
 ]]></scriptCode>
       </scriptItem>
 
@@ -213,7 +338,7 @@ HCNetworkEditor(kwargs['editor']).deselectAllNodes()
         <label>Select Next Connected Group</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).selectComponent('next')
+HCNetworkEditor(kwargs['editor']).run('Select Next Group', 'selectComponent', 'next')
 ]]></scriptCode>
       </scriptItem>
 
@@ -221,7 +346,7 @@ HCNetworkEditor(kwargs['editor']).selectComponent('next')
         <label>Select Previous Connected Group</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).selectComponent('prev')
+HCNetworkEditor(kwargs['editor']).run('Select Prev Group', 'selectComponent', 'prev')
 ]]></scriptCode>
       </scriptItem>
 
@@ -235,7 +360,7 @@ HCNetworkEditor(kwargs['editor']).selectComponent('prev')
         <label>Toggle Bypass</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).toggleBypassFlag()
+HCNetworkEditor(kwargs['editor']).run('Toggle Bypass', 'toggleBypassFlag')
 ]]></scriptCode>
       </scriptItem>
 
@@ -243,7 +368,7 @@ HCNetworkEditor(kwargs['editor']).toggleBypassFlag()
         <label>Toggle Template</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).toggleTemplateFlag()
+HCNetworkEditor(kwargs['editor']).run('Toggle Template', 'toggleTemplateFlag')
 ]]></scriptCode>
       </scriptItem>
 
@@ -251,7 +376,7 @@ HCNetworkEditor(kwargs['editor']).toggleTemplateFlag()
         <label>Set Display</label>
         <scriptCode><![CDATA[
 from hc import HCNetworkEditor
-HCNetworkEditor(kwargs['editor']).setDisplayFlag()
+HCNetworkEditor(kwargs['editor']).run('Set Display', 'setDisplayFlag')
 ]]></scriptCode>
       </scriptItem>
 
diff --git a/config/NodeGraphDark.inc b/config/NodeGraphDark.inc
index daa1f36..c97ebef 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.18)

+GraphGridLines:                   grey(0.28)

 GraphGridPoints:                  border
 
 //
diff --git a/config/UIDark.hcs b/config/UIDark.hcs
index 4a05648..d348e8d 100644
--- a/config/UIDark.hcs
+++ b/config/UIDark.hcs
@@ -1,4 +1,4 @@
-//
+/
 // Scheme name
 //
 
@@ -17,90 +17,102 @@ Scheme: hc-dark
 // Base colors
 //
 
-BackColor:                            bg_high           // auto contrast code linked to BorderDark
-BorderLight:                          bg_high           // line left of node graph pane & some menus
-BorderDark:                           bg_low            // annoying status bar border
-TextColor:                            text
-HiliteTextColor:                      text_highlight
-HiliteTextBG:                         hl_high
-SecondaryText:                        grey(0.5)
-LinkText:                             #64ACFF
-DropIndicator                         0.0 0.0 1.0
-ForegroundColor:                      grey(0.02)        // line in parameter pane (auto contrast code)
-BackgroundColor:                      unknown           // ?
-PaperColor:                           bg_low            // line below pane tabs, other lines
-TextHighlightColor:                   red    // ?
-BlueTextLinkColor:                    #FF00FF
-LinkerPartialLinkColor:               grey(0.8)         // Linker tree views (?)
-DisabledTextColor:                    arrow
-DisabledTextDropShadowColor:          grey(0.8)
+BackColor:           bg_high // Auto contrast code linked to BorderDark
+BorderLight:         bg_high // Line left of node graph pane & some menus
+BorderDark:          bg_low  // Annoying status bar border
+TextColor:           text
+HiliteTextColor:     text_highlight
+HiliteTextBG:        hl_high
+SecondaryText:       grey(0.5)
+LinkText:            #64ACFF
+DropIndicator        0.0 0.0 1.0
+ForegroundColor:     grey(0.02) // Line in param pane (auto contrast code)
+BackgroundColor:     unknown    // ?
+PaperColor:          bg_low     // line below pane tabs, other lines
+TextHighlightColor:  red        // ?
+BlueTextLinkColor:   #FF00FF
+LinkerPartialLinkColor:      grey(0.8)         // Linker tree views (?)
+DisabledTextColor:           arrow
+DisabledTextDropShadowColor: grey(0.8)
 
 //
 // Channel editor
 //
 
-ChannelEditorBackground:              0.00 0.00 0.00    // graph bg
-ChannelEditorBGOutsideGlobal:         grey(0.1)         // graph bg outside global time range
-ChannelEditorBGOutsidePlayback:       0.093 0.086 0.051 // graph bg outside playback range
-ChannelEditorTimebar:                 1.00 1.00 1.00    // timebar to be xored on top
-ChannelEditorGridLine:                0.20 0.20 0.20    // graph grid line
-ChannelEditorGridDashedLine:          0.20 0.20 0.20    // graph grid dashed line
-ChannelEditorHandleDefault:           0.50 0.50 0.50    // handles
-ChannelEditorHandleSelected:          1.00 1.00 0.00    // selected handle
-ChannelEditorHandleLocated:           0.80 0.80 0.80    // handle on cursor hover
-ChannelEditorHandleSelectedLocated:   1.00 1.00 0.80    // selected handle on cursor hover
-ChannelEditorStatsText:               1.00 1.00 1.00    // stats text
-ChannelEditorTimeText:                edit_bg           // bottom time text
-ChannelEditorViewLock:                grey(0.5)
-ChannelListCircleOutline:             black
+ChannelEditorBackground:            0.00 0.00 0.00 // Graph bg
+ChannelEditorBGOutsideGlobal:       grey(0.1)      // Graph bg outside global time range
+ChannelEditorBGOutsidePlayback:     0.09 0.08 0.05 // Graph bg outside playback range
+ChannelEditorTimebar:               1.00 1.00 1.00 // Timebar to be xored on top
+ChannelEditorGridLine:              0.20 0.20 0.20 // Graph grid line
+ChannelEditorGridDashedLine:        0.20 0.20 0.20 // Graph grid dashed line
+ChannelEditorHandleDefault:         0.50 0.50 0.50 // handles
+ChannelEditorHandleSelected:        1.00 1.00 0.00 // selected handle
+ChannelEditorHandleLocated:         0.80 0.80 0.80 // Handle hover
+ChannelEditorHandleSelectedLocated: 1.00 1.00 0.80 // Sel handle on hover
+ChannelEditorStatsText:             1.00 1.00 1.00 // Stats text
+ChannelEditorTimeText:              edit_bg        // Bottom time text
+ChannelEditorViewLock:              grey(0.5)
+ChannelListCircleOutline:           black
 
 //
 // Channel colors
 //
 
-NormalColor:                          0.0 0.0 0.0       // not used
-LockedColor:                          hsv 200 0.3 0.3   // locked parameter
-PendingColor:                         hsv 60 0.5 0.5    // pending change
-IsKeyColor:                           hsv 120 0.5 0.55  // keyframe
-IsSoftKeyColor:                       #0D3D28           // constant() or raw() usually
-IsNotKeyColor:                        hsv 180 0.5 0.5   // not at a key (expression)
-DisabledColor:                        hsv 207 0.27 0.75 // disabled (muted) from dopesheet
-ChannelLockedColor:                   hsv 0 0 0.2       // has "lock()" expression
-OverrideColor:                        hsv 45 1 0.65     // parm overridden by chop
-OtherLanguageColor:                   #653D66           // expr lang differs from node lang
-AutoTakeExcluded:                     hsv 42 0.17 0.31  // exluded from take and auto-take is on
-
-NonDefAccentColor:                    unknown // accent for non-default parms
-ChanRefAccentColor:                   unknown // accent for !!ch()!! parms
-DisabledAccentColor:                  unknown // accents are this color when disabled
-
-ScopedBlend:                          0.9 0.25 0.97
-ScopedBaseColor:                      edit_bg
-ScopedColor:                          HSV 40 0.825 0.525
-ScopedBaseSelectedColor:              edit_bg
-ScopedSelectedColor:                  white
-#AutoScopedColor:                     hsv 40 0.125 0.375  // low sat. orange grey, matches orange selection hue
-AutoScopedColor:                      hsv 210 0.125 0.375 // low sat. blue grey, matches blue slider hue
-ScopedAlpha:                          1.0 1.0 1.0
+NormalColor:        0.0 0.0 0.0       // Unused
+LockedColor:        hsv 200 0.3 0.3   // Locked
+PendingColor:       hsv 60 0.5 0.5    // Pending change
+IsKeyColor:         hsv 120 0.5 0.55  // Keyframe
+IsSoftKeyColor:     #0D3D28           // Constant() or raw(), usually.
+IsNotKeyColor:      hsv 180 0.5 0.5   // Not at a key (expression)
+DisabledColor:      hsv 207 0.27 0.75 // disabled (muted) from dopesheet
+ChannelLockedColor: hsv 0 0 0.2       // has "lock()" expression
+OverrideColor:      hsv 45 1 0.65     // parm overridden by chop
+OtherLanguageColor: #653D66           // Expr lang differs from node lang
+AutoTakeExcluded:   hsv 42 0.17 0.31  // Exluded from take & autotake on
+
+NonDefAccentColor:       unknown // Accent for non-default parms
+ChanRefAccentColor:      unknown // Accent for !!ch()!! parms
+DisabledAccentColor:     unknown // Accents when disabled
+
+ScopedBlend:             0.9 0.25 0.97
+ScopedBaseColor:         edit_bg
+ScopedColor:             HSV 40 0.825 0.525
+ScopedBaseSelectedColor: edit_bg
+ScopedSelectedColor:     white
+#AutoScopedColor:        hsv 40 0.125 0.375
+AutoScopedColor:         hsv 210 0.125 0.375
+ScopedAlpha:             1.0 1.0 1.0
 
+//
 // Borders indicating associated handle
-InvisibleHandleColor:                 0.70 0.00 0.00 // invisible, probably XCF. See rows/columns in Torus SOP
-VisibleHandleColor:                   0.00 0.50 0.50 // visible e.g. xform handle
-ActiveHandleColor:                    selection_base // active
+//
+
+// Invisible, probably XCF. See rows/columns in Torus SOP.
+InvisibleHandleColor:                 0.70 0.00 0.00
+// Visible, e.g. xform handle.
+VisibleHandleColor:                   0.00 0.50 0.50
+ActiveHandleColor:                    selection_base
 
 //
 // Channels
 //
 
-// Red: ch. names ending x/r
-// Green: ch. names ending y/g
-// Blue: ch. names ending z/b
-// Other: ch. names ending other
-// ch. names ending r/g/b use first entry of each of their respective group
-// Each color group contains an arbitrary number of similar colors, uniquely
-// determined by their numeric suffix.  When determining the color of a
-// ch., its node and ch. name is used ex. point1/tx and point1/vx will reds of
-// different hues, which will differ from point2/tx and point2/vx
+// Red:   channel names ending x/r
+// Green: channel names ending y/g
+// Blue:  channel names ending z/b
+// Other: channel names ending other
+
+// Channel names ending in r/g/b use the first entry of each of their
+// respective group.
+
+// Each color group contains an arbitrary number of similar colors,
+// uniquely determined by their numeric suffix.
+
+// When determining the color of a channel its node and channel name is
+// used.
+
+// Example:  point1/tx and point1/vx will reds of different hues, which
+// will differ from point2/tx and point2/vx.
 
 ChannelColorRed1:                     0.8 0.15 0.15      // red
 ChannelColorRed2:                     hsv 320.0 1.0 0.75 // before red
@@ -141,9 +153,8 @@ FileDepRejectColor:                   1.0 0.0 0.0
 // Geometry groups
 //
 
-// These two colors specify opposite corners of a quadrilateral in HSV space.
-// Colors for geometry groups are sampled from this quad.
-
+// Two colors specify opposite corners of a quadrilateral in HSV space.
+// Geometry group colors are sampled from this quad.
 GeoGroupColor1:                       hsv 90.0 0.3 1.0
 GeoGroupColor2:                       hsv 330.0 0.6 1.0
 
@@ -173,7 +184,7 @@ AltKeyHilite:                         #AEC39D
 CmdKeyBorder:                         #9A7B49
 CmdKeyFill:                           #BBAB28
 CmdKeyHilite:                         #DBCB48
-HotkeyMgrUnboundActionFG:             shadow3 // may be unused
+HotkeyMgrUnboundActionFG:             shadow3 // Maybe unused
 
 //
 // Icons
@@ -182,7 +193,7 @@ HotkeyMgrUnboundActionFG:             shadow3 // may be unused
 IconBackgroundColor:                  black     // ?
 IconOutlineColor:                     grey(0.8) // ?
 IconShadowColor:                      black     // ?
-IconLocatedColor:                     unknown       // ?
+IconLocatedColor:                     unknown   // ?
 IconBrightness:                       1.0
 IconContrast:                         1
 IconGamma:                            1
@@ -196,27 +207,30 @@ ErrorFlag:                            0.9 0.1 0.1
 WarningFlag:                          1 1 0
 MessageFlag:                          0.8 0.8 0.8
 FilteredFlag:                         0.6 0.6 0.6
-NoSaveFlag:                           0.0 0.4 0.1 // used in the resource managers for .nosave files
+// Used in resource managers for .nosave files:
+NoSaveFlag:                           0.0 0.4 0.1
 
 //
-// Lists/spreadsheets
+// Lists (spreadsheets)
 //
 
+// Some variables also affect popups, e.g. "Download Color Schemes"
+
 ListShadow:                           bg_low
 ListHighlight:                        bg_low
 ListBorder:                           bg_low
-ListBG:                               bg_low        // bg in download color scheme window
+ListBG:                               bg_low
 ListEntry1:                           list_b
 ListEntry2:                           list_a
 ListEntrySelected:                    selection
-ListTitleShadow:                      grey(0.1)    // ??
-ListTitleHighlight:                   bg_low        // ss row number border
-ListTitleGradHi:                      bg_low        // ss row number bg
-ListTitleGradLow:                     bg_low        // ss row number bg
-ListTitleHighlightGradHi:             hsv 36 1 0.8 // ??
-ListTitleHighlightGradLow:            hsv 36 1 0.8 // ??
+ListTitleShadow:                      grey(0.1)    // ?
+ListTitleHighlight:                   bg_low       // Row number border
+ListTitleGradHi:                      bg_low       // Row number bg
+ListTitleGradLow:                     bg_low       // Row number bg
+ListTitleHighlightGradHi:             hsv 36 1 0.8 // ?
+ListTitleHighlightGradLow:            hsv 36 1 0.8 // ?
 ListTitleFilteredText:                selection_base
-ListText:                             text         // download color scheme popup, package browser
+ListText:                             text
 ListAltText:                          hsv 40 0.85 1.0
 ListHoverHighlight:                   grey(0.28)
 
@@ -224,17 +238,17 @@ ListTitleColor:                       edit_bg
 ListEntryColor:                       grey(0.62)
 ListEntrySelectedColor:               0.93 0.86 0.51
 
-SSBorder:                             selection_base             // divider line between spreadsheet columns
-SSOutline:                            drop_indicator   // drag And Drop Outline in spreadsheet
-SSHeader:                             grey(0.29)      // ??
-SSTableBackgroundColor:               0.9 0.9 0.9     // ??
-SSTableSelectedColor:                 0.125 0.125 1.0 // ??
-SSTableBackgroundColor:               0.9 0.9 0.9
+SSBorder:                             separator       // Column separator
+SSOutline:                            drop_indicator  // Drag/drop outline
+SSHeader:                             grey(0.29)      // ?
+SSTableBackgroundColor:               0.9   0.9   0.9     // ?
+SSTableSelectedColor:                 0.125 0.125 1.0 // ?
+SSTableBackgroundColor:               0.9   0.9   0.9
 SSTableSelectedColor:                 0.125 0.125 1.0
 
-// highlight Color Alpha in channel lister
-// R: displayed ch alpha
-// G: graph selected ch alpha
+// Highlight alpha in the channel lister.
+// R: displayed channel alpha
+// G: selected channel alpha
 SSHighlightAlpha:                     0.25 0.55 0.0
 
 //
@@ -250,10 +264,11 @@ MplayColorCorrectWarn:                1 1 0.75
 // Operator properties
 //
 
-// These colors are used to highlight the names of operators that have a given
+// Used to highlight names of operators that have a given
 // property (e.g. modified parameters).
-// Houdini's list mode tries to be smart and changes the value based on some
-// arbitrary thing (network BG color?). Anyway, leaving the value at one seems
+// Houdini's list mode tries to change value based on some
+// arbitrary thing (network BG?).
+// Anyway, leaving the value at one seems
 // to force it into a happy state.
 
 OpNameNoneColor:                      text
@@ -410,7 +425,7 @@ HyperTextLinkColor:                   1.0 0.0 1.0 // buttons that simulate a hyp
 // Parameter - Check
 //
 
-CheckBG:                              bg_low
+CheckBG:                              text_box
 CheckLocated:                         selection
 CheckColor:                           text
 
@@ -474,11 +489,11 @@ RampKeyFG:                            hl_high
 RampKeyDisabled:                      shadow2
 RampKeyHoverBG:                       hl_high
 RampKeyHoverFG:                       hl_low
-RampHandleBG:                         red
-RampHandleFG:                         red
-RampHandleHoverBG:                    red
+RampHandleBG:                         0.6 1 0.6
+RampHandleFG:                         0.6 1 0.6
+RampHandleHoverBG:                    0.6 1 0.6
 RampHandleHoverFG:                    white
-RampHandleLines:                      red
+RampHandleLines:                      0.6 1 0.6
 RampHandleLinesHover:                 white
 RampBoxSelect:                        grey(0.3)
 RampMarkerHover:                      hl_high
@@ -535,7 +550,7 @@ SliderThumbRangeArrow:                black
 // Parameter - Textbox
 //
 
-TextboxBG:                            text_box // python shell shadow
+TextboxBG:                            text_box     // Python shell shadow
 TextboxInvalidBG:                     0.95 0.56 0.56
 TextboxBorderPrimary:                 bg_high
 TextboxBorderHighlight:               bg_high
@@ -543,11 +558,11 @@ TextboxBorderStippled:                hl_high
 TextboxBorderKeyFocus:                hl_high
 TextboxError:                         error
 TextboxGutter:                        bg_high
-TextboxDropMarker:                    hsv 238 0.40 0.88
+TextboxDropMarker:                    hsv 240 0.40 0.88
 TextboxFeedbackFG:                    grey(0.5)
 TextboxFeedbackBG:                    unknown
 SelectedTextFG:                       white
-SelectedTextBG:                       text_highlight // selection, cursor
+SelectedTextBG:                       text_highlight // Selection/cursor
 SelectedTextDisabledBG:               grey(0.3)
 TextboxLineNumberBG:                  bg_low
 TextboxLineNumberFG:                  text
@@ -594,93 +609,93 @@ NetworkPerformanceWarn:               #B0A957
 // Playbar/dope sheet
 //
 
-PlayBarOnColor:                       0.05 0.8 0.05
-PlayBarOffColor:                      0.25 0.25 0.25
-PlayBarDisabledColor:                 0.4 0.4 0.4
-PlayBarResetColor:                    1 1 0.0
-PlayBarNoPending:                     0 0.8 0
-PlayBarNoPending2:                    0 1 0
-PlayBarPending:                       0.75 0.75 0.5
-PlayBarPending2:                      1 1 0.5
-PlayBarAudioWaveColor:                0.643 0.643 0.643
-PlayBarStartEndFieldBG:               edit_bg
-PlayBarGStartEndFieldBG:              grey(0.125)
-
-Bookmark1:                            0.11 0.62 0.47
-Bookmark2:                            0.85 0.37 0.10
-Bookmark3:                            0.46 0.44 0.70
-Bookmark4:                            0.91 0.16 0.54
-Bookmark5:                            0.40 0.65 0.12
-Bookmark6:                            0.90 0.67 0.10
-Bookmark7:                            0.65 0.46 0.11
-
-KeyframeButtonSelectedGradHi:         selection
-KeyframeButtonSelectedGradLow:        selection
-KeyframeButtonLocatedGradHi:          grey(0.405)
-KeyframeButtonLocatedGradLow:         grey(0.274)
-KeyframeButtonGradHi:                 grey(0.337)
-KeyframeButtonGradLow:                grey(0.228)
-KeyframeButtonCircleKey:              #7ED321
-KeyframeButtonCircleSomeKey:          #356600
-KeyframeButtonCircleNoKey:            hsv 180  0.5  0.5
-KeyframeButtonCirclePending:          hsv 39 1 0.9
-KeyframeButtonBorder:                 black
-KeyframeButtonCircleFillOpacity:      grey(0.2)
-
-DopeSheetCurrentFrameFG:              white
-DopeSheetCurrentFrameBG:              black
-DopeSheetHoverFrameFG:                white
-DopeSheetHoverFrameBG:                bg_low // playbar frame number on hover
-DopeSheetHoverLine:                   grey(0.5)
-DopeSheetSimulationPlaying:           0.59 0.48 0.7
-DopeSheetTimeline:                    grey(0.7)
-DopeSheetIndicatorLine:               black
-DopeSheetIndicatorClickLine:          white
-DopeSheetIndicatorOverLine:           grey(0.5)
-DopeSheetKnob:                        black
-DopeSheetKnobClick:                   white
-DopeSheetKnobOver:                    grey(0.5)
-DopeSheetKnobPendingKey:              selection_base
-DopeSheetKnobOutlier:                 1 1 0
-DopeSheetKeyActive:                   HSV 88 0.96 0.46
-DopeSheetKeyPartial2:                 HSV 88 1.0 0.3
-DopeSheetKeyPartial:                  HSV 88 0.96 0.7
-DopeSheetKeyPending:                  selection_base
-DopeSheetKeyMuted:                    grey(0.2)
-DopeSheetKeyOverlay:                  1 0 0
-DopeSheetKeyDisabled:                 0.5 0 1
-DopeSheetKeySelected:                 selection_highlight2
-DopeSheetKeySelectedBorder:           black
-DopeSheetKeySelecting:                1 0.45 0.25
-DopeSheetKeySelectingBorder:          red
-DopeSheetOutsideRangeBG:              grey(0.3)
-DopeSheetSimulation:                  hc_blue // Sim cache
-DopeSheetSimulationReset:             1 0.5 0
-DopeSheetTimelineAudio:               grey(0.5)
-DopeSheetTickLarge:                   grey(0.6)
-DopeSheetTickSmall:                   grey(0.45)
-DopeSheetTickSmallAudio:              grey(0.35)
-DopeSheetTickNumber:                  grey(0.5)
-DopeSheetRangeNumber:                 grey(0.3)
-DopeSheetRowDisabled:                 black
-DopeSheetRowLight:                    list_a
-DopeSheetRowDark:                     list_b
-DopeSheetColLine:                     black
-DopeSheetRowLine:                     black
-DopeSheetViewLock:                    grey(0.5)
-DopeSheetSelection:                   hsv 60 1.0 0.5
-DopeSheetSelectionHover:              hsv 60 0.5 0.9
-DopeSheetSelectionActive:             hsv 60 0.5 0.9
-DopeSheetSelectionInactive:           hsv 60 0.9 0.7
-DopeSheetSelectionInactiveLock:       1 0 1
-DopeSheetSelectionText:               black
-DopeSheetSelectionRemove:             1 0 0
-DopeSheetOverwriteHover:              1 0 0
-DopeSheetOverwriteActive:             0.7 0 0
-DopeSheetOverwriteInactive:           0.9 0 0
-DopeSheetSpreadSheetText:             black
-DopeSheetSpreadSheetDisabled:         0.07 0 0
-DopeSheetRippleTool:                  1 1 0
+PlayBarOnColor:                    0.05 0.8 0.05
+PlayBarOffColor:                   0.25 0.25 0.25
+PlayBarDisabledColor:              0.4 0.4 0.4
+PlayBarResetColor:                 1 1 0.0
+PlayBarNoPending:                  0 0.8 0
+PlayBarNoPending2:                 0 1 0
+PlayBarPending:                    0.75 0.75 0.5
+PlayBarPending2:                   1 1 0.5
+PlayBarAudioWaveColor:             0.643 0.643 0.643
+PlayBarStartEndFieldBG:            edit_bg
+PlayBarGStartEndFieldBG:           grey(0.125)
+
+Bookmark1:                         0.11 0.62 0.47
+Bookmark2:                         0.85 0.37 0.10
+Bookmark3:                         0.46 0.44 0.70
+Bookmark4:                         0.91 0.16 0.54
+Bookmark5:                         0.40 0.65 0.12
+Bookmark6:                         0.90 0.67 0.10
+Bookmark7:                         0.65 0.46 0.11
+
+KeyframeButtonSelectedGradHi:      selection
+KeyframeButtonSelectedGradLow:     selection
+KeyframeButtonLocatedGradHi:       grey(0.405)
+KeyframeButtonLocatedGradLow:      grey(0.274)
+KeyframeButtonGradHi:              grey(0.337)
+KeyframeButtonGradLow:             grey(0.228)
+KeyframeButtonCircleKey:           #7ED321
+KeyframeButtonCircleSomeKey:       #356600
+KeyframeButtonCircleNoKey:         hsv 180  0.5  0.5
+KeyframeButtonCirclePending:       hsv 39 1 0.9
+KeyframeButtonBorder:              black
+KeyframeButtonCircleFillOpacity:   grey(0.2)
+
+DopeSheetCurrentFrameFG:           white
+DopeSheetCurrentFrameBG:           black
+DopeSheetHoverFrameFG:             white
+DopeSheetHoverFrameBG:             bg_low // Playbar frame number on hover
+DopeSheetHoverLine:                grey(0.5)
+DopeSheetSimulationPlaying:        0.59 0.48 0.7
+DopeSheetTimeline:                 grey(0.7)
+DopeSheetIndicatorLine:            black
+DopeSheetIndicatorClickLine:       white
+DopeSheetIndicatorOverLine:        grey(0.5)
+DopeSheetKnob:                     black
+DopeSheetKnobClick:                white
+DopeSheetKnobOver:                 grey(0.5)
+DopeSheetKnobPendingKey:           selection_base
+DopeSheetKnobOutlier:              1 1 0
+DopeSheetKeyActive:                HSV 88 0.96 0.46
+DopeSheetKeyPartial2:              HSV 88 1.0 0.3
+DopeSheetKeyPartial:               HSV 88 0.96 0.7
+DopeSheetKeyPending:               selection_base
+DopeSheetKeyMuted:                 grey(0.2)
+DopeSheetKeyOverlay:               1 0 0
+DopeSheetKeyDisabled:              0.5 0 1
+DopeSheetKeySelected:              selection_highlight2
+DopeSheetKeySelectedBorder:        black
+DopeSheetKeySelecting:             1 0.45 0.25
+DopeSheetKeySelectingBorder:       red
+DopeSheetOutsideRangeBG:           grey(0.3)
+DopeSheetSimulation:               hc_blue // Sim cache
+DopeSheetSimulationReset:          1 0.5 0
+DopeSheetTimelineAudio:            grey(0.5)
+DopeSheetTickLarge:                grey(0.6)
+DopeSheetTickSmall:                grey(0.45)
+DopeSheetTickSmallAudio:           grey(0.35)
+DopeSheetTickNumber:               grey(0.5)
+DopeSheetRangeNumber:              grey(0.3)
+DopeSheetRowDisabled:              black
+DopeSheetRowLight:                 list_a
+DopeSheetRowDark:                  list_b
+DopeSheetColLine:                  black
+DopeSheetRowLine:                  black
+DopeSheetViewLock:                 grey(0.5)
+DopeSheetSelection:                hsv 60 1.0 0.5
+DopeSheetSelectionHover:           hsv 60 0.5 0.9
+DopeSheetSelectionActive:          hsv 60 0.5 0.9
+DopeSheetSelectionInactive:        hsv 60 0.9 0.7
+DopeSheetSelectionInactiveLock:    1 0 1
+DopeSheetSelectionText:            black
+DopeSheetSelectionRemove:          1 0 0
+DopeSheetOverwriteHover:           1 0 0
+DopeSheetOverwriteActive:          0.7 0 0
+DopeSheetOverwriteInactive:        0.9 0 0
+DopeSheetSpreadSheetText:          black
+DopeSheetSpreadSheetDisabled:      0.07 0 0
+DopeSheetRippleTool:               1 1 0
 
 //
 // Popups
diff --git a/config/hc_colors.inc b/config/hc_colors.inc
index a29f6cd..d5ccffa 100644
--- a/config/hc_colors.inc
+++ b/config/hc_colors.inc
@@ -1,103 +1,103 @@
-//

-// Basics

-//

-

-#define grey(g)              g g g

-#define black                grey(0.0)

-#define white                grey(1)

-

-#define red                  #FF0000

-#define green                0 1 0

-#define blue                 0 0 1

-#define cyan                 0 1 1

-#define yellow               1 1 0

-#define magenta              1 0 1

-#define hc_blue              #ffffff

-

-#define pink                 1 0.8 0.8

-#define mint                 0.8 1 0.8

-#define light_blue           0.5 0.5 1

-#define light_yellow         1 1 0.5

-#define orange               1 0.5 0

-#define brown                #8C6238

-

-//

-// New variables

-//

-

-#define bg_high              #1E2221

-#define bg_medium            #203030

-#define bg_low               #1E1E1F

-#define bg_graph             #1E2221

-

-#define button               #404040

-#define button_high          #202828

-#define button_low           bg_high

-

-#define hl_high              #c0a080

-#define hl_low               #205050

-#define hl_menu              #556666

-

-#define selection            #bbbbff

-#define border               #609090

-#define disabled             grey(0.2)

-

-#define bevel_high           grey(0.52)

-#define bevel_low            black

-

-#define separator            #203030

-#define error                1 0 0

-

-#define text                 #ACDAD5

-#define text_dim             grey(0.5)

-#define text_box             #304040

-#define text_highlight       hl_high

-

-#define arrow                #609090

-#define arrow_dim            text_dim

-

-#define graph_wire           hl_high

-#define graph_wire_unused    #ff0000

-#define graph_node_connector hl_high

-#define graph_node_current   hl_high

-#define graph_node_current_non_selected graph_node_current

-#define graph_node_selected  #c0c0c0

-#define graph_node_flag      #303030

-

-//

-// Old variables

-//

-

-#define dark_base            grey(0.179) // Textbox line number bg

-#define dark_grad_high       grey(0.1)

-#define dark_grad_low        #1F1F1F

-#define well                 grey(0.15)

-#define list_a               DRKBASE

-#define list_b               DRKGRADLOW

-#define toolbar              #1A0F00

-

-#define unknown              1 0 1

-

-#define dark                 grey(0.2)

-

-#define shadow1_5            grey(0.699)

-#define shadow2              grey(0.63)

-#define shadow3              grey(0.58)

-

-#define pressed_high         #4A5E73

-#define pressed_low          #768EA7

-

-#define selection_base       #FF9600

-#define selection_highlight2 1 0.9 0.5

-#define slection_shadow      1 0.4 0

-

-#define button_selection     selection_base

-#define button_press         selection_base

-#define base_alt             grey(0.151) // effect in spreadsheet

-

-#define empty                dark_base

-

-

-#define edit_bg              grey(0.075) // Textbox bg

-#define edit_bg_highlight    grey(0.18)  // Textbox border

-#define edit_bg_outline      grey(0.14)  // Textbox border

+//
+// Basics
+//
+
+#define grey(g)              g g g
+#define black                grey(0.0)
+#define white                grey(1)
+
+#define red                  #FF0000
+#define green                0 1 0
+#define blue                 0 0 1
+#define cyan                 0 1 1
+#define yellow               1 1 0
+#define magenta              1 0 1
+#define hc_blue              #ffffff
+
+#define pink                 1 0.8 0.8
+#define mint                 0.8 1 0.8
+#define light_blue           0.5 0.5 1
+#define light_yellow         1 1 0.5
+#define orange               1 0.5 0
+#define brown                #8C6238
+
+//
+// New variables
+//
+
+#define bg_high              #1E2221
+#define bg_medium            #203030
+#define bg_low               #1E1E1F
+#define bg_graph             #1E2221
+
+#define button               #404040
+#define button_high          #202828
+#define button_low           bg_high
+
+#define hl_high              #c0a080
+#define hl_low               #205050
+#define hl_menu              #556666
+
+#define selection            #bbbbff
+#define border               #609090
+#define disabled             grey(0.2)
+
+#define bevel_high           grey(0.52)
+#define bevel_low            black
+
+#define separator            #203030
+#define error                1 0 0
+
+#define text                 #ACDAD5
+#define text_dim             grey(0.5)
+#define text_box             #27302b
+#define text_highlight       hl_high
+
+#define arrow                #609090
+#define arrow_dim            text_dim
+
+#define graph_wire           hl_high
+#define graph_wire_unused    #ff0000
+#define graph_node_connector hl_high
+#define graph_node_current   hl_high
+#define graph_node_current_non_selected graph_node_current
+#define graph_node_selected  #c0c0c0
+#define graph_node_flag      #303030
+
+//
+// Old variables
+//
+
+#define dark_base            grey(0.179) // Textbox line number bg
+#define dark_grad_high       grey(0.1)
+#define dark_grad_low        #1F1F1F
+#define well                 grey(0.15)
+#define list_a               DRKBASE
+#define list_b               DRKGRADLOW
+#define toolbar              #1A0F00
+
+#define unknown              1 0 1
+
+#define dark                 grey(0.2)
+
+#define shadow1_5            grey(0.699)
+#define shadow2              grey(0.63)
+#define shadow3              grey(0.58)
+
+#define pressed_high         #4A5E73
+#define pressed_low          #768EA7
+
+#define selection_base       #FF9600
+#define selection_highlight2 1 0.9 0.5
+#define slection_shadow      1 0.4 0
+
+#define button_selection     selection_base
+#define button_press         selection_base
+#define base_alt             grey(0.151) // effect in spreadsheet
+
+#define empty                dark_base
+
+
+#define edit_bg              grey(0.075) // Textbox bg
+#define edit_bg_highlight    grey(0.18)  // Textbox border
+#define edit_bg_outline      grey(0.14)  // Textbox border
diff --git a/config/hcnetcursor_assets/hcnetcursor_10x1.png b/config/hcnetcursor_assets/hcnetcursor_10x1.png
new file mode 100644
index 0000000..80148d3
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_10x1.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_10x10.png b/config/hcnetcursor_assets/hcnetcursor_10x10.png
new file mode 100644
index 0000000..03089ea
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_10x10.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_10x11.png b/config/hcnetcursor_assets/hcnetcursor_10x11.png
new file mode 100644
index 0000000..56e3407
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_10x11.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_10x12.png b/config/hcnetcursor_assets/hcnetcursor_10x12.png
new file mode 100644
index 0000000..ad18510
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_10x12.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_10x2.png b/config/hcnetcursor_assets/hcnetcursor_10x2.png
new file mode 100644
index 0000000..85c257e
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_10x2.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_10x3.png b/config/hcnetcursor_assets/hcnetcursor_10x3.png
new file mode 100644
index 0000000..0992444
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_10x3.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_10x4.png b/config/hcnetcursor_assets/hcnetcursor_10x4.png
new file mode 100644
index 0000000..bfa9f1d
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_10x4.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_10x5.png b/config/hcnetcursor_assets/hcnetcursor_10x5.png
new file mode 100644
index 0000000..75ab68c
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_10x5.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_10x6.png b/config/hcnetcursor_assets/hcnetcursor_10x6.png
new file mode 100644
index 0000000..0a241da
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_10x6.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_10x7.png b/config/hcnetcursor_assets/hcnetcursor_10x7.png
new file mode 100644
index 0000000..14cf439
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_10x7.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_10x8.png b/config/hcnetcursor_assets/hcnetcursor_10x8.png
new file mode 100644
index 0000000..ba0d06f
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_10x8.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_10x9.png b/config/hcnetcursor_assets/hcnetcursor_10x9.png
new file mode 100644
index 0000000..7a506a8
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_10x9.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_11x1.png b/config/hcnetcursor_assets/hcnetcursor_11x1.png
new file mode 100644
index 0000000..708ae0e
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_11x1.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_11x10.png b/config/hcnetcursor_assets/hcnetcursor_11x10.png
new file mode 100644
index 0000000..28dee78
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_11x10.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_11x11.png b/config/hcnetcursor_assets/hcnetcursor_11x11.png
new file mode 100644
index 0000000..d6ca30e
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_11x11.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_11x12.png b/config/hcnetcursor_assets/hcnetcursor_11x12.png
new file mode 100644
index 0000000..d437b06
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_11x12.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_11x2.png b/config/hcnetcursor_assets/hcnetcursor_11x2.png
new file mode 100644
index 0000000..38ecbeb
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_11x2.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_11x3.png b/config/hcnetcursor_assets/hcnetcursor_11x3.png
new file mode 100644
index 0000000..0c729ea
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_11x3.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_11x4.png b/config/hcnetcursor_assets/hcnetcursor_11x4.png
new file mode 100644
index 0000000..d15c2f4
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_11x4.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_11x5.png b/config/hcnetcursor_assets/hcnetcursor_11x5.png
new file mode 100644
index 0000000..1bd2173
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_11x5.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_11x6.png b/config/hcnetcursor_assets/hcnetcursor_11x6.png
new file mode 100644
index 0000000..72d7bcf
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_11x6.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_11x7.png b/config/hcnetcursor_assets/hcnetcursor_11x7.png
new file mode 100644
index 0000000..523dd46
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_11x7.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_11x8.png b/config/hcnetcursor_assets/hcnetcursor_11x8.png
new file mode 100644
index 0000000..74c6b9a
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_11x8.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_11x9.png b/config/hcnetcursor_assets/hcnetcursor_11x9.png
new file mode 100644
index 0000000..21ee4f9
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_11x9.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_12x1.png b/config/hcnetcursor_assets/hcnetcursor_12x1.png
new file mode 100644
index 0000000..f704803
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_12x1.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_12x10.png b/config/hcnetcursor_assets/hcnetcursor_12x10.png
new file mode 100644
index 0000000..d632a89
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_12x10.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_12x11.png b/config/hcnetcursor_assets/hcnetcursor_12x11.png
new file mode 100644
index 0000000..5add66b
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_12x11.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_12x12.png b/config/hcnetcursor_assets/hcnetcursor_12x12.png
new file mode 100644
index 0000000..b29ce39
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_12x12.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_12x2.png b/config/hcnetcursor_assets/hcnetcursor_12x2.png
new file mode 100644
index 0000000..08868c4
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_12x2.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_12x3.png b/config/hcnetcursor_assets/hcnetcursor_12x3.png
new file mode 100644
index 0000000..7bd9ddc
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_12x3.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_12x4.png b/config/hcnetcursor_assets/hcnetcursor_12x4.png
new file mode 100644
index 0000000..f678392
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_12x4.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_12x5.png b/config/hcnetcursor_assets/hcnetcursor_12x5.png
new file mode 100644
index 0000000..0b2d7d9
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_12x5.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_12x6.png b/config/hcnetcursor_assets/hcnetcursor_12x6.png
new file mode 100644
index 0000000..d621798
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_12x6.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_12x7.png b/config/hcnetcursor_assets/hcnetcursor_12x7.png
new file mode 100644
index 0000000..4bbcdb3
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_12x7.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_12x8.png b/config/hcnetcursor_assets/hcnetcursor_12x8.png
new file mode 100644
index 0000000..ca50fb9
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_12x8.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_12x9.png b/config/hcnetcursor_assets/hcnetcursor_12x9.png
new file mode 100644
index 0000000..332138c
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_12x9.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_1x1.png b/config/hcnetcursor_assets/hcnetcursor_1x1.png
new file mode 100644
index 0000000..6a90f95
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_1x1.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_1x10.png b/config/hcnetcursor_assets/hcnetcursor_1x10.png
new file mode 100644
index 0000000..96097dd
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_1x10.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_1x11.png b/config/hcnetcursor_assets/hcnetcursor_1x11.png
new file mode 100644
index 0000000..bc7f7fb
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_1x11.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_1x12.png b/config/hcnetcursor_assets/hcnetcursor_1x12.png
new file mode 100644
index 0000000..be02c85
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_1x12.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_1x2.png b/config/hcnetcursor_assets/hcnetcursor_1x2.png
new file mode 100644
index 0000000..5c64bcc
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_1x2.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_1x3.png b/config/hcnetcursor_assets/hcnetcursor_1x3.png
new file mode 100644
index 0000000..46e8f0b
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_1x3.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_1x4.png b/config/hcnetcursor_assets/hcnetcursor_1x4.png
new file mode 100644
index 0000000..74ff20c
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_1x4.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_1x5.png b/config/hcnetcursor_assets/hcnetcursor_1x5.png
new file mode 100644
index 0000000..43de33a
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_1x5.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_1x6.png b/config/hcnetcursor_assets/hcnetcursor_1x6.png
new file mode 100644
index 0000000..827cf82
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_1x6.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_1x7.png b/config/hcnetcursor_assets/hcnetcursor_1x7.png
new file mode 100644
index 0000000..2358d12
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_1x7.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_1x8.png b/config/hcnetcursor_assets/hcnetcursor_1x8.png
new file mode 100644
index 0000000..57b4dfb
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_1x8.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_1x9.png b/config/hcnetcursor_assets/hcnetcursor_1x9.png
new file mode 100644
index 0000000..dfbe00b
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_1x9.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_2x1.png b/config/hcnetcursor_assets/hcnetcursor_2x1.png
new file mode 100644
index 0000000..85432ca
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_2x1.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_2x10.png b/config/hcnetcursor_assets/hcnetcursor_2x10.png
new file mode 100644
index 0000000..9cecd47
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_2x10.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_2x11.png b/config/hcnetcursor_assets/hcnetcursor_2x11.png
new file mode 100644
index 0000000..77b950d
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_2x11.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_2x12.png b/config/hcnetcursor_assets/hcnetcursor_2x12.png
new file mode 100644
index 0000000..355a9a1
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_2x12.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_2x2.png b/config/hcnetcursor_assets/hcnetcursor_2x2.png
new file mode 100644
index 0000000..3f92ac5
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_2x2.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_2x3.png b/config/hcnetcursor_assets/hcnetcursor_2x3.png
new file mode 100644
index 0000000..bbd4cb5
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_2x3.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_2x4.png b/config/hcnetcursor_assets/hcnetcursor_2x4.png
new file mode 100644
index 0000000..c1f23f6
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_2x4.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_2x5.png b/config/hcnetcursor_assets/hcnetcursor_2x5.png
new file mode 100644
index 0000000..15cc0c6
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_2x5.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_2x6.png b/config/hcnetcursor_assets/hcnetcursor_2x6.png
new file mode 100644
index 0000000..2b366b0
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_2x6.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_2x7.png b/config/hcnetcursor_assets/hcnetcursor_2x7.png
new file mode 100644
index 0000000..54e872b
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_2x7.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_2x8.png b/config/hcnetcursor_assets/hcnetcursor_2x8.png
new file mode 100644
index 0000000..dd03913
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_2x8.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_2x9.png b/config/hcnetcursor_assets/hcnetcursor_2x9.png
new file mode 100644
index 0000000..9a80c36
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_2x9.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_3x1.png b/config/hcnetcursor_assets/hcnetcursor_3x1.png
new file mode 100644
index 0000000..e981767
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_3x1.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_3x10.png b/config/hcnetcursor_assets/hcnetcursor_3x10.png
new file mode 100644
index 0000000..fca83a8
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_3x10.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_3x11.png b/config/hcnetcursor_assets/hcnetcursor_3x11.png
new file mode 100644
index 0000000..8b1db3d
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_3x11.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_3x12.png b/config/hcnetcursor_assets/hcnetcursor_3x12.png
new file mode 100644
index 0000000..071fd94
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_3x12.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_3x2.png b/config/hcnetcursor_assets/hcnetcursor_3x2.png
new file mode 100644
index 0000000..a744007
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_3x2.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_3x3.png b/config/hcnetcursor_assets/hcnetcursor_3x3.png
new file mode 100644
index 0000000..592ec22
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_3x3.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_3x4.png b/config/hcnetcursor_assets/hcnetcursor_3x4.png
new file mode 100644
index 0000000..99f91b0
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_3x4.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_3x5.png b/config/hcnetcursor_assets/hcnetcursor_3x5.png
new file mode 100644
index 0000000..a737e81
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_3x5.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_3x6.png b/config/hcnetcursor_assets/hcnetcursor_3x6.png
new file mode 100644
index 0000000..d9af65b
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_3x6.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_3x7.png b/config/hcnetcursor_assets/hcnetcursor_3x7.png
new file mode 100644
index 0000000..7a9e27a
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_3x7.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_3x8.png b/config/hcnetcursor_assets/hcnetcursor_3x8.png
new file mode 100644
index 0000000..6747c57
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_3x8.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_3x9.png b/config/hcnetcursor_assets/hcnetcursor_3x9.png
new file mode 100644
index 0000000..41a9582
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_3x9.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_4x1.png b/config/hcnetcursor_assets/hcnetcursor_4x1.png
new file mode 100644
index 0000000..3335b6b
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_4x1.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_4x10.png b/config/hcnetcursor_assets/hcnetcursor_4x10.png
new file mode 100644
index 0000000..a98931d
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_4x10.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_4x11.png b/config/hcnetcursor_assets/hcnetcursor_4x11.png
new file mode 100644
index 0000000..6a5bc79
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_4x11.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_4x12.png b/config/hcnetcursor_assets/hcnetcursor_4x12.png
new file mode 100644
index 0000000..0f3cf02
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_4x12.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_4x2.png b/config/hcnetcursor_assets/hcnetcursor_4x2.png
new file mode 100644
index 0000000..f9b086f
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_4x2.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_4x3.png b/config/hcnetcursor_assets/hcnetcursor_4x3.png
new file mode 100644
index 0000000..7d4723f
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_4x3.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_4x4.png b/config/hcnetcursor_assets/hcnetcursor_4x4.png
new file mode 100644
index 0000000..8731417
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_4x4.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_4x5.png b/config/hcnetcursor_assets/hcnetcursor_4x5.png
new file mode 100644
index 0000000..61f923d
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_4x5.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_4x6.png b/config/hcnetcursor_assets/hcnetcursor_4x6.png
new file mode 100644
index 0000000..2989737
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_4x6.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_4x7.png b/config/hcnetcursor_assets/hcnetcursor_4x7.png
new file mode 100644
index 0000000..022ef2e
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_4x7.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_4x8.png b/config/hcnetcursor_assets/hcnetcursor_4x8.png
new file mode 100644
index 0000000..20585a6
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_4x8.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_4x9.png b/config/hcnetcursor_assets/hcnetcursor_4x9.png
new file mode 100644
index 0000000..f6bd879
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_4x9.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_5x1.png b/config/hcnetcursor_assets/hcnetcursor_5x1.png
new file mode 100644
index 0000000..f71014e
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_5x1.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_5x10.png b/config/hcnetcursor_assets/hcnetcursor_5x10.png
new file mode 100644
index 0000000..0aa7910
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_5x10.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_5x11.png b/config/hcnetcursor_assets/hcnetcursor_5x11.png
new file mode 100644
index 0000000..a2cbb3d
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_5x11.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_5x12.png b/config/hcnetcursor_assets/hcnetcursor_5x12.png
new file mode 100644
index 0000000..ddda85e
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_5x12.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_5x2.png b/config/hcnetcursor_assets/hcnetcursor_5x2.png
new file mode 100644
index 0000000..7de2a1f
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_5x2.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_5x3.png b/config/hcnetcursor_assets/hcnetcursor_5x3.png
new file mode 100644
index 0000000..6308826
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_5x3.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_5x4.png b/config/hcnetcursor_assets/hcnetcursor_5x4.png
new file mode 100644
index 0000000..96b90f0
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_5x4.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_5x5.png b/config/hcnetcursor_assets/hcnetcursor_5x5.png
new file mode 100644
index 0000000..dbffaa4
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_5x5.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_5x6.png b/config/hcnetcursor_assets/hcnetcursor_5x6.png
new file mode 100644
index 0000000..04de009
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_5x6.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_5x7.png b/config/hcnetcursor_assets/hcnetcursor_5x7.png
new file mode 100644
index 0000000..3c0bbc2
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_5x7.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_5x8.png b/config/hcnetcursor_assets/hcnetcursor_5x8.png
new file mode 100644
index 0000000..f05354b
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_5x8.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_5x9.png b/config/hcnetcursor_assets/hcnetcursor_5x9.png
new file mode 100644
index 0000000..44891b2
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_5x9.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_6x1.png b/config/hcnetcursor_assets/hcnetcursor_6x1.png
new file mode 100644
index 0000000..85313ee
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_6x1.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_6x10.png b/config/hcnetcursor_assets/hcnetcursor_6x10.png
new file mode 100644
index 0000000..6f1790c
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_6x10.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_6x11.png b/config/hcnetcursor_assets/hcnetcursor_6x11.png
new file mode 100644
index 0000000..38756c9
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_6x11.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_6x12.png b/config/hcnetcursor_assets/hcnetcursor_6x12.png
new file mode 100644
index 0000000..808905d
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_6x12.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_6x2.png b/config/hcnetcursor_assets/hcnetcursor_6x2.png
new file mode 100644
index 0000000..1929584
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_6x2.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_6x3.png b/config/hcnetcursor_assets/hcnetcursor_6x3.png
new file mode 100644
index 0000000..5bea3c7
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_6x3.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_6x4.png b/config/hcnetcursor_assets/hcnetcursor_6x4.png
new file mode 100644
index 0000000..6cda597
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_6x4.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_6x5.png b/config/hcnetcursor_assets/hcnetcursor_6x5.png
new file mode 100644
index 0000000..ec82922
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_6x5.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_6x6.png b/config/hcnetcursor_assets/hcnetcursor_6x6.png
new file mode 100644
index 0000000..a88c7dd
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_6x6.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_6x7.png b/config/hcnetcursor_assets/hcnetcursor_6x7.png
new file mode 100644
index 0000000..85e5777
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_6x7.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_6x8.png b/config/hcnetcursor_assets/hcnetcursor_6x8.png
new file mode 100644
index 0000000..a276f84
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_6x8.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_6x9.png b/config/hcnetcursor_assets/hcnetcursor_6x9.png
new file mode 100644
index 0000000..b5594fe
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_6x9.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_7x1.png b/config/hcnetcursor_assets/hcnetcursor_7x1.png
new file mode 100644
index 0000000..2b45822
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_7x1.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_7x10.png b/config/hcnetcursor_assets/hcnetcursor_7x10.png
new file mode 100644
index 0000000..d32ca6b
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_7x10.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_7x11.png b/config/hcnetcursor_assets/hcnetcursor_7x11.png
new file mode 100644
index 0000000..f7a91d3
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_7x11.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_7x12.png b/config/hcnetcursor_assets/hcnetcursor_7x12.png
new file mode 100644
index 0000000..e5d1364
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_7x12.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_7x2.png b/config/hcnetcursor_assets/hcnetcursor_7x2.png
new file mode 100644
index 0000000..4467645
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_7x2.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_7x3.png b/config/hcnetcursor_assets/hcnetcursor_7x3.png
new file mode 100644
index 0000000..07fa672
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_7x3.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_7x4.png b/config/hcnetcursor_assets/hcnetcursor_7x4.png
new file mode 100644
index 0000000..e531370
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_7x4.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_7x5.png b/config/hcnetcursor_assets/hcnetcursor_7x5.png
new file mode 100644
index 0000000..b6c6c81
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_7x5.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_7x6.png b/config/hcnetcursor_assets/hcnetcursor_7x6.png
new file mode 100644
index 0000000..faa1ff4
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_7x6.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_7x7.png b/config/hcnetcursor_assets/hcnetcursor_7x7.png
new file mode 100644
index 0000000..a8edcb5
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_7x7.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_7x8.png b/config/hcnetcursor_assets/hcnetcursor_7x8.png
new file mode 100644
index 0000000..73fb9d4
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_7x8.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_7x9.png b/config/hcnetcursor_assets/hcnetcursor_7x9.png
new file mode 100644
index 0000000..bcb3c1e
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_7x9.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_8x1.png b/config/hcnetcursor_assets/hcnetcursor_8x1.png
new file mode 100644
index 0000000..1a9bd78
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_8x1.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_8x10.png b/config/hcnetcursor_assets/hcnetcursor_8x10.png
new file mode 100644
index 0000000..f292faf
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_8x10.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_8x11.png b/config/hcnetcursor_assets/hcnetcursor_8x11.png
new file mode 100644
index 0000000..9f52977
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_8x11.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_8x12.png b/config/hcnetcursor_assets/hcnetcursor_8x12.png
new file mode 100644
index 0000000..5783df3
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_8x12.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_8x2.png b/config/hcnetcursor_assets/hcnetcursor_8x2.png
new file mode 100644
index 0000000..8983603
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_8x2.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_8x3.png b/config/hcnetcursor_assets/hcnetcursor_8x3.png
new file mode 100644
index 0000000..268d08c
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_8x3.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_8x4.png b/config/hcnetcursor_assets/hcnetcursor_8x4.png
new file mode 100644
index 0000000..20ce809
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_8x4.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_8x5.png b/config/hcnetcursor_assets/hcnetcursor_8x5.png
new file mode 100644
index 0000000..5816095
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_8x5.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_8x6.png b/config/hcnetcursor_assets/hcnetcursor_8x6.png
new file mode 100644
index 0000000..6b1184e
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_8x6.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_8x7.png b/config/hcnetcursor_assets/hcnetcursor_8x7.png
new file mode 100644
index 0000000..03eb669
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_8x7.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_8x8.png b/config/hcnetcursor_assets/hcnetcursor_8x8.png
new file mode 100644
index 0000000..2042f43
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_8x8.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_8x9.png b/config/hcnetcursor_assets/hcnetcursor_8x9.png
new file mode 100644
index 0000000..23cf7b8
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_8x9.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_9x1.png b/config/hcnetcursor_assets/hcnetcursor_9x1.png
new file mode 100644
index 0000000..92ce1e5
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_9x1.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_9x10.png b/config/hcnetcursor_assets/hcnetcursor_9x10.png
new file mode 100644
index 0000000..0825986
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_9x10.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_9x11.png b/config/hcnetcursor_assets/hcnetcursor_9x11.png
new file mode 100644
index 0000000..7f00afc
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_9x11.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_9x12.png b/config/hcnetcursor_assets/hcnetcursor_9x12.png
new file mode 100644
index 0000000..f16bd0a
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_9x12.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_9x2.png b/config/hcnetcursor_assets/hcnetcursor_9x2.png
new file mode 100644
index 0000000..3c0831a
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_9x2.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_9x3.png b/config/hcnetcursor_assets/hcnetcursor_9x3.png
new file mode 100644
index 0000000..0c5b766
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_9x3.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_9x4.png b/config/hcnetcursor_assets/hcnetcursor_9x4.png
new file mode 100644
index 0000000..b765d6b
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_9x4.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_9x5.png b/config/hcnetcursor_assets/hcnetcursor_9x5.png
new file mode 100644
index 0000000..a3e2537
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_9x5.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_9x6.png b/config/hcnetcursor_assets/hcnetcursor_9x6.png
new file mode 100644
index 0000000..0cb5cd2
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_9x6.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_9x7.png b/config/hcnetcursor_assets/hcnetcursor_9x7.png
new file mode 100644
index 0000000..4b89cd7
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_9x7.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_9x8.png b/config/hcnetcursor_assets/hcnetcursor_9x8.png
new file mode 100644
index 0000000..7d4b75d
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_9x8.png differ
diff --git a/config/hcnetcursor_assets/hcnetcursor_9x9.png b/config/hcnetcursor_assets/hcnetcursor_9x9.png
new file mode 100644
index 0000000..0462859
Binary files /dev/null and b/config/hcnetcursor_assets/hcnetcursor_9x9.png differ
diff --git a/desktop/hc_attached.desk b/desktop/hc_attached.desk
index dd6676b..13f4697 100644
--- a/desktop/hc_attached.desk
+++ b/desktop/hc_attached.desk
@@ -4,7 +4,7 @@ playbar      -1
 shelf        0
 menubar      -1
 statusbar    -1
-radial menu set sculpt
+radial menu set main
 root {
     split 0.621347 H {
         split 0.274328 V R {
@@ -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":"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
+networkeditor -p '{\n	"backgroundimagebrightness":"1.0",\n	"gridmode":"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	"showgrid":"0",\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/desktop/hc_detached.desk b/desktop/hc_detached.desk
index e0ed864..3823e8e 100644
--- a/desktop/hc_detached.desk
+++ b/desktop/hc_detached.desk
@@ -4,11 +4,49 @@ playbar      -1
 shelf        0
 menubar      -1
 statusbar    -1
+radial menu set main
 root {
     tabs 0 0 {
-        pane panetab0 -a 1 -l 10 -H /obj/ -T OBJ -m sceneview
+        pane panetab0 -a 1 -l 10 -H /obj/geo1/ -T SOP -m sceneview
+    }
+
+    panel panel1 -w 1938 54 1884 2272 {
+        tabs 0 2 {
+            pane parmeditor -a 1 -l 10 -H /obj/geo1/ -T SOP -m parmeditor
+        }
+    }
+
+    panel panel2 -w 18 1217 1884 1109 {
+        tabs 0 2 {
+            pane neteditor -a 0 -l 0 -H /obj/geo1/ -T SOP -m neteditor
+        }
     }
 }
+shelfdock -o 0 -l icon_and_text
 viewerstow -x stow -b stow -l stow -m hide -c hide -d stow ${DESKNAME}.panetab0.world
 vieweroption -a 2 ${DESKNAME}.panetab0.world
 viewlayout -s 2 ${DESKNAME}.panetab0.world
+viewtype -t ortho_right ${DESKNAME}.panetab0.world.`vpname("${DESKNAME}.panetab0.world",4)`
+viewtype -t ortho_front ${DESKNAME}.panetab0.world.`vpname("${DESKNAME}.panetab0.world",3)`
+viewtype -t ortho_top ${DESKNAME}.panetab0.world.`vpname("${DESKNAME}.panetab0.world",1)`
+viewtype -t perspective ${DESKNAME}.panetab0.world.`vpname("${DESKNAME}.panetab0.world",2)`
+parmeditor -T 0.25 -w 0 parmeditor
+networkeditor -p '{\n	"backgroundimagebrightness":"1.0",\n	"gridmode":"2",\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":"300 300",\n	"showtypesidebar":"0",\n	"showtree":"0",\n	"overviewbounds":"0.000000 0.000000 565.200000 312.300000",\n	"taskgraphperfstatname":"Cook Time",\n	"gridystep":"1.0",\n	"taskgraphsortcriteria":"Automatic",\n	"workitemattrtocollapseby":"",\n	"grouplistsplit":"0.75"\n}\n' neteditor
+netcolumns -c display,xray,select,origin,capture,bone -n OBJ neteditor
+netcolumns -c display,render,template,selectabletemplate,highlight,unload,bypass,lock -n SOP neteditor
+netcolumns -c delscript,comment -n CHOPNET neteditor
+netcolumns -c display,export,audio,bypass,lock -n CHOP neteditor
+netcolumns -c startrender,bypass,lock -n ROP neteditor
+netcolumns -c delscript,comment -n SHOP neteditor
+netcolumns -c display,render,template,bypass -n COP2 neteditor
+netcolumns -c delscript,comment -n IMG neteditor
+netcolumns -c delscript,comment,input -n VOP neteditor
+netcolumns -c delscript,comment -n VOPNET neteditor
+netcolumns -c delscript,comment -n DOP neteditor
+netcolumns -c delscript,comment -n TOP neteditor
+netcolumns -c delscript,comment -n TOPNET neteditor
+netcolumns -c display,bypass,delscript,comment,input -n LOP neteditor
+netcolumns -c display,export,template,selectabletemplate,bypass -n COP neteditor
+netcolumns -c delscript,comment -n MGR neteditor
+netcolumns -c delscript,comment -n DIR neteditor
+netcolumns -c delscript,comment -n DATA neteditor
diff --git a/hc_hotkeys.json b/hc_hotkeys.json
index 21409b2..c5e7d4a 100644
--- a/hc_hotkeys.json
+++ b/hc_hotkeys.json
@@ -1,76 +1,106 @@
 {
     "assignments": {
-        "h.hc_panel":                             "alt+shift+d",
-        "h.hc_session_keycam":                    "shift+k",
-        "h.hc_session_rotatesplit":               "alt+tab",
-        "h.hc_session_opensettings":              "alt+,",
-        "h.hc_session_toggletabs":                "alt+t",
-        "h.hc_session_newtabdialog":              "alt+shift+t",
-        "h.hc_session_togglemainmenu":            "alt+m",
+        "h.hc_panel": "alt+d",
+        "h.hc_session_keycam": "shift+k",
+        "h.hc_session_rotatesplit": "alt+tab",
+        "h.hc_session_opensettings": "alt+,",
+        "h.hc_session_toggletabs": "alt+t",
+        "h.hc_session_newtabdialog": "alt+shift+t",
+        "h.hc_session_togglemainmenu": "alt+m",
+        "h.hc_session_togglestatusbar": "alt+b",
         "h.pane.gview.state.obj.keycam.trans_up": "shift+k",
-        "h.pane.nexttab":                         "ctrl+tab",
-        "h.pane.prevtab":                         "ctrl+shift+tab",
-        "h.pane.gview.refplane":                  "shift+g",
+        "h.pane.gview.state.obj.keycam.cycle_draw_mode": "ctrl+d",
+        "h.pane.gview.state.obj.keycam.cycle_layout": "ctrl+l",
+        "h.pane.gview.state.obj.keycam.view_top": "1",
+        "h.pane.gview.state.obj.keycam.view_bottom": "2",
+        "h.pane.gview.state.obj.keycam.view_front": "3",
+        "h.pane.gview.state.obj.keycam.view_back": "4",
+        "h.pane.gview.state.obj.keycam.view_left": "5",
+        "h.pane.gview.state.obj.keycam.view_right": "6",
+        "h.pane.nexttab": "ctrl+tab",
+        "h.pane.prevtab": "ctrl+shift+tab",
+        "h.pane.gview.refplane": "g",
+        "h.pane.gview.hc_toggle_group_list": "shift+g",
+        "h.pane.gview.hc_toggle_point_numbers": "ctrl+p",
+        "h.pane.gview.hc_toggle_prim_numbers": "shift+p",
         "h.pane.gview.state.view.home_selection": "shift+s",
-        "h.pane.parms.edit_expression":           "alt+e",
-        "h.pane.editparms.selectall":             "alt+h",
-        "h.pane.wsheet.hc_nav_up":                "ctrl+k",
-        "h.pane.wsheet.hc_nav_down":              "ctrl+j",
-        "h.pane.wsheet.hc_nav_left":              "ctrl+h",
-        "h.pane.wsheet.hc_nav_right":             "ctrl+l",
-        "h.pane.wsheet.hc_select_up":             "ctrl+shift+k",
-        "h.pane.wsheet.hc_select_down":           "ctrl+shift+j",
-        "h.pane.wsheet.hc_select_left":           "ctrl+shift+h",
-        "h.pane.wsheet.hc_select_right":          "ctrl+shift+l",
-        "h.pane.wsheet.hc_move_up":               "alt+k",
-        "h.pane.wsheet.hc_move_down":             "alt+j",
-        "h.pane.wsheet.hc_move_left":             "alt+h",
-        "h.pane.wsheet.hc_move_right":            "alt+l",
-        "h.pane.wsheet.hc_view_up":               "k",
-        "h.pane.wsheet.hc_view_down":             "j",
-        "h.pane.wsheet.hc_view_left":             "h",
-        "h.pane.wsheet.hc_view_right":            "l",
-        "h.pane.wsheet.hc_zoom_in":               "=",
-        "h.pane.wsheet.hc_zoom_out":              "-",
-        "h.pane.wsheet.hc_toggle_grid":           "shift+g",
-        "h.pane.wsheet.hc_select_all":            "ctrl+a",
-        "h.pane.wsheet.hc_deselect_all":          "ctrl+d",
+        "h.pane.parms.hc_focus_next_parameter": "tab",
+        "h.pane.parms.hc_focus_prev_parameter": "shift+tab",
+        "h.pane.parms.edit_expression": "alt+e",
+        "h.pane.editparms.selectall": "alt+h",
+        "h.pane.wsheet.hc_nav_up": "k",
+        "h.pane.wsheet.hc_nav_down": "j",
+        "h.pane.wsheet.hc_nav_left": "h",
+        "h.pane.wsheet.hc_nav_right": "l",
+        "h.pane.wsheet.hc_select_up": "shift+k",
+        "h.pane.wsheet.hc_select_down": "shift+j",
+        "h.pane.wsheet.hc_select_left": "shift+h",
+        "h.pane.wsheet.hc_select_right": "shift+l",
+        "h.pane.wsheet.hc_move_up": "alt+k",
+        "h.pane.wsheet.hc_move_down": "alt+j",
+        "h.pane.wsheet.hc_move_left": "alt+h",
+        "h.pane.wsheet.hc_move_right": "alt+l",
+        "h.pane.wsheet.hc_move_descendants_up": "ctrl+alt+k",
+        "h.pane.wsheet.hc_move_descendants_down": "ctrl+alt+j",
+        "h.pane.wsheet.hc_move_descendants_left": "ctrl+alt+h",
+        "h.pane.wsheet.hc_move_descendants_right": "ctrl+alt+l",
+        "h.pane.wsheet.hc_move_ancestors_up": "shift+alt+k",
+        "h.pane.wsheet.hc_move_ancestors_down": "shift+alt+j",
+        "h.pane.wsheet.hc_move_ancestors_left": "shift+alt+h",
+        "h.pane.wsheet.hc_move_ancestors_right": "shift+alt+l",
+        "h.pane.wsheet.hc_view_up": "ctrl+k",
+        "h.pane.wsheet.hc_view_down": "ctrl+j",
+        "h.pane.wsheet.hc_view_left": "ctrl+h",
+        "h.pane.wsheet.hc_view_right": "ctrl+l",
+        "h.pane.wsheet.hc_zoom_in": "=",
+        "h.pane.wsheet.hc_zoom_out": "-",
+        "h.pane.wsheet.hc_reset_cursor_size": ";",
+        "h.pane.wsheet.hc_zz": "z",
+        "h.pane.wsheet.hc_paste": "ctrl+v",
+        "h.pane.gview.hc_frame_selection": "shift+f",
+        "h.pane.wsheet.hc_toggle_grid": "shift+g",
+        "h.pane.wsheet.hc_frame_all": "shift+f",
+        "h.pane.wsheet.hc_frame_component": "alt+f",
+        "h.pane.wsheet.hc_select_all": "ctrl+a",
+        "h.pane.wsheet.hc_deselect_all": "ctrl+d",
         "h.pane.wsheet.hc_select_next_component": "shift+]",
         "h.pane.wsheet.hc_select_prev_component": "shift+[",
-        "h.pane.wsheet.hc_toggle_bypass":         "q",
-        "h.pane.wsheet.hc_toggle_template":       "w",
-        "h.pane.wsheet.hc_set_display":           "e",
-        "h.pane.wsheet.drop_on_wire_mode":        "pad8",
-        "h.pane.wsheet.home_selected":            "shift+s",
-        "h.pane.wsheet.node_quick_nav":           "ctrl+f",
-        "h.pane.wsheet.rename_node":              "ctrl+r",
-        "h.pane.textport.up":                     "ctrl+p",
-        "h.pane.textport.down":                   "ctrl+n",
-        "h.pane.textport.next":                   "ctrl+f",
-        "h.pane.textport.prev":                   "ctrl+b",
-        "h.pane.textport.top":                    "ctrl+a",
-        "h.pane.textport.bottom":                 "ctrl+e",
-        "h.pane.textport.nextword":               "alt+f",
-        "h.pane.textport.pageup":                 "alt+v",
-        "h.pane.textport.pagedown":               "ctrl+v",
-        "h.pane.textport.firstline":              "ctrl+shift+,",
-        "h.pane.textport.lastline":               "ctrl+shift+.",
-        "h.pane.textport.jump_to_line":           "alt+g",
-        "h.pane.textport.selectup":               "ctrl+shift+p",
-        "h.pane.textport.selectdown":             "ctrl+shift+n",
-        "h.pane.textport.selectnext":             "ctrl+shift+f",
-        "h.pane.textport.selectprev":             "ctrl+shift+b",
-        "h.pane.textport.selectbottom":           "ctrl+shift+e",
-        "h.pane.textport.selecttop":              "ctrl+shift+a",
-        "h.pane.textport.selectnextword":         "alt+shift+f",
-        "h.pane.textport.toggle_comment":         "ctrl+/",
-        "inputfield.up":                          "ctrl+p",
-        "inputfield.down":                        "ctrl+n",
-        "inputfield.next":                        "ctrl+f",
-        "inputfield.prev":                        "ctrl+b",
-        "inputfield.next_word":                   "alt+f",
-        "inputfield.prev_word":                   "alt+b",
-        "inputfield.end":                         "ctrl+e",
-        "inputfield.home":                        "ctrl+a"
+        "h.pane.wsheet.hc_toggle_bypass": "q",
+        "h.pane.wsheet.hc_toggle_template": "w",
+        "h.pane.wsheet.hc_set_display": "e",
+        "h.pane.wsheet.drop_on_wire_mode": "pad8",
+        "h.pane.wsheet.home_selected": "shift+s",
+        "h.pane.wsheet.node_quick_nav": "ctrl+f",
+        "h.pane.wsheet.rename_node": "ctrl+r",
+        "h.pane.textport.up": "ctrl+p",
+        "h.pane.textport.down": "ctrl+n",
+        "h.pane.textport.next": "ctrl+f",
+        "h.pane.textport.prev": "ctrl+b",
+        "h.pane.textport.top": "ctrl+a",
+        "h.pane.textport.bottom": "ctrl+e",
+        "h.pane.textport.nextword": "alt+f",
+        "h.pane.textport.pageup": "alt+v",
+        "h.pane.textport.firstline": "ctrl+shift+,",
+        "h.pane.textport.lastline": "ctrl+shift+.",
+        "h.pane.textport.jump_to_line": "alt+g",
+        "h.pane.textport.selectup": "ctrl+shift+p",
+        "h.pane.textport.selectdown": "ctrl+shift+n",
+        "h.pane.textport.selectnext": "ctrl+shift+f",
+        "h.pane.textport.selectprev": "ctrl+shift+b",
+        "h.pane.textport.selectbottom": "ctrl+shift+e",
+        "h.pane.textport.selecttop": "ctrl+shift+a",
+        "h.pane.textport.selectnextword": "alt+shift+f",
+        "h.pane.textport.toggle_comment": "ctrl+/",
+        "inputfield.up": "ctrl+p",
+        "inputfield.down": "ctrl+n",
+        "inputfield.next": "ctrl+f",
+        "inputfield.prev": "ctrl+b",
+        "inputfield.next_word": "alt+f",
+        "inputfield.prev_word": "alt+b",
+        "inputfield.end": "ctrl+e",
+        "inputfield.home": "ctrl+a",
+        "h.hc_session_toggle_spreadsheet": "alt+p",
+        "h.pane.wsheet.hc_replace_node": "alt+r",
+        "h.pane.wsheet.hc_frame_cursor": "f"
     }
 }
diff --git a/hc_settings.json b/hc_settings.json
index aa46f05..3fc906f 100644
--- a/hc_settings.json
+++ b/hc_settings.json
@@ -1,11 +1,14 @@
 {
-    "desktop_mode": "detached",
+    "desktop_mode": "attached",
+    "startup": {
+        "default_autosave_state": false
+    },
     "keycam": {
         "guides": {
             "axis_size": 0.05,
             "bbox": 0,
             "cam_axis": 0,
-            "cam_geo": 1,
+            "cam_geo": 0,
             "perim": 0,
             "pivot_2d": 0,
             "pivot_3d": 0,
@@ -20,13 +23,20 @@
         },
         "units": {
             "delta_ow": 1,
-            "delta_r": 7.5,
+            "delta_r": 15.0,
             "delta_t": 0.2,
             "delta_z": 1
-        }
+        },
+        "drag_sensitivity": 0.25
     },
     "node_graph": {
         "node_shape": "rect",
-        "node_color": "#607070"
+        "node_color": "#607070",
+        "zoom_center": "hc_cursor",
+        "hcnetcursor": true,
+        "grid_x_step": 1.99,
+        "grid_y_step": 0.99,
+        "node_center_offset_x": 0.5,
+        "node_center_offset_y": 0.15
     }
 }
diff --git a/mcpserver/houdini_mcp.py b/mcpserver/houdini_mcp.py
deleted file mode 100644
index 3edae4a..0000000
--- a/mcpserver/houdini_mcp.py
+++ /dev/null
@@ -1,99 +0,0 @@
-#!/usr/bin/env python3
-import os
-import rpyc
-from mcp.server.fastmcp import FastMCP
-
-HOUDINI_HOST = os.environ.get("HOUDINI_MCP_HOST", "localhost")
-HOUDINI_PORT = int(os.environ.get("HOUDINI_MCP_PORT", "18811"))
-
-mcp = FastMCP("houdini")
-
-
-_REMOTE_RUNNER_SRC = r'''
-def _hcrun(code):
-    import io, contextlib, traceback, sys
-    buf = io.StringIO()
-    result_repr = None
-    error = None
-    main_ns = sys.modules["__main__"].__dict__
-    try:
-        import hou
-        if "hou" not in main_ns:
-            main_ns["hou"] = hou
-    except ImportError:
-        pass
-    try:
-        with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
-            try:
-                result = eval(compile(code, "<mcp>", "eval"), main_ns)
-                if result is not None:
-                    try:
-                        result_repr = repr(result)
-                    except Exception as _e:
-                        result_repr = "<unrepr-able: " + repr(_e) + ">"
-            except SyntaxError:
-                exec(compile(code, "<mcp>", "exec"), main_ns)
-    except Exception:
-        error = traceback.format_exc()
-    return buf.getvalue(), result_repr, error
-'''
-
-
[email protected]()
-def houdini_eval(code: str) -> str:
-    """Run Python code inside the running Houdini session.
-
-    The `hou` module and anything in __main__ is available. Captures stdout/stderr.
-    If the code is a single expression, its repr is returned too.
-    Requires Houdini to be running with hrpyc.start_server() called (happens
-    automatically in uiready.py for this project).
-    """
-    try:
-        conn = rpyc.classic.connect(HOUDINI_HOST, HOUDINI_PORT)
-    except ConnectionRefusedError:
-        return (
-            f"ERROR: Could not connect to Houdini at {HOUDINI_HOST}:{HOUDINI_PORT}. "
-            "Is Houdini running with hrpyc started?"
-        )
-
-    try:
-        conn.execute(_REMOTE_RUNNER_SRC)
-        stdout, result_repr, error = conn.namespace["_hcrun"](code)
-        stdout = str(stdout) if stdout else ""
-        result_repr = str(result_repr) if result_repr else None
-        error = str(error) if error else None
-    finally:
-        try:
-            conn.close()
-        except Exception:
-            pass
-
-    parts = []
-    if stdout:
-        parts.append(stdout.rstrip())
-    if result_repr is not None:
-        parts.append(f"=> {result_repr}")
-    if error:
-        parts.append(error.rstrip())
-    return "\n".join(parts) if parts else "(no output)"
-
-
[email protected]()
-def houdini_ping() -> str:
-    """Check whether the Houdini RPC server is reachable."""
-    try:
-        conn = rpyc.classic.connect(HOUDINI_HOST, HOUDINI_PORT)
-        try:
-            version = conn.modules.hou.applicationVersionString()
-            hip = conn.modules.hou.hipFile.path()
-            return f"OK — Houdini {version}, hip: {hip}"
-        finally:
-            conn.close()
-    except ConnectionRefusedError:
-        return f"Unreachable at {HOUDINI_HOST}:{HOUDINI_PORT}"
-    except Exception as e:
-        return f"Error: {e!r}"
-
-
-if __name__ == "__main__":
-    mcp.run()
diff --git a/mcpserver/requirements.txt b/mcpserver/requirements.txt
deleted file mode 100644
index 84b85ea..0000000
--- a/mcpserver/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-mcp>=1.0
-rpyc>=4.1,<5
diff --git a/notes/GEM production notes.md b/notes/GEM production notes.md
deleted file mode 100644
index 6aff1f1..0000000
--- a/notes/GEM production notes.md	
+++ /dev/null
@@ -1,18 +0,0 @@
-# Notes about the second working cast
-## June 2024
-
-### Scale
-I've tried to use a 1 mm:1 unit equivalancy. This scale is so much broader than any scale I've worked with before and the numbers become arbitrarily large. I think that I should use 1 centimeter:1 unit scale in the future.
-
-## GEM Mold Shell
-One of the most important parameters is the thickness range of the GEM Mold Shell operator. It is necessary to choose a thickness that is thick enough to print properly and maintain adequate rigidity while being thin enough to remove post-resin cure post-hydrocal cure.
-
-According to the original cast which had very good parameters for the GEM Mold Shell, the parameters were set to the following choices:
-```
-    Maximum Thickness..... 0.75 (mm/units)
-    Minumum Thickness..... 0.6  (mm/units)
-    Remesh Division Size.. 0.9
-    Ramp.................. Linear
-```
-## UV Curing
-Using my Peopoly Phenom curing box, I have basically been using the default settings from 3-6 minutes. I think it would be a good idea to do like 5 minutes of interval curing, if I can figure out how.
diff --git a/notes/GEM.md b/notes/GEM.md
deleted file mode 100644
index 31f8090..0000000
--- a/notes/GEM.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# GEM
-## General Export Methods - tools for getting things out of the computer
-
-# Operators
-
-## GEM Build Area
-
-## GEM Flange Monoid
-
-## GEM Halo Merge
-
-## GEM Mold Flange
-
-## GEM Mold Preprocess
-
-## GEM Mold Shell
-
-## GEM Mold Solver
-
-## GEM Mold Structure
-
-## GEM Orient
-
-## GEM Partition
diff --git a/notes/hou-developer.md b/notes/hou-developer.md
deleted file mode 100644
index dea2d8e..0000000
--- a/notes/hou-developer.md
+++ /dev/null
@@ -1,237 +0,0 @@
-# Developer Houdini Plugin 
-### Version 0.1 
-### Author: Lucas Galante
-### Document Updated: December 2025
-
-# Outline
-
-Developer operators are combined in SOP solver and DOP contexts in Houdini. Creating lifelike elements in digital containers.
-
-
-# Looking Ahead 
-
-## Attributes Or Groups?
-    
-This question easily leads into the larger topic of how data is stored for dynamical operations. There are two main types of data in Shapeshifter:
-### 1. Live data
-Fluid data which runs like a stream through the simulation.
-
-### 2. Derivative data
-Data calculated anew every frame. It is calculated based on live data.
-
-## Regions
-    
-After making the GEM mold operators, I am more experienced with treating a model as a composite of distinct parts and overlapping attributes. It has also helped me visualize what a developed form with distinct surface regions can look like and how the regions might evolve during the model's life. The idea of regions in a model reminds me of organs which fit together physically and programmatically.
-
-It is interesting to consider how regions in a model are like programmatic elements in architecture and ecology. These are both structures where multiple functions are built into a one body. Disease may spread through a limb and leave a trail of evidence or destroy its host. Living models host mechanisms which influence their status. Viruses can travel within their host in directions informed by sense input and change the host's nature depending on where they travel. Morphogen gradients are interesting because they demonstrate how distribution of matter on one scale can influence form on another scale.
-
-Another property of a living model is how definitively its boundaries are defined. Before they are really there, the boundary of a living model is predictable. It requires an environment which supplies the real elements of itself, but information about where those elements will go is present ahead of time.
-
-## Operator Categorirs
-
-### Pre-Simulation
-The embryo or seed is created, initial selections and value assignments are made, attributes that will be used are introduced and initial conditions are set. Pre-simulation operators define the characteristics and landscape of a simulation before it runs. Attribute templates and starting geometry are supplied to be fed into the simulation network.
-
-### Simulation Attribute Operators
-Value operators operate on simulation attributes which are present for the entire simulation.Simulation attributes are persistent from the end of one frame to the beginning of another.
-
-### Development Attribute Operators
-During this stage of the simulation network, simulation attributes are composited into development attributes. Development attributes are cleared at the end of each frame and need to be constructed from the same corresponding simulation attributes for every frame. Development attributes tell the surface development operators how the surface will change.
-
-### Surface Development Operators
-Surface Development Operators read development attributes and use that information to transform the mesh or volume. Aside from position and normal, attributes are untouched. At this point, Value assignments remain the same until the next frame.
-
-### Meshing Operators
-Once the geometry has been deformed, meshing operators are the final step in the simulation process.Meshing operators adjust the topology of geometry. This ensures that the number of data points in a mesh is always proportional to ts surface area. The surface is remeshed to keep make primitives and points conform to some regular formation.
-
-### Attributes
-Most attributes are assigned to the geometry instance in the preparation phase.
-Attributes belong to categories based on their function.
-
-### Age
-Values have ages which are affected by a few different criteria.
-    
-    Increment
-      - 1
-      - The value of the attribute\
-      - Some factor of a scale / normalized value
-
-      Reset 
-		- 
-
-
-## Larger Ideas
-
-### Development
-To effectively develop this package, categories need to be created which
-
-### Topological Considerations
-At this point in time, the Shapeshifter operator package is primarily concerned with the surface of digital models. Unlike many other approaches to similar design queries its operators involve direct analysis and development of closed forms, not an underlying network or structure to which is later assigned volume. 
-    
-### Operator Categorization
-Nodes are distributed into categories based on their function. Nodes which operate on or output scalar or vector attributes are separated. The reason for this is simplifying the development of nodes. Nodes with more complex options are more difficult to modify, and because a holistic vision for the inerface hasn't materialized, it's easier to keep them separated, at least for now.
-
-### Geometry Classes
-
-#### Vertices
-As of yet there aren't any well-defined use cases for vertex attributes.    
-
-#### Points
-The majority of nodes operate on point attributes. The keystone of shapeshifter is the develop and remesh node pair. The develop node requires point attributes to modify the topology of surfaces over time, so it makes sense to focuse primarily on point attributes.
-
-#### Primitives
-If a clear use-case for primitive attributes becomes clear.  
-
-#### Detail
-Global attributes are useful for storing statistics and analyses.
-
-## Operator Categories
-
-### Scalar
-Scalar nodes which edit the values of scalar attributes.
-
-### Vector
-Vector sodes which edit the values of vector attributes.
-
-### Surface
-Surface nodes utilize input attributes to alter the topology of the geometry.
-
-### Volume
-Volume nodes modify volumes that exist in tandem with mesh geometry. Volume nodes are not fleshed out and need very basic development.
-
-Looking ahead idea #1: Volumes can be reservoirs of value when a vector directs value to flow either inside of (Shapeshifter Core.) the geometry or away from the surface of the geometry (Shapeshifter Miasma.)
-
-
-# Operators
-
-## Scalar Analysis
-Analyze poperties of scalar attributes.
-
-Analysis attributes provide specific information about scalar attributes.
-
-Analyses can be toggled on and off in the operator's parameter interface.
-
-Some analyses are per-point, and others are operations involving sets of points.
-
-## Scalar Composite
-Combine attributes in different configurations.
-
-Will likely be combined with "Scalar Combine".
-
-## Scalar Combine
-Combines attributes using different methods.
-
-### Looking Ahead
-A standard Attribute Combine operator is shipped with Houdini. It has several interesting options, so it is not clear if the Shapeshifter version should be kept in the package.
-
-## Scalar Concentrate
-Similar in function to "Scalar Diffuse", only in reverse.    
-
-###  Looking Ahead
-It's possible that this operator can be replaced by setting "Scalar Diffuse" to a negative number.
-
-## Scalar Diffuse
-Diffuses attribute values over a surface.
-
-## Scalar Initialize
-Pre-sim: Initializes attributes that will be used during simulation.
-
-## Scalar Map
-
-## Scalar Migrate 
-
-##  Scalar Normalize 
-    
-## Scalar Ramp 
-
-## Scalar Weight
-
-## Submute Begin 
-
-## Submute End 
-
-## Lead 
-
-## Vector Analysis
-
-## Vector Composite 
-
-## Vector Conform 
-
-## Vector Diffuse 
-
-## Vector Direct 
-
-## Vector From Scalar 
-
-## Vector Migrate 
-
-## Vector Normalize 
-
-## Vector Rotate 
-
-## Vector Unify 
-
-## Vector Weight 
-
-## Surface Adapt
-
-## Surface Open 
-
-## Surface Suture 
-
-## Subdivide 
-
-## Remesh 
-
-## Detangle 
-      
-## Develop 
-
-## Time 
-
-## Time Ramp 
-
-## Time Switch
-
-## Solver 
-
-## Region (Tentative) 
-
-## Analyze (Tentative) 
-
-## Vitality 
-
-## Edge Analysis 
-
-## Analyze Change 
-
-## Volume/Miasma
-
-## Volume/Core
-
-## Filter/Select
-
-## Region/Region Center 
-
-## Attribute/Promote 
-
-## Visualize/Panel
-
-## ID
-
-## Age/Expire
-
-## Energize 
-
-## Create/Embryo
-
-## Filter/Cull
-
-## Combine
-
-## Constant
-
-## Measure 
-      
-## Metamax (Irrelevant?)
diff --git a/notes/moldtekPartitionViewerState b/notes/moldtekPartitionViewerState
deleted file mode 100755
index 8720dfe..0000000
--- a/notes/moldtekPartitionViewerState
+++ /dev/null
@@ -1,21 +0,0 @@
-rows:
-	modes:
-		add/remove
-		edit
-
-
--add new partition
--add first prim to partition
--add additional prims to partition
--remove prim from partition
--remove final prim from partition
--remove partition
-
--change draft angle for partition globally
--change draft angle for partition locally
--change reach of partition globally
--change reach of partition locallt
-
--transfer prim to different partition
--merge partitions
--split partition
\ No newline at end of file
diff --git a/notes/network_editor_setttings_reference b/notes/network_editor_setttings_reference
deleted file mode 100755
index 9368617..0000000
--- a/notes/network_editor_setttings_reference
+++ /dev/null
@@ -1,148 +0,0 @@
-{'TopInOutColorTag_0': '',
-'TopInOutColorTag_1': '',
-'TopInOutColorTag_10': '',
-'TopInOutColorTag_11': '',
-'TopInOutColorTag_12': '',
-'TopInOutColorTag_13': '',
-'TopInOutColorTag_14': '',
-'TopInOutColorTag_15': '',
-'TopInOutColorTag_16': '',
-'TopInOutColorTag_17': '',
-'TopInOutColorTag_18': '',
-'TopInOutColorTag_19': '',
-'TopInOutColorTag_2': '',
-'TopInOutColorTag_3': '',
-'TopInOutColorTag_4': '',
-'TopInOutColorTag_5': '',
-'TopInOutColorTag_6': '',
-'TopInOutColorTag_7': '',
-'TopInOutColorTag_8': '',
-'TopInOutColorTag_9': '',
-'TopInOutColor_0': '',
-'TopInOutColor_1': '',
-'TopInOutColor_10': '',
-'TopInOutColor_11': '',
-'TopInOutColor_12': '',
-'TopInOutColor_13': '',
-'TopInOutColor_14': '',
-'TopInOutColor_15': '',
-'TopInOutColor_16': '',
-'TopInOutColor_17': '',
-'TopInOutColor_18': '',
-'TopInOutColor_19': '',
-'TopInOutColor_2': '',
-'TopInOutColor_3': '',
-'TopInOutColor_4': '',
-'TopInOutColor_5': '',
-'TopInOutColor_6': '',
-'TopInOutColor_7': '',
-'TopInOutColor_8': '',
-'TopInOutColor_9': '',
-'allowdiveintohdas': '1',
-'allowdroponwire': '1',
-'autoscroll': '1',
-'backgroundimagebrightness': '1.0',
-'backgroundimageediting': '0',
-'badge64bit': 'normal',
-'badgeLoadFromDisk': 'large',
-'badgeNodeDiveable': 'normal',
-'badgechilderror': 'large',
-'badgecomment': 'normal',
-'badgeconstraints': 'normal',
-'badgedynamictop': 'normal',
-'badgeerror': 'large',
-'badgehastakedata': 'hide',
-'badgehdadelaysync': 'normal',
-'badgehdalocked': 'normal',
-'badgehdaunlocked': 'normal',
-'badgekinematics': 'hide',
-'badgelopdebug': 'normal',
-'badgeloppopulationmask': 'normal',
-'badgelopunloadedpayloads': 'normal',
-'badgemessage': 'normal',
-'badgemqclientsauth': 'normal',
-'badgemqserverauth': 'normal',
-'badgeneedscook': 'hide',
-'badgenodehasdata': 'hide',
-'badgenodelocked': 'normal',
-'badgenodeunload': 'normal',
-'badgenotcompilable': 'normal',
-'badgetimedep': 'normal',
-'badgevexcache': 'normal',
-'badgewarning': 'normal',
-'connectorsnapradius': '0.700000',
-'copypathstoclipboard': '1',
-'dimunusednodes': '0',
-'distancefordrag': '0.050000',
-'doautomovenodes': '0',
-'dosnapping': '1',
-'flagsallowpreselect': '1',
-'gridmode': '2',
-'gridsnapping': '1',
-'gridxstep': '2.0',
-'gridystep': '1.0',
-'grouplistsplit': '0.75',
-'listmode': '0',
-'maxflyoutscale': '10000.000000',
-'maxnameheight': '0.000000',
-'maxnamewidth': '10000.000000',
-'maxtaskgraphdepth': '0',
-'maxtaskgraphrows': '20',
-'maxworkitemsperrow': '10',
-'minnameheight': '0.000000',
-'overviewbounds': '0.000000 0.000000 91.361262 205.933953',
-'overviewmode': '2',
-'palettecolors': '',
-'palettemode': '0',
-'palettesize': '225 225',
-'perfstatname': 'Time',
-'shakesensitivity': '1.000000',
-'showanimations': '1',
-'showcancelledworkitems': '1',
-'showchilddep': '1',
-'showcookedworkitems': '1',
-'showcookingworkitems': '1',
-'showdep': '0',
-'showdirtyworkitems': '1',
-'showfailedworkitems': '1',
-'showgrouplist': '0',
-'showloplayercolor': '1',
-'showmenu': '0',
-'shownodeshapes': '1',
-'shownodetypes': '1',
-'showparmdialog': '0',
-'showparmdialogmax': '1',
-'showpartitionworkitems': '1',
-'showperfstats': '1',
-'showpreviews': '1',
-'showprompttext': '1',
-'showsimpleshape': '0',
-'showspareinputdep': '1',
-'showstackedlook': '1',
-'showstaticworkitems': '1',
- 'showtaskgraph': '1',
-'showtaskgraphperf': '0',
-'showtasksmenu': '1',
-'showtree': '0',
-'showtypesidebar': '0',
-'showvopinoutlabels': '0',
-'snapradius': '0.100000',
-'solohighlightedworkitems': '0',
-'taskgraphcollapsemode': 'Off',
-'taskgraphperfstatname': 'Cook Time',
-'taskgraphsortcriteria': 'Automatic',
-'taskgraphsortdirection': '0',
-'textbadgecomment': 'truncated',
-'textbadgecontextoptiondeps': 'selected_full',
-'textbadgedescriptiveparm': 'truncated',
-'textbadgedetailid': 'hide',
-'textbadgelayercount': 'truncated',
-'textbadgeloplastmodifiedprim': 'truncated',
-'textbadgeoutputforview': 'truncated',
-'transientinfo': '1',
-'treesplit': '0.25',
-'typesidebarsplit': '0.25',
-'useworkitemcolorattribute': '0',
-'wirefadefactor': '1.000000',
-'workitemattrtocollapseby': '',
-'workitemcolorattribute': ''}
diff --git a/notes/sop_verbs b/notes/sop_verbs
deleted file mode 100755
index c33fdc8..0000000
--- a/notes/sop_verbs
+++ /dev/null
@@ -1,345 +0,0 @@
-'add':                               <hou.SopVerb add>,
-'attribcreate::2.0':                 <hou.SopVerb attribcreate::2.0>,
-'line':                              <hou.SopVerb line>,
-'opencl':                            <hou.SopVerb opencl>,
-'pointgenerate':                     <hou.SopVerb pointgenerate>,
-
-{
-'agentlayer::2.0':                   <hou.SopVerb agentlayer::2.0>,
-'agentunpack':                       <hou.SopVerb agentunpack>,
-'apex::invokegraph':                 <hou.SopVerb apex::invokegraph>,
-'attribcast':                        <hou.SopVerb attribcast>,
-'attribcombine':                     <hou.SopVerb attribcombine>,
-'attribcomposite':                   <hou.SopVerb attribcomposite>,
-'attribcopy':                        <hou.SopVerb attribcopy>,
-'attribfill':                        <hou.SopVerb attribfill>,
-'attribfromparm':                    <hou.SopVerb attribfromparm>,
-'attribfromvolume':                  <hou.SopVerb attribfromvolume>,
-'attribinterpolate':                 <hou.SopVerb attribinterpolate>,
-'attribmirror':                      <hou.SopVerb attribmirror>,
-'attribpromote':                     <hou.SopVerb attribpromote>,
-'attribreorient':                    <hou.SopVerb attribreorient>,
-'attribstringedit':                  <hou.SopVerb attribstringedit>,
-'attribswap':                        <hou.SopVerb attribswap>,
-'attribtransfer':                    <hou.SopVerb attribtransfer>,
-'attribute':                         <hou.SopVerb attribute>,
-'attribvop':                         <hou.SopVerb attribvop>,
-'basis':                             <hou.SopVerb basis>,
-'blast':                             <hou.SopVerb blast>,
-'blendshapes':                       <hou.SopVerb blendshapes>,
-'blendshapes::2.0':                  <hou.SopVerb blendshapes::2.0>,
-'bonecapturebiharmonic':             <hou.SopVerb bonecapturebiharmonic>,
-'bonedeform':                        <hou.SopVerb bonedeform>,
-'boolean::2.0':                      <hou.SopVerb boolean::2.0>,
-'bound':                             <hou.SopVerb bound>,
-'box':                               <hou.SopVerb box>,
-'cache':                             <hou.SopVerb cache>,
-'cacheif':                           <hou.SopVerb cacheif>,
-'cap':                               <hou.SopVerb cap>,
-'captureattribpack':                 <hou.SopVerb captureattribpack>,
-'captureattribunpack':               <hou.SopVerb captureattribunpack>,
-'capturepaintcore':                  <hou.SopVerb capturepaintcore>,
-'carve':                             <hou.SopVerb carve>,
-'circle':                            <hou.SopVerb circle>,
-'circlefromedges':                   <hou.SopVerb circlefromedges>,
-'clip':                              <hou.SopVerb clip>,
-'clip::2.0':                         <hou.SopVerb clip::2.0>,
-'cluster':                           <hou.SopVerb cluster>,
-'clustermesh':                       <hou.SopVerb clustermesh>,
-'connectivity':                      <hou.SopVerb connectivity>,
-'control':                           <hou.SopVerb control>,
-'convert':                           <hou.SopVerb convert>,
-'convertvdb':                        <hou.SopVerb convertvdb>,
-'convertvdbpoints':                  <hou.SopVerb convertvdbpoints>,
-'convertvolume':                     <hou.SopVerb convertvolume>,
-'convexdecomposition':               <hou.SopVerb convexdecomposition>,
-'cop2net':                           <hou.SopVerb cop2net>,
-'copytopoints':                      <hou.SopVerb copytopoints>,
-'copytopoints::2.0':                 <hou.SopVerb copytopoints::2.0>,
-'copyxform':                         <hou.SopVerb copyxform>,
-'crease':                            <hou.SopVerb crease>,
-'crowdmotionpathavoidcore':          <hou.SopVerb crowdmotionpathavoidcore>,
-'crowdmotionpatheditcore':           <hou.SopVerb crowdmotionpatheditcore>,
-'crowdmotionpathevaluatecore':       <hou.SopVerb crowdmotionpathevaluatecore>,
-'curve':                             <hou.SopVerb curve>,
-'curvesect':                         <hou.SopVerb curvesect>,
-'delete':                            <hou.SopVerb delete>,
-'deltamush':                         <hou.SopVerb deltamush>,
-'detangle':                          <hou.SopVerb detangle>,
-'dissolve::2.0':                     <hou.SopVerb dissolve::2.0>,
-'divide':                            <hou.SopVerb divide>,
-'dopimport::2.0':                    <hou.SopVerb dopimport::2.0>,
-'edgecollapse':                      <hou.SopVerb edgecollapse>,
-'edgecusp':                          <hou.SopVerb edgecusp>,
-'edgedivide':                        <hou.SopVerbedgedivide>,
-'edgeequalize':                      <hou.SopVerb edgeequalize>,
-'edgeflip':                          <hou.SopVerb edgeflip>,
-'edgestraighten':                    <hou.SopVerb edgestraighten>,
-'edgetransport':                     <hou.SopVerb edgetransport>,
-'ends':                              <hou.SopVerb ends>,
-'enumerate':                         <hou.SopVerb enumerate>,
- 'error':                            <hou.SopVerb error>,
-'extractcentroid':                   <hou.SopVerb extractcentroid>,
-'extracttransform':                  <hou.SopVerb extracttransform>,
-'facet':                             <hou.SopVerb facet>,
-'featherattribinterpolate':          <hou.SopVerb featherattribinterpolate>,
-'featherbarbtangents':               <hou.SopVerb featherbarbtangents>,
-'featherray':                        <hou.SopVerb featherray>,
-'feathertemplateinterpolatecore':    <hou.SopVerb feathertemplateinterpolatecore>,
-'file':                              <hou.SopVerb file>,
-'fit':                               <hou.SopVerb fit>,
-'font':                              <hou.SopVerb font>,
-'fractal':                           <hou.SopVerb fractal>,
-'fuse':                              <hou.SopVerb fuse>,
-'fuse::2.0':                         <hou.SopVerb fuse::2.0>,
-'grid':                              <hou.SopVerb grid>,
-'groupcombine':                      <hou.SopVerb groupcombine>,
-'groupcopy':                         <hou.SopVerb groupcopy>,
-'groupcreate':                       <hou.SopVerb groupcreate>,
-'groupdelete':                       <hou.SopVerb groupdelete>,
-'groupexpand':                       <hou.SopVerb groupexpand>,
-'groupfindpath':                     <hou.SopVerb groupfindpath>,
-'groupinvert':                       <hou.SopVerb groupinvert>,
-'grouppromote':                      <hou.SopVerb grouppromote>,
-'grouprange':                        <hou.SopVerb grouprange>,
-'grouprename':                       <hou.SopVerb grouprename>,
-'groupsfromname':                    <hou.SopVerb groupsfromname>,
-'grouptransfer':                     <hou.SopVerb grouptransfer>,
-'guidegroomcore':                    <hou.SopVerb guidegroomcore>,
-'guidemask':                         <hou.SopVerb guidemask>,
-'guideprocesscore':                  <hou.SopVerb guideprocesscore>,
-'hairclump':                         <hou.SopVerb hairclump>,
-'hairgencore':                       <hou.SopVerb hairgencore>,
-'heatgeodesic':                      <hou.SopVerb heatgeodesic>,
-'hole':                              <hou.SopVerb hole>,
-'inflate':                           <hou.SopVerb inflate>,
-'intersectionanalysis':              <hou.SopVerb intersectionanalysis>,
-'intersectionstitch':                <hou.SopVerb intersectionstitch>,
-'invoke':                            <hou.SopVerb invoke>,
-'invokegraph':                       <hou.SopVerb invokegraph>,
-'isooffset':                         <hou.SopVerb isooffset>,
-'join':                              <hou.SopVerb join>,
-'kinefx::agentfromrigcore':          <hou.SopVerb kinefx::agentfromrigcore>,
-'kinefx::attribtransformcompute':    <hou.SopVerb kinefx::attribtransformcompute>,
-'kinefx::attribtransformextract':    <hou.SopVerb kinefx::attribtransformextract>,
-'kinefx::characterblendshapesadd':   <hou.SopVerb kinefx::characterblendshapesadd>,
-'kinefx::characterblendshapescore':  <hou.SopVerb kinefx::characterblendshapescore>,
-'kinefx::computemotionclipcreate':   <hou.SopVerb kinefx::computemotionclipcreate>,
-'kinefx::computemotionclipretime':   <hou.SopVerb kinefx::computemotionclipretime>,
-'kinefx::computerigpose':            <hou.SopVerb kinefx::computerigpose>,
-'kinefx::computetransform':          <hou.SopVerb kinefx::computetransform>,
-'kinefx::configurerigvis::2.0':      <hou.SopVerb kinefx::configurerigvis::2.0>,
-'kinefx::extractrotationalmomentum': <hou.SopVerb kinefx::extractrotationalmomentum>,
-'kinefx::ikchains::2.0':             <hou.SopVerb kinefx::ikchains::2.0>,
-'kinefx::motionclipextractkeyposes': <hou.SopVerb kinefx::motionclipextractkeyposes>,
-'kinefx::motionclipmerge':           <hou.SopVerb kinefx::motionclipmerge>,
-'kinefx::motionclipposedelete::2.0': <hou.SopVerb kinefx::motionclipposedelete::2.0>,
-'kinefx::motionclipunpack':          <hou.SopVerb kinefx::motionclipunpack>,
-'kinefx::motionclipupdate':          <hou.SopVerb kinefx::motionclipupdate>,
-'kinefx::motionmixer':               <hou.SopVerb kinefx::motionmixer>,
-'kinefx::pendulummotioncore':        <hou.SopVerb kinefx::pendulummotioncore>,
-'kinefx::poseweightinterp':          <hou.SopVerb kinefx::poseweightinterp>,
-'kinefx::projectilemotioncore':      <hou.SopVerb kinefx::projectilemotioncore>,
-'kinefx::resamplesplinetransforms':  <hou.SopVerb kinefx::resamplesplinetransforms>,
-'kinefx::rigattribvop':              <hou.SopVerb kinefx::rigattribvop>,
-'kinefx::rotatebymomentum':          <hou.SopVerb kinefx::rotatebymomentum>,
-'kinefx::skeletonblend::2.0':        <hou.SopVerb kinefx::skeletonblend::2.0>,
-'kinefx::skeletonblend::3.0':        <hou.SopVerb kinefx::skeletonblend::3.0>,
-'kinefx::smoothmotioncore':          <hou.SopVerb kinefx::smoothmotioncore>,
-'kinefx::usdanimimport':             <hou.SopVerb kinefx::usdanimimport>,
-'kinefx::usdskinimport':             <hou.SopVerb kinefx::usdskinimport>,
-'knife':                             <hou.SopVerb knife>,
-'lidarimport':                       <hou.SopVerb lidarimport>,
-'linearsolver':                      <hou.SopVerb linearsolver>,
-'lopimport::2.0':                    <hou.SopVerb lopimport::2.0>,
-'matchtopology':                     <hou.SopVerb matchtopology>,
-'material':                          <hou.SopVerb material>,
-'measure':                           <hou.SopVerb measure>,
-'measure::2.0':                      <hou.SopVerb measure::2.0>,
-'merge':                             <hou.SopVerb merge>,
-'mergepacked':                       <hou.SopVerb mergepacked>,
-'mirror':                            <hou.SopVerb mirror>,
-'mlexamplecreatecore':               <hou.SopVerb mlexamplecreatecore>,
-'mlexampledecomposecore':            <hou.SopVerb mlexampledecomposecore>,
-'mlexampledeserializepacked':        <hou.SopVerb mlexampledeserializepacked>,
-'mlexampledeserializepoint':         <hou.SopVerb mlexampledeserializepoint>,
-'mlexampleserializepacked':          <hou.SopVerb mlexampleserializepacked>,
-'mlexampleserializepoint':           <hou.SopVerb mlexampleserializepoint>,
-'mlregressioninferencecore':         <hou.SopVerb mlregressioninferencecore>,
-'mlregressionproximitycore':         <hou.SopVerb mlregressionproximitycore>,
-'name':                              <hou.SopVerb name>,
-'neighborsearchcl':                  <hou.SopVerb neighborsearchcl>,
-'normal':                            <hou.SopVerb normal>,
-'null':                              <hou.SopVerb null>,
-'onnx':                              <hou.SopVerb onnx>,
-'orientalongcurve':                  <hou.SopVerb orientalongcurve>,
-'output':                            <hou.SopVerb output>,
-'pack':                              <hou.SopVerbpack>,
-'packededit':                        <hou.SopVerb packededit>,
-'packfolder':                        <hou.SopVerb packfolder>,
-'packinject':                        <hou.SopVerb packinject>,
-'packpoints':                        <hou.SopVerb packpoints>,
-'pca':                               <hou.SopVerb pca>,
-'peak':                              <hou.SopVerb peak>,
-'pointcapturecore':                  <hou.SopVerb pointcapturecore>,
-'pointcloudnormal':                  <hou.SopVerb pointcloudnormal>,
-'pointcloudreduce':                  <hou.SopVerb pointcloudreduce>,
-'pointcloudsurface':                 <hou.SopVerb pointcloudsurface>,
-'polybevel::3.0':                    <hou.SopVerb polybevel::3.0>,
-'polycut':                           <hou.SopVerb polycut>,
-'polydoctor':                        <hou.SopVerb polydoctor>,
-'polyexpand2d':                      <hou.SopVerb polyexpand2d>,
-'polyextrude::2.0':                  <hou.SopVerb polyextrude::2.0>,
-'polyfill':                          <hou.SopVerb polyfill>,
-'polyframe':                         <hou.SopVerb polyframe>,
-'polypatch':                         <hou.SopVerb polypatch>,
-'polyreduce::2.0':                   <hou.SopVerb polyreduce::2.0>,
-'polysoup':                          <hou.SopVerb polysoup>,
-'polysplit::2.0':                    <hou.SopVerbpolysplit::2.0>,
-'polywire':                          <hou.SopVerb polywire>,
-'primitive':                         <hou.SopVerb primitive>,
-'primitivesplit':                    <hou.SopVerb primitivesplit>,
-'quadremesh':                        <hou.SopVerb quadremesh>,
-'rawimport':                         <hou.SopVerb rawimport>,
-'ray':                               <hou.SopVerb ray>,
-'refine':                            <hou.SopVerb refine>,
-'relax':                             <hou.SopVerb relax>,
-'remesh':                            <hou.SopVerb remesh>,
-'remesh::2.0':                       <hou.SopVerb remesh::2.0>,
-'repack':                            <hou.SopVerb repack>,
-'resample':                          <hou.SopVerb resample>,
-'rest':                              <hou.SopVerb rest>,
-'reverse':                           <hou.SopVerb reverse>,
-'revolve':                           <hou.SopVerb revolve>,
-'rewire':                            <hou.SopVerb rewire>,
-'sblend':                            <hou.SopVerb sblend>,
-'sblend::2.0':                       <hou.SopVerb sblend::2.0>,
-'scatter::2.0':                      <hou.SopVerb scatter::2.0>,
-'shapediff':                         <hou.SopVerb shapediff>,
-'shrinkwrap::2.0':                   <hou.SopVerb shrinkwrap::2.0>,
-'simplexrefine':                     <hou.SopVerb simplexrefine>,
-'skin':                              <hou.SopVerb skin>,
-'smooth::2.0':                       <hou.SopVerb smooth::2.0>,
-'softpeak':                          <hou.SopVerb softpeak>,
-'softxform':                         <hou.SopVerb softxform>,
-'solidify':                          <hou.SopVerb solidify>,
-'sort':                              <hou.SopVerb sort>,
-'sphere':                            <hou.SopVerb sphere>,
-'splitpoints':                       <hou.SopVerb splitpoints>,
-'stash':                             <hou.SopVerb stash>,
-'subdivide':                         <hou.SopVerb subdivide>,
-'surfacesplat':                      <hou.SopVerb surfacesplat>,
-'sweep::2.0':                        <hou.SopVerb sweep::2.0>,
-'switch':                            <hou.SopVerb switch>,
-'switchif':                          <hou.SopVerb switchif>,
-'tangentfield':                      <hou.SopVerb tangentfield>,
-'tetcraft':                          <hou.SopVerb tetcraft>,
-'tetlayer':                          <hou.SopVerb tetlayer>,
-'tetpartition':                      <hou.SopVerb tetpartition>,
-'tetrahedralize':                    <hou.SopVerb tetrahedralize>,
-'tetrasurface':                      <hou.SopVerb tetrasurface>,
-'texture':                           <hou.SopVerb texture>,
-'texturefeature':                    <hou.SopVerb texturefeature>,
-'textureopticalflow':                <hou.SopVerb textureopticalflow>,
-'topnetsop':                         <hou.SopVerb topnetsop>,
-'topotransfer':                      <hou.SopVerb topotransfer>,
-'torus':                             <hou.SopVerb torus>,
-'trace':                             <hou.SopVerb trace>,
-'triangulate2d::2.0':                <hou.SopVerb triangulate2d::2.0>,
-'triangulate2d::3.0':                <hou.SopVerb triangulate2d::3.0>,
-'tribez':                            <hou.SopVerb tribez>,
-'tridivide':                         <hou.SopVerb tridivide>,
-'tristrip':                          <hou.SopVerb tristrip>,
-'tube':                              <hou.SopVerbtube>,
-'unpack':                            <hou.SopVerb unpack>,
-'unpackfolder':                      <hou.SopVerb unpackfolder>,
-'unpackpoints':                      <hou.SopVerb unpackpoints>,
-'unpackusd::2.0':                    <hou.SopVerb unpackusd::2.0>,
-'uvautoseam':                        <hou.SopVerb uvautoseam>,
-'uvflatten::2.0':                    <hou.SopVerb uvflatten::2.0>,
-'uvflatten::3.0':                    <hou.SopVerb uvflatten::3.0>,
-'uvlayout::2.0':                     <hou.SopVerb uvlayout::2.0>,
-'uvlayout::3.0':                     <hou.SopVerb uvlayout::3.0>,
-'uvproject':                         <hou.SopVerb uvproject>,
-'uvrelax':                           <hou.SopVerb uvrelax>,
-'uvtransform::2.0':                  <hou.SopVerb uvtransform::2.0>,
-'uvunwrap':                          <hou.SopVerb uvunwrap>,
-'vdb':                               <hou.SopVerb vdb>,
- 'vdbactivate':                      <hou.SopVerb vdbactivate>,
-'vdbactivatesdf':                    <hou.SopVerb vdbactivatesdf>,
-'vdbadvectpoints':                   <hou.SopVerb vdbadvectpoints>,
-'vdbadvectsdf':                      <hou.SopVerb vdbadvectsdf>,
-'vdbanalysis':                       <hou.SopVerb vdbanalysis>,
-'vdbclip':                           <hou.SopVerb vdbclip>,
-'vdbcombine':                        <hou.SopVerb vdbcombine>,
-'vdbconvexclipsdf':                  <hou.SopVerb vdbconvexclipsdf>,
-'vdbcreatecl':                       <hou.SopVerb vdbcreatecl>,
-'vdbdiagnostics':                    <hou.SopVerb vdbdiagnostics>,
-'vdbextrapolate':                    <hou.SopVerb vdbextrapolate>,
-'vdbfracture':                       <hou.SopVerb vdbfracture>,
-'vdbfromparticles':                  <hou.SopVerb vdbfromparticles>,
-'vdbfrompolygons':                   <hou.SopVerb vdbfrompolygons>,
-'vdblod':                            <hou.SopVerb vdblod>,
-'vdbmerge':                          <hou.SopVerb vdbmerge>,
-'vdbmorphsdf':                       <hou.SopVerb vdbmorphsdf>,
-'vdbocclusionmask':                  <hou.SopVerb vdbocclusionmask>,
-'vdbpointsdelete':                   <hou.SopVerb vdbpointsdelete>,
-'vdbpointsgroup':                    <hou.SopVerb vdbpointsgroup>,
-'vdbpotentialflow':                  <hou.SopVerb vdbpotentialflow>,
-'vdbprojectnondivergent':            <hou.SopVerb vdbprojectnondivergent>,
-'vdbrenormalizesdf':                 <hou.SopVerb vdbrenormalizesdf>,
-'vdbresample':                       <hou.SopVerb vdbresample>,
-'vdbreshapesdf':                     <hou.SopVerb vdbreshapesdf>,
-'vdbsegmentbyconnectivity':          <hou.SopVerb vdbsegmentbyconnectivity>,
-'vdbsmooth':                         <hou.SopVerb vdbsmooth>,
-'vdbsmoothsdf':                      <hou.SopVerb vdbsmoothsdf>,
-'vdbtopologytosdf':                  <hou.SopVerb vdbtopologytosdf>,
-'vdbtospheres':                      <hou.SopVerb vdbtospheres>,
-'vdbvectormerge':                    <hou.SopVerb vdbvectormerge>,
-'vdbvectorsplit':                    <hou.SopVerb vdbvectorsplit>,
-'vdbvisualizetree':                  <hou.SopVerb vdbvisualizetree>,
-'vertexsplit':                       <hou.SopVerb vertexsplit>,
-'visibility':                        <hou.SopVerb visibility>,
-'volume':                            <hou.SopVerb volume>,
-'volumeambientocclusion':            <hou.SopVerb volumeambientocclusion>,
-'volumeanalysis':                    <hou.SopVerb volumeanalysis>,
-'volumearrivaltime':                 <hou.SopVerb volumearrivaltime>,
-'volumebin':                         <hou.SopVerb volumebin>,
-'volumeblur':                        <hou.SopVerb volumeblur>,
-'volumebound':                       <hou.SopVerb volumebound>,
-'volumebreak':                       <hou.SopVerb volumebreak>,
-'volumecombine':                     <hou.SopVerb volumecombine>,
-'volumecompress':                    <hou.SopVerb volumecompress>,
-'volumeconvolve3':                   <hou.SopVerb volumeconvolve3>,
-'volumefeather':                     <hou.SopVerb volumefeather>,
-'volumefft':                         <hou.SopVerb volumefft>,
-'volumefromattrib':                  <hou.SopVerb volumefromattrib>,
- 'volumemerge':                      <hou.SopVerb volumemerge>,
-'volumemix':                         <hou.SopVerb volumemix>,
-'volumenormalize':                   <hou.SopVerb volumenormalize>,
-'volumeopticalflow':                 <hou.SopVerb volumeopticalflow>,
-'volumepatch':                       <hou.SopVerb volumepatch>,
-'volumerasterizelattice':            <hou.SopVerb volumerasterizelattice>,
-'volumerasterizeparticles':          <hou.SopVerb volumerasterizeparticles>,
-'volumereduce':                      <hou.SopVerb volumereduce>,
-'volumeresample':                    <hou.SopVerb volumeresample>,
-'volumeresize':                      <hou.SopVerb volumeresize>,
-'volumesdf':                         <hou.SopVerb volumesdf>,
-'volumeslice':                       <hou.SopVerb volumeslice>,
-'volumesplice':                      <hou.SopVerb volumesplice>,
-'volumetrail':                       <hou.SopVerb volumetrail>,
-'volumevectorjoin':                  <hou.SopVerb volumevectorjoin>,
-'volumevectorsplit':                 <hou.SopVerb volumevectorsplit>,
-'volumevisualization':               <hou.SopVerb volumevisualization>,
-'volumevop':                         <hou.SopVerb volumevop>,
-'voronoisplit':                      <hou.SopVerb voronoisplit>,
-'watershed':                         <hou.SopVerb watershed>,
-'weightarraybiharmonic':             <hou.SopVerb weightarraybiharmonic>,
-'weightarrayinterpolate':            <hou.SopVerb weightarrayinterpolate>,
-'windingnumber':                     <hou.SopVerb windingnumber>,
-'wire':                              <hou.SopVerb wire>,
-'wireblend':                         <hou.SopVerb wireblend>,
-'xform':                             <hou.SopVerb xform>,
-'xformaxis':                         <hou.SopVerb xformaxis>,
-'xformbyattrib':                     <hou.SopVerb xformbyattrib>
-}
diff --git a/notes/systems.md b/notes/systems.md
deleted file mode 100644
index 84fb35d..0000000
--- a/notes/systems.md
+++ /dev/null
@@ -1,27 +0,0 @@
-Processing feedback in dynamic systems
-
-In a dynamic systm or simulation, variables inform the procession of model states as the variables themselves develop.
-
-If the volume of an expanding object has an inverse correlation with its growth rate, the growth rate slows as the object grows.
-
-variables inform other variables. In a circulatory system made of pipes or blood vessels, matter flows continuously. It is a semi-closed loop with limited inputs and outputs. If there is buildup in the circuit, the function of the greater system of which it is part of will be altered.
-
-Macro-organs like blood vessels seem simple compared to their micro-counterparts. On small scales, chemical reactions destroy, alter and create themselves constantly. The behavior of macro organs is the effect of this unseen behavior.
-
-A fluid transportation system might have two aspects: the purpose of the system and its health. Its health is its ability to operate in the way that is understood to be correct. In a way, in biology, it is not really possible to determine the purpose of a strategy outside of the context of the maintenance of the things which are doing things.
-
-There is a disctinction between an object and what is around it and not the object. Entity-hood is a discernible quality of systems and things. Examples are:
-- a cell and a cell wall
-- a walled city and the land around it
-
-Prescribing intention and purpose to things is difficult.
-
-Two thoughts:
-1. Intuitive understanding of dependence and independence
-2. Assignment of traits such like desire, purpose and intent to items and circumstances aka things.
-
-Aqueduct problem: aqueous flora, mineral deposits and silt build up and constrict the vessel at some points. I am a Roman noble and lately my bath has been taking twice as long to fill. I need to use more wood to heat the water because it has more time to cool before my water basin is full. Therefore the servant who heats my bath has less time to peel shrimp. I have to assign another servant to peeling shrimp and now she has less time to talk to her friends at the spring, so she is more upset, and that is unpleasant to me. This demonstrates an evology.
-
-Idefining the behavior of a simulated dynamical system requires partitioning information commensurate with what the system should clarify. Programs analyzing force or diffusion in materials may consider some of the behavior of particles on a very minute scale, but there is always a limit, and the magnitude of data in silica does not begin to approach the real world, especially in organic scenarios. To be effective, a model must predict the summation of behavior at a smaller scale. If the behavior of groups of actors is predictable, a model can be made. Somebody might call this "emergent behavior" because it concerns a result borne from an aggregate of actors.
-
- Tissue growth relies on principles which yield observable results. I am modeling a system which reproduces a speculative observable result, usually without attempting to reproduce the actual chemical dynamics of my target phenomena. Cells multiply and are arranged so that they form tissue. The substance of tissue is a product of a metabolism, which is a system not unlike an aqueduct. Components are broken down into more basic chemical composition for use in the body along the way.
diff --git a/otls/sop_lsgalante.im_soft_transform.1.0.hdalc b/otls/sop_lsgalante.im_soft_transform.1.0.hdalc
index 1bfae4e..2d59fdc 100644
Binary files a/otls/sop_lsgalante.im_soft_transform.1.0.hdalc and b/otls/sop_lsgalante.im_soft_transform.1.0.hdalc differ
diff --git a/python3.13libs/hc/__init__.py b/python3.13libs/hc/__init__.py
index 50f066d..3cf1108 100755
--- a/python3.13libs/hc/__init__.py
+++ b/python3.13libs/hc/__init__.py
@@ -10,7 +10,6 @@ from .hcpane          import HCPane
 from .hcparametertab  import HCParameterTab
 from .hcpathtab       import HCPathTab
 from .hcsettings      import HCSettings, HCSettingsPanel
-from .hcradial        import HCRadial
 from .hcsceneviewer   import HCSceneViewer
 from .hcsession       import HCSession
 from .hcsplithandles  import HCSplitHandles
@@ -19,7 +18,6 @@ from .hcstatusbar     import HCStatusBar
 from .hcwindowwatcher import HCWindowWatcher
 from .hctab           import HCTab
 from .hcviewport      import HCViewport
-from .hcvisualizers   import HCVisualizers
 from .hcwidgets       import HCWidgets
 
 __version__ = "1.0.0"
@@ -36,7 +34,6 @@ __all__ = [
     "HCPane",
     "HCParameterTab",
     "HCPathTab",
-    "HCRadial",
     "HCSettings",
     "HCSettingsPanel",
     "HCSession",
@@ -47,6 +44,5 @@ __all__ = [
     "HCTab",
     "HCWindowWatcher",
     "HCViewport",
-    "HCVisualizers",
     "HCWidgets"
 ]
diff --git a/python3.13libs/hc/hcbindings.py b/python3.13libs/hc/hcbindings.py
index 71d5ceb..e4d2f2f 100644
--- a/python3.13libs/hc/hcbindings.py
+++ b/python3.13libs/hc/hcbindings.py
@@ -48,9 +48,10 @@ class HCBindings:
             if conflict_sym == symbol:
                 continue
             
-            # Allow Shift+k to exist globally as the entry point 
-            # even if it is also used inside the keycam state.
-            if conflict_sym == 'h.hc_session_keycam' and symbol.endswith('.trans_up'):
+            # Allow Shift+k to exist globally as the keycam entry point
+            # alongside more-specific uses in the keycam state and
+            # network editor. Houdini resolves by context specificity.
+            if conflict_sym == 'h.hc_session_keycam':
                 continue
 
             ok = hou.hotkeys.removeAssignment(conflict_ctx, conflict_sym, key)
diff --git a/python3.13libs/hc/hccam.py b/python3.13libs/hc/hccam.py
index 1fcc8ef..febd98c 100644
--- a/python3.13libs/hc/hccam.py
+++ b/python3.13libs/hc/hccam.py
@@ -1,4 +1,5 @@
 import hou
+from .hcframing import orthographic_frame_width, perspective_frame_distance
 from .hcgeo import HCGeo
 from .hcsceneviewer import HCSceneViewer
 
@@ -42,7 +43,6 @@ class HCCam:
         self.p = hou.Vector3(centroid)
 
     def frame(self):
-        import math
         geo = self.geo()
         if not geo:
             return
@@ -50,40 +50,23 @@ class HCCam:
         bbox = geo.boundingBox()
         centroid = bbox.center()
         size = bbox.sizevec()
-        
-        # Set pivot and basic target position to centroid
+
         self.p = hou.Vector3(centroid)
-        self.t = hou.Vector3(centroid)
-        
-        margin = 1.2
-        
+        self._syncFromCameraNode()
+
         if self.projection == 'perspective':
-            # Calculate FOV from camera parameters
-            # Horizontal FOV: 2 * atan(aperture / (2 * focal))
-            # Vertical FOV: 2 * atan((aperture/aspect) / (2 * focal))
-            aperture = self.cam_node.parm('aperture').eval()
-            focal = self.cam_node.parm('focal').eval()
-            aspect = self.aspect_ratio
-            
-            fov_h = 2.0 * math.atan(aperture / (2.0 * focal))
-            fov_v = 2.0 * math.atan((aperture / aspect) / (2.0 * focal))
-            
-            # Use the bounding sphere radius for a safe framing from any angle
-            radius = size.length() * 0.5
-            
-            # Required distance to fit the sphere in both H and V FOV
-            dist_h = (radius * margin) / math.sin(fov_h * 0.5)
-            dist_v = (radius * margin) / math.sin(fov_v * 0.5)
-            
-            dist = max(dist_h, dist_v)
-            self.setZoom(dist)
-            
+            dist = perspective_frame_distance(
+                bbox_size=(size[0], size[1], size[2]),
+                aperture=self.cam_node.parm('aperture').eval(),
+                focal=self.cam_node.parm('focal').eval(),
+                aspect_ratio=self.aspect_ratio,
+            )
+            self.t = hou.Vector3(centroid) + (self.local_z * dist)
         else:
-            # Orthographic framing
-            # We need to fit the bounding box into the orthowidth
-            # ow = max(width, height * aspect)
-            width = max(size[0], size[1], size[2])
-            self.ow = width * margin * 1.5 # Extra buffer for ortho
+            self.ow = orthographic_frame_width((size[0], size[1], size[2]))
+            self.t = hou.Vector3(centroid) + (self.local_z * max((self.t - self.p).length(), 0.001))
+
+        self._syncFromCameraNode()
 
     def home(self):
         geo = self.geo()
@@ -102,6 +85,7 @@ class HCCam:
         #     self.ow = 10
 
     def rotateUp(self):
+        self._syncFromCameraNode()
         delta = hou.Vector3(self.delta_r, 0, 0)
         self.r += delta
         m = hou.hmath.buildRotateAboutAxis(self.local_x, self.delta_r)
@@ -113,6 +97,7 @@ class HCCam:
         self.local_z *= m
 
     def rotateDown(self):
+        self._syncFromCameraNode()
         delta = hou.Vector3(-self.delta_r, 0, 0)
         self.r += delta
         m = hou.hmath.buildRotateAboutAxis(self.local_x, -self.delta_r)
@@ -124,6 +109,7 @@ class HCCam:
         self.local_z *= m
 
     def rotateLeft(self):
+        self._syncFromCameraNode()
         delta = hou.Vector3(0, -self.delta_r, 0)
         self.r += delta
         m = hou.hmath.buildRotateAboutAxis(self.global_y, -self.delta_r)
@@ -135,6 +121,7 @@ class HCCam:
         self.local_z *= m
 
     def rotateRight(self):
+        self._syncFromCameraNode()
         delta = hou.Vector3(0, self.delta_r, 0)
         self.r += delta
         m = hou.hmath.buildRotateAboutAxis(self.global_y, self.delta_r)
@@ -146,35 +133,42 @@ class HCCam:
         self.local_z *= m
 
     def setZoom(self, amt):
+        self._syncFromCameraNode()
         move = self.local_z * amt
         self.t += move
 
     def translateUp(self):
+        self._syncFromCameraNode()
         move = self.local_y * self.delta_t
         self.t += move
         self.p += move
 
     def translateDown(self):
+        self._syncFromCameraNode()
         move = self.local_y * self.delta_t * -1
         self.t += move
         self.p += move
 
     def translateLeft(self):
+        self._syncFromCameraNode()
         move = self.local_x * self.delta_t * -1
         self.t += move
         self.p += move
 
     def translateRight(self):
+        self._syncFromCameraNode()
         move = self.local_x * self.delta_t
         self.t += move
         self.p += move
 
     def zoom(self, dir):
+        self._syncFromCameraNode()
         dir_map = {'out': 1, 'in': -1}
         move = self.local_z * self.delta_zoom * dir_map[dir]
         self.t += move
 
     def zoomOrtho(self, dir):
+        self._syncFromCameraNode()
         dir_map = {'out': 1, 'in': -1}
         self.ow += self.delta_zoom * dir_map[dir]
 
@@ -188,6 +182,31 @@ class HCCam:
         ratio = viewport.size()[2] / viewport.size()[3]
         self.aspect_ratio = ratio
 
+    def resetAspectRatio(self):
+        self.aspect_ratio = 1.0
+
+    @staticmethod
+    def captureViewportState(viewport):
+        """Capture the current viewport's camera state before keycam takes over."""
+        xform = viewport.viewTransform()
+        is_persp = viewport.type() == hou.geometryViewportType.Perspective
+        parts = xform.explode(transform_order='srt', rotate_order='xyz')
+        return {
+            't': hou.Vector3(parts['translate']),
+            'r': hou.Vector3(parts['rotate']),  # XYZ order to match keycam xOrd=0
+            'p': hou.Vector3(viewport.viewPivot()),
+            'projection': 'perspective' if is_persp else 'ortho',
+        }
+
+    def initFromState(self, state):
+        """Initialize camera from a previously captured viewport state."""
+        self.t = state['t']
+        self.r = state['r']
+        self._p = state['p']
+        if state['projection'] != self._projection:
+            self.projection = state['projection']
+        self._syncFromCameraNode()
+
     def geo(self):
         return self.scene_viewer.geo()
 
@@ -212,6 +231,17 @@ class HCCam:
         self.global_y = hou.Vector3(0, 1, 0)
         self.global_z = hou.Vector3(0, 0, 1)
 
+    def _syncFromCameraNode(self):
+        self._t = hou.Vector3(self.cam_node.parmTuple('t').eval())
+        self._r = hou.Vector3(self.cam_node.parmTuple('r').eval())
+        self._ow = self.cam_node.parm('orthowidth').eval()
+        self._projection = self.cam_node.parm('projection').evalAsString()
+
+        world_xform = self.cam_node.worldTransform()
+        self.local_x = hou.Vector3(world_xform.at(0, 0), world_xform.at(0, 1), world_xform.at(0, 2)).normalized()
+        self.local_y = hou.Vector3(world_xform.at(1, 0), world_xform.at(1, 1), world_xform.at(1, 2)).normalized()
+        self.local_z = hou.Vector3(world_xform.at(2, 0), world_xform.at(2, 1), world_xform.at(2, 2)).normalized()
+
     def setView(self):
         view_map = {
             'top':    hou.Vector3(270, 0, 0),
@@ -221,7 +251,12 @@ class HCCam:
             'right':  hou.Vector3(0, 90, 0),
             'left':   hou.Vector3(0, 270, 0)
         }
+        self._syncFromCameraNode()
+        distance = max((self.t - self.p).length(), 0.001)
         self.r = view_map[self.view]
+        self._syncFromCameraNode()
+        self.t = self.p + (self.local_z * distance)
+        self._syncFromCameraNode()
 
     def toggleProjection(self):
         projection_map = {
@@ -234,7 +269,7 @@ class HCCam:
         self.viewport().lockCameraToView(0)
 
     def currentViewport(self):
-        return self.scene_viewer.allViewports()[3]
+        return self.scene_viewer.currentViewport()
 
 
     """ Props """
@@ -301,4 +336,3 @@ class HCCam:
     def aspect_ratio(self, val):
         self._aspect_ratio = val
         self.cam_node.parm('aspect').set(val)
-
diff --git a/python3.13libs/hc/hcconfigloader.py b/python3.13libs/hc/hcconfigloader.py
deleted file mode 100644
index 8af4721..0000000
--- a/python3.13libs/hc/hcconfigloader.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import json
-import os
-import hou
-
-def load_settings():
-    return
diff --git a/python3.13libs/hc/hcdefaultcam.py b/python3.13libs/hc/hcdefaultcam.py
index 8f019ff..cd85e3c 100644
--- a/python3.13libs/hc/hcdefaultcam.py
+++ b/python3.13libs/hc/hcdefaultcam.py
@@ -7,9 +7,9 @@ class HCDefaultCam:
 
     def frame(self):
         for viewport in self.scene_viewer.allViewports():
-            default_cam = viewport.camera()
-            # Is cam default or node.
-            if not hou_defaultcam:
+            # Only frame viewports still on the default camera; a viewport
+            # locked to a camera node is framed by moving that node instead.
+            if viewport.camera() is None:
                 viewport.frameAll()
 
     def nextView(self):
diff --git a/python3.13libs/hc/hcdrawmode.py b/python3.13libs/hc/hcdrawmode.py
new file mode 100644
index 0000000..7f3c6b3
--- /dev/null
+++ b/python3.13libs/hc/hcdrawmode.py
@@ -0,0 +1,26 @@
+DRAW_MODE_SEQUENCE = (
+    'Wire',
+    'Flat',
+    'FlatWire',
+)
+
+DRAW_MODE_LABELS = {
+    'Wire': 'Wireframe',
+    'Flat': 'Flat Shaded',
+    'FlatWire': 'Flat Wire Shaded',
+}
+
+
+def next_draw_mode(mode_name):
+    if mode_name not in DRAW_MODE_SEQUENCE:
+        return DRAW_MODE_SEQUENCE[0]
+
+    index = DRAW_MODE_SEQUENCE.index(mode_name)
+    return DRAW_MODE_SEQUENCE[(index + 1) % len(DRAW_MODE_SEQUENCE)]
+
+
+def get_draw_mode_label(mode_name):
+    if mode_name is None:
+        return 'Unknown'
+
+    return DRAW_MODE_LABELS.get(mode_name, mode_name)
diff --git a/python3.13libs/hc/hcframing.py b/python3.13libs/hc/hcframing.py
new file mode 100644
index 0000000..5c431b6
--- /dev/null
+++ b/python3.13libs/hc/hcframing.py
@@ -0,0 +1,16 @@
+import math
+
+
+def perspective_frame_distance(bbox_size, aperture, focal, aspect_ratio, margin=1.2):
+    aspect_ratio = max(aspect_ratio, 1e-6)
+    fov_h = 2.0 * math.atan(aperture / (2.0 * focal))
+    fov_v = 2.0 * math.atan((aperture / aspect_ratio) / (2.0 * focal))
+
+    radius = math.sqrt(sum(axis * axis for axis in bbox_size)) * 0.5
+    dist_h = (radius * margin) / max(math.sin(fov_h * 0.5), 1e-6)
+    dist_v = (radius * margin) / max(math.sin(fov_v * 0.5), 1e-6)
+    return max(dist_h, dist_v)
+
+
+def orthographic_frame_width(bbox_size, margin=1.8):
+    return max(bbox_size) * margin
diff --git a/python3.13libs/hc/hcgeo.py b/python3.13libs/hc/hcgeo.py
index e27f5c1..95aa8f5 100644
--- a/python3.13libs/hc/hcgeo.py
+++ b/python3.13libs/hc/hcgeo.py
@@ -13,6 +13,6 @@ class HCGeo:
         extract_centroid.setParms(
             {'partitiontype': 2}
         )
-        extract_centroid.execute(geo_out, [self.hou_geo])
+        extract_centroid.execute(geo_out, [self.geo])
         pt = geo_out.point(0)
         return pt.position()
diff --git a/python3.13libs/hc/hcgeometryutils.py b/python3.13libs/hc/hcgeometryutils.py
new file mode 100644
index 0000000..77db492
--- /dev/null
+++ b/python3.13libs/hc/hcgeometryutils.py
@@ -0,0 +1,13 @@
+def merged_visible_geo(hou_module, visible_nodes, apply_world_transform=False):
+    geo = hou_module.Geometry()
+
+    for node in visible_nodes:
+        node_geo = hou_module.Geometry()
+        node_geo.merge(node.geometry())
+
+        if apply_world_transform:
+            node_geo.transform(node.hou_node.parent().worldTransform())
+
+        geo.merge(node_geo)
+
+    return geo
diff --git a/python3.13libs/hc/hcguides.py b/python3.13libs/hc/hcguides.py
index 1c70e37..f454c25 100644
--- a/python3.13libs/hc/hcguides.py
+++ b/python3.13libs/hc/hcguides.py
@@ -1,4 +1,5 @@
 import hou
+import math
 
 class HCGuides:
     def __init__(self, state):
@@ -28,7 +29,10 @@ class HCGuides:
             geo_type=hou.drawableGeometryType.Line,
             name='pivot_axis',
             params={
-                'color1': hou.Vector4((1, 1, 1, 0.5))
+                'color1': hou.Vector4((1, 1, 1, 1)),
+                'color2': hou.Vector4((0.5, 0.5, 0.5, 0.5)),
+                'glow_width': 2,
+                'fade_factor': 0.0
             }
         )
 
@@ -144,39 +148,112 @@ class HCGuides:
             self.cam.local_y,
             self.cam.local_z
         )
+        # Saturated pure RGB for maximum visibility
         colors = (
-            [1.0, 0.7, 0.7],
-            [0.7, 1.0, 0.7],
-            [0.7, 0.7, 1.0]
+            [1.0, 0.2, 0.2],   # X = red
+            [0.2, 1.0, 0.2],   # Y = green
+            [0.4, 0.6, 1.0],   # Z = blue (slightly softened for contrast on dark bg)
         )
         geo = hou.Geometry()
-        geo.addAttrib(hou.attribType.Point, 'Cd', (0.1, 0.1, 0.1))
+
+        cone_r = scale * 0.08   # arrowhead radius
+        cone_h = scale * 0.25   # arrowhead height
+        n_sides = 6             # arrowhead facets
+
         for i in range(3):
-            P0 = self.cam.p + axes[i] * scale
-            P1 = self.cam.p + axes[i] * -scale
-            pts = geo.createPoints((P0, P1))
-            pts[0].setAttribValue('Cd', colors[i])
-            pts[1].setAttribValue('Cd', colors[i])
+            P_pos = self.cam.p + axes[i] * scale
+            P_neg = self.cam.p + axes[i] * -scale
+            pts = geo.createPoints((P_pos, P_neg))
             poly = geo.createPolygon(is_closed=False)
             poly.addVertex(pts[0])
             poly.addVertex(pts[1])
+
+            # Arrowhead: pyramid at positive end pointing outward
+            # Apex is at P_pos; base is set back by cone_h along the axis
+            apex_pt = geo.createPoint()
+            apex_pt.setPosition(P_pos)
+
+            # Pick two perpendicular vectors for the base ring
+            perp1 = axes[(i + 1) % 3].normalized()
+            perp2 = axes[(i + 2) % 3].normalized()
+            base_center = P_pos - axes[i] * cone_h
+
+            ring_pts = []
+            for s in range(n_sides):
+                angle = 2.0 * math.pi * s / n_sides
+                pos = base_center + perp1 * (cone_r * math.cos(angle)) \
+                                    + perp2 * (cone_r * math.sin(angle))
+                rp = geo.createPoint()
+                rp.setPosition(pos)
+                ring_pts.append(rp)
+
+            # Build triangular faces from apex around the ring
+            for s in range(n_sides):
+                face = geo.createPolygon()
+                face.addVertex(apex_pt)
+                face.addVertex(ring_pts[s])
+                face.addVertex(ring_pts[(s + 1) % n_sides])
+
+            # Base cap polygon
+            cap = geo.createPolygon()
+            for rp in ring_pts:
+                cap.addVertex(rp)
+
+        # Add Cd after all geometry is built
+        cd_attrib = geo.addAttrib(hou.attribType.Point, 'Cd', (1.0, 1.0, 1.0))
+        pt_list = list(geo.iterPoints())
+        # Each axis contributes: 2 line pts + 1 apex + n_sides ring pts
+        pts_per_axis = 2 + 1 + n_sides
+        running = 0
+        for i in range(3):
+            color = tuple(colors[i])
+            for j in range(pts_per_axis):
+                if running + j < len(pt_list):
+                    pt_list[running + j].setAttribValue(cd_attrib, color)
+            running += pts_per_axis
+
         self.pivot_axis_drawable.setGeometry(geo)
         self.pivot_axis_drawable.setParams(
-            {'fade_factor': 0.0}
+            {
+                'color1': hou.Vector4((1, 1, 1, 1)),
+                'color2': hou.Vector4((0.5, 0.5, 0.5, 0.4)),
+                'glow_width': 2,
+                'fade_factor': 0.0,
+            }
         )
 
     def makeBbox(self):
         guide_geo = hou.Geometry()
         target_geo = self.scene_viewer.geo()
-        bbox = target_geo.bbox()
-        box = hou.sopNodeTypeCategory().nodeVerb('box')
-        box.setParms(
-            {
-                'size': bbox.sizevec(),
-                't':    bbox.center()
-            }
+        if target_geo is None:
+            self.bbox_drawable.setGeometry(guide_geo)
+            return
+
+        bbox = target_geo.boundingBox()
+        mn = bbox.minvec()
+        mx = bbox.maxvec()
+
+        corners = (
+            hou.Vector3(mn[0], mn[1], mn[2]),
+            hou.Vector3(mx[0], mn[1], mn[2]),
+            hou.Vector3(mx[0], mx[1], mn[2]),
+            hou.Vector3(mn[0], mx[1], mn[2]),
+            hou.Vector3(mn[0], mn[1], mx[2]),
+            hou.Vector3(mx[0], mn[1], mx[2]),
+            hou.Vector3(mx[0], mx[1], mx[2]),
+            hou.Vector3(mn[0], mx[1], mx[2]),
         )
-        box.execute(guide_geo, [])
+        pts = guide_geo.createPoints(corners)
+        edges = (
+            (0, 1), (1, 2), (2, 3), (3, 0),
+            (4, 5), (5, 6), (6, 7), (7, 4),
+            (0, 4), (1, 5), (2, 6), (3, 7),
+        )
+        for a, b in edges:
+            poly = guide_geo.createPolygon(is_closed=False)
+            poly.addVertex(pts[a])
+            poly.addVertex(pts[b])
+
         self.bbox_drawable.setGeometry(guide_geo)
 
     def makePerim(self):
diff --git a/python3.13libs/hc/hclayout.py b/python3.13libs/hc/hclayout.py
index 0d9c157..11def8d 100644
--- a/python3.13libs/hc/hclayout.py
+++ b/python3.13libs/hc/hclayout.py
@@ -66,14 +66,47 @@ class HCLayout:
     def floatingPaneTabTypes(self):
         return [tab.type() for tab in self.floatingPaneTabs()]
 
+    def _initializePanelTabs(self, panel):
+        from .hcpane import HCPane
+        initialized_panes = set()
+        for hou_tab in panel.paneTabs():
+            pane = hou_tab.pane()
+            if pane is None:
+                continue
+            hc_pane = HCPane(pane)
+            if pane.id() not in initialized_panes:
+                try:
+                    hc_pane.initializeObject(pane)
+                except Exception as e:
+                    print(f"[HCLayout] initializeObject() failed for pane: {e!r}")
+                initialized_panes.add(pane.id())
+            else:
+                try:
+                    hc_pane.initializeObject(hou_tab)
+                except Exception as e:
+                    print(f"[HCLayout] initializeObject() failed for {hou_tab.type().name()}: {e!r}")
+
+    def _ensureSceneViewerDetailsTabInPanes(self, panes):
+        """Add a DetailsView tab to the first pane that has a SceneViewer but no DetailsView.
+
+        NOTE: Temporarily disabled -- pane.createTab(DetailsView) on a SceneViewer
+        pane causes SIGSEGV in Houdini 21.0.671 GL layer during startup. This
+        is a Houdini bug, not fixable from Python.
+        """
+        pass
+
     def launchFloating(self, pane_types=None):
         """Open one floating panel per pane type, skipping any already present.
 
         Each floating panel is a separate OS window the WM can tile. The main
-        Houdini window still exists alongside them. Each new pane tab gets
-        initialize() called on its HC* wrapper so per-type defaults can apply.
+        Houdini window still exists alongside them.
+
+        NOTE: Tab initialization (_initializePanelTabs) is intentionally NOT
+        called here -- the PySide6/GL context isn't ready during startup and
+        calling initialize() (which installs Qt event filters) causes SIGSEGV.
+        The caller is responsible for deferring initialization separately.
         """
-        from .hcpane import HCPane
+        import sys
         if pane_types is None:
             pane_types = DEFAULT_FLOATING_PANE_TYPES
         existing = set(self.floatingPaneTabTypes())
@@ -84,25 +117,20 @@ class HCLayout:
             panel = self.desktop().createFloatingPanel(pane_type, immediate=True)
             panel.attachToDesktop(True)
             created.append(panel)
-            initialized_panes = set()
-            for hou_tab in panel.paneTabs():
-                pane = hou_tab.pane()
-                if pane is None:
-                    continue
-                hc_pane = HCPane(pane)
-                if pane.id() not in initialized_panes:
-                    try:
-                        hc_pane.initialize()
-                    except Exception as e:
-                        print(f"[HCLayout] HCPane.initialize() failed: {e!r}")
-                    initialized_panes.add(pane.id())
-                hc_tab = hc_pane.convertTab(hou_tab)
-                try:
-                    hc_tab.initialize()
-                except Exception as e:
-                    print(f"[HCLayout] initialize() failed for {hou_tab.type().name()}: {e!r}")
         return created
 
+    def initializeFloatingPanels(self):
+        """Initialize (HC-wrap) all current floating panel tabs.
+
+        Safe to call deferred after the GL event loop has settled.
+        """
+        import sys
+        for panel in self.floatingPanels():
+            try:
+                self._initializePanelTabs(panel)
+            except Exception as e:
+                print(f"[HCLayout] initializeFloatingPanels failed: {e!r}", file=sys.stderr, flush=True)
+
 
     """ Snapshot """
 
@@ -121,82 +149,3 @@ class HCLayout:
 def launchFloatingLayout(pane_types=None):
     return HCLayout().launchFloating(pane_types)
 
-
-""" Legacy free functions (broken — kept for reference) """
-
-
-def setLayoutQuad(self):
-    session = HCSession()
-    session.clearLayout()
-    session.allTabs()[0].setType("PythonShell")
-    session.allPanes()[0].splitHorizontal()
-    session.allPanes()[0].splitVertical()
-    session.allPanes()[1].splitVertical()
-
-def setLayoutRamp(self):
-    session = HCSession()
-    session.removeEventLoopCallbacks()
-    session.clearLayout()
-    session.allPanes()[0].splitVertical()
-    session.allTabs()[1].setType("Parm")
-    session.allPanes()[1].setSplitRatio(0.3)
-    session.allPanes()[1].createTab()
-
-def setLayoutTriH(self):
-    session = HCSession()
-    session.removeEventLoopCallbacks()
-    session.clearLayout()
-    session.allTabs()[0].setType("PythonShell")
-    session.allPanes()[0].splitHorizontal()
-    session.allPanes()[1].splitHorizontal()
-    session.allPanes()[1].createTab("PythonShell")
-    session.allPanes()[1].allTabs()[0].setIsCurrentTab()
-    session.allPanes()[0].tabs()[0].setType("SceneViewer")
-    session.allPanes()[1].tabs()[0].setType("Parm")
-    session.allPanes()[1].tabs()[1].setType("DetailsView")
-    session.allPanes()[2].tabs()[0].setType("NetworkEditor")
-    session.allPanes()[0].setSplitFraction(0.5)
-    session.prev_pane = self.pane()
-    hou.ui.addEventLoopCallback(triHCallback)
-
-def triHCallback(session):
-    panes = session.panes()
-    pane = session.pane()
-    if str(pane) != str(hou.session.lastPane):
-        hou.session.last_pane = pane
-        if str(pane) == str(panes[1]):
-            pane.setSplitFraction(0.6)
-        elif str(pane) == str(panes[2]):
-            pane.setSplitFraction(0.3)
-    return True
-
-def setLayoutTriV(session):
-    session.removeEventLoopCallbacks()
-    session.clearLayout()
-    session.allPanes()[0].tabs()[0].setType("PythonShell")
-    session.allPanes()[0].splitHorizontal()
-    session.allPanes()[1].splitVertical()
-    session.allPanes()[1].createTab("PythonShell")
-    session.allPanes()[1].allTabs()[0].setIsCurrentTab()
-    session.allPanes()[0].tabs()[0].setType("SceneViewer")
-    session.allPanes()[1].tabs()[0].setType("Parm")
-    session.allPanes()[1].tabs()[1].setType("DetailsView")
-    session.allPanes()[2].tabs()[0].setType("NetworkEditor")
-    session.allPanes()[0].setSplitFraction(0.66)
-    hou.session.last_pane = self.pane()
-    hou.ui.addEventLoopCallback(self.triVCallback)
-
-def triVCallback(session):
-    panes = session.allPanes()
-    pane = session.currentPane()
-    if str(pane) != str(hou.session.last_pane):
-        hou.session.last_pane = pane
-        if str(pane) == str(panes[0]):
-            pane.setSplitFraction(0.7)
-        elif str(pane) == str(panes[1]):
-            panes[0].setSplitFraction(0.4)
-            pane.setSplitFraction(0.33)
-        elif str(pane) == str(panes[2]):
-            panes[0].setSplitFraction(0.4)
-            pane.setSplitFraction(0.66)
-    return True
diff --git a/python3.13libs/hc/hcmaps.py b/python3.13libs/hc/hcmaps.py
index 40c56ae..18b9632 100644
--- a/python3.13libs/hc/hcmaps.py
+++ b/python3.13libs/hc/hcmaps.py
@@ -11,7 +11,11 @@ class HCMaps:
             'Expand Pane':               pane.expand,
             'Floating Parameter Editor': session.floatingParameterEditor,
             'Hide Shelf':                session.hideShelf,
+            'HC Info':                   session.hcInfo,
             'Maximize Pane':             pane.toggleMaximize,
+            'New File':                  session.newFile,
+            'New Floating Pane':         session.newFloatingPaneDialog,
+            'New Tab':                   session.newTabDialog,
             'Next Tab':                  pane.nextTab,
             'Open File':                 session.openFile,
             'Open Preferences':          session.openPreferences,
@@ -24,12 +28,11 @@ class HCMaps:
             'Restart Houdini':           session.restartHoudini,
             # 'Rename Tabs':               session.renameTabs,
             'Save':                      session.save,
-            'Set Type: NetworkEditor':   lambda: self.tab.setType('NetworkEditor'),
-            'Set Type: Parm':            lambda: self.tab.setType('Parm'),
-            'Set Type: PythonShell':     lambda: self.tab.setType('PythonShell'),
-            'Set Type: SceneViewer':     lambda: self.tab.setType('SceneViewer'),
-            'Set Type: Spreadsheet':     lambda: self.tab.setType('DetailsView'),
+            'Save As':                   session.saveAs,
+            'Set Tab Type':              tab.changeTypeDialog,
             'Show Shelf':                session.showShelf,
+            'Split Pane Horizontally':   pane.splitHorizontal,
+            'Split Pane Vertically':     pane.splitVertical,
             'Split Rotate':              pane.splitRotate,
             'Split Swap':                pane.splitSwap,
             'Toggle All Menus':          session.toggleMenus,
@@ -39,6 +42,7 @@ class HCMaps:
             'Toggle Main Menu':          session.toggleMainMenu,
             'Toggle Split Handles':      session.toggleSplitHandles,
             'Refresh Split Handles':     session.refreshSplitHandles,
+            'Toggle Spreadsheet':        session.toggleSpreadsheet,
             'Toggle Window Watcher':     session.toggleWindowWatcher,
             'Toggle Stowbars':           session.toggleStowbars,
             'Toggle Tabs':               pane.toggleTabs,
@@ -49,6 +53,7 @@ class HCMaps:
 
     def tab_map_scene_viewer(self, session, pane, tab):
         return {
+            'Close Scene Viewer Toolbars': tab.closeToolbars,
             'Frame':                tab.frameAllViewports,
             'Home Viewports':       tab.homeAllViewports,
             'Keycam':               tab.keycam,
@@ -79,8 +84,11 @@ class HCMaps:
         return {
             'Deselect All':       tab.deselectAllNodes,
             'Frame All':          tab.frameAll,
+            'Frame HC Cursor':    tab.frameHcnetcursor,
+            'Recook Selection':   tab.recookSelection,
             'Reload Node Shapes': tab.reloadNodeShapes,
             'Rename Node':        tab.renameNode,
+            'Replace Node':       tab.replaceNode,
             'Set Node Colors':    tab.setNodeColors,
             'Set Node Shapes':    tab.setNodeShapes,
             'Show Path Message':  tab.showPathMessage,
diff --git a/python3.13libs/hc/hcmenuutils.py b/python3.13libs/hc/hcmenuutils.py
index 9b31488..5c298d7 100644
--- a/python3.13libs/hc/hcmenuutils.py
+++ b/python3.13libs/hc/hcmenuutils.py
@@ -20,6 +20,7 @@ def setVector3(kwargs, x, y, z):
     node.setParmExpressions({name0: x, name1: y, name2: z})
 
 def vector3XMaster(kwargs):
+    parms = kwargs['parms']
     name0 = parms[0].name()
     name1 = parms[1].name()
     name2 = parms[2].name()
diff --git a/python3.13libs/hc/hcnetworkeditor.py b/python3.13libs/hc/hcnetworkeditor.py
index 2bc4128..1641fb5 100644
--- a/python3.13libs/hc/hcnetworkeditor.py
+++ b/python3.13libs/hc/hcnetworkeditor.py
@@ -1,13 +1,17 @@
-import hou, math, types
+import hou, math, os, types
 from .hcpathtab import HCPathTab
 from .hcsettings import HCSettings
 
 
-# Per-network selection-extension paths, keyed by pwd path.
-# Each entry is a list of (node_path, arrived_via_direction) tuples from
-# the anchor (index 0, direction=None) to the head (index -1).
-_selection_paths = {}
-_OPPOSITE = {'up': 'down', 'down': 'up', 'left': 'right', 'right': 'left'}
+# hcnetcursor state, keyed by (pane id, pwd path).
+_hcnetcursors = {}
+# Last drawn overlay signature per key, so a redraw is skipped when nothing
+# visible changed. See updateCurrentNodeOverlay().
+_overlay_signatures = {}
+# Last grid step mirrored onto each pane's gridxstep/gridystep prefs.
+_grid_pref_sync = {}
+# hcnetcursor_NxN.png presence, resolved once per path.
+_cursor_asset_exists = {}
 
 
 class HCNetworkEditor(HCPathTab):
@@ -21,6 +25,29 @@ class HCNetworkEditor(HCPathTab):
         # Show the grid by default in hc-spawned network editors.
         self.hou_tab.setPref('gridmode', '2')
 
+        # Disable node previews in COP networks by default.
+        pwd = self.hou_tab.pwd()
+        if hasattr(pwd, 'childCat') and pwd.childCat() == 'Cop':
+            # Set the global preference for new nodes
+            try:
+                hou.pref.set('neteditor.cop_preview', False)
+            except (AttributeError, hou.Error):
+                pass
+            
+            # Disable previews for existing nodes in this network
+            for node in self.hou_tab.pwd().children():
+                try:
+                    node.setGenericFlag(hou.nodeFlag.Thumbnail, False)
+                except (AttributeError, hou.Error):
+                    pass
+
+        self._syncHcnetcursor()
+
+    def run(self, label, method_name, *args, **kwargs):
+        """Call a method by name and flash the label in the network editor."""
+        getattr(self, method_name)(*args, **kwargs)
+        self.hou_tab.flashMessage(None, label, 1.0)
+
 
     """ Navigation """
 
@@ -77,12 +104,7 @@ class HCNetworkEditor(HCPathTab):
         return None
 
     def navigateDirection(self, direction):
-        target = self._traversalTarget(direction)
-        if target is not None:
-            target.setSelected(True)
-            self.hou_tab.setCurrentNode(target)
-            self._frameSelectionInView()
-            self.updateCurrentNodeOverlay()
+        self.moveHcnetcursor(direction)
 
     def _frameSelectionInView(self, margin_frac=0.1):
         """Pan or zoom the view so every selected node is visible with a
@@ -134,16 +156,12 @@ class HCNetworkEditor(HCPathTab):
             view.translate(hou.Vector2(dx, dy))
             self.setBounds(view)
 
-    def updateCurrentNodeOverlay(self):
-        """Draw an arrow at the viewport edge pointing at the current node if
-        it is off-screen. Clears any overlay when the current node is in
-        view or there is no current node. Uses hou.NetworkShape primitives
-        so the arrow lives on the native overlay layer."""
+    def _currentNodeOverlayShapes(self):
+        """Return an arrow pointing at the current node when it is off-screen."""
         ed = self.hou_tab
         current = ed.currentNode()
         if current is None or current.parent() != ed.pwd():
-            ed.setOverlayShapes([])
-            return
+            return []
 
         rect = ed.itemRect(current)
         node_center = ed.posToScreen(hou.Vector2(rect.center()[0],
@@ -154,14 +172,12 @@ class HCNetworkEditor(HCPathTab):
 
         if (margin <= node_center[0] <= w - margin
                 and margin <= node_center[1] <= h - margin):
-            ed.setOverlayShapes([])
-            return
+            return []
 
         cx, cy = w / 2.0, h / 2.0
         dx, dy = node_center[0] - cx, node_center[1] - cy
         if dx == 0.0 and dy == 0.0:
-            ed.setOverlayShapes([])
-            return
+            return []
 
         tvals = []
         if dx != 0.0:
@@ -175,15 +191,14 @@ class HCNetworkEditor(HCPathTab):
                 if tt > 0:
                     tvals.append(tt)
         if not tvals:
-            ed.setOverlayShapes([])
-            return
+            return []
         t = min(min(tvals), 1.0)
         tip_x, tip_y = cx + t * dx, cy + t * dy
 
         angle = math.atan2(dy, dx)
         # Dimensions for the triangle
-        length = 18.0
-        width_ratio = 0.6  # Half-width relative to length
+        length = 28.0
+        width_ratio = 0.7  # Half-width relative to length
         
         # Calculate triangle vertices
         # tip is at (tip_x, tip_y)
@@ -199,13 +214,335 @@ class HCNetworkEditor(HCPathTab):
         )
 
         color = hou.Color((0.38, 0.56, 0.56))
-        width = 2.0
-        shapes = [
+        width = 3.0
+        return [
             hou.NetworkShapeLine(p1, p2, color=color, alpha=1.0, width=width, screen_space=True, smooth=True),
             hou.NetworkShapeLine(p2, p3, color=color, alpha=1.0, width=width, screen_space=True, smooth=True),
             hou.NetworkShapeLine(p3, p1, color=color, alpha=1.0, width=width, screen_space=True, smooth=True),
         ]
-        ed.setOverlayShapes(shapes)
+
+    def _hcnetcursorKey(self):
+        pane = self.hou_tab.pane()
+        pane_id = pane.id() if pane is not None else id(self.hou_tab)
+        return (pane_id, self.hou_tab.pwd().path())
+
+    def _gridStep(self):
+        """Return grid step from HCSettings, mirroring it onto the pane pref."""
+        ng = HCSettings().nodeGraph()
+        x = max(ng.get("grid_x_step", 2.0), 0.25)
+        y = max(ng.get("grid_y_step", 1.0), 0.25)
+        # Mirror onto the pane pref, but only when it actually changed --
+        # this runs on every network editor UI event, and setPref is a HOM
+        # write that is not free.
+        key = self._hcnetcursorKey()
+        if _grid_pref_sync.get(key) != (x, y):
+            try:
+                self.hou_tab.setPref("gridxstep", str(x))
+                self.hou_tab.setPref("gridystep", str(y))
+                _grid_pref_sync[key] = (x, y)
+            except hou.Error:
+                pass
+        return hou.Vector2(x, y)
+
+    def _nodeOffset(self):
+        """Return node center offset (pos + offset = center)."""
+        ng = HCSettings().nodeGraph()
+        return hou.Vector2(
+            ng.get("node_center_offset_x", 0.5),
+            ng.get("node_center_offset_y", 0.15),
+        )
+
+    def _initialHcnetcursorState(self):
+        step = self._gridStep()
+        anchor = None
+        current = self.hou_tab.currentNode()
+        if current is not None and current.parent() == self.hou_tab.pwd():
+            anchor = current.position()
+        if anchor is None:
+            nearest = self._nearestNodeToViewportCenter()
+            if nearest is not None:
+                anchor = nearest.position()
+        if anchor is None:
+            anchor = self.bounds().center()
+        center = self._snapToGrid(anchor)
+        origin = hou.Vector2(center[0] - step[0] * 0.5,
+                             center[1] - step[1] * 0.5)
+        return {
+            "origin": (origin[0], origin[1]),
+            "grow_x": 0,
+            "grow_y": 0,
+        }
+
+    def _hcnetcursorRectFromState(self, state):
+        if "origin" not in state and "center" not in state:
+            return hou.BoundingRect(
+                state['min'][0], state['min'][1],
+                state['max'][0], state['max'][1],
+            )
+
+        step = self._gridStep()
+        grow_x = int(state.get("grow_x", 0))
+        grow_y = int(state.get("grow_y", 0))
+        if "origin" in state:
+            origin = hou.Vector2(state["origin"][0], state["origin"][1])
+        else:
+            center = hou.Vector2(state["center"][0], state["center"][1])
+            origin = hou.Vector2(center[0] - step[0] * 0.5,
+                                 center[1] - step[1] * 0.5)
+
+        min_x = origin[0]
+        max_x = origin[0] + step[0]
+        min_y = origin[1]
+        max_y = origin[1] + step[1]
+
+        if grow_x > 0:
+            max_x += step[0] * grow_x
+        elif grow_x < 0:
+            min_x += step[0] * grow_x
+
+        if grow_y > 0:
+            max_y += step[1] * grow_y
+        elif grow_y < 0:
+            min_y += step[1] * grow_y
+
+        return hou.BoundingRect(min_x, min_y, max_x, max_y)
+
+    def _hcnetcursorState(self):
+        key = self._hcnetcursorKey()
+        state = _hcnetcursors.get(key)
+        if state is None:
+            state = self._initialHcnetcursorState()
+            _hcnetcursors[key] = state
+        elif "origin" not in state:
+            rect = self._hcnetcursorRectFromState(state)
+            min_v = rect.min()
+            state = {
+                "origin": (min_v[0], min_v[1]),
+                "grow_x": 0,
+                "grow_y": 0,
+            }
+            _hcnetcursors[key] = state
+        return dict(state)
+
+    def _setHcnetcursorState(self, state):
+        key = self._hcnetcursorKey()
+        _hcnetcursors[key] = {
+            "origin": (state["origin"][0], state["origin"][1]),
+            "grow_x": int(state.get("grow_x", 0)),
+            "grow_y": int(state.get("grow_y", 0)),
+        }
+
+    def _snapCursorEdgeDown(self, value, step):
+        return math.floor((value + step * 0.5) / step) * step - step * 0.5
+
+    def _snapCursorEdgeUp(self, value, step):
+        return math.ceil((value - step * 0.5) / step) * step + step * 0.5
+
+    def _selectedNodesRect(self):
+        selected = list(self.hou_tab.pwd().selectedChildren())
+        if not selected:
+            return None
+
+        rect = self.hou_tab.itemRect(selected[0])
+        for node in selected[1:]:
+            rect.enlargeToContain(self.hou_tab.itemRect(node))
+        return rect
+
+    def fitHcnetcursorToSelectedNodes(self):
+        rect = self._selectedNodesRect()
+        if rect is None:
+            return
+
+        step = self._gridStep()
+        min_v = rect.min()
+        max_v = rect.max()
+        snapped_min_x = self._snapCursorEdgeDown(min_v[0], step[0])
+        snapped_max_x = self._snapCursorEdgeUp(max_v[0], step[0])
+        snapped_min_y = self._snapCursorEdgeDown(min_v[1], step[1])
+        snapped_max_y = self._snapCursorEdgeUp(max_v[1], step[1])
+
+        width_cells = max(1, int(round((snapped_max_x - snapped_min_x) / step[0])))
+        height_cells = max(1, int(round((snapped_max_y - snapped_min_y) / step[1])))
+
+        self._setHcnetcursorState({
+            "origin": (snapped_min_x, snapped_min_y),
+            "grow_x": width_cells - 1,
+            "grow_y": height_cells - 1,
+        })
+        self.updateCurrentNodeOverlay()
+
+    def selectedNodesEnvelopeSignature(self):
+        selected = list(self.hou_tab.pwd().selectedChildren())
+        if not selected:
+            return ()
+
+        rect = self.hou_tab.itemRect(selected[0])
+        for node in selected[1:]:
+            rect.enlargeToContain(self.hou_tab.itemRect(node))
+
+        min_v = rect.min()
+        max_v = rect.max()
+        return (
+            tuple(sorted(node.path() for node in selected)),
+            round(min_v[0], 4),
+            round(min_v[1], 4),
+            round(max_v[0], 4),
+            round(max_v[1], 4),
+        )
+
+    def hcnetcursorRect(self):
+        return self._hcnetcursorRectFromState(self._hcnetcursorState())
+
+    def _frameRectInView(self, rect, margin_frac=0.1):
+        view = self.hou_tab.visibleBounds()
+        view_size = view.size()
+        margin = hou.Vector2(view_size[0] * margin_frac,
+                             view_size[1] * margin_frac)
+        framed = hou.BoundingRect(rect.min()[0], rect.min()[1],
+                                  rect.max()[0], rect.max()[1])
+        framed.expand(margin)
+
+        needed = framed.size()
+        if needed[0] > view_size[0] or needed[1] > view_size[1]:
+            aspect = view_size[0] / view_size[1]
+            nw, nh = needed[0], needed[1]
+            if nw / nh > aspect:
+                nh = nw / aspect
+            else:
+                nw = nh * aspect
+            center = framed.center()
+            self.setBounds(hou.BoundingRect(
+                center[0] - nw / 2, center[1] - nh / 2,
+                center[0] + nw / 2, center[1] + nh / 2,
+            ))
+            return
+
+        vmin, vmax = view.min(), view.max()
+        rmin, rmax = framed.min(), framed.max()
+        dx = 0.0
+        if rmin[0] < vmin[0]:
+            dx = rmin[0] - vmin[0]
+        elif rmax[0] > vmax[0]:
+            dx = rmax[0] - vmax[0]
+        dy = 0.0
+        if rmin[1] < vmin[1]:
+            dy = rmin[1] - vmin[1]
+        elif rmax[1] > vmax[1]:
+            dy = rmax[1] - vmax[1]
+        if dx or dy:
+            view.translate(hou.Vector2(dx, dy))
+            self.setBounds(view)
+
+    def _hcnetcursorOverlayShapes(self):
+        return []
+
+    def _hcnetcursorImagePath(self):
+        hc_path = hou.getenv("HC_PATH")
+        if not hc_path:
+            hc_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
+        return os.path.join(hc_path, "config", "hcnetcursor_assets")
+
+    def _hcnetcursorCellSize(self):
+        state = self._hcnetcursorState()
+        width_cells = 1 + abs(int(state.get("grow_x", 0)))
+        height_cells = 1 + abs(int(state.get("grow_y", 0)))
+        return width_cells, height_cells
+
+    def _hcnetcursorImageFile(self):
+        asset_dir = self._hcnetcursorImagePath()
+        width_cells, height_cells = self._hcnetcursorCellSize()
+        width_cells = min(width_cells, 12)
+        height_cells = min(height_cells, 12)
+        candidate = os.path.join(
+            asset_dir,
+            f"hcnetcursor_{width_cells}x{height_cells}.png",
+        )
+        # Runs per UI event; the asset set does not change while Houdini is up.
+        exists = _cursor_asset_exists.get(candidate)
+        if exists is None:
+            exists = os.path.exists(candidate)
+            _cursor_asset_exists[candidate] = exists
+        if exists:
+            return candidate
+
+        return os.path.join(asset_dir, "hcnetcursor_1x1.png")
+
+    def _backgroundImagesWithoutHcnetcursor(self):
+        asset_dir = self._hcnetcursorImagePath()
+        images = []
+        for image in self.hou_tab.backgroundImages():
+            path = image.path()
+            if not (path.startswith(asset_dir) and os.path.basename(path).startswith("hcnetcursor_")):
+                images.append(image)
+        return images
+
+    def _updateHcnetcursorBackground(self, rect, cursor_path):
+        images = self._backgroundImagesWithoutHcnetcursor()
+
+        cursor_image = hou.NetworkImage(cursor_path, rect)
+        cursor_image.setBrightness(1.0)
+        cursor_image.setRelativeToPath("")
+        images.append(cursor_image)
+        self.hou_tab.setBackgroundImages(tuple(images))
+
+    def _overlaySignature(self, rect, cursor_path):
+        """Everything the drawn overlay depends on, as a hashable tuple.
+
+        The cursor box (a background image) moves with its rect; the
+        off-screen arrow moves with the current node's *screen* position, so
+        panning and zooming change it even when nothing in the scene did.
+        Screen coordinates are rounded to whole pixels -- sub-pixel drift is
+        not worth a redraw.
+        """
+        if rect is None:
+            box = None
+        else:
+            rmin, rmax = rect.min(), rect.max()
+            box = (round(rmin[0], 4), round(rmin[1], 4),
+                   round(rmax[0], 4), round(rmax[1], 4), cursor_path)
+
+        ed = self.hou_tab
+        current = ed.currentNode()
+        if current is None or current.parent() != ed.pwd():
+            arrow = None
+        else:
+            center = ed.itemRect(current).center()
+            screen = ed.posToScreen(hou.Vector2(center[0], center[1]))
+            size = ed.screenBounds().size()
+            arrow = (current.path(), round(screen[0]), round(screen[1]),
+                     round(size[0]), round(size[1]))
+
+        return (box, arrow)
+
+    def updateCurrentNodeOverlay(self, force=False):
+        """Redraw the hcnetcursor box and off-screen arrow.
+
+        nodegraphhooks calls this from the network editor event handler, so it
+        runs on every mouse move. Rebuilding the background image list and
+        calling redraw() unconditionally made panning visibly stutter; skip
+        the work when the resulting picture would be identical.
+        """
+        enabled = HCSettings().hcnetcursorEnabled()
+        rect = self.hcnetcursorRect() if enabled else None
+        cursor_path = self._hcnetcursorImageFile() if enabled else None
+
+        key = self._hcnetcursorKey()
+        signature = self._overlaySignature(rect, cursor_path)
+        if not force and _overlay_signatures.get(key) == signature:
+            return
+        _overlay_signatures[key] = signature
+
+        if enabled:
+            self._updateHcnetcursorBackground(rect, cursor_path)
+        else:
+            self.hou_tab.setBackgroundImages(tuple(self._backgroundImagesWithoutHcnetcursor()))
+        self.hou_tab.setShapes(self._hcnetcursorOverlayShapes())
+        self.hou_tab.setOverlayShapes(self._currentNodeOverlayShapes())
+        self.hou_tab.redraw()
+
+    def refreshHcnetcursor(self):
+        """Reapply the cursor box and overlay without changing its state."""
+        self.updateCurrentNodeOverlay(force=True)
 
     def _nearestNodeToViewportCenter(self):
         bounds = self.bounds()
@@ -222,57 +559,95 @@ class HCNetworkEditor(HCPathTab):
                 best_dist = dist
         return best
 
+    def _syncHcnetcursor(self):
+        rect = self.hcnetcursorRect()
+        parent = self.hou_tab.pwd()
+        selected = []
+
+        rect_center = rect.center()
+        for node in parent.children():
+            inside = rect.intersects(self.hou_tab.itemRect(node))
+            node.setSelected(inside)
+            if inside:
+                selected.append(node)
+
+        if selected:
+            target = min(
+                selected,
+                key=lambda node: (
+                    self.hou_tab.itemRect(node).center()[0] - rect_center[0]
+                ) ** 2 + (
+                    self.hou_tab.itemRect(node).center()[1] - rect_center[1]
+                ) ** 2,
+            )
+            self.hou_tab.setCurrentNode(target, pick_node=False)
 
-    """ Selection """
+        self._frameRectInView(rect)
+        self.updateCurrentNodeOverlay()
 
+    def moveHcnetcursor(self, direction, distance=1):
+        state = self._hcnetcursorState()
+        step = self._gridStep()
+        origin = hou.Vector2(state["origin"][0], state["origin"][1])
+        delta = {
+            'up': hou.Vector2(0, step[1] * distance),
+            'down': hou.Vector2(0, step[1] * distance * -1),
+            'left': hou.Vector2(step[0] * distance * -1, 0),
+            'right': hou.Vector2(step[0] * distance, 0),
+        }[direction]
+        origin += delta
+        state["origin"] = (origin[0], origin[1])
+        self._setHcnetcursorState(state)
+        self._syncHcnetcursor()
+
+    def moveHcnetcursorToPosition(self, position):
+        """Move the hcnetcursor to a network-space position, snapped to grid."""
+        step = self._gridStep()
+        snapped = self._snapToGrid(position)
+        origin = hou.Vector2(snapped[0] - step[0] * 0.5,
+                             snapped[1] - step[1] * 0.5)
+        self._setHcnetcursorState({
+            "origin": (origin[0], origin[1]),
+            "grow_x": 0,
+            "grow_y": 0,
+        })
+        self._syncHcnetcursor()
+
+    def translateHcnetcursorByDelta(self, delta):
+        state = self._hcnetcursorState()
+        origin = hou.Vector2(state["origin"][0], state["origin"][1])
+        origin += delta
+        state["origin"] = (origin[0], origin[1])
+        self._setHcnetcursorState(state)
+        self.updateCurrentNodeOverlay()
 
-    def addToSelection(self, direction):
-        """Extend or contract a selection chain anchored at the current node.
-        Mirrors text-editor shift+arrow behavior: each press walks the head
-        by one hop via wires; pressing the opposite direction contracts
-        (deselects) the tail node. The path auto-resets whenever the
-        current node no longer matches the anchor."""
-        parent = self.hou_tab.pwd()
-        current = self.hou_tab.currentNode()
-        
-        # If no current node, find the nearest one to start the selection
-        if current is None:
-            current = self._nearestNodeToViewportCenter()
-            if current:
-                current.setSelected(True)
-                self.hou_tab.setCurrentNode(current)
-            return
+    def expandHcnetcursor(self, direction, distance=1):
+        state = self._hcnetcursorState()
+        if direction == 'right':
+            state["grow_x"] += distance
+        elif direction == 'left':
+            state["grow_x"] -= distance
+        elif direction == 'up':
+            state["grow_y"] += distance
+        elif direction == 'down':
+            state["grow_y"] -= distance
+        self._setHcnetcursorState(state)
+        self._syncHcnetcursor()
 
-        key = parent.path()
-        path = _selection_paths.get(key, [])
-
-        # Reset if stale: anchor must still be the current node, and every
-        # node referenced in the path must still exist.
-        if (not path
-                or path[0][0] != current.path()
-                or any(hou.node(p) is None for p, _ in path)):
-            path = [(current.path(), None)]
-
-        if len(path) >= 2 and _OPPOSITE.get(path[-1][1]) == direction:
-            head = hou.node(path[-1][0])
-            if head is not None:
-                head.setSelected(False)
-            path.pop()
-            _selection_paths[key] = path
-            return
 
-        head = hou.node(path[-1][0])
-        target = self._traversalTarget(direction, from_node=head)
-        if target is None:
-            return
-        if any(p == target.path() for p, _ in path):
-            return
+    def resetHcnetcursorSize(self):
+        state = self._hcnetcursorState()
+        state["grow_x"] = 0
+        state["grow_y"] = 0
+        self._setHcnetcursorState(state)
+        self._syncHcnetcursor()
+
+
+    """ Selection """
 
-        current.setSelected(True)
-        target.setSelected(True)
-        path.append((target.path(), direction))
-        _selection_paths[key] = path
-        self._frameSelectionInView()
+
+    def addToSelection(self, direction):
+        self.expandHcnetcursor(direction)
 
     def currentNode(self):
         return self.hou_tab.currentNode()
@@ -285,6 +660,26 @@ class HCNetworkEditor(HCPathTab):
         for node in nodes:
             node.setSelected(True)
 
+    def recookSelection(self):
+        nodes = list(self.hou_tab.pwd().selectedChildren())
+        if not nodes:
+            hou.ui.setStatusMessage("No selected nodes to recook",
+                                    hou.severityType.Warning)
+            return
+
+        cooked = 0
+        for node in nodes:
+            try:
+                node.cook(force=True)
+                cooked += 1
+            except hou.Error as e:
+                hou.ui.setStatusMessage(f"Failed to recook {node.path()}: {e}",
+                                        hou.severityType.Error)
+                return
+
+        hou.ui.setStatusMessage(f"Recooked {cooked} selected node(s)",
+                                hou.severityType.Message)
+
 
     """ Connectivity """
 
@@ -398,71 +793,143 @@ class HCNetworkEditor(HCPathTab):
         self.updateCurrentNodeOverlay()
 
 
-    """ Legacy / broken connectivity stubs """
-
-
-    def chunk(self):
-        search_nodes = [self.currentNode()]
-        tree = []
-        search = True
-        while search:
-            if len(search_nodes) == 0:
-                break
-            for node in search_nodes:
-                tree.append(node)
-                search_nodes.remove(node)
-                input_nodes = node.inputs()
-                output_nodes = node.outputs()
-                search_nodes.append(input_nodes)
-                search_nodes.append(output_nodes)
-            continue
-        return tree
-
-
-
-    def chunks(self):
-        nodes = self.network_node.children()
-        # for node in nodes:
-
-
-
     """ Flags """
 
 
     def setDisplayFlag(self):
-        self.currentNode().setDisplayFlag(True)
-        self.currentNode().setRenderFlag(True)
+        node = self.currentNode()
+        if node is None:
+            return
+        node.setDisplayFlag(True)
+        node.setRenderFlag(True)
 
     def toggleBypassFlag(self):
-        self.currentNode().bypass(not self.currentNode().isBypassed())
+        selected = list(self.hou_tab.pwd().selectedChildren())
+        if not selected:
+            current = self.currentNode()
+            if current is None:
+                return
+            selected = [current]
+
+        bypass_state = not any(node.isBypassed() for node in selected)
+        for node in selected:
+            node.bypass(bypass_state)
 
     def toggleTemplateFlag(self):
-        self.currentNode().setTemplateFlag(not self.currentNode().isTemplateFlagSet())
+        node = self.currentNode()
+        if node is None:
+            return
+        node.setTemplateFlag(not node.isTemplateFlagSet())
 
 
     """ Objects and node control """
 
 
+    def _snapToGrid(self, position):
+        step = self._gridStep()
+        return hou.Vector2(
+            round(position[0] / step[0]) * step[0],
+            round(position[1] / step[1]) * step[1],
+        )
+
     def snapToGrid(self, node):
+        # In this Houdini version, node.position() is the bottom-left corner.
+        # Standard node size discovered via RPC is 1.0 x 0.3.
+        # Center offset is configurable via node_graph.node_center_offset_x/y.
+        offset = self._nodeOffset()
         pos = node.position()
-        pos[0] = round(pos[0] - 0.5) + 0.5
-        pos[1] = round(pos[1] - 0.85) + 0.85
-        node.setPosition(pos)
-
-    def translateSelectedNodes(self, direction):
-        for node in self.selectedNodes():
-            self.snapToGrid(node)
-            pos = node.position()
-            # do the move
-            if direction == 'up':
-                pos[1] += 1
-            elif direction == 'down':
-                pos[1] -= 1
-            elif direction == 'left':
-                pos[0] -= 1
-            elif direction == 'right':
-                pos[0] += 1
-            node.setPosition(pos)
+        center = pos + offset
+        G = self._snapToGrid(center)
+        # To put center at G, set pos to G - offset
+        node.setPosition(G - offset)
+
+    def _isOnGrid(self, pos, tol=1e-6):
+        # Check if the center (pos + offset) is on the grid
+        center = pos + self._nodeOffset()
+        snapped = self._snapToGrid(center)
+        return abs(center[0] - snapped[0]) < tol and abs(center[1] - snapped[1]) < tol
+
+    def _outputDescendants(self, node, visited=None):
+        if visited is None:
+            visited = set()
+        visited.add(node)
+        for output in node.outputs():
+            if output not in visited:
+                self._outputDescendants(output, visited)
+        return visited
+
+    def translateSelectedNodes(self, direction, hierarchy=None):
+        step = self._gridStep()
+        delta = {
+            'up': hou.Vector2(0, step[1]),
+            'down': hou.Vector2(0, -step[1]),
+            'left': hou.Vector2(-step[0], 0),
+            'right': hou.Vector2(step[0], 0),
+        }[direction]
+        
+        # Determine nodes to move: Houdini selection, or nodes under the HC cursor
+        hou_selection = list(self.hou_tab.pwd().selectedChildren())
+        if not hou_selection:
+            rect = self.hcnetcursorRect()
+            nodes_to_move = set()
+            for node in self.hou_tab.pwd().children():
+                if rect.contains(self.hou_tab.itemRect(node).center()):
+                    nodes_to_move.add(node)
+        else:
+            nodes_to_move = set(hou_selection)
+
+        if not nodes_to_move:
+            return
+
+        final_nodes = set(nodes_to_move)
+        if hierarchy == 'ancestors':
+            for node in nodes_to_move:
+                final_nodes.update(node.inputAncestors())
+        elif hierarchy == 'descendants':
+            for node in nodes_to_move:
+                final_nodes.update(self._outputDescendants(node))
+
+        before_rect = self._selectedNodesRect()
+        moved_any = False
+        offset = self._nodeOffset()
+        for hou_node in final_nodes:
+            pos = hou_node.position()
+            center = pos + offset
+            G = self._snapToGrid(center)
+
+            if not self._isOnGrid(pos):
+                hou_node.setPosition(G - offset)
+            else:
+                hou_node.setPosition(G + delta - offset)
+            moved_any = True
+        if moved_any:
+            after_rect = self._selectedNodesRect()
+            if before_rect is None or after_rect is None:
+                self.updateCurrentNodeOverlay()
+                return
+
+            before_min = before_rect.min()
+            after_min = after_rect.min()
+            actual_delta = hou.Vector2(
+                after_min[0] - before_min[0],
+                after_min[1] - before_min[1],
+            )
+
+            before_size = before_rect.size()
+            after_size = after_rect.size()
+            size_changed = (
+                abs(after_size[0] - before_size[0]) > 1e-6 or
+                abs(after_size[1] - before_size[1]) > 1e-6
+            )
+
+            # Preserve expanded cursor size when only translating; refit only if
+            # the selection envelope actually changed size.
+            if size_changed:
+                self.fitHcnetcursorToSelectedNodes()
+            elif abs(actual_delta[0]) > 1e-6 or abs(actual_delta[1]) > 1e-6:
+                self.translateHcnetcursorByDelta(actual_delta)
+            else:
+                self.updateCurrentNodeOverlay()
 
 
     """ Viewport """
@@ -474,9 +941,90 @@ class HCNetworkEditor(HCPathTab):
     def cursorPosition(self):
         return self.hou_tab.cursorPosition()
 
+    def hcnetcursorCenter(self):
+        return self.hcnetcursorRect().center()
+
+    def pasteAtCursor(self):
+        """Paste nodes from clipboard centered on the hc cursor position."""
+        cursor = self.hcnetcursorCenter()
+        self.hou_tab.pwd().pasteItemsFromClipboard(cursor)
+
+    def zoomPivot(self):
+        node_graph = HCSettings().prefs().get("node_graph", {})
+        behavior = node_graph.get("zoom_center", "mouse_cursor")
+        if behavior == "hc_cursor":
+            return self.hcnetcursorCenter()
+        return self.cursorPosition()
+
     def frameAll(self):
         self.hou_tab.requestZoomReset()
 
+    def frameCurrentComponent(self):
+        """Frame the connected component containing selected nodes, but only
+        if all selected nodes belong to a single component and no nodes from
+        other components are selected."""
+        components = self.connectedComponents()
+        if not components:
+            return
+        selected_set = set(self.hou_tab.pwd().selectedChildren())
+        if not selected_set:
+            return
+        # Find the component(s) that intersect the selection.
+        touched = [c for c in components if set(c) & selected_set]
+        if len(touched) != 1:
+            return
+        # Verify the selection is a subset of that component
+        # (no nodes from other components selected).
+        if not selected_set.issubset(set(touched[0])):
+            return
+        # Select the full component.
+        component = touched[0]
+        for c in components:
+            for n in c:
+                n.setSelected(n in component)
+        # Frame using Houdini's built-in fit, which centers properly.
+        self.hou_tab.homeToSelection()
+
+    def centerOnCursor(self):
+        """Pan the viewport so the hc cursor is centered, preserving zoom."""
+        cursor = self.hcnetcursorCenter()
+        view = self.hou_tab.visibleBounds()
+        view_center = view.center()
+        delta = cursor - view_center
+        view.translate(delta)
+        self.setBounds(view)
+
+    def frameHcnetcursor(self, margin_cells=1.0, min_view_cells=(6.0, 4.0)):
+        """Zoom/pan the viewport to the hcnetcursor, ignoring node selection."""
+        rect = self.hcnetcursorRect()
+        step = self._gridStep()
+        size = rect.size()
+        view = self.hou_tab.visibleBounds()
+        view_size = view.size()
+        aspect = view_size[0] / view_size[1] if view_size[1] else 1.0
+
+        width = max(
+            size[0] + (step[0] * margin_cells * 2.0),
+            step[0] * min_view_cells[0],
+        )
+        height = max(
+            size[1] + (step[1] * margin_cells * 2.0),
+            step[1] * min_view_cells[1],
+        )
+
+        if width / height > aspect:
+            height = width / aspect
+        else:
+            width = height * aspect
+
+        center = rect.center()
+        self.setBounds(hou.BoundingRect(
+            center[0] - width / 2.0,
+            center[1] - height / 2.0,
+            center[0] + width / 2.0,
+            center[1] + height / 2.0,
+        ))
+
     def screenSize(self):
         return self.hou_tab.screenBounds().size()
 
@@ -502,15 +1050,18 @@ class HCNetworkEditor(HCPathTab):
         factor = 1.0 - amount if direction == 'in' else 1.0 + amount
         bounds = self.bounds()
         if pivot is None:
-            pivot = bounds.center()
+            pivot = self.zoomPivot()
         bounds.translate(pivot * -1)
         bounds.scale((factor, factor))
         bounds.translate(pivot)
         self.setBounds(bounds)
 
     def zoomLevel(self):
-        zoomlevel = self.size()[0] / self.size()[0]
-        return zoomlevel
+        """Network units per screen pixel horizontally. Larger when zoomed out."""
+        screen_width = self.screenSize()[0]
+        if not screen_width:
+            return 1.0
+        return self.size()[0] / screen_width
 
 
     """ Interface"""
@@ -526,11 +1077,6 @@ class HCNetworkEditor(HCPathTab):
     def showPathMessage(self):
         self.hou_tab.flashMessage(image=None, message=self.path(), duration=1)
 
-    # def showRadialMenu(self):
-        # from .hcradialutils import editorRadialMain
-        # menu = editorRadialMain()
-        # self.hou_tab.displayRadialMenu("hc_editor_radial_menu")
-
     def toggleDimUnusedNodes(self):
         map = {
             '0': '1',
@@ -557,6 +1103,90 @@ class HCNetworkEditor(HCPathTab):
         self.hou_tab.setPref('showmenu', map[mode])
 
 
+    """ Node replacement """
+
+
+    def replaceNode(self):
+        """Open a fuzzy-search node type picker and replace the single
+        selected node with the chosen type, preserving position and wiring."""
+        parent = self.hou_tab.pwd()
+        selected = list(parent.selectedChildren())
+
+        if len(selected) != 1:
+            hou.ui.setStatusMessage("Select exactly one node to replace",
+                                    hou.severityType.Warning)
+            return
+
+        old_node = selected[0]
+        context = old_node.type().category().name()
+
+        # Collect all node types in the same category context
+        node_types = []
+        try:
+            categories = hou.nodeTypeCategories()
+            if context in categories:
+                node_types = list(categories[context].nodeTypes().values())
+        except hou.Error as e:
+            hou.ui.setStatusMessage(f"Cannot list node types: {e}",
+                                    hou.severityType.Error)
+            return
+
+        if not node_types:
+            hou.ui.setStatusMessage("No node types found",
+                                    hou.severityType.Error)
+            return
+
+        def do_replace(name):
+            if not name or name == old_node.type().name():
+                return
+            try:
+                new_node = parent.createNode(name)
+            except Exception as e:
+                hou.ui.setStatusMessage(f"Cannot create {name}: {e}",
+                                        hou.severityType.Error)
+                return
+
+            # Copy position
+            new_node.setPosition(old_node.position())
+
+            # Rewire: connect old_node's inputs to new_node
+            for i, input_node in enumerate(old_node.inputs()):
+                if input_node is not None:
+                    new_node.setInput(i, input_node)
+
+            # Rewire: connect old_node's outputs to new_node
+            for output_node in list(old_node.outputs()):
+                for conn in output_node.inputConnections():
+                    if conn.inputNode() == old_node:
+                        output_node.setInput(conn.inputIndex(), new_node)
+
+            # Copy display/render flags
+            new_node.setDisplayFlag(old_node.isDisplayFlagSet())
+            new_node.setRenderFlag(old_node.isRenderFlagSet())
+            new_node.bypass(old_node.isBypassed())
+            new_node.setTemplateFlag(old_node.isTemplateFlagSet())
+            new_node.setColor(old_node.color())
+
+            old_node.destroy()
+            new_node.setCurrent(True, clear_all_selected=True)
+            hou.ui.setStatusMessage(f"Replaced with {name}",
+                                    hou.severityType.Message)
+
+        # Use description (English label) for display, name for creation
+        labeled = [(nt.description(), nt.name()) for nt in node_types]
+        labeled.sort(key=lambda x: x[0])
+        list_dict = {label: (lambda n=name: do_replace(n))
+                     for label, name in labeled}
+
+        from .hcwidgets import HCWidgets
+        anchor_geometry = self.hou_tab.pane().qtScreenGeometry()
+        dialog = HCWidgets.SelectionDialog('Replace Node', list_dict,
+                                           anchor_geometry=anchor_geometry)
+        dialog.show()
+        dialog.raise_()
+        dialog.activateWindow()
+
+
     """ Utility """
 
     def type(self):
@@ -567,17 +1197,35 @@ class HCNetworkEditor(HCPathTab):
 
     def renameNode(self):
         node = self.currentNode()
-        name = hou.ui.readInput("Rename_node", buttons=("Yes", "No"))
-        if name[0] == 0:
-            node.setName(name[1])
+        if node is None:
+            hou.ui.setStatusMessage("No current node to rename",
+                                    hou.severityType.Warning)
+            return
+        button, name = hou.ui.readInput(
+            "Rename node", buttons=("Rename", "Cancel"), initial_contents=node.name()
+        )
+        if button != 0 or not name.strip():
+            return
+        try:
+            node.setName(name.strip(), unique_name=True)
+        except hou.OperationFailed as e:
+            hou.ui.setStatusMessage(f"Cannot rename: {e}", hou.severityType.Error)
 
     def setNodeColors(self):
-        color = hou.ui.selectColor(hou.Color(HCSettings().node_color))
-        nodes = self.pwd().selectedChildren()
+        nodes = list(self.hou_tab.pwd().selectedChildren())
+        if not nodes:
+            hou.ui.setStatusMessage("No selected nodes to color",
+                                    hou.severityType.Warning)
+            return
+        color = hou.ui.selectColor(HCSettings().nodeColor())
+        if color is None:  # dialog cancelled
+            return
         for node in nodes:
             node.setColor(color)
 
     def setNodeShapes(self):
-        nodes = self.pwd().children()
+        shape = HCSettings().nodeShape()
+        nodes = list(self.hou_tab.pwd().children())
         for node in nodes:
-            node.setUserData("nodeshape", "rect")
+            node.setUserData("nodeshape", shape)
+        hou.ui.setStatusMessage(f"Set {len(nodes)} node shape(s) to {shape}")
diff --git a/python3.13libs/hc/hcnode.py b/python3.13libs/hc/hcnode.py
index 4fb38d8..28d7adc 100644
--- a/python3.13libs/hc/hcnode.py
+++ b/python3.13libs/hc/hcnode.py
@@ -1,4 +1,6 @@
 from .hcgeo import HCGeo
+from .hcgeometryutils import merged_visible_geo
+from .hcvisibility import collect_visible_nodes
 import hou
 
 class HCNode:
@@ -37,21 +39,7 @@ class HCNode:
             return None
 
     def visibleNodes(self):
-        stack = self.children()
-        visible_nodes = []
-
-        while stack:
-            hc_node = stack.pop()
-            if hc_node.hou_node.isSubNetwork():
-                child_cat = hc_node.childCat()
-                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)
-
-        return visible_nodes
+        return collect_visible_nodes(self.children())
 
 
     """ Geometry """
@@ -60,10 +48,7 @@ class HCNode:
         return self.hou_node.geometry()
 
     def geo(self):
-        geo = hou.Geometry()
-        for node in self.visibleNodes():
-            geo.merge(node.geometry())
-        return geo
+        return merged_visible_geo(hou, self.visibleNodes(), apply_world_transform=True)
 
 
     """ Children """
diff --git a/python3.13libs/hc/hcpane.py b/python3.13libs/hc/hcpane.py
index 8ddef6a..e978b4a 100644
--- a/python3.13libs/hc/hcpane.py
+++ b/python3.13libs/hc/hcpane.py
@@ -14,6 +14,59 @@ class HCPane:
         after a pane is created so per-pane defaults can apply."""
         self.showTabs(False)
 
+    """ Initialization """
+
+    def initializeObject(self, obj):
+        """Initialize a newly created hou.Pane or hou.PaneTab by type.
+
+        This centralizes the post-create setup used by split panes, new tabs,
+        floating panels, etc. Pane-level setup runs once for pane objects;
+        tab-level setup is delegated through convertTab() so HCNetworkEditor,
+        HCSceneViewer, HCParameterTab, etc. can apply type-specific defaults.
+        """
+        if obj is None:
+            return None
+
+        # Pane-like object: has currentTab()/tabs(), but not necessarily type().
+        if hasattr(obj, 'currentTab') and hasattr(obj, 'tabs'):
+            pane = HCPane(obj)
+            pane.initialize()
+            for hou_tab in obj.tabs():
+                pane.initializeObject(hou_tab)
+            return pane
+
+        # PaneTab-like object: has type()/pane().
+        if hasattr(obj, 'type') and hasattr(obj, 'pane'):
+            tab = self.convertTab(obj)
+            tab.initialize()
+            return tab
+
+        return None
+
+    def _newPanesAfter(self, before_ids):
+        panes = []
+        desktop = hou.ui.curDesktop()
+        if desktop is None:
+            return panes
+        for pane in desktop.panes():
+            if pane.id() not in before_ids:
+                panes.append(pane)
+        return panes
+
+    def _splitAndInitialize(self, split_fn):
+        before_ids = {pane.id() for pane in hou.ui.curDesktop().panes()}
+        result = split_fn()
+        initialized = self.initializeObject(result)
+        if initialized is not None:
+            return initialized
+
+        # Some HOM split methods may return None; discover newly created panes.
+        new_panes = self._newPanesAfter(before_ids)
+        initialized_panes = []
+        for pane in new_panes:
+            initialized_panes.append(self.initializeObject(pane))
+        return initialized_panes[0] if len(initialized_panes) == 1 else initialized_panes
+
     """ Adjust size """
 
     def setIsMaximized(self, bool):
@@ -26,7 +79,7 @@ class HCPane:
         self.hou_pane.setSplitFraction(fraction)
 
     def splitHorizontal(self):
-        self.hou_pane.splitHorizontally()
+        return self._splitAndInitialize(self.hou_pane.splitHorizontally)
 
     def splitRotate(self):
         self.hou_pane.splitRotate()
@@ -35,7 +88,7 @@ class HCPane:
         self.hou_pane.splitSwap()
 
     def splitVertical(self):
-        self.hou_pane.splitVertically()
+        return self._splitAndInitialize(self.hou_pane.splitVertically)
 
     def contract(self):
         fraction = round(self.splitFraction(), 3) + 0.1
@@ -105,23 +158,29 @@ class HCPane:
         return self.hou_pane.isShowingPaneTabs()
 
     def newTab(self, tab_type, python_panel_interface=None):
-        return self.hou_pane.createTab(tab_type, python_panel_interface)
+        hou_tab = self.hou_pane.createTab(tab_type, python_panel_interface)
+        initialized = self.initializeObject(hou_tab)
+        hou.ui.setStatusMessage('Created new tab', hou.severityType.Message)
+        return initialized
+
+    def _stepTab(self, step):
+        # Index by tab object, not name: two tabs of the same type in one pane
+        # share a name, and looking up by name always lands on the first one.
+        hou_tabs = list(self.hou_pane.tabs())
+        current = self.hou_pane.currentTab()
+        if len(hou_tabs) < 2 or current is None:
+            return
+        try:
+            index = hou_tabs.index(current)
+        except ValueError:
+            index = 0
+        hou_tabs[(index + step) % len(hou_tabs)].setIsCurrentTab()
 
     def nextTab(self):
-        tabs = self.tabs()
-        tab_names = self.tabNames()
-        current_tab_name = self.currentTab().name()
-        current_tab_index = tab_names.index(current_tab_name)
-        new_tab_index = (current_tab_index + 1) % len(tabs)
-        tabs[new_tab_index].setCurrent()
+        self._stepTab(1)
 
     def prevTab(self):
-        tabs = self.tabs()
-        tab_names = self.tabNames()
-        current_tab_name = self.currentTab().name()
-        current_tab_index = tab_names.index(current_tab_name)
-        new_tab_index = (current_tab_index - 1) % len(tabs)
-        tabs[new_tab_index].setCurrent()
+        self._stepTab(-1)
 
     def showTabs(self, value):
         self.hou_pane.showPaneTabs(value)
diff --git a/python3.13libs/hc/hcparametertab.py b/python3.13libs/hc/hcparametertab.py
index 272ca1f..4bb9d91 100644
--- a/python3.13libs/hc/hcparametertab.py
+++ b/python3.13libs/hc/hcparametertab.py
@@ -1,6 +1,17 @@
+_last_focused_parm_by_pane = {}
+
+import hou
 from .hcpathtab import HCPathTab
 
 
+def _deferredEcho(label):
+    """Set status message on next event loop tick, then self-remove."""
+    def _cb():
+        hou.ui.setStatusMessage(label, hou.severityType.Message)
+        hou.ui.removeEventLoopCallback(_cb)
+    hou.ui.addEventLoopCallback(_cb)
+
+
 class HCParameterTab(HCPathTab):
     def __init__(self, hou_tab):
         self.hou_tab = hou_tab
@@ -10,3 +21,54 @@ class HCParameterTab(HCPathTab):
 
     def initialize(self):
         self.showNetworkControls(False)
+
+    def _paneKey(self):
+        pane = self.hou_tab.pane()
+        return pane.id() if pane is not None else id(self.hou_tab)
+
+    def _focusableParms(self):
+        """Return list of (parm_tuple, first_parm) for focusable parameters."""
+        result = []
+        for parm_tuple in self.hou_tab.visibleParms():
+            try:
+                if len(parm_tuple) > 0:
+                    result.append((parm_tuple, parm_tuple[0]))
+            except TypeError:
+                pass
+        return result
+
+    def _currentFocusIndex(self, entries):
+        pane_key = self._paneKey()
+        parm_path = _last_focused_parm_by_pane.get(pane_key)
+        if parm_path is None:
+            return None
+        for index, (_, parm) in enumerate(entries):
+            if parm.path() == parm_path:
+                return index
+        return None
+
+    def focusNextParameter(self):
+        entries = self._focusableParms()
+        if not entries:
+            return
+
+        current_index = self._currentFocusIndex(entries)
+        next_index = 0 if current_index is None else (current_index + 1) % len(entries)
+        parm_tuple, parm = entries[next_index]
+        self.hou_tab.moveFocusTo(parm)
+        _last_focused_parm_by_pane[self._paneKey()] = parm.path()
+        label = parm_tuple.parmTemplate().label()
+        _deferredEcho(label)
+
+    def focusPreviousParameter(self):
+        entries = self._focusableParms()
+        if not entries:
+            return
+
+        current_index = self._currentFocusIndex(entries)
+        prev_index = len(entries) - 1 if current_index is None else (current_index - 1) % len(entries)
+        parm_tuple, parm = entries[prev_index]
+        self.hou_tab.moveFocusTo(parm)
+        _last_focused_parm_by_pane[self._paneKey()] = parm.path()
+        label = parm_tuple.parmTemplate().label()
+        _deferredEcho(label)
diff --git a/python3.13libs/hc/hcpathtab.py b/python3.13libs/hc/hcpathtab.py
index 7250b7b..874a61e 100644
--- a/python3.13libs/hc/hcpathtab.py
+++ b/python3.13libs/hc/hcpathtab.py
@@ -1,5 +1,6 @@
 import hou
 from .hcgeo  import HCGeo
+from .hcgeometryutils import merged_visible_geo
 from .hcnode import HCNode
 from .hctab  import HCTab
 
@@ -11,11 +12,14 @@ class HCPathTab(HCTab):
         return HCGeo(self.geo())
 
     def geo(self):
+        if self.pwd().childCat() == 'Sop':
+            display_node = self.pwd().displayNode()
+            if display_node:
+                return display_node.geometry()
+            return hou.Geometry()
+
         visible_nodes = self.pwd().visibleNodes()
-        geo = hou.Geometry()
-        for node in visible_nodes:
-            geo.merge(node.geometry())
-        return geo
+        return merged_visible_geo(hou, visible_nodes, apply_world_transform=True)
 
     def path(self):
         return self.pwd().path()
diff --git a/python3.13libs/hc/hcradial.py b/python3.13libs/hc/hcradial.py
deleted file mode 100644
index 837626d..0000000
--- a/python3.13libs/hc/hcradial.py
+++ /dev/null
@@ -1,186 +0,0 @@
-import hou
-
-#
-# PaneTab object is retrieved by calling kwargs["pane"]
-#
-
-class HCRadial:
-    def __init__(self):
-        return
-
-    def createItem(label="label", pos=0, submenu=False, script="script"):
-        item = hou.ui.createRadialItem(submenu=submenu)
-        item.setLabel(label)
-        item.setScript(script)
-        hou.ui.injectRadialItem(pos, item)
-
-    #
-    # Scene Viewer
-    #
-
-    def sceneViewerMain(**kwargs):
-        menu = hou.ui.createRadialMenu("hc_radial_sceneviewer_main", "HC Radial Scene Viewer Main")
-        createItem(
-            pos=0,
-            label="UI",
-            submenu=True, script="from hc import HCRadial; HCRadial.sceneViewerUI()"
-        )
-        createItem(
-            pos=7,
-            label="Operation bar",
-            script="from hc import HCSceneViewer; HCSceneViewer(kwargs['pane']).toggleOperationBar()"
-        )
-        createItem(
-            pos=6,
-            label="Display bar",
-            script="from hc import HCSceneViewer; HCSceneViewer(kwargs['pane']).toggleDisplayOptionsToolbar()"
-        )
-        createItem(
-            pos=5,
-            label="Selection bar",
-            script="from hc import HCSceneViewer; HCSceneViewer(kwargs['pane']).toggleSelectionBar()"
-        )
-        createItem(
-            pos=4,
-            label="Keycam",
-            script="from hc import HCSceneViewer; HCSceneViewer(kwargs['pane']).keycam()"
-        )
-        return menu
-
-    def sceneViewerLayout(**kwargs):
-        menu = hou.ui.createRadialMenu("hc_radial_sceneviewer_layout", "HC Radial - Scene Viewer Layout")
-        createItem(
-            pos=0,
-            label="DoubleSide",
-            script="from hc import HCSceneViewer; HCSceneViewer(kwargs['pane']).setLayout('DoubleSide')"
-        )
-        createItem(
-            pos=1,
-            label="DoubleStack",
-            script="from hc import HCSceneViewer; HCSceneViewer(kwargs['pane']).setLayout('DoubleStack')"
-        )
-        createItem(
-            pos=2,
-            label="Quad",
-            script="from hc import HCSceneViewer; HCSceneViewer(kwargs['pane']).setLayout('Quad')"
-        )
-        createItem(
-            pos=3,
-            label="QuadBottomSplit",
-            script="from hc import HCSceneViewer; HCSceneViewer(kwargs['pane']).setLayout('QuadBottomSplit')"
-        )
-        createItem(
-            pos=4,
-            label="QuadLeftSplit",
-            script="from hc import HCSceneViewer; HCSceneViewer(kwargs['pane']).setLayout('QuadLeftSplit')"
-        )
-        createItem(
-            pos=5,
-            label="Single",
-            script="from hc import HCSceneViewer; HCSceneViewer(kwargs['pane']).setLayout('Single')"
-        )
-        createItem(
-            pos=6,
-            label="TripleBottomSplit",
-            script="from hc import HCSceneViewer; HCSceneViewer(kwargs['pane']).setLayout('TripleBottomSplit')"
-        )
-        createItem(
-            pos=7,
-            label="TripleLeftSplit",
-            script="from hc import HCSceneViewer; HCSceneViewer(kwargs['pane']).setLayout('TripleLeftSplit')"
-        )
-        return menu
-
-    def sceneViewerUI(**kwargs):
-        menu = hou.ui.createRadialMenu("hc_radial_sceneviewer_ui", "HC Radial - Scene Viewer UI")
-        createItem(
-            pos=0,
-            label="Tabs",
-            script="from hc import HCTab; HCTab(kwargs['pane']).pane().toggleTabs()"
-        )
-        createItem(
-            pos=7,
-            label="Path",
-            script="from hc import HCTab; HCTab(kwargs['pane']).toggleNetworkControls()"
-        )
-        createItem(
-            pos=6,
-            label="Maximize",
-            script="from hc import HCTab; HCTab(kwargs['pane']).pane().toggleMaximize()"
-        )
-        createItem(
-            pos=5,
-            label="Stowbars",
-            script="from hc import HCSession; HCSession().toggleStowbars()"
-        )
-        createItem(
-            pos=4,
-            label="All tabs",
-            script="from hc import HCSession; HCSession().toggleTabs()"
-        )
-        createItem(
-            pos=3,
-            label="All paths",
-            script="from hc import HCSession; HCSession().toggleNetworkControls()"
-        )
-        createItem(
-            pos=2,
-            label="Layout",
-            submenu=True, script="from hc import HCRadial; HCRadial.viewerRadialLayout()"
-        )
-        return menu
-
-
-    def networkEditorMain(**kwargs):
-        menu = hou.ui.createRadialMenu("hc_radial_networkeditor", "HC Radial - Network Editor")
-        createItem(
-            pos=0,
-            submenu=True,
-            label="UI",
-            script="from hc import HCRadial; HCRadial.networkEditorUI()"
-        )
-        createItem(
-            pos=7,
-            label="Menu",
-            script="from hc import HCNetworkEditor; HCNetworkEditor(kwargs['pane']).toggleMenu()"
-        )
-        createItem(
-            pos=6,
-            label="Frame all",
-            script="from hc import HCNetworkEditor; HCNetworkEditor(kwargs['pane']).frameAll()"
-        )
-        return menu
-
-    def networkEditorUI(**kwargs):
-        menu = hou.ui.createRadialMenu("hc_radial_networkeditor", "HC Radial - Network Editor")
-        createItem(
-            pos=0,
-            label="Tabs",
-            script="from hc import HCTab; HCTab(kwargs['pane']).pane().toggleTabs()"
-        )
-        createItem(
-            pos=7,
-            label="Path",
-            script="from hc import HCTab; HCTab(kwargs['pane']).toggleNetworkControls()"
-        )
-        createItem(
-            pos=6,
-            label="All tabs",
-            script="from hc import HCSession; HCSession().toggleTabs()"
-        )
-        createItem(
-            pos=5,
-            label="All paths",
-            script="from hc import HCSession; HCSession().toggleNetworkControls()"
-        )
-        createItem(
-            pos=4,
-            label="Stowbars",
-            script="from hc import HCSession; HCSession().toggleStowbars()"
-        )
-        createItem(
-            pos=3,
-            label="Maximize",
-            script="from hc import HCTab; HCTab(kwargs['pane']).pane().toggleMaximize()"
-        )
-        return menu
diff --git a/python3.13libs/hc/hcsceneviewer.py b/python3.13libs/hc/hcsceneviewer.py
index 4ad7d8e..24dabd8 100644
--- a/python3.13libs/hc/hcsceneviewer.py
+++ b/python3.13libs/hc/hcsceneviewer.py
@@ -1,4 +1,5 @@
 import hou, types
+from .hcdrawmode import get_draw_mode_label, next_draw_mode
 from .hcpathtab import HCPathTab
 from .hcviewport import HCViewport
 
@@ -128,6 +129,31 @@ class HCSceneViewer(HCPathTab):
             else:
                 settings.setVectorScale(1)
 
+    def cycleDrawMode(self):
+        next_mode_name = None
+        display_sets = self.allDisplaySets()
+
+        for display_set in display_sets:
+            current_mode = display_set.shadedMode()
+            next_mode_name = next_draw_mode(current_mode.name())
+            break
+
+        if next_mode_name is None:
+            next_mode_name = next_draw_mode(None)
+
+        next_mode = getattr(hou.glShadingType, next_mode_name)
+        for display_set in display_sets:
+            display_set.setShadedMode(next_mode)
+
+    def drawMode(self):
+        for display_set in self.allDisplaySets():
+            return display_set.shadedMode().name()
+
+        return None
+
+    def drawModeLabel(self):
+        return get_draw_mode_label(self.drawMode())
+
     """ Grids """
 
     def referencePlane(self):
@@ -164,29 +190,24 @@ class HCSceneViewer(HCPathTab):
             (2, 1, 0, 3),
             (3, 1, 0),
             (2, 3, 1),
-            (3),
+            (3,),
         )
 
     def nextLayout(self):
         layouts = self.layouts()
         layout = self.layout()
-        index = layouts.index(layout)
+        try:
+            index = layouts.index(layout)
+        except ValueError:
+            index = 0
         index = (index+1) % len(layouts)
         self.setLayout(layouts[index])
         return
 
     def setLayout(self, layout):
-        layout_map = {
-            "DoubleSide":        hou.geometryViewportLayout.DoubleSide,
-            "DoubleStack":       hou.geometryViewportLayout.DoubleStack,
-            "Quad":              hou.geometryViewportLayout.Quad,
-            "QuadBottomSplit":   hou.geometryViewportLayout.QuadBottomSplit,
-            "QuadLeftSplit":     hou.geometryViewportLayout.QuadLeftSplit,
-            "Single":            hou.geometryViewportLayout.Single,
-            "TripleBottomSplit": hou.geometryViewportLayout.TripleBottomSplit,
-            "TripleLeftSplit":   hou.geometryViewportLayout.TripleLeftSplit
-        }
-        self.hou_tab.setViewportLayout(layout_map[layout])
+        if isinstance(layout, str):
+            layout = getattr(hou.geometryViewportLayout, layout, layout)
+        self.hou_tab.setViewportLayout(layout)
 
 
     """ UI """
@@ -219,7 +240,7 @@ class HCSceneViewer(HCPathTab):
         self.showDisplayBar(not self.isVisibleDisplayBar())
 
     def toggleGroupList(self):
-        self.showGroupList(not self.isVisibleGroupList)
+        self.showGroupList(not self.isVisibleGroupList())
 
     def toggleOperationBar(self):
         self.showOperationBar(not self.isVisibleOperationBar())
@@ -238,6 +259,35 @@ class HCSceneViewer(HCPathTab):
             self.showDisplayBar(1)
             self.showSelectionBar(1)
 
+    def closeToolbars(self):
+        """Hide all currently visible toolbars/bars in this scene viewer pane."""
+        closed = 0
+
+        if self.hou_tab.isShowingOperationBar():
+            self.hou_tab.showOperationBar(False)
+            closed += 1
+
+        if self.hou_tab.isShowingDisplayOptionsBar():
+            self.hou_tab.showDisplayOptionsBar(False)
+            closed += 1
+
+        if self.hou_tab.isShowingSelectionBar():
+            self.hou_tab.showSelectionBar(False)
+            closed += 1
+
+        if hasattr(self.hou_tab, 'isShowingColorCorrectionBar') and self.hou_tab.isShowingColorCorrectionBar():
+            self.hou_tab.showColorCorrectionBar(False)
+            closed += 1
+
+        if hasattr(self.hou_tab, 'isShowingMemoryBar') and self.hou_tab.isShowingMemoryBar():
+            self.hou_tab.showMemoryBar(False)
+            closed += 1
+
+        hou.ui.setStatusMessage('Closed {} scene viewer toolbar{}'.format(
+            closed,
+            '' if closed == 1 else 's'
+        ), hou.severityType.Message)
+
 
     """ Events """
 
@@ -290,9 +340,4 @@ class HCSceneViewer(HCPathTab):
 
     def frameAllViewports(self):
         for viewport in self.allViewports():
-            cam = viewport.camera()
-            # Is camera node or default
-            if not cam:
-                viewport.frameAll()
-            else:
-                viewport.frameAll()
+            viewport.frameAll()
diff --git a/python3.13libs/hc/hcsession.py b/python3.13libs/hc/hcsession.py
index fac2e88..646bb77 100644
--- a/python3.13libs/hc/hcsession.py
+++ b/python3.13libs/hc/hcsession.py
@@ -1,4 +1,6 @@
 import hou
+from PySide6.QtWidgets import QApplication
+from PySide6.QtCore import QTimer
 from .hcbindings import HCBindings
 from .hcnetworkeditor import HCNetworkEditor
 from .hcsceneviewer import HCSceneViewer
@@ -54,18 +56,47 @@ class HCSession:
 
     def allViewports(self):
         viewports = []
-        scene_viewers = self.allSceneViewers
+        scene_viewers = self.allSceneViewers()
         for scene_viewer in scene_viewers:
             for viewport in scene_viewer.allViewports():
                 viewports.append(viewport)
         return viewports
 
     def currentPane(self):
-        hou_pane = hou.ui.paneUnderCursor()
+        hou_pane = self._paneFromQtFocus()
+        if hou_pane is None:
+            hou_pane = hou.ui.paneUnderCursor()
         if hou_pane is None:
             hou_pane = self._floatingPaneUnderCursor()
         return HCPane(hou_pane) if hou_pane is not None else None
 
+    def _paneFromQtFocus(self):
+        app = QApplication.instance()
+        if app is None:
+            return None
+
+        global_pos = None
+        focus_widget = app.focusWidget()
+        if focus_widget is not None:
+            rect = focus_widget.rect()
+            global_pos = focus_widget.mapToGlobal(rect.center())
+        else:
+            active_window = app.activeWindow()
+            if active_window is not None:
+                rect = active_window.frameGeometry()
+                global_pos = rect.center()
+
+        if global_pos is None:
+            return None
+
+        for pane in self.allPanes():
+            try:
+                if pane.qtScreenGeometry().contains(global_pos):
+                    return pane.hou_pane
+            except Exception:
+                continue
+        return None
+
     def _floatingPaneUnderCursor(self):
         from PySide6.QtGui import QCursor
         pos = QCursor.pos()
@@ -90,10 +121,14 @@ class HCSession:
 
     def floatingParameterEditor(self):
         tab = self.currentTab()
-        if tab.type() == 'HCNetworkEditor':
-            hou.ui.showFloatingParameterEditor(self.currentNode())
-        else:
+        if tab is None or tab.type() != 'HCNetworkEditor':
             hou.ui.setStatusMessage('Not a network editor', hou.severityType.Error)
+            return
+        node = tab.currentNode()
+        if node is None:
+            hou.ui.setStatusMessage('No current node', hou.severityType.Warning)
+            return
+        hou.ui.showFloatingParameterEditor(node)
 
     def hcPanel(self):
         from .hcmaps import HCMaps
@@ -112,14 +147,42 @@ class HCSession:
             list_dict = list_dict | maps.tab_map_scene_viewer(self, pane, tab)
 
 
-        panel = HCWidgets.SelectionDialog('HC Panel', list_dict)
+        anchor_geometry = pane.qtScreenGeometry() if pane is not None else None
+        panel = HCWidgets.SelectionDialog('hcpanel', list_dict, anchor_geometry=anchor_geometry)
         panel.show()
         panel.raise_()
         panel.activateWindow()
 
+    def newFile(self):
+        file_path = hou.ui.selectFile(
+            title="New File", 
+            file_type=hou.fileType.Hip, 
+            chooser_mode=hou.fileChooserMode.Write
+        )
+        if file_path:
+            QTimer.singleShot(0, lambda: self._executeNewFile(file_path))
+
+    def _executeNewFile(self, file_path):
+        file_path = self._normalizedHipPath(file_path)
+        
+        try:
+            hou.hipFile.clear()
+            hou.hipFile.save(file_path)
+            self._verifyHipFileSaved(file_path, "Created new file")
+        except (hou.Error, OSError) as e:
+            hou.ui.displayMessage(f"Error creating file: {e}", severity=hou.severityType.Error)
+
     def openFile(self):
-        file = hou.ui.selectFile()
-        hou.hipFile.load(file)
+        file_path = hou.ui.selectFile(title="Open File", file_type=hou.fileType.Hip)
+        if file_path:
+            QTimer.singleShot(0, lambda: self._executeOpenFile(file_path))
+
+    def _executeOpenFile(self, file_path):
+        try:
+            hou.hipFile.load(file_path)
+            hou.ui.setStatusMessage(f"Opened file: {file_path}")
+        except hou.Error as e:
+            hou.ui.displayMessage(f"Error opening file: {e}", severity=hou.severityType.Error)
 
     def openPreferences(self):
         hou.ui.openPreferences('ui', '')
@@ -135,6 +198,16 @@ class HCSession:
             existing.show()
             existing.raise_()
 
+    def toggleStatusBar(self):
+        from .hcstatusbar import HCStatusBar
+        bar = HCStatusBar()
+        if bar.isVisible():
+            bar.hide()
+        else:
+            bar._ensureOverlay()
+            bar.overlay.show()
+            bar.overlay.raise_()
+
     def openSettings(self):
         from .hcsettings import HCSettingsPanel
         existing = hou.qt.mainWindow().findChild(HCSettingsPanel, HCSettingsPanel.OBJECT_NAME)
@@ -153,6 +226,20 @@ class HCSession:
     def toggleSplitHandles(self):
         self.splitHandles().toggle()
 
+    def toggleSpreadsheet(self):
+        # Search for any floating panel containing a DetailsView tab
+        for fp in hou.ui.floatingPanels():
+            for pane in fp.panes():
+                for tab in pane.tabs():
+                    if tab.type() == hou.paneTabType.DetailsView:
+                        fp.close()
+                        hou.ui.setStatusMessage("Closed Spreadsheet")
+                        return
+
+        # If not found, open a new floating one with DetailsView
+        self.newFloatingPane(hou.paneTabType.DetailsView)
+        hou.ui.setStatusMessage("Opened Spreadsheet")
+
     def refreshSplitHandles(self):
         self.splitHandles().refresh()
 
@@ -182,6 +269,61 @@ class HCSession:
     def reloadColorSchemes(self):
         hou.ui.reloadColorScheme()
         hou.ui.reloadViewportColorSchemes()
+        self.updateNodeColors()
+
+    def hcInfo(self):
+        from . import __version__
+        from PySide6.QtCore import Qt
+        from PySide6.QtWidgets import (
+            QDialog, QVBoxLayout, QLabel, QTextBrowser, QPushButton,
+        )
+
+        win = QDialog(hou.qt.mainWindow())
+        win.setObjectName("hc_info_window")
+        win.setWindowTitle("HC Info")
+        win.setWindowFlags(Qt.Tool)
+        win.resize(960, 740)
+
+        layout = QVBoxLayout(win)
+
+        header = QLabel(f"<b>hou-control</b> v{__version__}")
+        header.setStyleSheet("font-size: 16px; padding-bottom: 6px;")
+        layout.addWidget(header)
+
+        info = QTextBrowser()
+        info.setReadOnly(True)
+        info.setOpenExternalLinks(True)
+
+        hc_path = hou.getenv("HC_PATH") or "(unknown)"
+        parts = [
+            f"<b>HC_PATH:</b> {hc_path}",
+            f"<b>Houdini:</b> {hou.applicationVersionString()}",
+            "",
+            "<b>Known limitations</b>",
+            "",
+            "<i>Color scheme switching</i>",
+            "Houdini 21 has no runtime API to switch UI color schemes. "
+            "hou.ui.reloadColorScheme() only re-reads the current .hcs "
+            "from disk (used for live-editing). The 'colors.scheme' "
+            "preference controls the scheme loaded at startup. Use "
+            "Edit > Preferences > UI to change it.",
+            "",
+            "<i>Gestures (removed)</i>",
+            "Multi-touch pinch-to-zoom was attempted via a global Qt "
+            "event filter (HCGestureFilter) but never worked reliably. "
+            "The gesture code caused launch segfaults and was removed.",
+        ]
+        info.setHtml("<br>".join(parts))
+
+        layout.addWidget(info)
+
+        close_btn = QPushButton("Close")
+        close_btn.clicked.connect(win.close)
+        layout.addWidget(close_btn)
+
+        win.show()
+        win.raise_()
+        win.activateWindow()
 
     def reloadHC(self):
         import sys
@@ -199,27 +341,72 @@ class HCSession:
             del sys.modules[m]
         import hc
         hou.ui.setStatusMessage(f"Reloaded hc ({len(removed)} modules)")
+        # Restore the status bar overlay (destroyed above)
+        from .hcstatusbar import HCStatusBar
+        HCStatusBar().show()
 
     def reloadHotkeys(self):
         HCBindings().load()
 
+    def initializeNetworkEditors(self):
+        initialized = 0
+        for editor in self.allNetworkEditors():
+            try:
+                editor.initialize()
+                initialized += 1
+            except Exception as e:
+                print(f"[HCSession] Failed to initialize network editor: {e!r}")
+        return initialized
+
+    def initializeNetworkEditorsDeferred(self):
+        callback_name = "_hc_init_network_editors_callback"
+        existing = getattr(hou.session, callback_name, None)
+        if existing is not None:
+            try:
+                hou.ui.removeEventLoopCallback(existing)
+            except Exception:
+                pass
+
+        def _callback():
+            try:
+                self.initializeNetworkEditors()
+            finally:
+                try:
+                    hou.ui.removeEventLoopCallback(_callback)
+                except Exception:
+                    pass
+                if getattr(hou.session, callback_name, None) is _callback:
+                    delattr(hou.session, callback_name)
+
+        setattr(hou.session, callback_name, _callback)
+        hou.ui.addEventLoopCallback(_callback)
+
     def updateNodeColors(self):
         from .hcsettings import HCSettings
-        prefs = HCSettings().prefs()
-        hex_color = prefs.get('node_graph').get('node_color').lstrip('#')
-        r = int(hex_color[0:2], 16) / 255.0
-        g = int(hex_color[2:4], 16) / 255.0
-        b = int(hex_color[4:6], 16) / 255.0
-        new_color = hou.Color(r, g, b)
+        settings = HCSettings()
+        new_color = settings.nodeColor()
 
         count = 0
         for node in hou.node("/").allSubChildren():
-            # Check for the custom tag we just added to OnCreated.py
-            if node.userData("hc_custom_color") == "1":
+            # Only nodes tagged by OnCreated.py, so hand-colored nodes survive.
+            if node.userData("hc_custom_color") != "1":
+                continue
+            try:
+                if node.isInsideLockedHDA():
+                    continue
                 node.setColor(new_color)
-                count += 1
-        
-        hou.ui.setStatusMessage(f"Updated {count} custom-colored nodes to {hex_color}")
+            except hou.Error:
+                continue
+            count += 1
+
+        # 456.py calls this on every hip load, including under hython and
+        # batch renders, where hou.ui does not exist.
+        if hou.isUIAvailable():
+            hou.ui.setStatusMessage(
+                f"Updated {count} custom-colored nodes to "
+                f"{settings.nodeGraph().get('node_color')}"
+            )
+        return count
 
     def keycam(self):
         tab = self.currentTab()
@@ -251,11 +438,14 @@ class HCSession:
         HCStatusBar().updateAutosave()
 
     def toggleUpdateMode(self):
-        mode_map = {
-            'updateMode.AutoUpdate': hou.updateMode.Manual,
-            'updateMode.Manual': hou.updateMode.AutoUpdate
-        }
-        hou.setUpdateMode(mode_map(str(hou.updateModeSetting)))
+        # hou.updateModeSetting is a function; the old code stringified the
+        # function object and then called the lookup dict, so this never ran.
+        if hou.updateModeSetting() == hou.updateMode.Manual:
+            mode = hou.updateMode.AutoUpdate
+        else:
+            mode = hou.updateMode.Manual
+        hou.setUpdateMode(mode)
+        hou.ui.setStatusMessage(f"Update mode: {mode.name()}")
 
     def updateMainMenuBar(self):
         hou.ui.updateMainMenuBar()
@@ -302,8 +492,60 @@ class HCSession:
         import os
         os._exit(0)
 
+    def _verifyHipFileSaved(self, file_path, action_label):
+        import os
+
+        saved_path = self._normalizedHipPath(file_path)
+        if not os.path.isfile(saved_path):
+            raise hou.OperationFailed(f"{action_label} reported success, but file was not created: {saved_path}")
+
+        size = os.path.getsize(saved_path)
+        if size <= 0:
+            raise hou.OperationFailed(f"{action_label} reported success, but file is empty: {saved_path}")
+
+        hou.ui.setStatusMessage(
+            f"{action_label}: {saved_path} ({size:,} bytes)",
+            severity=hou.severityType.Message,
+        )
+        return saved_path
+
     def save(self):
-        hou.hipFile.save()
+        try:
+            hou.hipFile.save()
+            self._verifyHipFileSaved(hou.hipFile.path(), "Saved")
+        except (hou.Error, OSError) as e:
+            hou.ui.displayMessage(f"Error saving file: {e}", severity=hou.severityType.Error)
+
+    def saveAs(self):
+        file_path = hou.ui.selectFile(
+            title="Save As", 
+            file_type=hou.fileType.Hip, 
+            chooser_mode=hou.fileChooserMode.Write
+        )
+        if file_path:
+            QTimer.singleShot(0, lambda: self._executeSaveAs(file_path))
+
+    def _normalizedHipPath(self, file_path):
+        import os
+
+        # Houdini's file chooser can return paths containing variables like
+        # $HOME/$HIP.  hou.hipFile.save() usually expands them, but not in every
+        # chooser/QTimer path, and the error message can remain misleadingly
+        # literal.  Normalize before extension checks and saving.
+        file_path = hou.text.expandString(file_path)
+        file_path = os.path.expandvars(os.path.expanduser(file_path))
+
+        if not any(file_path.endswith(ext) for ext in ('.hip', '.hiplc', '.hipnc')):
+            file_path += '.hip'
+        return file_path
+
+    def _executeSaveAs(self, file_path):
+        file_path = self._normalizedHipPath(file_path)
+        try:
+            hou.hipFile.save(file_path)
+            self._verifyHipFileSaved(file_path, "Saved as")
+        except (hou.Error, OSError) as e:
+            hou.ui.displayMessage(f"Error saving file: {e}", severity=hou.severityType.Error)
 
 
     """ Layout """
@@ -389,11 +631,14 @@ class HCSession:
         hou.setPreference('showmenu.val', str(value))
 
     def showShelf(self):
-        self.shelfDock().show(1)
+        self.desktop().shelfDock().show(1)
 
     def toggleMainMenu(self):
         value = (self.isVisibleMainMenu()+1) % 2
         self.showMainMenu(value)
+        tab = self.currentTab()
+        if tab is not None and tab.type() == 'HCNetworkEditor':
+            tab.toggleMenu()
 
     def toggleMenus(self):
         visible = self.isVisibleMenus()
@@ -439,47 +684,35 @@ class HCSession:
         if pane is not None:
             pane.toggleTabs()
 
+    def _paneTabTypeDialog(self, title, action, anchor_geometry=None):
+        type_map = HCTab(None).paneTabTypes()
+        list_dict = {name: (lambda t=tab_type: action(t)) for name, tab_type in type_map.items()}
+        dialog = HCWidgets.SelectionDialog(title, list_dict, anchor_geometry=anchor_geometry)
+        dialog.show()
+        dialog.raise_()
+        dialog.activateWindow()
+        return dialog
+
     def newTabDialog(self):
         pane = self.currentPane()
         if pane is None:
             hou.ui.setStatusMessage("No pane under cursor", hou.severityType.Warning)
-            return
-        type_map = {
-            "Apex Editor":         hou.paneTabType.ApexEditor,
-            "Asset Browser":       hou.paneTabType.AssetBrowser,
-            "Bundle List":         hou.paneTabType.BundleList,
-            "Channel Editor":      hou.paneTabType.ChannelEditor,
-            "Channel List":        hou.paneTabType.ChannelList,
-            "Channel Viewer":      hou.paneTabType.ChannelViewer,
-            "Compositor Viewer":   hou.paneTabType.CompositorViewer,
-            "Context Viewer":      hou.paneTabType.ContextViewer,
-            "Data Tree":           hou.paneTabType.DataTree,
-            "Details View":        hou.paneTabType.DetailsView,
-            "Handle List":         hou.paneTabType.HandleList,
-            "Help Browser":        hou.paneTabType.HelpBrowser,
-            "IPR Viewer":          hou.paneTabType.IPRViewer,
-            "Light Linker":        hou.paneTabType.LightLinker,
-            "Material Palette":    hou.paneTabType.MaterialPalette,
-            "Network Editor":      hou.paneTabType.NetworkEditor,
-            "Output Viewer":       hou.paneTabType.OutputViewer,
-            "Parameter Editor":    hou.paneTabType.Parm,
-            "Parameter Spreadsheet": hou.paneTabType.ParmSpreadsheet,
-            "Performance Monitor": hou.paneTabType.PerformanceMonitor,
-            "Python Panel":        hou.paneTabType.PythonPanel,
-            "Python Shell":        hou.paneTabType.PythonShell,
-            "Render Gallery":      hou.paneTabType.RenderGallery,
-            "Scene Viewer":        hou.paneTabType.SceneViewer,
-            "Shader Viewer":       hou.paneTabType.ShaderViewer,
-            "Take List":           hou.paneTabType.TakeList,
-            "Textport":            hou.paneTabType.Textport,
-            "Tree View":           hou.paneTabType.TreeView,
-        }
-        list_dict = {name: (lambda t=pt: pane.newTab(t)) for name, pt in type_map.items()}
-        from .hcwidgets import HCWidgets
-        dialog = HCWidgets.SelectionDialog("New Pane Tab", list_dict)
-        dialog.show()
-        dialog.raise_()
-        dialog.activateWindow()
+            return None
+        anchor_geometry = pane.qtScreenGeometry()
+        return self._paneTabTypeDialog("New Tab", pane.newTab, anchor_geometry=anchor_geometry)
+
+    def newFloatingPane(self, tab_type):
+        panel = self.desktop().createFloatingPanel(tab_type, immediate=True)
+        panel.attachToDesktop(True)
+        for pane in panel.panes():
+            HCPane(pane).initializeObject(pane)
+        hou.ui.setStatusMessage('Created new floating pane', hou.severityType.Message)
+        return panel
+
+    def newFloatingPaneDialog(self):
+        pane = self.currentPane()
+        anchor_geometry = pane.qtScreenGeometry() if pane is not None else None
+        return self._paneTabTypeDialog("New Floating Pane", self.newFloatingPane, anchor_geometry=anchor_geometry)
 
     def toggleAllTabs(self):
         visible = 0
@@ -489,4 +722,3 @@ class HCSession:
                 visible = 1
         for pane in panes:
             pane.showTabs(not visible)
-
diff --git a/python3.13libs/hc/hcsettings.py b/python3.13libs/hc/hcsettings.py
index ac2ac82..fba5e23 100644
--- a/python3.13libs/hc/hcsettings.py
+++ b/python3.13libs/hc/hcsettings.py
@@ -1,3 +1,4 @@
+import copy
 import hou
 import json
 from pathlib import Path
@@ -11,9 +12,11 @@ from PySide6.QtWidgets import (
     QDoubleSpinBox,
     QFormLayout,
     QGroupBox,
+    QHBoxLayout,
     QLabel,
     QLineEdit,
     QScrollArea,
+    QSlider,
     QSpinBox,
     QTabWidget,
     QVBoxLayout,
@@ -23,14 +26,31 @@ from PySide6.QtWidgets import (
 
 DESKTOP_MODES = ("attached", "detached")
 NODE_SHAPES = ("rect", "rounded_rect", "circle", "diamond", "tilted_rect", "trapezoid_down", "trapezoid_up")
+NODE_GRAPH_ZOOM_BEHAVIORS = ("mouse_cursor", "hc_cursor")
+
+# prefs() sits on the network editor's hot path -- nodegraphhooks reads it on
+# every UI event, and a single overlay update reads it several times over. Hold
+# the parsed file and re-read only when its mtime/size changes, so an external
+# edit (or the settings panel writing it) is still picked up immediately.
+_cache_stat = None
+_cache_prefs = None
 
 
 class HCSettings:
     DEFAULTS = {
         "desktop_mode": "attached",
+        "startup": {
+            "default_autosave_state": True,
+        },
         "node_graph": {
             "node_shape": "rect",
             "node_color": "#607070",
+            "zoom_center": "mouse_cursor",
+            "hcnetcursor": True,
+            "grid_x_step": 2.0,
+            "grid_y_step": 1.0,
+            "node_center_offset_x": 0.5,
+            "node_center_offset_y": 0.15,
         },
     }
 
@@ -43,26 +63,63 @@ class HCSettings:
             return None
         return Path(base) / "hc_settings.json"
 
+    @classmethod
+    def _merged(cls, defaults, overrides):
+        """Recursively overlay `overrides` onto `defaults`, returning a copy."""
+        merged = dict(defaults)
+        for key, value in overrides.items():
+            base = merged.get(key)
+            if isinstance(base, dict) and isinstance(value, dict):
+                merged[key] = cls._merged(base, value)
+            else:
+                merged[key] = value
+        return merged
+
     def prefs(self):
+        """Settings from disk, overlaid on DEFAULTS.
+
+        Callers index straight into nested sections (prefs()['node_graph']
+        ['node_color']), so a key missing from the file must never produce a
+        missing key here -- an unmerged read used to raise AttributeError on
+        None during startup, before any of hc had loaded.
+        """
+        global _cache_stat, _cache_prefs
+
         path = self._path()
-        if path is None or not path.exists():
+        if path is None:
             return dict(self.DEFAULTS)
         try:
-            return json.loads(path.read_text())
-        except Exception:
+            st = path.stat()
+        except OSError:
             return dict(self.DEFAULTS)
 
+        stat_key = (str(path), st.st_mtime_ns, st.st_size)
+        if _cache_stat == stat_key and _cache_prefs is not None:
+            return _cache_prefs
+
+        try:
+            data = json.loads(path.read_text())
+        except (OSError, ValueError) as e:
+            print(f"[HCSettings] could not read {path}: {e}")
+            return dict(self.DEFAULTS)
+        if not isinstance(data, dict):
+            return dict(self.DEFAULTS)
+
+        _cache_prefs = self._merged(self.DEFAULTS, data)
+        _cache_stat = stat_key
+        return _cache_prefs
+
+    def prefsCopy(self):
+        """A deep copy of prefs(), safe to mutate before writing back."""
+        return copy.deepcopy(self.prefs())
+
     def get(self, key, default=None):
         return self.prefs().get(key, default)
 
     def set(self, key, value):
-        path = self._path()
-        if path is None:
-            return False
-        data = self.prefs()
+        data = self.prefsCopy()
         data[key] = value
-        path.write_text(json.dumps(data, indent=4) + "\n")
-        return True
+        return self.write(data)
 
     def write(self, data):
         path = self._path()
@@ -79,6 +136,26 @@ class HCSettings:
             raise ValueError(f"invalid desktop mode: {mode}")
         self.set("desktop_mode", mode)
 
+    def nodeGraph(self):
+        return self.get("node_graph", {})
+
+    def hcnetcursorEnabled(self):
+        return bool(self.nodeGraph().get("hcnetcursor"))
+
+    def nodeShape(self):
+        return self.nodeGraph().get("node_shape")
+
+    def nodeColor(self):
+        """The configured default node color as a hou.Color."""
+        value = str(self.nodeGraph().get("node_color", "")).lstrip("#")
+        if len(value) != 6:
+            return hou.Color(self.node_color)
+        try:
+            r, g, b = (int(value[i:i + 2], 16) / 255.0 for i in (0, 2, 4))
+        except ValueError:
+            return hou.Color(self.node_color)
+        return hou.Color((r, g, b))
+
 
 class HCSettingsPanel(QDialog):
     OBJECT_NAME = "hc_settings_panel"
@@ -87,6 +164,15 @@ class HCSettingsPanel(QDialog):
         "pivot_2d", "pivot_3d", "pivot_axis", "ray",
         "tie_axis_to_radius", "center_on_geo", "lock_cam", "reset",
     }
+    # Keys that render as a QSlider + QDoubleSpinBox combo.
+    # Value is (min, max, decimals) for the spinbox; slider uses 0..1000 integer range.
+    SLIDER_RANGES = {
+        "drag_sensitivity": (0.0, 1.0, 2),
+        "grid_x_step": (0.25, 8.0, 2),
+        "grid_y_step": (0.25, 8.0, 2),
+        "node_center_offset_x": (0.0, 2.0, 2),
+        "node_center_offset_y": (0.0, 2.0, 2),
+    }
 
     def __init__(self):
         super().__init__(hou.qt.mainWindow())
@@ -189,6 +275,8 @@ class HCSettingsPanel(QDialog):
             parent_layout.addWidget(group)
 
     def _prettify(self, key):
+        if key == "hcnetcursor":
+            return "hcnetcursor"
         return key.replace("_", " ").title()
 
     def _makeWidget(self, key, value):
@@ -200,6 +288,8 @@ class HCSettingsPanel(QDialog):
             w = QCheckBox()
             w.setChecked(bool(value))
             return w
+        if key in self.SLIDER_RANGES:
+            return self._makeSliderWidget(key, value)
         if isinstance(value, float):
             w = QDoubleSpinBox()
             w.setDecimals(4)
@@ -225,10 +315,80 @@ class HCSettingsPanel(QDialog):
                     w.addItem(value)
                 w.setCurrentText(value)
                 return w
+            if key == "zoom_center":
+                w = QComboBox()
+                w.addItem("Mouse Cursor", "mouse_cursor")
+                w.addItem("HC Cursor", "hc_cursor")
+                index = w.findData(value)
+                if index >= 0:
+                    w.setCurrentIndex(index)
+                else:
+                    w.addItem(value, value)
+                    w.setCurrentIndex(w.count() - 1)
+                return w
+            if key == "hcnetcursor":
+                w = QComboBox()
+                w.addItem("Enabled", True)
+                w.addItem("Disabled", False)
+                index = w.findData(bool(value))
+                if index >= 0:
+                    w.setCurrentIndex(index)
+                return w
             w = QLineEdit(value)
             return w
         return QLabel(repr(value))
 
+    def _makeSliderWidget(self, key, value):
+        """Create a QSlider + QDoubleSpinBox combo that stay in sync."""
+        vmin, vmax, decimals = self.SLIDER_RANGES[key]
+
+        container = QWidget()
+        layout = QHBoxLayout(container)
+        layout.setContentsMargins(0, 0, 0, 0)
+
+        slider = QSlider(Qt.Horizontal)
+        slider.setMinimum(0)
+        slider.setMaximum(1000)
+        slider.setPageStep(50)
+
+        spin = QDoubleSpinBox()
+        spin.setDecimals(decimals)
+        spin.setRange(vmin, vmax)
+        spin.setSingleStep(round((vmax - vmin) / 100, decimals))
+        spin.setValue(value)
+
+        # Map float value -> slider position [0..1000]
+        def _float_to_pos(v):
+            return int((v - vmin) / (vmax - vmin) * 1000)
+
+        def _pos_to_float(pos):
+            return vmin + (pos / 1000.0) * (vmax - vmin)
+
+        # Sync slider -> spinbox
+        def _on_slider_changed(pos):
+            spin.blockSignals(True)
+            spin.setValue(round(_pos_to_float(pos), decimals))
+            spin.blockSignals(False)
+
+        # Sync spinbox -> slider
+        def _on_spin_changed(v):
+            slider.blockSignals(True)
+            slider.setValue(_float_to_pos(v))
+            slider.blockSignals(False)
+
+        slider.valueChanged.connect(_on_slider_changed)
+        spin.valueChanged.connect(_on_spin_changed)
+
+        # Set initial slider position
+        slider.setValue(_float_to_pos(value))
+
+        layout.addWidget(slider, 3)   # slider takes most space
+        layout.addWidget(spin, 1)     # spinbox for precise input
+
+        # Store references so _readWidget can get the value
+        container._spinbox = spin
+        return container
+
 
     """ IO """
 
@@ -236,18 +396,22 @@ class HCSettingsPanel(QDialog):
     def _readWidget(self, widget, original_value):
         if isinstance(widget, QCheckBox):
             return int(widget.isChecked()) if isinstance(original_value, int) and not isinstance(original_value, bool) else widget.isChecked()
+        if hasattr(widget, '_spinbox'):
+            # Slider+spinbox combo -- read from the spinbox
+            return widget._spinbox.value()
         if isinstance(widget, QDoubleSpinBox):
             return widget.value()
         if isinstance(widget, QSpinBox):
             return widget.value()
         if isinstance(widget, QComboBox):
-            return widget.currentText()
+            data = widget.currentData()
+            return data if data is not None else widget.currentText()
         if isinstance(widget, QLineEdit):
             return widget.text()
         return original_value
 
     def _save(self):
-        data = self.settings.prefs()
+        data = self.settings.prefsCopy()
         for path, widget in self._fields.items():
             original = self._lookup(data, path)
             new_value = self._readWidget(widget, original)
diff --git a/python3.13libs/hc/hctab.py b/python3.13libs/hc/hctab.py
index f469771..fde882e 100644
--- a/python3.13libs/hc/hctab.py
+++ b/python3.13libs/hc/hctab.py
@@ -17,9 +17,8 @@ class HCTab():
         self.hou_tab.close()
 
     def closeOtherTabs(self):
-        from .hcpane import HCPane
         for tab in self.pane().tabs():
-            if tab != self.tab:
+            if tab.hou_tab != self.hou_tab:
                 tab.close()
 
     def name(self):
@@ -34,6 +33,9 @@ class HCTab():
 
     """ Type """
 
+    def type(self):
+        return self.__class__.__name__
+
     def tab_type(self):
         return 'HCTab'
 
@@ -52,40 +54,76 @@ class HCTab():
     def setTypeSceneViewer(self):
         self.setType(hou.paneTabType.SceneViewer)
 
+    def paneTabTypes(self):
+        type_specs = (
+            ('Apex Editor', 'ApexEditor'),
+            ('Asset Browser', 'AssetBrowser'),
+            ('Bundle List', 'BundleList'),
+            ('Channel Editor', 'ChannelEditor'),
+            ('Channel List', 'ChannelList'),
+            ('Channel Viewer', 'ChannelViewer'),
+            ('Compositor Viewer', 'CompositorViewer'),
+            ('Context Viewer', 'ContextViewer'),
+            ('Data Tree', 'DataTree'),
+            ('Details View', 'DetailsView'),
+            ('Engine Session Sync', 'EngineSessionSync'),
+            ('Handle List', 'HandleList'),
+            ('Help Browser', 'HelpBrowser'),
+            ('IPR Viewer', 'IPRViewer'),
+            ('Light Linker', 'LightLinker'),
+            ('Material Palette', 'MaterialPalette'),
+            ('Network Editor', 'NetworkEditor'),
+            ('Output Viewer', 'OutputViewer'),
+            ('Parameter Editor', 'Parm'),
+            ('Parameter Spreadsheet', 'ParmSpreadsheet'),
+            ('Performance Monitor', 'PerformanceMonitor'),
+            ('Python Panel', 'PythonPanel'),
+            ('Python Shell', 'PythonShell'),
+            ('Render Gallery', 'RenderGallery'),
+            ('Scene Viewer', 'SceneViewer'),
+            ('Shader Viewer', 'ShaderViewer'),
+            ('Take List', 'TakeList'),
+            ('Textport', 'Textport'),
+            ('Tree View', 'TreeView'),
+        )
+
+        type_map = {}
+        for label, attr in type_specs:
+            tab_type = getattr(hou.paneTabType, attr, None)
+            if tab_type is not None:
+                type_map[label] = tab_type
+        return type_map
+
+    def changeTypeDialog(self):
+        from .hcwidgets import HCWidgets
+
+        type_map = self.paneTabTypes()
+        list_dict = {name: (lambda t=tab_type: self.setType(t)) for name, tab_type in type_map.items()}
+        anchor_geometry = self.pane().qtScreenGeometry()
+        dialog = HCWidgets.SelectionDialog('Set Tab Type', list_dict, anchor_geometry=anchor_geometry)
+        dialog.show()
+        dialog.raise_()
+        dialog.activateWindow()
+
     def setType(self, tab_type):
-        tab_type_map = {
-            'AssetBrowser':       hou.paneTabType.AssetBrowser,
-            'ApexEditor':         hou.paneTabType.ApexEditor,
-            'BundleList':         hou.paneTabType.BundleList,
-            'ChannelEditor':      hou.paneTabType.ChannelEditor,
-            'ChannelList':        hou.paneTabType.ChannelList,
-            'ChannelViewer':      hou.paneTabType.ChannelViewer,
-            'CompositorViewer':   hou.paneTabType.CompositorViewer,
-            'ContextViewer':      hou.paneTabType.ContextViewer,
-            'DataTree':           hou.paneTabType.DataTree,
-            'DetailsView':        hou.paneTabType.DetailsView,
-            'EngineSessionSync':  hou.paneTabType.EngineSessionSync,
-            'HandleList':         hou.paneTabType.HandleList,
-            'HelpBrowser':        hou.paneTabType.HelpBrowser,
-            'IPRViewer':          hou.paneTabType.IPRViewer,
-            'LightLinker':        hou.paneTabType.LightLinker,
-            'MaterialPalette':    hou.paneTabType.MaterialPalette,
-            'NetworkEditor':      hou.paneTabType.NetworkEditor,
-            'OutputViewer':       hou.paneTabType.OutputViewer,
-            'Parm':               hou.paneTabType.Parm,
-            'ParmSpreadsheet':    hou.paneTabType.ParmSpreadsheet,
-            'PerformanceMonitor': hou.paneTabType.PerformanceMonitor,
-            'PythonPanel':        hou.paneTabType.PythonPanel,
-            'PythonShell':        hou.paneTabType.PythonShell,
-            'RenderGallery':      hou.paneTabType.RenderGallery,
-            'SceneViewer':        hou.paneTabType.SceneViewer,
-            'ShaderViewer':       hou.paneTabType.ShaderViewer,
-            'TakeList':           hou.paneTabType.TakeList,
-            'Textport':           hou.paneTabType.Textport,
-            'TreeView':           hou.paneTabType.TreeView
-        }
-        tab = self.hou_tab.setType(type)
-        return tab
+        if isinstance(tab_type, str):
+            tab_type_map = self.paneTabTypes()
+            compact_map = {label.replace(' ', ''): value for label, value in tab_type_map.items()}
+            compact_map['Parm'] = tab_type_map.get('Parameter Editor')
+            compact_map['DetailsView'] = tab_type_map.get('Details View')
+            tab_type = tab_type_map.get(tab_type) or compact_map.get(tab_type)
+        if tab_type is None:
+            hou.ui.setStatusMessage('Unknown pane tab type', hou.severityType.Error)
+            return None
+
+        new_tab = self.hou_tab.setType(tab_type)
+        if new_tab is None:
+            new_tab = self.hou_tab.pane().currentTab()
+
+        from .hcpane import HCPane
+        initialized = HCPane(new_tab.pane()).initializeObject(new_tab)
+        hou.ui.setStatusMessage('Changed tab type', hou.severityType.Message)
+        return initialized
 
 
     """ UI """
@@ -93,9 +131,6 @@ class HCTab():
     def hasNetworkControls(self):
         return self.hou_tab.hasNetworkControls()
 
-    def hcPanel(self, list_items):
-        HCSession.hcPanel(list_items)
-
     def isPin(self):
         return self.hou_tab.isPin()
 
@@ -115,4 +150,3 @@ class HCTab():
 
     def togglePin(self):
         self.setPin(not self.isPin())
-
diff --git a/python3.13libs/hc/hcviewport.py b/python3.13libs/hc/hcviewport.py
index dcaa665..18ca0f0 100644
--- a/python3.13libs/hc/hcviewport.py
+++ b/python3.13libs/hc/hcviewport.py
@@ -4,10 +4,13 @@ class HCViewport():
     def __init__(self, viewport):
         self.viewport = viewport
 
+    def settings(self):
+        return self.viewport.settings()
+
     def displaySet(self):
-        settings = self.viewport.settings()
+        settings = self.settings()
         display_set = settings.displaySet(hou.displaySetType.DisplayModel)
-        return displaySet
+        return display_set
 
     def lockCameraToView(self, boolean):
         self.viewport.lockCameraToView(boolean)
@@ -25,4 +28,3 @@ class HCViewport():
         category = hou.viewportVisualizerCategory.Scene
         vis_arr = hou.viewportVisualizers.visualizers(category)
         return vis_arr
-
diff --git a/python3.13libs/hc/hcvisibility.py b/python3.13libs/hc/hcvisibility.py
new file mode 100644
index 0000000..7fd0168
--- /dev/null
+++ b/python3.13libs/hc/hcvisibility.py
@@ -0,0 +1,21 @@
+def collect_visible_nodes(children):
+    stack = list(children)
+    visible_nodes = []
+
+    while stack:
+        hc_node = stack.pop()
+        child_cat = hc_node.childCat()
+
+        if child_cat == 'Sop' and hc_node.hou_node.isDisplayFlagSet():
+            if hc_node.hou_node.type().name() == 'cam':
+                continue
+
+            display_node = hc_node.displayNode()
+            if display_node:
+                visible_nodes.append(display_node)
+            continue
+
+        if child_cat == 'Object' and hc_node.hou_node.isSubNetwork() and hc_node.hou_node.isDisplayFlagSet():
+            stack.extend(hc_node.children())
+
+    return visible_nodes
diff --git a/python3.13libs/hc/hcvisualizers.py b/python3.13libs/hc/hcvisualizers.py
deleted file mode 100644
index 1099cfc..0000000
--- a/python3.13libs/hc/hcvisualizers.py
+++ /dev/null
@@ -1,133 +0,0 @@
-import hou
-from fuzzyfinder import fuzzyfinder
-from PySide6 import QtCore
-from PySide6.QtWidgets import QListWidget, QListWidgetItem, QDialog, QLineEdit, QBoxLayout
-from PySide6.QtCore import Qt, QEvent
-from .hcsession import HCSession
-
-class HCVisualizers(QDialog):
-    def __init__(self):
-        super(VisualizerMenu, self).__init__(hou.qt.mainWindow())
-
-        # Resources
-        self.hc_session = HCSession()
-        self.hc_viewport = self.hc_session.currentViewport()
-        self.vis_arr = self.hc_viewport.visualizers()
-
-        # Filter box
-        self.filter_box = filterBox()
-        self.filter_box.onTab.connect(self.listNext)
-        self.filter_box.returnPressed.connect(self.itemToggle)
-        self.filter_box.textEdited.connect(self.listFilter)
-
-        # List widget
-        self.list_widget = QListWidget()
-        if self.vis_arr:
-            for vis in self.vis_arr:
-                list_item = QListWidgetItem()
-                item_label = vis.label()
-                item_state = vis.isActive(self.curViewport)
-
-                list_item.setText(item_label)
-
-                if item_state:
-                    list_item.setCheckState(Qt.Checked)
-                else:
-                    list_item.setCheckState(Qt.Unchecked)
-
-                self.list_widget.addItem(list_item)
-
-        self.list_widget.itemClicked.connect(self.item_toggle)
-
-        # Layout
-        self.layout = QBoxLayout(QBoxLayout.Direction.TopToBottom)
-        self.layout.addWidget(self.filter_box)
-        self.layout.addWidget(self.list_widget)
-        self.setLayout(self.layout)
-        self.listSetIndex(0)
-
-    def closeEvent(self, event):
-        print("Closing")
-        self.setParent(None)
-
-    def itemToggle(self):
-        current_item = self.list_widget.selectedItems()[0]
-        item_name = current_tem.text()
-        items = self.listGetItems()
-        item_names = [item.text() for item in items]
-        index = item_names.index(item_name)
-
-        vis = self.vis_arr[index]
-        state = vis.isActive(self.viewport)
-
-        vis.setIsActive(not state, self.viewport)
-
-        if state:
-            current_item.setCheckState(Qt.Unchecked)
-        else:
-            current_item.setCheckState(Qt.Checked)
-
-    def listFilter(self):
-        text = self.input_box.text()
-        items = self.getItems()
-        names = [item.text() for item in items]
-        suggestions = fuzzyfinder(text, names)
-        suggestions = list(suggestions)
-        for item in items:
-            if item.text() in suggestions:
-                item.setHidden(0)
-            else:
-                item.setHidden(1)
-        self.listSetIndex(0)
-
-    def listGetItems(self):
-        item_count = self.list_widget.count()
-        items = [self.list_widget.item(i) for i in range(item_count)]
-        return(items)
-
-    def listGetVisibleItems(self):
-        item_count = self.list_widget.count()
-        items = []
-        for i in range(item_count):
-            item = self.list_widget.item(i)
-            if not item.isHidden():
-                items.append(item)
-        return(items)
-
-    def listNext(self):
-        visible_items = self.listGetVisibleItems()
-        current_item = self.list_widget.selectedItems()[0]
-        index = visible_items.index(current_item)
-        index = (index + 1) % len(visible_items)
-        self.listSetIndex(index)
-
-    def listPrev(self):
-        visible_items = self.listGetVisibleItems()
-        current_item = self.list_widget.selectedItems()[0]
-        index = visible_items.index(current_item)
-        index = (index - 1) % len(visible_items)
-        self.listSetIndex(index)
-
-    def listSetIndex(self, index):
-        visible_items = self.listGetVisibleItems()
-        counter = 0
-        for visible_item in visible_items:
-            if counter == index:
-                self.list_widget.setItemSelected(visible_item, 1)
-                return
-            counter += 1
-
-    class filterBox(QLineEdit):
-        def __init__(self):
-            # Key handler
-            self.onTab = QtCore.Signal()
-
-        def event(self, event):
-            if event.type() == QEvent.Type.KeyPress:
-                key = event.key()
-                if key == Qt.Key_Tab:
-                    self.onTab.emit()
-                    return True
-            return QLineEdit.event(self, event)
-
-
diff --git a/python3.13libs/hc/hcwidgets.py b/python3.13libs/hc/hcwidgets.py
index 179d14d..ead78e9 100644
--- a/python3.13libs/hc/hcwidgets.py
+++ b/python3.13libs/hc/hcwidgets.py
@@ -22,11 +22,12 @@ class HCWidgets:
 
 
     class SelectionDialog(QtWidgets.QDialog):
-        def __init__(self, window_title, list_dict):
+        def __init__(self, window_title, list_dict, anchor_geometry=None):
             super().__init__(hou.qt.mainWindow())
             self.resize(900, 600)
             self.setWindowTitle(window_title)
             self.setWindowFlags(Qt.Tool | Qt.WindowStaysOnTopHint)
+            self._anchor_geometry = anchor_geometry
 
             self.list = self.List()
             self.list_dict = list_dict
@@ -51,32 +52,57 @@ class HCWidgets:
             self.setLayout(self.layout)
 
             """ Do this last """
+            self._positionNearAnchor()
             self.input_line.setFocus()
 
+        def _positionNearAnchor(self):
+            if self._anchor_geometry is None:
+                return
+            anchor = self._anchor_geometry
+            frame = self.frameGeometry()
+            x = int(anchor.x() + (anchor.width() - frame.width()) / 2)
+            y = int(anchor.y() + (anchor.height() - frame.height()) / 2)
+            self.move(x, y)
+
         def closeEvent(self, event):
             self.setParent(None)
 
         def keyPressEvent(self, event):
             if event.key() == Qt.Key_Up:
-                self.list.selectPrevious()
+                self.list.selectPrev()
                 return
             elif event.key() == Qt.Key_Down:
                 self.list.selectNext()
+                return
             elif event.key() == Qt.Key_P and event.modifiers() == Qt.ControlModifier:
-                self.list.selectPrevious()
+                self.list.selectPrev()
                 return
             elif event.key() == Qt.Key_N and event.modifiers() == Qt.ControlModifier:
                 self.list.selectNext()
                 return
             super().keyPressEvent(event)
 
-        def execute(self):
-            items = self.list.allItems()
-            index = items.index(self.list.currentItem())
-            # item = items[index]
-            item = self.list_items[index]
-            value = self.list_dict[item]()
-            return
+        def execute(self, *args):
+            item = self.list.currentItem()
+            if item is None:
+                return
+            label = item.text()
+            action = self.list_dict.get(label)
+            if action is None:
+                return
+            self.accept()
+            # Without this, any exception raised by the command is swallowed by
+            # Qt's slot dispatch and the user just sees "nothing happens" -- the
+            # single worst thing to debug in this codebase. Surface it instead.
+            try:
+                action()
+            except Exception as e:
+                import traceback
+                traceback.print_exc()
+                hou.ui.setStatusMessage(
+                    f"{label}: {type(e).__name__}: {e}",
+                    hou.severityType.Error,
+                )
 
         def populate(self, item_list):
             items = []
@@ -94,7 +120,8 @@ class HCWidgets:
                 return items
 
             def currentItem(self):
-                return self.selectedItems()[0]
+                selected = self.selectedItems()
+                return selected[0] if selected else None
 
             def visibleItems(self):
                 items = []
@@ -117,14 +144,26 @@ class HCWidgets:
 
             def selectNext(self):
                 items = self.visibleItems()
-                index = items.index(self.currentItem())
-                index = (index + 1) % len(items)
+                if not items:
+                    return
+                try:
+                    current = self.selectedItems()[0]
+                    index = items.index(current)
+                    index = (index + 1) % len(items)
+                except (IndexError, ValueError):
+                    index = 0
                 self.setIndex(index)
 
             def selectPrev(self):
                 items = self.visibleItems()
-                index = items.index(self.currentItem())
-                index = (index - 1) % len(items)
+                if not items:
+                    return
+                try:
+                    current = self.selectedItems()[0]
+                    index = items.index(current)
+                    index = (index - 1) % len(items)
+                except (IndexError, ValueError):
+                    index = len(items) - 1
                 self.setIndex(index)
 
             def setIndex(self, index):
@@ -133,4 +172,3 @@ class HCWidgets:
                     if counter == index:
                         self.setCurrentItem(item)
                     counter += 1
-
diff --git a/python3.13libs/nodegraphhooks.py b/python3.13libs/nodegraphhooks.py
index 4065ee6..d6f2988 100755
--- a/python3.13libs/nodegraphhooks.py
+++ b/python3.13libs/nodegraphhooks.py
@@ -1,18 +1,111 @@
 import hou
+import time
 from canvaseventtypes import *
+import nodegraphbase as base
 import nodegraphdisplay as display
 from hc import HCNetworkEditor
 
+_modifier_state_by_editor = {}
+_selection_state_by_editor = {}
+
+
+class _PendingSelectionSyncAction(base.PendingDelayedAction):
+    def __init__(self, editor, editor_key, delay=0.0):
+        super().__init__(editor, delay)
+        self.editor_key = editor_key
+
+    def runDelayedAction(self):
+        hc_editor = HCNetworkEditor(self.editor)
+        selection_state = hc_editor.selectedNodesEnvelopeSignature()
+        previous_selection_state = _selection_state_by_editor.get(self.editor_key)
+        if selection_state != previous_selection_state:
+            _selection_state_by_editor[self.editor_key] = selection_state
+            if selection_state:
+                hc_editor.fitHcnetcursorToSelectedNodes()
+            else:
+                # Preserve the current hcnetcursor size when the selection is
+                # cleared. Manual reset remains available via the reset command.
+                hc_editor.updateCurrentNodeOverlay()
+
+
+def _queueSelectionSync(editor, editor_key, pending_actions):
+    for action in pending_actions:
+        if isinstance(action, _PendingSelectionSyncAction) and action.editor == editor:
+            return
+    pending_actions.append(_PendingSelectionSyncAction(editor, editor_key))
+
 
 def createEventHandler(uievent, pending_actions):
+    editor = getattr(uievent, "editor", None)
+
+    if editor is not None:
+        hc_editor = HCNetworkEditor(editor)
+        pane = editor.pane()
+        editor_key = pane.id() if pane is not None else id(editor)
+        modifierstate = getattr(uievent, "modifierstate", None)
+        current_state = {
+            "ctrl": bool(getattr(modifierstate, "ctrl", False)),
+            "shift": bool(getattr(modifierstate, "shift", False)),
+            "alt": bool(getattr(modifierstate, "alt", False)),
+        }
+        previous_state = _modifier_state_by_editor.get(
+            editor_key,
+            {"ctrl": False, "shift": False, "alt": False},
+        )
+        if current_state != previous_state:
+            # Houdini repaints the graph when a modifier changes, which can
+            # drop the cursor's background image; force it back on.
+            hc_editor.refreshHcnetcursor()
+            _modifier_state_by_editor[editor_key] = current_state
+        else:
+            # Runs on every event, mouse moves included -- this is a no-op
+            # unless the drawn overlay would actually differ.
+            hc_editor.updateCurrentNodeOverlay()
 
     if isinstance(uievent, MouseEvent):
+        if editor is not None:
+            if uievent.eventtype == 'mousedown' and not uievent.located:
+                hc_editor.moveHcnetcursorToPosition(uievent.mousepos)
+            selection_state = hc_editor.selectedNodesEnvelopeSignature()
+            previous_selection_state = _selection_state_by_editor.get(editor_key)
+            if selection_state != previous_selection_state:
+                _selection_state_by_editor[editor_key] = selection_state
+                if selection_state:
+                    hc_editor.fitHcnetcursorToSelectedNodes()
+                else:
+                    # Preserve the current hcnetcursor size when the selection
+                    # is cleared. Manual reset remains available via the reset
+                    # command.
+                    hc_editor.updateCurrentNodeOverlay()
+            if uievent.eventtype in ('mousedown', 'mouseup', 'mousedoubleclick'):
+                _queueSelectionSync(editor, editor_key, pending_actions)
         # 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 
+        # Force the unmodified f key to frame the hcnetcursor instead of
+        # Houdini's selection-based home/zoom behavior.  This lives in the
+        # event hook (not only hc_hotkeys.json) so it takes effect immediately
+        # after Reload HC and does not depend on NetworkViewMenu.xml symbols
+        # being rebuilt by a Houdini restart.
+        key = str(getattr(uievent, "key", "")).lower()
+        rawkey = str(getattr(uievent, "rawkey", "")).lower()
+        eventtype = str(getattr(uievent, "eventtype", "")).lower()
+        if (
+            editor is not None
+            and (key == "f" or rawkey == "f")
+        ):
+            modifierstate = getattr(uievent, "modifierstate", None)
+            has_modifier = (
+                bool(getattr(modifierstate, "ctrl", False)) or
+                bool(getattr(modifierstate, "shift", False)) or
+                bool(getattr(modifierstate, "alt", False))
+            )
+            if not has_modifier and not (eventtype.endswith("up") or "release" in eventtype):
+                hc_editor.frameHcnetcursor()
+                return None, True
+        # Keyboard events are primarily handled via hc_hotkeys.json and
+        # NetworkViewMenu.xml, but this hook remains for future
         # context-sensitive overrides.
         return None, False
 
diff --git a/python3.13libs/uiready.py b/python3.13libs/uiready.py
index adba96c..fcb8f40 100644
--- a/python3.13libs/uiready.py
+++ b/python3.13libs/uiready.py
@@ -5,26 +5,74 @@ from hc import HCSession, HCSettings
 hc_session = HCSession()
 
 hc_session.reloadHotkeys()
+hc_session.initializeNetworkEditorsDeferred()
+hc_session.updateNodeColors()
 
-def set_current_desktop(name):
-    for d in hou.ui.desktops():
-        if d.name() == name:
-            d.setAsCurrent()
-            return
+import sys
 
 desktop_mode = HCSettings().desktopMode()
 
 if desktop_mode == "detached":
-    set_current_desktop("hc_detached")
-    hc_session.toggleStowbars()
-    hc_session.hideShelf()
-    hc_session.showMainMenu(0)
-    from hc import HCStatusBar
-    HCStatusBar().show()
-    from hc.hclayout import launchFloatingLayout
-    launchFloatingLayout()
+    def _deferredInit():
+        """Two-phase deferred init for detached mode.
+
+        Phase 1 (tick 1): Desktop setup + initialize existing floating panels.
+        Phase 2 (tick 5): Create any missing floating panels + initialize.
+
+        ALL setup is deferred to avoid posting Qt events during
+        uiready.py module-level execution, which triggers re-entrant
+        event processing that SIGSEGVs in PySide::getWrapperForQObject
+        (Houdini 21.0.700 bug).
+
+        Floating panel initialization (showTabs, showNetworkControls, etc.)
+        is safe at tick 1 because all initialize() methods are pure HOM
+        calls -- no PySide6 event filter installation.
+        """
+        global _layout
+        count = getattr(hou.session, '_hc_deferred_count', 0) + 1
+        hou.session._hc_deferred_count = count
+
+        if count == 1:
+            # Desktop setup + initialize panels already loaded from .desk
+            for d in hou.ui.desktops():
+                if d.name() == "hc_detached":
+                    d.setAsCurrent()
+                    break
+            hc_session.toggleStowbars()
+            hc_session.hideShelf()
+            hc_session.showMainMenu(0)
+            from hc import HCStatusBar
+            HCStatusBar().show()
+            from hc.hclayout import HCLayout
+            _layout = HCLayout()
+            _layout.initializeFloatingPanels()
+
+        if count == 5:
+            # Create any panels not already in .desk, initialize immediately
+            from hc.hclayout import launchFloatingLayout
+            launchFloatingLayout()
+            _layout.initializeFloatingPanels()
+            hou.session._hc_deferred_done = True
+            try:
+                hou.ui.removeEventLoopCallback(_safeDeferredInit)
+            except Exception:
+                pass
+
+    def _safeDeferredInit():
+        if getattr(hou.session, '_hc_deferred_done', False):
+            return
+        _deferredInit()
+
+    _layout = None
+    hou.session._hc_deferred_count = 0
+    hou.session._hc_deferred_done = False
+    hou.ui.addEventLoopCallback(_safeDeferredInit)
 else:
-    set_current_desktop("hc_attached")
+    # Attached mode runs at module level (has been stable)
+    for d in hou.ui.desktops():
+        if d.name() == "hc_attached":
+            d.setAsCurrent()
+            break
     hc_session.toggleStowbars()
     hc_session.splitHandles().show()
     from hc import HCStatusBar
diff --git a/scripts/123.py b/scripts/123.py
index 521ccfc..f6880c7 100644
--- a/scripts/123.py
+++ b/scripts/123.py
@@ -134,7 +134,10 @@ else:
     if last_file and not Path(last_file).is_file():
         last_file = ""
 
-    if last_file:
+    # Also runs under hython and batch, where there is no hou.qt and
+    # StartupPrompt raises AttributeError before the rest of the package
+    # has loaded. Skip the prompt and leave the session file-less.
+    if last_file and hou.isUIAvailable():
         dialog = StartupPrompt(last_file)
         dialog.exec()
 
diff --git a/scripts/OnCreated.py b/scripts/OnCreated.py
index 38288a2..82ff52e 100644
--- a/scripts/OnCreated.py
+++ b/scripts/OnCreated.py
@@ -1,19 +1,26 @@
 import hou
 from hc import HCSettings
 from hc import HCNode
-# import from hc.HCNetworkEditor import snapToGrid
 
 node = HCNode(kwargs['node'])
-prefs = HCSettings().prefs()
+settings = HCSettings()
+node_graph = settings.nodeGraph()
 
-node.setUserData("nodeshape", "rect")
+node.setUserData("nodeshape", settings.nodeShape())
 node.setUserData("hc_custom_color", "1")
+node.setColor(settings.nodeColor())
 
-hex_color = prefs.get('node_graph').get('node_color')
-hex_color = hex_color.lstrip('#')
-r = int(hex_color[0:2], 16) / 255.0
-g = int(hex_color[2:4], 16) / 255.0
-b = int(hex_color[4:6], 16) / 255.0
-print(r, g, b)
-node.setColor(hou.Color(r, g, b))
-# snapToGrid(hc_node)
+# Snap to the same grid HCNetworkEditor uses. This used to hardcode a 1.0 grid
+# with a 0.5/0.85 offset, so any non-default grid_*_step left every new node
+# off-grid until it was nudged.
+step_x = max(node_graph.get("grid_x_step", 2.0), 0.25)
+step_y = max(node_graph.get("grid_y_step", 1.0), 0.25)
+offset_x = node_graph.get("node_center_offset_x", 0.5)
+offset_y = node_graph.get("node_center_offset_y", 0.15)
+
+pos = node.position()
+# position() is the bottom-left corner; the grid applies to the node's center.
+node.setPosition(hou.Vector2(
+    round((pos[0] + offset_x) / step_x) * step_x - offset_x,
+    round((pos[1] + offset_y) / step_y) * step_y - offset_y,
+))
diff --git a/shortcomings.md b/shortcomings.md
new file mode 100644
index 0000000..71ac679
--- /dev/null
+++ b/shortcomings.md
@@ -0,0 +1,60 @@
+# Houdini Shortcomings & Workarounds
+
+Documented limitations of the Houdini API (H21) that affect hou-control, and any workarounds used.
+
+## Network Editor
+
+### No API to Open the Native Tab Menu
+There is no public Python API to programmatically trigger Houdini's native node-creation tab menu from the network editor. Qt event simulation (postKeyPress etc.) is unreliable and can be silently swallowed by Houdini's internal event handling.
+
+**Workaround:** Custom fuzzy-search node type picker via `HCWidgets.SelectionDialog` (used in `HCNetworkEditor.replaceNode()`).
+
+### Node Type Name Subtitle Is Not Toggleable
+Houdini shows a subtitle with the node type name (e.g. "attribwrangle") only when the node's name has been manually edited. For nodes with default names, no type subtitle appears because Houdini considers it redundant. The prefs `shownodetypenames` (per-pane) and `networkeditor.shownodetypes` (global) do not control this subtitle behavior.
+
+No known workaround.
+
+### Volume Wrangle Cannot Create New Volume Grids
+The Volume Wrangle SOP can only modify existing volume grids, not create new ones. Writing `@Cd` does not create a Cd grid if none exists on input. `vex_outputmask` only filters existing grids. To create new grids, use a **Volume VOP** node instead.
+
+### VDB Viewport Coloring: Prim Cd vs Volume Grids
+The viewport uses prim Cd attributes for uniform per-volume coloring. For per-voxel coloring (e.g. density-based intersection highlighting), you need actual Cd volume grids (VDB float3 `Cd.r`/`Cd.g`/`Cd.b`). No workaround -- must use Volume VOP to create them.
+
+### Parameter Visibility Is Conditional
+Many Houdini parameters are hidden or disabled based on other parameter values. For example, the Convert VDB node's "VDB Class" menu only appears when "Conversion" is set to "VDB". Always check `disableWhen` / `hideWhen` conditions on the `ParmTemplate`, or set prerequisite parameters first before inspecting.
+
+## UI Preferences
+
+### No Runtime Color Scheme Switching
+There is no runtime API to switch UI color schemes in Houdini 21. `hou.ui.reloadColorScheme()` only re-reads the current `.hcs` from disk -- it does not accept a scheme name. The `colors.scheme` preference controls what loads on startup but has no runtime effect. Switching schemes requires a Houdini restart.
+
+### `hou.refreshPreferences()` Can Crash
+Calling `hou.refreshPreferences()` to apply preference changes at runtime can crash Houdini. Avoid calling it.
+
+### `HCStatusBar` Overlay Cannot Be Re-shown
+Once a `QLabel` overlay created by `HCStatusBar` is hidden or its parent destroyed (e.g. during `reloadHC()`), calling `show()` or `raise_()` on the old instance does not bring it back. The widget must be re-created from scratch.
+
+**Workaround:** `HCSession.reloadHC()` already handles this by destroying and re-creating HC-owned Qt widgets. Any new code that manages persistent widgets must follow the same pattern.
+
+## Houdini Hotkey System
+
+### `fn` Key Not Supported in Symbol Names
+Houdini's hotkey symbol paths do not recognize `fn` as a modifier key. Binding to `fn+key` is not possible through the `hou.hotkeys` API.
+
+## PySide6
+
+### `setOpenExternalLinks()` Is `QTextBrowser`-only
+`setOpenExternalLinks()` belongs to `QTextBrowser`, not `QTextEdit`. Using the wrong base class causes a silent `AttributeError` inside `SelectionDialog` callables. Always check the Qt class hierarchy before using widget methods.
+
+## `SelectionDialog`
+
+### Exceptions Were Silently Swallowed (fixed)
+`HCWidgets.SelectionDialog.execute()` used to call the `self.list_dict`
+callable bare, so any exception was eaten by Qt's slot dispatch and the user
+just saw "nothing happens." It now catches, prints a traceback, and reports
+`<command>: <ExceptionType>: <message>` to the status bar.
+
+This masked a whole crop of real bugs for a long time -- `Close Other Tabs`,
+`Show Shelf`, `Toggle Update Mode` and `Floating Parameter Editor` were all
+dead on arrival. When adding a command, still verify it once by hand: a
+command that never raises but does the wrong thing is invisible to this.
diff --git a/tools/check.py b/tools/check.py
new file mode 100644
index 0000000..657878b
--- /dev/null
+++ b/tools/check.py
@@ -0,0 +1,198 @@
+"""Headless smoke check for the hc package.
+
+There is no test runner in this project -- everything runs inside Houdini's
+embedded interpreter, and a broken command in the HC Panel just looks like
+"nothing happened". This catches the cheap half of that class of bug without
+launching the GUI:
+
+    $HFS/bin/hython3.13 tools/check.py
+
+Run it from the repo root. It imports hc, binds every HCMaps entry (so a
+command whose method was renamed or deleted fails loudly), and exercises the
+pure logic that does not need a UI. It cannot replace testing in Houdini --
+anything touching hou.ui, panes or the network editor still has to be driven
+by hand -- but a failure here is always a real one.
+"""
+
+import os
+import sys
+import time
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+sys.path.insert(0, str(ROOT / "python3.13libs"))
+os.environ.setdefault("HC_PATH", str(ROOT))
+
+import hou  # noqa: E402
+
+from hc import (  # noqa: E402
+    HCNetworkEditor,
+    HCPane,
+    HCParameterTab,
+    HCPathTab,
+    HCSceneViewer,
+    HCSession,
+    HCSettings,
+    HCTab,
+)
+from hc.hcmaps import HCMaps  # noqa: E402
+
+
+passed = 0
+failed = 0
+
+
+def check(label, fn):
+    global passed, failed
+    try:
+        result = fn()
+    except Exception as e:
+        print(f"  FAIL {label}: {type(e).__name__}: {e}")
+        failed += 1
+    else:
+        print(f"  ok   {label}: {result}")
+        passed += 1
+
+
+def blank(cls):
+    """An uninitialized instance -- enough to resolve bound methods."""
+    return cls.__new__(cls)
+
+
+def check_settings():
+    print("settings")
+    settings = HCSettings()
+
+    def merged_defaults():
+        # A section missing from hc_settings.json must still come back whole:
+        # callers index straight into prefs()['node_graph']['node_color'].
+        node_graph = HCSettings._merged(HCSettings.DEFAULTS, {"node_graph": {}})["node_graph"]
+        missing = set(HCSettings.DEFAULTS["node_graph"]) - set(node_graph)
+        assert not missing, f"defaults dropped: {sorted(missing)}"
+        return f"{len(node_graph)} node_graph keys"
+
+    check("prefs() overlays DEFAULTS", merged_defaults)
+    check("nodeColor() parses the hex", lambda: settings.nodeColor())
+    check("nodeShape()", lambda: settings.nodeShape())
+    check("hcnetcursorEnabled()", lambda: settings.hcnetcursorEnabled())
+
+    def copy_is_independent():
+        original = settings.prefs()["node_graph"]["node_color"]
+        settings.prefsCopy()["node_graph"]["node_color"] = "#000000"
+        assert settings.prefs()["node_graph"]["node_color"] == original, \
+            "prefsCopy() aliased the cache"
+        return original
+
+    check("prefsCopy() does not alias the cache", copy_is_independent)
+
+    def cache_is_warm():
+        start = time.perf_counter()
+        for _ in range(2000):
+            settings.prefs()
+        micros = (time.perf_counter() - start) / 2000 * 1e6
+        assert micros < 200, f"{micros:.0f}us per read -- cache not working"
+        return f"{micros:.1f}us per read"
+
+    check("prefs() is cached on the hot path", cache_is_warm)
+
+
+def check_maps():
+    print("HCMaps")
+    maps = HCMaps()
+    session, pane = blank(HCSession), blank(HCPane)
+    specs = (
+        ("base", "tab_map_base", HCTab),
+        ("path", "tab_map_path", HCPathTab),
+        ("parm", "tab_map_path", HCParameterTab),
+        ("network editor", "tab_map_network_editor", HCNetworkEditor),
+        ("scene viewer", "tab_map_scene_viewer", HCSceneViewer),
+    )
+    for label, name, cls in specs:
+        check(
+            f"{label} map binds",
+            lambda n=name, c=cls: f"{len(getattr(maps, n)(session, pane, blank(c)))} commands",
+        )
+
+
+def check_node_ops():
+    print("node operations")
+
+    def replace_rewires():
+        """The rewire in HCNetworkEditor.replaceNode, minus the UI."""
+        geo = hou.node("/obj").createNode("geo")
+        source = geo.createNode("box")
+        old = geo.createNode("mountain")
+        sink = geo.createNode("null")
+        old.setInput(0, source)
+        sink.setInput(0, old)
+
+        new = geo.createNode("twist")
+        new.setPosition(old.position())
+        for index, upstream in enumerate(old.inputs()):
+            if upstream is not None:
+                new.setInput(index, upstream)
+        for downstream in list(old.outputs()):
+            for conn in downstream.inputConnections():
+                if conn.inputNode() == old:
+                    downstream.setInput(conn.inputIndex(), new)
+        new.setDisplayFlag(old.isDisplayFlagSet())
+        new.setRenderFlag(old.isRenderFlagSet())
+        new.bypass(old.isBypassed())
+        new.setTemplateFlag(old.isTemplateFlagSet())
+        new.setColor(old.color())
+        old.destroy()
+
+        assert sink.inputs()[0] == new, "downstream lost its input"
+        assert new.inputs()[0] == source, "upstream was not reconnected"
+        assert len(geo.children()) == 3, "the replaced node outlived the swap"
+        geo.destroy()
+        return "inputs and outputs preserved"
+
+    check("replaceNode rewire", replace_rewires)
+
+    def update_mode_toggles():
+        before = hou.updateModeSetting()
+        target = (hou.updateMode.AutoUpdate if before == hou.updateMode.Manual
+                  else hou.updateMode.Manual)
+        hou.setUpdateMode(target)
+        after = hou.updateModeSetting()
+        hou.setUpdateMode(before)
+        assert after == target, "update mode did not change"
+        return f"{before.name()} -> {after.name()}"
+
+    check("toggleUpdateMode", update_mode_toggles)
+
+    def snap_lands_on_grid():
+        """OnCreated.py and HCNetworkEditor must agree on where the grid is."""
+        node_graph = HCSettings().nodeGraph()
+        step_x = max(node_graph.get("grid_x_step", 2.0), 0.25)
+        step_y = max(node_graph.get("grid_y_step", 1.0), 0.25)
+        offset_x = node_graph.get("node_center_offset_x", 0.5)
+        offset_y = node_graph.get("node_center_offset_y", 0.15)
+
+        position = hou.Vector2(3.7, 2.3)
+        snapped = hou.Vector2(
+            round((position[0] + offset_x) / step_x) * step_x - offset_x,
+            round((position[1] + offset_y) / step_y) * step_y - offset_y,
+        )
+        for center, step, axis in (
+            (snapped[0] + offset_x, step_x, "x"),
+            (snapped[1] + offset_y, step_y, "y"),
+        ):
+            assert abs(center / step - round(center / step)) < 1e-9, \
+                f"{axis} center is off grid"
+        return f"step ({step_x}, {step_y}): {tuple(position)} -> {tuple(snapped)}"
+
+    check("new-node grid snap", snap_lands_on_grid)
+
+
+def main():
+    check_settings()
+    check_maps()
+    check_node_ops()
+    print(f"\n{passed} passed, {failed} failed")
+    return 1 if failed else 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/viewer_states/keycam.py b/viewer_states/keycam.py
index d9f7411..62dd07b 100644
--- a/viewer_states/keycam.py
+++ b/viewer_states/keycam.py
@@ -1,5 +1,6 @@
 import hou
 from hc import HCCam, HCSettings, HCGeo, HCGuides, HCSceneViewer
+from hc.hcdrawmode import get_draw_mode_label
 
 
 # Required to pass in hou.SceneViewer object
@@ -21,14 +22,23 @@ class State(object):
         # Get scene viewer
         self.scene_viewer = HCSceneViewer(scene_viewer)
 
+        # Capture current viewport camera state BEFORE keycam takes over
+        viewport = self.scene_viewer.currentViewport()
+        self._captured_state = HCCam.captureViewportState(viewport)
+
         # Assign state name
         self.state_name = state_name
 
-        # Setup hc cam
+        # Setup hc cam (this locks and resets to origin)
         self.cam = HCCam(self.cam_node, self.scene_viewer)
+        # Restore captured viewport state so keycam starts where you were
+        self.cam.initFromState(self._captured_state)
         prefs = HCSettings().prefs().get('keycam')
         self.cam.delta_r = prefs.get('units').get('delta_r')
         self.cam.delta_t = prefs.get('units').get('delta_t')
+        # drag_sensitivity is read live from settings each event (not cached)
+        # so changes in HC Settings take effect immediately without reload
+        self._settings = HCSettings()
 
 
 
@@ -57,10 +67,20 @@ class State(object):
         self.guides.update()
         self.hud = Hud(self)
         self.hud.update()
-        
+
         # Register viewer event callback for resizing
         self.scene_viewer.hou_tab.addEventCallback(self._onViewerEvent)
 
+        # Deferred aspect ratio fit -- viewport dims may not be final yet
+        state = self
+
+        def _deferredInitFit():
+            state.cam.fitAspectRatio()
+            hou.ui.removeEventLoopCallback(_deferredInitFit)
+            return False
+
+        hou.ui.addEventLoopCallback(_deferredInitFit)
+
     def onDraw(self, kwargs):
         self.guides.draw(kwargs)
 
@@ -80,15 +100,100 @@ class State(object):
         """Keys are now handled via PluginHotkeyDefinitions and onMenuAction."""
         return False
 
+    def onMouseEvent(self, kwargs):
+        """Handle click-and-drag for camera rotation."""
+        ui_event = kwargs.get('ui_event')
+        if not ui_event:
+            return False
+
+        device = ui_event.device()
+        reason = ui_event.reason()
+
+        # Only respond to left-button drag
+        if not device.isLeftButton():
+            return False
+
+        # Start drag: record initial position
+        if reason == hou.uiEventReason.Start:
+            self._drag_start = (device.mouseX(), device.mouseY())
+            self._drag_last = self._drag_start
+            self._dragging = True
+            return True
+
+        # Active drag: rotate based on mouse delta
+        if getattr(self, '_dragging', False) and reason in (hou.uiEventReason.Active, hou.uiEventReason.Changed):
+            mx, my = device.mouseX(), device.mouseY()
+            lx, ly = getattr(self, '_drag_last', (mx, my))
+
+            dx = mx - lx
+            dy = my - ly
+
+            if dx != 0 or dy != 0:
+                # Horizontal movement -> yaw (left/right)
+                # Vertical movement   -> pitch (up/down)
+                # Sensitivity 0..1: higher = more degrees per pixel (more responsive)
+                # Maps to approx 0.03 deg/px (smooth) .. 3.0 deg/px (twitchy)
+                sensitivity = self._settings.prefs().get('keycam', {}).get('drag_sensitivity', 0.25)
+                scale = 0.03 + sensitivity * 2.97
+
+                if abs(dx) >= 1:
+                    angle_y = dx * scale
+                    self._rotateByDelta(yaw=angle_y)
+
+                if abs(dy) >= 1:
+                    angle_x = -dy * scale  # invert so dragging down rotates down
+                    self._rotateByDelta(pitch=angle_x)
+
+                self._drag_last = (mx, my)
+                self.guides.update()
+                self.hud.update()
+
+            return True
+
+        return False
+
+    def _rotateByDelta(self, pitch=0, yaw=0):
+        """Apply incremental rotation by pitch (x-axis) and/or yaw (y-axis) in degrees."""
+        self.cam._syncFromCameraNode()
+        if pitch != 0:
+            m = hou.hmath.buildRotateAboutAxis(self.cam.local_x, pitch)
+            self.cam.t -= self.cam.p
+            self.cam.t *= m
+            self.cam.t += self.cam.p
+            self.cam.local_x *= m
+            self.cam.local_y *= m
+            self.cam.local_z *= m
+            self.cam.r += hou.Vector3(pitch, 0, 0)
+        if yaw != 0:
+            m = hou.hmath.buildRotateAboutAxis(self.cam.global_y, yaw)
+            self.cam.t -= self.cam.p
+            self.cam.t *= m
+            self.cam.t += self.cam.p
+            self.cam.local_x *= m
+            self.cam.local_y *= m
+            self.cam.local_z *= m
+            self.cam.r += hou.Vector3(0, yaw, 0)
+
     def onKeyboardEvent(self, kwargs):
         return self.onKeyEvent(kwargs)
 
     def onMenuAction(self, kwargs):
         menu_item = kwargs['menu_item']
-        
+
+        if menu_item in ('zoom_persp_in', 'zoom_persp_out', 'zoom_ortho_in', 'zoom_ortho_out'):
+            if self.cam.projection == 'ortho':
+                swap_map = {
+                    'zoom_persp_in': 'zoom_ortho_in',
+                    'zoom_persp_out': 'zoom_ortho_out',
+                    'zoom_ortho_in': 'zoom_persp_in',
+                    'zoom_ortho_out': 'zoom_persp_out',
+                }
+                menu_item = swap_map[menu_item]
+
         action_map = {
             'frame':          self.cam.frame,
             'reset':          self.cam.reset,
+            'reset_aspect_ratio': self.cam.resetAspectRatio,
             'rotate_left':    self.cam.rotateLeft,
             'rotate_right':   self.cam.rotateRight,
             'rotate_up':      self.cam.rotateUp,
@@ -98,15 +203,32 @@ class State(object):
             'trans_up':       self.cam.translateUp,
             'trans_down':     self.cam.translateDown,
             'toggle_proj':    self.cam.toggleProjection,
+            'cycle_layout':   self._cycleLayoutWithAspectFit,
+            'set_layout_single':           lambda: self._setLayout('Single'),
+            'set_layout_doubleside':       lambda: self._setLayout('DoubleSide'),
+            'set_layout_doublestack':      lambda: self._setLayout('DoubleStack'),
+            'set_layout_quad':             lambda: self._setLayout('Quad'),
+            'set_layout_quadbottomsplit':  lambda: self._setLayout('QuadBottomSplit'),
+            'set_layout_quadleftsplit':    lambda: self._setLayout('QuadLeftSplit'),
+            'set_layout_triplebottomsplit':lambda: self._setLayout('TripleBottomSplit'),
+            'set_layout_tripleleftsplit':  lambda: self._setLayout('TripleLeftSplit'),
             'zoom_persp_in':  lambda: self.cam.zoom('in'),
             'zoom_persp_out': lambda: self.cam.zoom('out'),
             'zoom_ortho_in':  lambda: self.cam.zoomOrtho('in'),
             'zoom_ortho_out': lambda: self.cam.zoomOrtho('out'),
+            'cycle_draw_mode': self.scene_viewer.cycleDrawMode,
+            'view_top':       lambda: self._setView('top'),
+            'view_bottom':    lambda: self._setView('bottom'),
+            'view_front':     lambda: self._setView('front'),
+            'view_back':      lambda: self._setView('back'),
+            'view_left':      lambda: self._setView('left'),
+            'view_right':     lambda: self._setView('right'),
         }
 
         if menu_item in action_map:
             res = action_map[menu_item]()
             self.guides.update()
+            self.hud.update()
             return res
         
         # Handle Guide toggles
@@ -118,6 +240,35 @@ class State(object):
             
         return False
 
+    def _setView(self, view_name):
+        self.cam.view = view_name
+        self.cam.setView()
+        return True
+
+    def _cycleLayoutWithAspectFit(self):
+        self.scene_viewer.nextLayout()
+        state = self
+
+        def _deferredFit():
+            state.cam.fitAspectRatio()
+            hou.ui.removeEventLoopCallback(_deferredFit)
+            return False
+
+        hou.ui.addEventLoopCallback(_deferredFit)
+        return True
+
+    def _setLayout(self, layout_name):
+        self.scene_viewer.setLayout(layout_name)
+        state = self
+
+        def _deferredFit():
+            state.cam.fitAspectRatio()
+            hou.ui.removeEventLoopCallback(_deferredFit)
+            return False
+
+        hou.ui.addEventLoopCallback(_deferredFit)
+        return True
+
     def onMenuPreOpen(self, kwargs):
         menu_id = kwargs['menu']
         menu_item_states = kwargs['menu_item_states']
@@ -163,6 +314,13 @@ class Hud:
         self.template = {
             'title': 'Keycam',
             'rows': [
+                {
+                    'id':    'draw_mode',
+                    'type':  'plain',
+                    'label': 'Draw',
+                    'value': 'Wireframe',
+                    'key':   'Ctrl + D'
+                },
                 {
                     'id':    'layout',
                     'type':  'plain',
@@ -230,6 +388,15 @@ class Hud:
             ]
         }
 
+    def _draw_mode_label(self):
+        if hasattr(self.scene_viewer, 'drawModeLabel'):
+            return self.scene_viewer.drawModeLabel()
+
+        for display_set in self.scene_viewer.allDisplaySets():
+            return get_draw_mode_label(display_set.shadedMode().name())
+
+        return get_draw_mode_label(None)
+
     def update(self):
         layout_map = {
             'geometryViewportLayout.DoubleSide':        2,
@@ -242,15 +409,16 @@ class Hud:
             'geometryViewportLayout.Single':            1,
         }
         layout = self.scene_viewer.layout()
-        n_viewports = layout_map[str(layout)]
-        self.template['rows'][3]['count'] = n_viewports
+        n_viewports = layout_map.get(str(layout), 1)
+        self.template['rows'][4]['count'] = n_viewports
 
         # Updates
         hou_scene_viewer = self.scene_viewer.hou_tab
         hou_scene_viewer.hudInfo(template=self.template)
         updates = {
-            'layout':   str(self.scene_viewer.layout())[23:],
-            'layout_g': self.scene_viewer.layouts().index(self.scene_viewer.layout()),
+            'layout':    str(self.scene_viewer.layout())[23:],
+            'layout_g':  self.scene_viewer.layouts().index(self.scene_viewer.layout()),
+            'draw_mode': self._draw_mode_label(),
         }
         hou_scene_viewer.hudInfo(hud_values=updates)
 
@@ -281,11 +449,28 @@ def createViewerStateTemplate():
         'trans_down':    ('Translate Down', ['Shift+j']),
         'frame':         ('Frame',         ['f']),
         'toggle_proj':   ('Toggle Projection', ['o']),
+        'cycle_layout':  ('Cycle Layout',      ['Ctrl+l']),
         'zoom_persp_in': ('Zoom In (Persp)',   ['=']),
         'zoom_persp_out':('Zoom Out (Persp)',  ['-']),
         'zoom_ortho_in': ('Zoom In (Ortho)',   ['Shift+=']),
         'zoom_ortho_out':('Zoom Out (Ortho)',  ['Shift+-']),
+        'cycle_draw_mode': ('Cycle Draw Mode', ['Ctrl+d']),
+        'view_top':      ('Top View',          ['1']),
+        'view_bottom':   ('Bottom View',       ['2']),
+        'view_front':    ('Front View',        ['3']),
+        'view_back':     ('Back View',         ['4']),
+        'view_left':     ('Left View',         ['5']),
+        'view_right':    ('Right View',        ['6']),
         'reset':         ('Reset',             ['r']),
+        'reset_aspect_ratio': ('Reset Aspect Ratio', []),
+        'set_layout_single':           ('Single',           []),
+        'set_layout_doubleside':       ('DoubleSide',       []),
+        'set_layout_doublestack':      ('DoubleStack',      []),
+        'set_layout_quad':             ('Quad',             []),
+        'set_layout_quadbottomsplit':  ('QuadBottomSplit',  []),
+        'set_layout_quadleftsplit':    ('QuadLeftSplit',    []),
+        'set_layout_triplebottomsplit':('TripleBottomSplit', []),
+        'set_layout_tripleleftsplit':  ('TripleLeftSplit',  []),
     }
     
     for cmd_id, (label, defaults) in commands.items():
@@ -457,14 +642,15 @@ def createViewerStateTemplate():
     
     menu.addActionItem('frame', 'Frame', hotkey=f'{ctx}.frame')
     menu.addActionItem('reset', 'Reset', hotkey=f'{ctx}.reset')
+    menu.addActionItem('reset_aspect_ratio', 'Reset Aspect Ratio', hotkey=f'{ctx}.reset_aspect_ratio')
 
     set_view_menu = hou.ViewerStateMenu('set_view_menu', 'Set View')
-    set_view_menu.addActionItem('top',    'Top')
-    set_view_menu.addActionItem('bottom', 'Bottom')
-    set_view_menu.addActionItem('front',  'Front')
-    set_view_menu.addActionItem('back',   'Back')
-    set_view_menu.addActionItem('left',   'Left')
-    # set_view_menu.addActionItem('right', 'Right')
+    set_view_menu.addActionItem('view_top',    'Top',    hotkey=f'{ctx}.view_top')
+    set_view_menu.addActionItem('view_bottom', 'Bottom', hotkey=f'{ctx}.view_bottom')
+    set_view_menu.addActionItem('view_front',  'Front',  hotkey=f'{ctx}.view_front')
+    set_view_menu.addActionItem('view_back',   'Back',   hotkey=f'{ctx}.view_back')
+    set_view_menu.addActionItem('view_left',   'Left',   hotkey=f'{ctx}.view_left')
+    set_view_menu.addActionItem('view_right',  'Right',  hotkey=f'{ctx}.view_right')
     menu.addMenu(set_view_menu)
 
     guide_menu = hou.ViewerStateMenu('guide_menu', 'Guides')
@@ -477,8 +663,21 @@ def createViewerStateTemplate():
     guide_menu.addToggleItem('ray',        'Ray',         0)
     menu.addMenu(guide_menu)
 
-    menu.addSeparator()
+    layout_menu = hou.ViewerStateMenu('layout_menu', 'Layout')
+    layout_menu.addActionItem('cycle_layout',  'Cycle Layout', hotkey=f'{ctx}.cycle_layout')
+    layout_menu.addSeparator()
+    layout_menu.addActionItem('set_layout_single',           'Single',           hotkey=f'{ctx}.set_layout_single')
+    layout_menu.addActionItem('set_layout_doubleside',       'DoubleSide',       hotkey=f'{ctx}.set_layout_doubleside')
+    layout_menu.addActionItem('set_layout_doublestack',      'DoubleStack',      hotkey=f'{ctx}.set_layout_doublestack')
+    layout_menu.addActionItem('set_layout_quad',             'Quad',             hotkey=f'{ctx}.set_layout_quad')
+    layout_menu.addActionItem('set_layout_quadbottomsplit',  'QuadBottomSplit',  hotkey=f'{ctx}.set_layout_quadbottomsplit')
+    layout_menu.addActionItem('set_layout_quadleftsplit',    'QuadLeftSplit',    hotkey=f'{ctx}.set_layout_quadleftsplit')
+    layout_menu.addActionItem('set_layout_triplebottomsplit','TripleBottomSplit', hotkey=f'{ctx}.set_layout_triplebottomsplit')
+    layout_menu.addActionItem('set_layout_tripleleftsplit',  'TripleLeftSplit',  hotkey=f'{ctx}.set_layout_tripleleftsplit')
+    menu.addMenu(layout_menu)
+
     menu.addToggleItem('toggle_proj', 'Toggle Projection', 0, hotkey=f'{ctx}.toggle_proj')
+    menu.addActionItem('cycle_draw_mode', 'Cycle Draw Mode', hotkey=f'{ctx}.cycle_draw_mode')
     
     # We add the remaining hotkey actions to a submenu to keep them registered
     # but out of the main menu if desired.
@@ -499,9 +698,10 @@ def createViewerStateTemplate():
     nav_menu.addActionItem('zoom_ortho_out', 'Zoom Out (Ortho)', hotkey=f'{ctx}.zoom_ortho_out')
     
     menu.addMenu(nav_menu)
-    
+
+    menu.addSeparator()
+
     template.bindMenu(menu)
 
     # Ok
     return template
-