SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git
netcursor: the box is a margin around the nodes, not a scale of the cells
The drawn cursor box was the cursor's cells scaled about their centre
by hcnetcursor_scale. Nodes are 1 by 0.3 units in 1.99 by 0.99 cells,
so no scale gives the same gap on every side, and a 1-by-2 cursor at
0.64 drew a 1.27-unit square that clipped its two nodes top and bottom
while leaving 0.14 units of air at the sides.
The box is now the block of nodes the cursor frames (each cell's centre
plus and minus the node offset) grown by hcnetcursor_margin, in units,
on every side; the setting replaces hcnetcursor_scale, default 0.15,
which is what the old 0.64 gave at the sides of one cell. Measured
live: 0.15 on all four sides of a 1-by-1 and around a 2-by-2 block.
The picture is painted per unit of the box (100 px) instead of per cell,
since a box that is no longer cell-shaped stretched the old per-cell
picture's outline unevenly; the outline is now the same width on every
side and at every cursor size.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
hc_settings.json | 2 +-
python3.13libs/hc/hcnetcursorimage.py | 35 +++++++++++++------------
python3.13libs/hc/hcnetworkeditor.py | 48 +++++++++++++++++++----------------
python3.13libs/hc/hcschema.py | 7 ++---
python3.13libs/hc/hcsettings.py | 12 ++++-----
tools/check.py | 14 +++++-----
6 files changed, 64 insertions(+), 54 deletions(-)
diff --git a/hc_settings.json b/hc_settings.json
index 08d7704..f02b224 100644
--- a/hc_settings.json
+++ b/hc_settings.json
@@ -39,7 +39,7 @@
"current_node_arrow_color": "#618f8f",
"hcnetcursor": true,
"hcnetcursor_color": "#d8b34b",
- "hcnetcursor_scale": 0.64,
+ "hcnetcursor_margin": 0.15,
"grid_snap": true,
"drop_swap": true,
"grid_x_step": 1.99,
diff --git a/python3.13libs/hc/hcnetcursorimage.py b/python3.13libs/hc/hcnetcursorimage.py
index 5419fef..a3a91ba 100644
--- a/python3.13libs/hc/hcnetcursorimage.py
+++ b/python3.13libs/hc/hcnetcursorimage.py
@@ -7,10 +7,11 @@ config/hcnetcursor_assets, one per cell size, all the same gold; now the
picture is painted the first time a (cells, colour) pair is asked for and
cached in the session's temp dir.
-The look is the old asset set's: CELL_PIXELS per grid cell, transparent
-inside, a BORDER-pixel outline. The outline is a fixed pixel width in the
-image, and the image is stretched over the rect, so on screen it stays the
-same fraction of a cell whatever the cursor's size.
+The look is the old asset set's: transparent inside, a BORDER-pixel
+outline. The picture is painted at UNIT_PIXELS per network unit of the rect
+it will be stretched over, so the outline is the same width on every side
+and the same width whatever the cursor's size; the old assets were painted
+per cell, and a box that is not cell-shaped stretched their outline unevenly.
"""
import os
@@ -18,9 +19,9 @@ import tempfile
import hou
-CELL_PIXELS = (200, 100) # image pixels per grid cell, x and y
+UNIT_PIXELS = 100 # image pixels per network unit
BORDER = 5 # outline width, in image pixels
-MAX_CELLS = 12 # the old asset set stopped here; larger cursors reuse it
+MAX_PIXELS = 2400 # a cursor wider than this reuses the largest picture
_generated = {}
@@ -40,28 +41,30 @@ def is_cursor_image(path):
return os.path.basename(path).startswith("hcnetcursor_")
-def image_file(width_cells, height_cells, color_hex):
- """Path of the cursor picture for this many cells in this colour."""
- width_cells = max(1, min(int(width_cells), MAX_CELLS))
- height_cells = max(1, min(int(height_cells), MAX_CELLS))
+def pixel_size(width_units, height_units):
+ """Image size for a box this many network units across, as whole pixels."""
+ return (max(2 * BORDER, min(int(round(width_units * UNIT_PIXELS)), MAX_PIXELS)),
+ max(2 * BORDER, min(int(round(height_units * UNIT_PIXELS)), MAX_PIXELS)))
+
+
+def image_file(width_units, height_units, color_hex):
+ """Path of the cursor picture for a box this size in this colour."""
+ width, height = pixel_size(width_units, height_units)
color_hex = color_hex.lstrip("#").lower()
- path = os.path.join(cache_dir(),
- f"hcnetcursor_{width_cells}x{height_cells}_{color_hex}.png")
+ path = os.path.join(cache_dir(), f"hcnetcursor_{width}x{height}_{color_hex}.png")
# Asked on every network editor event; paint once per session.
if _generated.get(path) or os.path.exists(path):
_generated[path] = True
return path
- paint(path, width_cells, height_cells, "#" + color_hex)
+ paint(path, width, height, "#" + color_hex)
_generated[path] = True
return path
-def paint(path, width_cells, height_cells, color_hex):
+def paint(path, width, height, color_hex):
from PySide6.QtCore import Qt
from PySide6.QtGui import QColor, QImage, QPainter, QPen
- width = CELL_PIXELS[0] * width_cells
- height = CELL_PIXELS[1] * height_cells
image = QImage(width, height, QImage.Format_ARGB32)
image.fill(Qt.transparent)
painter = QPainter(image)
diff --git a/python3.13libs/hc/hcnetworkeditor.py b/python3.13libs/hc/hcnetworkeditor.py
index 18679d6..fc27c17 100644
--- a/python3.13libs/hc/hcnetworkeditor.py
+++ b/python3.13libs/hc/hcnetworkeditor.py
@@ -470,30 +470,34 @@ class HCNetworkEditor(HCPathTab):
def _hcnetcursorOverlayShapes(self):
return []
- 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):
- width_cells, height_cells = self._hcnetcursorCellSize()
+ def _hcnetcursorImageFile(self, draw_rect):
+ size = draw_rect.size()
return hcnetcursorimage.image_file(
- width_cells, height_cells, HCSettings().hcnetcursorColorHex())
+ size[0], size[1], HCSettings().hcnetcursorColorHex())
def _hcnetcursorDrawRect(self, rect):
- """The rect the picture is stretched over: the cursor's cells, scaled
- about their centre by the hcnetcursor_scale setting. Selection and
- movement keep using the unscaled cells."""
- scale = HCSettings().hcnetcursorScale()
- if scale == 1.0:
- return rect
- center = rect.center()
- size = rect.size()
- half_w = size[0] * scale * 0.5
- half_h = size[1] * scale * 0.5
- return hou.BoundingRect(center[0] - half_w, center[1] - half_h,
- center[0] + half_w, center[1] + half_h)
+ """The rect the picture is stretched over.
+
+ The nodes a cursor frames sit at its cells' centres, each spanning
+ the node offset either side of its centre (a node is 1 by 0.3 units
+ in 1.99 by 0.99 cells). The box is that block of nodes grown by the
+ hcnetcursor_margin setting on every side, so the gap between box and
+ node is the same left, right, top and bottom whatever the cell shape
+ and however many cells the cursor covers. Scaling the cells instead
+ gave a 1-by-2 cursor a square box that clipped its nodes top and
+ bottom while leaving air at the sides. Selection and movement keep
+ using the whole cells.
+ """
+ step = self._gridStep()
+ offset = self._nodeOffset()
+ margin = HCSettings().hcnetcursorMargin()
+ rmin, rmax = rect.min(), rect.max()
+ return hou.BoundingRect(
+ rmin[0] + step[0] * 0.5 - offset[0] - margin,
+ rmin[1] + step[1] * 0.5 - offset[1] - margin,
+ rmax[0] - step[0] * 0.5 + offset[0] + margin,
+ rmax[1] - step[1] * 0.5 + offset[1] + margin,
+ )
def _backgroundImagesWithoutHcnetcursor(self):
return [image for image in self.hou_tab.backgroundImages()
@@ -550,7 +554,7 @@ class HCNetworkEditor(HCPathTab):
return
enabled = HCSettings().hcnetcursorEnabled()
rect = self._hcnetcursorDrawRect(self.hcnetcursorRect()) if enabled else None
- cursor_path = self._hcnetcursorImageFile() if enabled else None
+ cursor_path = self._hcnetcursorImageFile(rect) if enabled else None
signature = self._overlaySignature(rect, cursor_path)
if not force and _overlays.get(self.hou_tab) == signature:
diff --git a/python3.13libs/hc/hcschema.py b/python3.13libs/hc/hcschema.py
index 85bee79..3bc4d10 100644
--- a/python3.13libs/hc/hcschema.py
+++ b/python3.13libs/hc/hcschema.py
@@ -150,9 +150,10 @@ SCHEMA = {
help="The cursor's outline. The picture is painted on demand, so "
"a change shows on the next network editor event.",
),
- "hcnetcursor_scale": Setting(
- "slider", 1.0, range=(0.25, 2.0), label="Cursor Scale", group="Network Cursor",
- help="Size of the drawn box relative to the grid cells it covers. "
+ "hcnetcursor_margin": Setting(
+ "slider", 0.15, range=(0.0, 1.0), label="Cursor Margin", group="Network Cursor",
+ help="Gap between the drawn box and the nodes in the cells it "
+ "covers, in network units, the same on every side. "
"Selection and movement always use the whole cells.",
),
"grid_snap": Setting(
diff --git a/python3.13libs/hc/hcsettings.py b/python3.13libs/hc/hcsettings.py
index a40a173..1a8c536 100644
--- a/python3.13libs/hc/hcsettings.py
+++ b/python3.13libs/hc/hcsettings.py
@@ -223,15 +223,15 @@ class HCSettings:
def hcnetcursorColorHex(self):
return self.colorHex("node_graph", "hcnetcursor_color")
- def hcnetcursorScale(self):
- """Size of the drawn cursor box relative to the cells it covers."""
- setting = hcschema.lookup(("node_graph", "hcnetcursor_scale"))
+ def hcnetcursorMargin(self):
+ """Gap between the drawn cursor box and the nodes it frames, in units."""
+ setting = hcschema.lookup(("node_graph", "hcnetcursor_margin"))
try:
- scale = float(self.nodeGraph().get("hcnetcursor_scale", setting.default))
+ margin = float(self.nodeGraph().get("hcnetcursor_margin", setting.default))
except (TypeError, ValueError):
- scale = setting.default
+ margin = setting.default
low, high = setting.range
- return min(max(scale, low), high)
+ return min(max(margin, low), high)
def dropSwapEnabled(self):
"""Dropping a node on another node's cell swaps their positions."""
diff --git a/tools/check.py b/tools/check.py
index 57d5830..15dc983 100644
--- a/tools/check.py
+++ b/tools/check.py
@@ -1808,7 +1808,7 @@ def check_cursor_image():
from PySide6.QtGui import QImage
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "hcnetcursor_3x2_a1b2c3.png")
- hcnetcursorimage.paint(path, 3, 2, "#a1b2c3")
+ hcnetcursorimage.paint(path, 600, 200, "#a1b2c3")
image = QImage(path)
assert (image.width(), image.height()) == (600, 200), \
f"{image.width()}x{image.height()}"
@@ -1830,7 +1830,9 @@ def check_cursor_image():
assert a == b, "colour case made a second file"
assert a != c, "different colours share a file"
assert os.path.exists(a) and os.path.exists(c)
- assert hcnetcursorimage.image_file(40, 1, "#d8b34b").endswith("_12x1_d8b34b.png"), \
+ assert hcnetcursorimage.image_file(1.99, 0.99, "#d8b34b").endswith("_199x99_d8b34b.png"), \
+ "picture not sized by the box in units"
+ assert hcnetcursorimage.image_file(40, 1, "#d8b34b").endswith("_2400x100_d8b34b.png"), \
"oversize cursor not clamped"
return os.path.dirname(a)
@@ -1850,16 +1852,16 @@ def check_cursor_image():
check("stale cursor images are filtered", old_paths_recognised)
def settings_declared():
- for key in ("hcnetcursor", "hcnetcursor_color", "hcnetcursor_scale"):
+ for key in ("hcnetcursor", "hcnetcursor_color", "hcnetcursor_margin"):
setting = hcschema.lookup(("node_graph", key))
assert setting is not None, f"{key} missing"
assert setting.group == "Network Cursor", f"{key} in {setting.group!r}"
assert hcschema.lookup(("node_graph", "hcnetcursor")).label == "Network Cursor"
settings = HCSettings()
assert settings.hcnetcursorColorHex().startswith("#")
- low, high = hcschema.lookup(("node_graph", "hcnetcursor_scale")).range
- assert low <= settings.hcnetcursorScale() <= high
- return f"colour {settings.hcnetcursorColorHex()}, scale {settings.hcnetcursorScale()}"
+ low, high = hcschema.lookup(("node_graph", "hcnetcursor_margin")).range
+ assert low <= settings.hcnetcursorMargin() <= high
+ return f"colour {settings.hcnetcursorColorHex()}, margin {settings.hcnetcursorMargin()}"
check("cursor settings", settings_declared)