SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git
hc: pull the last hardcoded color into the schema, unhook Reload Colors
Two loose ends in the node color work.
The off-screen current-node arrow drawn over the network editor carried
the only hardcoded hou.Color left in the package. It is now a declared
`color` setting, which means it gets a swatch in the settings panel for
free. Resolving it needed a per-color accessor rather than another
nodeColor()-shaped method, so HCSettings.color/colorRGB/colorHex take a
schema path and fall back to that setting's own declared default --
nodeColor() is now one line on top of it.
Reload Colors called updateNodeColors(), so rereading a .hcs file off
disk also rewrote node colors throughout the open scene. They share a
word and nothing else: one reloads config, the other edits the hip.
Update Node Colors already had its own menu item and is now a @command
too, so unhooking it costs no reach.
tools/check.py asserts every declared color default actually parses, that
each color falls back to its own default rather than a shared one, and
that the arrow kept the value it was hardcoded to.
Co-Authored-By: Claude Opus 5 <[email protected]>
python3.13libs/hc/hcnetworkeditor.py | 2 +-
python3.13libs/hc/hcschema.py | 4 ++++
python3.13libs/hc/hcsession.py | 9 ++++++++-
python3.13libs/hc/hcsettings.py | 32 ++++++++++++++++++++----------
tools/check.py | 38 ++++++++++++++++++++++++++++++++++++
5 files changed, 73 insertions(+), 12 deletions(-)
diff --git a/python3.13libs/hc/hcnetworkeditor.py b/python3.13libs/hc/hcnetworkeditor.py
index 5239660..e44a494 100644
--- a/python3.13libs/hc/hcnetworkeditor.py
+++ b/python3.13libs/hc/hcnetworkeditor.py
@@ -211,7 +211,7 @@ class HCNetworkEditor(HCPathTab):
tip_y - length * math.sin(angle + math.atan(width_ratio))
)
- color = hou.Color((0.38, 0.56, 0.56))
+ color = HCSettings().color("node_graph", "current_node_arrow_color")
width = 3.0
return [
hou.NetworkShapeLine(p1, p2, color=color, alpha=1.0, width=width, screen_space=True, smooth=True),
diff --git a/python3.13libs/hc/hcschema.py b/python3.13libs/hc/hcschema.py
index e69a8df..d408668 100644
--- a/python3.13libs/hc/hcschema.py
+++ b/python3.13libs/hc/hcschema.py
@@ -97,6 +97,10 @@ SCHEMA = {
"node_color": Setting("color", "#607070"),
"zoom_center": Setting("choice", "mouse_cursor", choices=ZOOM_CENTERS),
"hcnetcursor": Setting("bool", True, label="hcnetcursor"),
+ "current_node_arrow_color": Setting(
+ "color", "#618f8f", label="Current Node Arrow Color",
+ help="The off-screen current-node arrow drawn over the network editor.",
+ ),
"grid_x_step": Setting("slider", 2.0, range=(0.25, 8.0)),
"grid_y_step": Setting("slider", 1.0, range=(0.25, 8.0)),
"node_center_offset_x": Setting("slider", 0.5, range=(0.0, 2.0)),
diff --git a/python3.13libs/hc/hcsession.py b/python3.13libs/hc/hcsession.py
index fd3c36e..b5cb39d 100644
--- a/python3.13libs/hc/hcsession.py
+++ b/python3.13libs/hc/hcsession.py
@@ -360,9 +360,15 @@ class HCSession:
@command("Reload Colors")
def reloadColorSchemes(self):
+ """Reload the UI and viewport color schemes from config/.
+
+ This used to call updateNodeColors() as well, so rereading a .hcs file
+ also rewrote node colors throughout the open scene. They share a word
+ and nothing else: one reloads config off disk, the other edits the hip.
+ Update Node Colors is its own command and its own menu item.
+ """
hou.ui.reloadColorScheme()
hou.ui.reloadViewportColorSchemes()
- self.updateNodeColors()
@command("HC Info")
def hcInfo(self):
@@ -485,6 +491,7 @@ class HCSession:
setattr(hou.session, callback_name, _callback)
hou.ui.addEventLoopCallback(_callback)
+ @command("Update Node Colors")
def updateNodeColors(self):
"""Recolor HC-managed nodes to the configured default.
diff --git a/python3.13libs/hc/hcsettings.py b/python3.13libs/hc/hcsettings.py
index 6ba4b8e..5c5714f 100644
--- a/python3.13libs/hc/hcsettings.py
+++ b/python3.13libs/hc/hcsettings.py
@@ -56,9 +56,9 @@ def formatHex(color):
return "#" + "".join(f"{max(0, min(255, round(c * 255))):02x}" for c in rgb)
-def _defaultNodeColorRGB():
- """The schema's node_color default, as an (r, g, b) tuple."""
- setting = hcschema.lookup(("node_graph", "node_color"))
+def _schemaDefaultRGB(path):
+ """The declared default of a `color` setting, as an (r, g, b) tuple."""
+ setting = hcschema.lookup(path)
return parseHex(setting.default if setting else None) or (0.0, 0.0, 0.0)
@@ -181,15 +181,27 @@ class HCSettings:
def nodeShape(self):
return self.nodeGraph().get("node_shape")
- def nodeColorRGB(self):
- """The configured default node color as an (r, g, b) tuple.
+ def colorRGB(self, *path):
+ """A `color` setting as an (r, g, b) tuple, addressed by schema path.
- An unparseable value falls back to the schema default, not to a second
- hardcoded color -- this used to answer with a blue-purple that appears
- nowhere in hcschema, so a typo in the hex silently produced a color the
- settings panel never showed.
+ An unparseable value falls back to that setting's own declared default,
+ never to a hardcoded stand-in somewhere else -- node_color used to
+ answer with a blue-purple that appears nowhere in hcschema, so a typo
+ in the hex silently produced a color the settings panel never showed.
"""
- return parseHex(self.nodeGraph().get("node_color")) or _defaultNodeColorRGB()
+ stored = self.section(*path[:-1]).get(path[-1])
+ return parseHex(stored) or _schemaDefaultRGB(path)
+
+ def color(self, *path):
+ """A `color` setting as a hou.Color."""
+ return hou.Color(self.colorRGB(*path))
+
+ def colorHex(self, *path):
+ """A `color` setting normalised to '#rrggbb'."""
+ return formatHex(self.colorRGB(*path))
+
+ def nodeColorRGB(self):
+ return self.colorRGB("node_graph", "node_color")
def nodeColor(self):
"""The configured default node color as a hou.Color."""
diff --git a/tools/check.py b/tools/check.py
index 8d5e560..5544202 100644
--- a/tools/check.py
+++ b/tools/check.py
@@ -664,6 +664,44 @@ def check_node_colors():
check("hex helpers", hex_round_trips)
+ def every_color_default_parses():
+ """A `color` whose default is not a color hands the panel, and every
+ caller, a fallback that is itself broken."""
+ found = []
+
+ def walk(schema, path):
+ for key, value in schema.items():
+ if isinstance(value, dict):
+ walk(value, path + (key,))
+ elif value.kind == "color":
+ assert parseHex(value.default) is not None, \
+ f"{'.'.join(path + (key,))} default {value.default!r} is not a color"
+ found.append(".".join(path + (key,)))
+
+ walk(hcschema.SCHEMA, ())
+ assert found, "no color settings declared"
+ return ", ".join(found)
+
+ check("color defaults", every_color_default_parses)
+
+ def fallback_is_per_setting():
+ """Each color falls back to its own declared default, not a shared one."""
+ arrow = ("node_graph", "current_node_arrow_color")
+ node = ("node_graph", "node_color")
+ assert settings.colorHex(*arrow) != settings.colorHex(*node), \
+ "two colors resolved to the same value"
+ for path in (arrow, node):
+ declared = hcschema.lookup(path).default
+ # A key absent from the file must still resolve to its own default.
+ assert formatHex(parseHex(declared)) == settings.colorHex(*path), \
+ f"{path[-1]} did not resolve to its declared default"
+ # The arrow color has to still be the value it was hardcoded to.
+ assert colorsMatch(settings.color(*arrow), hou.Color((0.38, 0.56, 0.56))), \
+ "the arrow color moved when it was pulled into the schema"
+ return f"{settings.colorHex(*arrow)} arrow, {settings.colorHex(*node)} node"
+
+ check("per-setting color fallback", fallback_is_per_setting)
+
def bad_hex_falls_back_to_schema():
# Not to a second hardcoded color: the fallback has to be the value the
# settings panel shows for a missing key.