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

python3.13libs/hc/hcnetcursorimage.py (3.2K)

 1 """The network cursor's picture, drawn on demand.
 2 
 3 The cursor is a hou.NetworkImage stretched over the cells it covers. Houdini
 4 can only brighten a background image, not tint it, so a configurable colour
 5 means a differently coloured file. This used to be 144 static PNGs in
 6 config/hcnetcursor_assets, one per cell size, all the same gold; now the
 7 picture is painted the first time a (cells, colour) pair is asked for and
 8 cached in the session's temp dir.
 9 
10 The look is the old asset set's: transparent inside, a BORDER-pixel
11 outline. The picture is painted at UNIT_PIXELS per network unit of the rect
12 it will be stretched over, so the outline is the same width on every side
13 and the same width whatever the cursor's size; the old assets were painted
14 per cell, and a box that is not cell-shaped stretched their outline unevenly.
15 """
16 
17 import os
18 import tempfile
19 
20 import hou
21 
22 UNIT_PIXELS = 100          # image pixels per network unit
23 BORDER = 5                 # outline width, in image pixels
24 MAX_PIXELS = 2400          # a cursor wider than this reuses the largest picture
25 
26 _generated = {}
27 
28 
29 def cache_dir():
30     base = hou.getenv("HOUDINI_TEMP_DIR") or tempfile.gettempdir()
31     return os.path.join(base, "hc_hcnetcursor")
32 
33 
34 def is_cursor_image(path):
35     """Whether a background image path is one of ours, from any session.
36 
37     Matched on the basename only: a hip saved with the cursor showing keeps
38     the image in the network's background list, and the directory in that
39     path may be an old temp dir or the retired config/hcnetcursor_assets.
40     """
41     return os.path.basename(path).startswith("hcnetcursor_")
42 
43 
44 def pixel_size(width_units, height_units):
45     """Image size for a box this many network units across, as whole pixels."""
46     return (max(2 * BORDER, min(int(round(width_units * UNIT_PIXELS)), MAX_PIXELS)),
47             max(2 * BORDER, min(int(round(height_units * UNIT_PIXELS)), MAX_PIXELS)))
48 
49 
50 def image_file(width_units, height_units, color_hex):
51     """Path of the cursor picture for a box this size in this colour."""
52     width, height = pixel_size(width_units, height_units)
53     color_hex = color_hex.lstrip("#").lower()
54     path = os.path.join(cache_dir(), f"hcnetcursor_{width}x{height}_{color_hex}.png")
55     # Asked on every network editor event; paint once per session.
56     if _generated.get(path) or os.path.exists(path):
57         _generated[path] = True
58         return path
59     paint(path, width, height, "#" + color_hex)
60     _generated[path] = True
61     return path
62 
63 
64 def paint(path, width, height, color_hex):
65     from PySide6.QtCore import Qt
66     from PySide6.QtGui import QColor, QImage, QPainter, QPen
67 
68     image = QImage(width, height, QImage.Format_ARGB32)
69     image.fill(Qt.transparent)
70     painter = QPainter(image)
71     pen = QPen(QColor(color_hex), BORDER)
72     pen.setJoinStyle(Qt.MiterJoin)
73     painter.setPen(pen)
74     painter.setBrush(Qt.NoBrush)
75     # A pen is centred on the path; inset by half so the outline sits fully
76     # inside the image, as the old assets did.
77     half = BORDER / 2.0
78     painter.drawRect(half, half, width - BORDER, height - BORDER)
79     painter.end()
80 
81     os.makedirs(os.path.dirname(path), exist_ok=True)
82     if not image.save(path):
83         raise RuntimeError(f"could not write cursor image {path}")
84     return path