SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git
neteditor: make the view follow the hcnetcursor off the edge
_frameRectInView already panned by the overshoot on every cursor move, but
hou.NetworkEditor.setVisibleBounds drops a bounds change that keeps the
zoom unless set_center_when_scale_rejected is passed -- the docs describe
the flag the other way round; measured live, a translated rect without it
is ignored and with it applies. setBounds now passes it, which also brings
centerOnCursor and translateView back to life. Nodes moved by the arrow
keys carry the cursor along, so that path frames the cursor too.
check.py drives _frameRectInView through a fake tab and asserts the pan
reaches the editor with the flag, keeps the zoom and covers the cursor.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
python3.13libs/hc/hcnetworkeditor.py | 10 +++++++-
tools/check.py | 46 ++++++++++++++++++++++++++++++++++++
2 files changed, 55 insertions(+), 1 deletion(-)
diff --git a/python3.13libs/hc/hcnetworkeditor.py b/python3.13libs/hc/hcnetworkeditor.py
index 53384ea..c8b09cb 100644
--- a/python3.13libs/hc/hcnetworkeditor.py
+++ b/python3.13libs/hc/hcnetworkeditor.py
@@ -601,6 +601,9 @@ class HCNetworkEditor(HCPathTab):
origin += delta
state["origin"] = (origin[0], origin[1])
self._setHcnetcursorState(state)
+ # The cursor rides along with nodes moved by the arrow keys; keep it
+ # in view the same way a plain cursor move does.
+ self._frameRectInView(self.hcnetcursorRect())
self.updateCurrentNodeOverlay()
def expandHcnetcursor(self, direction, distance=1):
@@ -1089,7 +1092,12 @@ class HCNetworkEditor(HCPathTab):
return self.bounds().size()
def setBounds(self, bounds):
- self.hou_tab.setVisibleBounds(bounds)
+ # The last argument is misdocumented. Measured in a live session:
+ # without it a bounds change that keeps the zoom is dropped entirely,
+ # so every pure pan here -- the cursor follow in _frameRectInView,
+ # centerOnCursor, translateView -- silently did nothing, while frames
+ # that changed the zoom worked. Houdini's own framing code passes it.
+ self.hou_tab.setVisibleBounds(bounds, 0.0, 0.0, True)
self.updateCurrentNodeOverlay()
def translateView(self, direction):
diff --git a/tools/check.py b/tools/check.py
index ca07a5d..48cbd60 100644
--- a/tools/check.py
+++ b/tools/check.py
@@ -786,6 +786,52 @@ def check_node_ops():
check("post-action sweep", sweep_snaps_the_network)
+ def pans_reach_the_editor():
+ """hou.NetworkEditor.setVisibleBounds drops a change that keeps the
+ zoom unless set_center_when_scale_rejected is passed (the docs say
+ the reverse; measured live). Every pan in the package funnels through
+ setBounds, so that one call must pass it -- with it missing, the
+ cursor follow, centerOnCursor and translateView were all no-ops."""
+ from hc import hcnetworkeditor as ne
+
+ class FakePane:
+ def id(self):
+ return 987656
+
+ class FakeTab:
+ def __init__(self):
+ self.calls = []
+ self.view = hou.BoundingRect(0.0, 0.0, 10.0, 5.0)
+
+ def pane(self):
+ return FakePane()
+
+ def setPref(self, name, value):
+ pass
+
+ def visibleBounds(self):
+ return hou.BoundingRect(self.view)
+
+ def setVisibleBounds(self, bounds, *args):
+ self.calls.append((hou.BoundingRect(bounds), args))
+ self.view = hou.BoundingRect(bounds)
+
+ tab = FakeTab()
+ editor = ne.HCNetworkEditor(tab)
+ editor.updateCurrentNodeOverlay = lambda force=False: None
+
+ # A cursor rect just past the right edge: a pure pan, zoom unchanged.
+ editor._frameRectInView(hou.BoundingRect(11.0, 2.0, 12.0, 3.0))
+ assert len(tab.calls) == 1, f"expected one pan, got {len(tab.calls)}"
+ bounds, args = tab.calls[0]
+ assert args and args[-1] is True, \
+ f"pan called setVisibleBounds{(bounds,) + args}: the same-zoom flag is missing"
+ assert abs(bounds.size()[0] - 10.0) < 1e-9, "a follow pan must not change the zoom"
+ assert bounds.max()[0] >= 12.0, f"view {bounds} does not reach the cursor"
+ return "same-zoom flag passed; follow pan keeps zoom and reaches the cursor"
+
+ check("pans reach the editor", pans_reach_the_editor)
+
def pwd_is_a_real_node():
"""HCPathTab.pwd() returned an HCNode with no path(), so path() -- and
the Show Path Message command through it -- raised AttributeError."""