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

commit61a0cc43b96255e73d555f5b60652dd7a43689b9
parent6614db58b6
authorLucas Galante <[email protected]>
date2026-09-15 12:46
developer_panel: visualizers in a tabbed multiparm

The per-visualizer tabs were spare parms appended to each instance by
an Add button, with a Python module that reindexed them on removal and
a folder-index lookup to relabel tabs. They are now instances of a
tabbed multiparm; every instance parm routes to one sync() callback
that pushes the instance onto its visualizer, and the block's own
callback rebuilds the set when instances are added or removed. A
Rebuild button recreates the visualizers from the parms, and
adopt_visualizers() fills the multiparm from the visualizers an old
instance already carries, since Houdini persists those itself.

The Attribute fields now list the input's point attributes instead of
a fixed mag0..dir3 menu. Dead ramp-preset code pointing at
~/src/developer/scripts is gone, as is the visualizer rename:
ViewportVisualizer.setName() segfaulted Houdini 22.0.429 from a
callback, and the name is cosmetic.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 otls/sop_lsgalante.developer_panel.1.0.hdalc | Bin 24039 -> 16795 bytes
 tools/check.py                               |  31 +++++++++++++++++++++++++++
 2 files changed, 31 insertions(+)

diff --git a/otls/sop_lsgalante.developer_panel.1.0.hdalc b/otls/sop_lsgalante.developer_panel.1.0.hdalc
index bbde53c..2d51194 100755
Binary files a/otls/sop_lsgalante.developer_panel.1.0.hdalc and b/otls/sop_lsgalante.developer_panel.1.0.hdalc differ
diff --git a/tools/check.py b/tools/check.py
index 618ec9e..7af4c1b 100644
--- a/tools/check.py
+++ b/tools/check.py
@@ -621,6 +621,36 @@ def check_open_recent():
     check("recentHipFiles() only lists existing files", recent_files_exist)
 
 
+def check_developer_panel():
+    """The Developer Panel HDA's visualizers live in a tabbed multiparm
+    whose instance parms all route to one sync() callback."""
+    print("developer panel")
+    import os
+    path = ROOT / "otls" / "sop_lsgalante.developer_panel.1.0.hdalc"
+
+    def multiparm_interface():
+        hou.hda.installFile(str(path))
+        d = [x for x in hou.hda.definitionsInFile(str(path))][0]
+        ptg = d.parmTemplateGroup()
+        block = ptg.find("visualizers")
+        assert isinstance(block, hou.FolderParmTemplate), "no visualizers folder"
+        assert block.folderType() == hou.folderType.TabbedMultiparmBlock, block.folderType()
+        names = [pt.name() for pt in block.parmTemplates()]
+        for expected in ("attr#", "type#", "coloring#", "coloring_attr#", "bounds#", "ramp#", "color#", "len#", "state#"):
+            assert expected in names, f"{expected} missing from {names}"
+        for pt in block.parmTemplates():
+            assert pt.scriptCallback() == "hou.phm().sync(kwargs)", (pt.name(), pt.scriptCallback())
+        assert block.scriptCallback() == "hou.phm().count_changed(kwargs)"
+        module = d.sections()["PythonModule"].contents()
+        compile(module, "PythonModule", "exec")
+        for fn in ("sync", "rebuild", "count_changed", "attr_menu", "adopt_visualizers"):
+            assert f"def {fn}(" in module, fn
+        assert ".setName(" not in module, "setName segfaults Houdini 22.0.429 from a callback"
+        return f"{len(names)} instance parms, module compiles"
+
+    check("tabbed multiparm, one sync callback", multiparm_interface)
+
+
 def check_status_circle():
     """The status circle's menu is built from the main-menu XML files with
     everything Python cannot run filtered out."""
@@ -1775,6 +1805,7 @@ def main():
     check_split_handles()
     check_nodegraph_hooks()
     check_open_recent()
+    check_developer_panel()
     check_status_circle()
     check_geometry()
     check_node_ops()