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

commita83f530b315c5878e8cf89049b0422ae823f9440
parent4458de0059
authorLucas Galante <[email protected]>
date2026-09-12 09:19
hc: add Reset Node Colors, the way back into the setting

Set Node Colors drops the hc_custom_color tag, so a node colored by hand
is never touched by updateNodeColors() again -- which was a one-way door:
nothing could put the node back under the configured default.

Reset Node Colors restores the default color on the selection and
re-records the tag, so the node follows the setting once more. It takes
the whole selection as one undo, which Set Node Colors still does not.

tools/check.py drives the round trip -- opt a node out, confirm the next
pass leaves it alone, opt it back in, confirm the pass maintains it.

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

 python3.13libs/hc/hcnetworkeditor.py | 25 +++++++++++++++++++++++++
 tools/check.py                       | 30 ++++++++++++++++++++++++++++++
 2 files changed, 55 insertions(+)

diff --git a/python3.13libs/hc/hcnetworkeditor.py b/python3.13libs/hc/hcnetworkeditor.py
index 0bab9fd..fbab051 100644
--- a/python3.13libs/hc/hcnetworkeditor.py
+++ b/python3.13libs/hc/hcnetworkeditor.py
@@ -1245,6 +1245,31 @@ class HCNetworkEditor(HCPathTab):
             # would otherwise revert it to the default on the next hip load.
             node.destroyUserData("hc_custom_color", must_exist=False)
 
+    @command("Reset Node Colors")
+    def resetNodeColors(self):
+        """Put selected nodes back under the configured default color.
+
+        Set Node Colors deliberately drops the `hc_custom_color` tag, so a node
+        colored by hand is never touched by updateNodeColors() again. This is
+        the way back: it restores the default color and re-records the tag, so
+        the node follows the setting once more. Without it, opting a node out
+        was a one-way door.
+        """
+        nodes = list(self.hou_tab.pwd().selectedChildren())
+        if not nodes:
+            hou.ui.setStatusMessage("No selected nodes to reset",
+                                    hou.severityType.Warning)
+            return
+        settings = HCSettings()
+        color = settings.nodeColor()
+        color_hex = settings.nodeColorHex()
+        # One undo for the whole selection, not one per node.
+        with hou.undos.group("Reset Node Colors"):
+            for node in nodes:
+                node.setColor(color)
+                node.setUserData("hc_custom_color", color_hex)
+        hou.ui.setStatusMessage(f"Reset {len(nodes)} node(s) to {color_hex}")
+
     @command("Set Node Shapes")
     def setNodeShapes(self):
         shape = HCSettings().nodeShape()
diff --git a/tools/check.py b/tools/check.py
index fc99a9b..771c398 100644
--- a/tools/check.py
+++ b/tools/check.py
@@ -733,6 +733,36 @@ def check_node_colors():
 
     check("updateNodeColors is idempotent", second_pass_writes_nothing)
 
+    def opting_out_is_reversible():
+        """Set Node Colors / Reset Node Colors, minus the UI.
+
+        Dropping the tag has to be undoable by hand, or coloring one node is a
+        one-way door out of the setting.
+        """
+        default = settings.nodeColor()
+        default_hex = settings.nodeColorHex()
+        geo = hou.node("/obj").createNode("geo")
+        node = geo.createNode("box")
+        session = blank(HCSession)
+
+        # Set Node Colors: an explicit color, tag dropped.
+        node.setColor(RED)
+        node.destroyUserData("hc_custom_color", must_exist=False)
+        session.updateNodeColors()
+        assert colorsMatch(node.color(), RED), "opting out did not stick"
+
+        # Reset Node Colors: default color, tag re-recorded.
+        node.setColor(default)
+        node.setUserData("hc_custom_color", default_hex)
+        session.updateNodeColors()
+        assert colorsMatch(node.color(), default), "reset node was not maintained"
+        assert node.userData("hc_custom_color") == default_hex, "reset lost the tag"
+
+        geo.destroy()
+        return "opt out, then opt back in"
+
+    check("resetNodeColors round trip", opting_out_is_reversible)
+
 
 def main():
     check_settings()