houdini-node-library 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hnl/__init__.py +3 -0
- hnl/__main__.py +5 -0
- hnl/_houdini/OPmenu.xml +15 -0
- hnl/_houdini/python/hnl_bridge/__init__.py +2 -0
- hnl/_houdini/python/hnl_bridge/autostart.py +12 -0
- hnl/_houdini/python/hnl_bridge/cmdio.py +98 -0
- hnl/_houdini/python/hnl_bridge/export.py +292 -0
- hnl/_houdini/python/hnl_bridge/extras.py +439 -0
- hnl/_houdini/python/hnl_bridge/hdas.py +137 -0
- hnl/_houdini/python/hnl_bridge/importer.py +533 -0
- hnl/_houdini/python/hnl_bridge/launcher.py +83 -0
- hnl/_houdini/python/hnl_bridge/scan.py +392 -0
- hnl/_houdini/python/hnl_bridge/server.py +416 -0
- hnl/_houdini/python/hnl_bridge/structure.py +122 -0
- hnl/_houdini/python/hnl_bridge/userset.py +342 -0
- hnl/_houdini/python3.11libs/uiready.py +7 -0
- hnl/_houdini/python3.13libs/uiready.py +7 -0
- hnl/_houdini/toolbar/hnl.shelf +42 -0
- hnl/app.py +146 -0
- hnl/core/__init__.py +1 -0
- hnl/core/cmdio.py +38 -0
- hnl/core/cmdscan.py +38 -0
- hnl/core/cmdtext.py +87 -0
- hnl/core/collect.py +212 -0
- hnl/core/index.py +219 -0
- hnl/core/library.py +474 -0
- hnl/core/model.py +152 -0
- hnl/core/refrules.py +128 -0
- hnl/core/settings.py +108 -0
- hnl/core/structure.py +195 -0
- hnl/core/thumbmake.py +67 -0
- hnl/core/trust.py +77 -0
- hnl/core/vocab.py +46 -0
- hnl/houdini/__init__.py +1 -0
- hnl/houdini/client.py +215 -0
- hnl/houdini/launcher.py +110 -0
- hnl/houdini/package.py +129 -0
- hnl/ui/__init__.py +1 -0
- hnl/ui/app_icon.py +57 -0
- hnl/ui/detail_cmd.py +215 -0
- hnl/ui/detail_header.py +207 -0
- hnl/ui/detail_info.py +130 -0
- hnl/ui/detail_panel.py +219 -0
- hnl/ui/detail_tables.py +145 -0
- hnl/ui/empty_state.py +34 -0
- hnl/ui/grid.py +322 -0
- hnl/ui/hou_icons.py +203 -0
- hnl/ui/import_dialog.py +305 -0
- hnl/ui/import_flow.py +233 -0
- hnl/ui/import_logic.py +483 -0
- hnl/ui/item_actions.py +117 -0
- hnl/ui/item_model.py +144 -0
- hnl/ui/item_table.py +74 -0
- hnl/ui/main_toolbar.py +72 -0
- hnl/ui/main_window.py +400 -0
- hnl/ui/md_pane.py +420 -0
- hnl/ui/md_render.py +225 -0
- hnl/ui/rebuild.py +32 -0
- hnl/ui/register/__init__.py +5 -0
- hnl/ui/register/capture_dialogs.py +120 -0
- hnl/ui/register/dialog.py +393 -0
- hnl/ui/register/logic.py +137 -0
- hnl/ui/register/meta_form.py +162 -0
- hnl/ui/register/picker.py +42 -0
- hnl/ui/register/protocol.py +17 -0
- hnl/ui/register/refs_tab.py +314 -0
- hnl/ui/register/simple_tabs.py +240 -0
- hnl/ui/register/structure_tab.py +21 -0
- hnl/ui/register/thumb_panel.py +320 -0
- hnl/ui/register/workers.py +55 -0
- hnl/ui/register_flow.py +55 -0
- hnl/ui/settings_dialog.py +348 -0
- hnl/ui/sidebar.py +247 -0
- hnl/ui/structure_view.py +231 -0
- hnl/ui/tag_edit.py +329 -0
- hnl/ui/theme.py +211 -0
- hnl/ui/theme_icons.py +61 -0
- hnl/ui/thumbs.py +93 -0
- hnl/ui/vcs_controller.py +431 -0
- hnl/ui/vcs_dialogs.py +63 -0
- hnl/ui/vcs_history.py +71 -0
- hnl/ui/vcs_ops.py +121 -0
- hnl/ui/vcs_style.py +62 -0
- hnl/ui/vocab_combo.py +73 -0
- hnl/vcs/__init__.py +33 -0
- hnl/vcs/base.py +142 -0
- hnl/vcs/none.py +69 -0
- hnl/vcs/svn.py +393 -0
- houdini_node_library-0.1.0.dist-info/METADATA +128 -0
- houdini_node_library-0.1.0.dist-info/RECORD +94 -0
- houdini_node_library-0.1.0.dist-info/WHEEL +5 -0
- houdini_node_library-0.1.0.dist-info/entry_points.txt +5 -0
- houdini_node_library-0.1.0.dist-info/licenses/LICENSE +21 -0
- houdini_node_library-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""cmd に入らない情報(付箋・ネットワークボックス・ドット・シーン設定)の保存と復元。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
import hou
|
|
9
|
+
|
|
10
|
+
from . import scan
|
|
11
|
+
|
|
12
|
+
VERSION = 1
|
|
13
|
+
# set -g に最初から入っている変数(ユーザー定義ではない)
|
|
14
|
+
BUILTIN_VARS = frozenset("""F FF T FPS NFRAMES TEND TLENGTH TSTART FSTART FEND RFSTART RFEND UNITLENGTH
|
|
15
|
+
UNITMASS CHOPMOTIONSAMPLES oldFF ACTIVETAKE E EYE HIP HIPFILE HIPNAME JOB PI POSE status HFS HH HOME""".split())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def sub_children(n):
|
|
19
|
+
try:
|
|
20
|
+
return n.allSubChildren(recurse_in_locked_nodes=False)
|
|
21
|
+
except TypeError:
|
|
22
|
+
return n.allSubChildren()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def rel_path(node, base) -> str:
|
|
26
|
+
return node.path()[len(base.path()):].lstrip("/")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _rgb(c) -> list:
|
|
30
|
+
return [round(float(x), 6) for x in c.rgb()]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _locked(n) -> bool:
|
|
34
|
+
return bool(getattr(n, "isLockedHDA", lambda: False)())
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _dot_source(d):
|
|
38
|
+
it = d.inputItem()
|
|
39
|
+
for _ in range(1000):
|
|
40
|
+
if not isinstance(it, hou.NetworkDot):
|
|
41
|
+
break
|
|
42
|
+
it = it.inputItem()
|
|
43
|
+
return it
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _overlaps(s, rects) -> bool:
|
|
47
|
+
x, y = s.position()
|
|
48
|
+
w, h = s.size()
|
|
49
|
+
return any(x < r[2] and x + w > r[0] and y < r[3] and y + h > r[1] for r in rects)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def collect(parent, top_nodes, whole: bool, scope=None) -> dict:
|
|
53
|
+
"""parent 配下の付箋・ボックス・ドットを集める。whole=False なら parent 直下は選択に関係するものだけ。"""
|
|
54
|
+
sel = set(top_nodes)
|
|
55
|
+
ex = {"version": VERSION, "source_parent": parent.path(), "stickies": [], "boxes": [], "dots": []}
|
|
56
|
+
nets = [parent] + [m for n in top_nodes for m in (n,) + tuple(sub_children(n))
|
|
57
|
+
if m.isNetwork() and not _locked(m)]
|
|
58
|
+
rects = [(n.position()[0], n.position()[1], n.position()[0] + n.size()[0],
|
|
59
|
+
n.position()[1] + n.size()[1]) for n in top_nodes]
|
|
60
|
+
for net in nets:
|
|
61
|
+
top = net == parent
|
|
62
|
+
sh = None # ロックされた HDA の中のネットワーク: 出荷時から持つ項目は除く
|
|
63
|
+
if scope is not None and not top:
|
|
64
|
+
sh = scope.shipped_items(net)
|
|
65
|
+
if sh is False:
|
|
66
|
+
continue
|
|
67
|
+
rel = "" if top else rel_path(net, parent)
|
|
68
|
+
flt = top and not whole
|
|
69
|
+
for b in net.networkBoxes():
|
|
70
|
+
if sh and b.name() in sh["boxes"]:
|
|
71
|
+
continue
|
|
72
|
+
names = [n.name() for n in b.nodes() if not flt or n in sel]
|
|
73
|
+
if flt and not names and not b.isSelected():
|
|
74
|
+
continue
|
|
75
|
+
ex["boxes"].append({
|
|
76
|
+
"parent": rel, "name": b.name(), "pos": list(b.position()), "size": list(b.restoredSize()),
|
|
77
|
+
"color": _rgb(b.color()), "comment": b.comment(), "minimized": b.isMinimized(),
|
|
78
|
+
"members": names})
|
|
79
|
+
for s in net.stickyNotes():
|
|
80
|
+
if sh and s.name() in sh["stickies"]:
|
|
81
|
+
continue
|
|
82
|
+
if flt and not (s.isSelected() or _overlaps(s, rects)):
|
|
83
|
+
continue
|
|
84
|
+
ex["stickies"].append({
|
|
85
|
+
"parent": rel, "name": s.name(), "text": s.text(), "pos": list(s.position()),
|
|
86
|
+
"size": list(s.restoredSize()), "color": _rgb(s.color()), "text_color": _rgb(s.textColor()),
|
|
87
|
+
"text_size": s.textSize(), "draw_background": s.drawBackground(), "minimized": s.isMinimized()})
|
|
88
|
+
for d in net.networkDots():
|
|
89
|
+
if sh and d.name() in sh["dots"]:
|
|
90
|
+
continue
|
|
91
|
+
if flt:
|
|
92
|
+
src = _dot_source(d)
|
|
93
|
+
if src not in sel or not any(c.outputItem() in sel for c in d.outputConnections()):
|
|
94
|
+
continue
|
|
95
|
+
it = d.inputItem()
|
|
96
|
+
inp = None
|
|
97
|
+
if isinstance(it, hou.NetworkDot):
|
|
98
|
+
inp = {"kind": "dot", "name": it.name()}
|
|
99
|
+
elif it is not None:
|
|
100
|
+
inp = {"kind": "node", "name": it.name(), "out": d.inputItemOutputIndex()}
|
|
101
|
+
outs = [{"node": c.outputItem().name(), "idx": c.inputIndex()} for c in d.outputConnections()
|
|
102
|
+
if isinstance(c.outputItem(), hou.Node) and (not flt or c.outputItem() in sel)]
|
|
103
|
+
ex["dots"].append({"parent": rel, "name": d.name(), "pos": list(d.position()),
|
|
104
|
+
"color": _rgb(d.color()), "pinned": d.isPinned(), "input": inp, "outputs": outs})
|
|
105
|
+
return ex
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def top_rects(ex: dict) -> list:
|
|
109
|
+
"""parent 直下の付箋・ボックス・ドットの矩形 (x0, y0, x1, y1)。"""
|
|
110
|
+
out = []
|
|
111
|
+
for k in ("boxes", "stickies"):
|
|
112
|
+
for e in ex.get(k, []):
|
|
113
|
+
if not e["parent"]:
|
|
114
|
+
out.append((e["pos"][0], e["pos"][1], e["pos"][0] + e["size"][0], e["pos"][1] + e["size"][1]))
|
|
115
|
+
out += [(d["pos"][0], d["pos"][1], d["pos"][0], d["pos"][1]) for d in ex.get("dots", []) if not d["parent"]]
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------- 検証(extras.json は他人から届く入力)
|
|
119
|
+
|
|
120
|
+
def _is_num(x, lo=-1e7, hi=1e7) -> bool:
|
|
121
|
+
return isinstance(x, (int, float)) and not isinstance(x, bool) and math.isfinite(x) and lo <= x <= hi
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _vec(v, n=2, lo=-1e6, hi=1e6) -> bool:
|
|
125
|
+
return isinstance(v, (list, tuple)) and len(v) == n and all(_is_num(x, lo, hi) for x in v)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _color(v) -> bool:
|
|
129
|
+
return _vec(v, 3, 0.0, 1.0)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _node_name(s) -> bool:
|
|
133
|
+
"""親を含まない単一のノード名(/ や .. を含まない)。"""
|
|
134
|
+
return (isinstance(s, str) and 0 < len(s) <= 256 and "/" not in s and "\\" not in s
|
|
135
|
+
and s not in (".", "..") and s.isprintable())
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _rel(s) -> bool:
|
|
139
|
+
"""取り込み先からの相対パス。空は取り込み先そのもの。絶対パスや .. は不可。"""
|
|
140
|
+
return s == "" or (isinstance(s, str) and len(s) <= 1024 and all(_node_name(p) for p in s.split("/")))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _text(s) -> bool:
|
|
144
|
+
return isinstance(s, str) and len(s) <= 200000
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _int(x, hi=10000) -> bool:
|
|
148
|
+
return isinstance(x, int) and not isinstance(x, bool) and 0 <= x <= hi
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _valid_box(e) -> bool:
|
|
152
|
+
return (isinstance(e, dict) and _rel(e.get("parent")) and _node_name(e.get("name")) and _vec(e.get("pos"))
|
|
153
|
+
and _vec(e.get("size"), 2, 0, 1e5) and _color(e.get("color")) and _text(e.get("comment", ""))
|
|
154
|
+
and isinstance(e.get("minimized", False), bool)
|
|
155
|
+
and isinstance(e.get("members"), list) and len(e["members"]) <= 100000
|
|
156
|
+
and all(_node_name(m) for m in e["members"]))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _valid_sticky(e) -> bool:
|
|
160
|
+
return (isinstance(e, dict) and _rel(e.get("parent")) and _node_name(e.get("name")) and _text(e.get("text", ""))
|
|
161
|
+
and _vec(e.get("pos")) and _vec(e.get("size"), 2, 0, 1e5) and _color(e.get("color"))
|
|
162
|
+
and _color(e.get("text_color")) and _is_num(e.get("text_size"), 0, 1000)
|
|
163
|
+
and isinstance(e.get("draw_background"), bool) and isinstance(e.get("minimized", False), bool))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _valid_dot(e) -> bool:
|
|
167
|
+
if not (isinstance(e, dict) and _rel(e.get("parent")) and _node_name(e.get("name")) and _vec(e.get("pos"))
|
|
168
|
+
and _color(e.get("color")) and isinstance(e.get("pinned"), bool)):
|
|
169
|
+
return False
|
|
170
|
+
inp = e.get("input")
|
|
171
|
+
if inp is not None:
|
|
172
|
+
if not (isinstance(inp, dict) and inp.get("kind") in ("dot", "node") and _node_name(inp.get("name"))):
|
|
173
|
+
return False
|
|
174
|
+
if inp["kind"] == "node" and not _int(inp.get("out")):
|
|
175
|
+
return False
|
|
176
|
+
outs = e.get("outputs")
|
|
177
|
+
return (isinstance(outs, list) and len(outs) <= 10000
|
|
178
|
+
and all(isinstance(o, dict) and _node_name(o.get("node")) and _int(o.get("idx")) for o in outs))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def sanitize(ex) -> tuple[dict, list[str]]:
|
|
182
|
+
"""extras の項目を検証し、不正な項目を除いた dict と警告を返す。"""
|
|
183
|
+
warns: list[str] = []
|
|
184
|
+
if not isinstance(ex, dict):
|
|
185
|
+
return {}, ["extras.json の形式が不正なため適用しませんでした"]
|
|
186
|
+
out: dict = {}
|
|
187
|
+
sp = ex.get("source_parent")
|
|
188
|
+
if isinstance(sp, str) and sp.startswith("/") and len(sp) <= 1024:
|
|
189
|
+
out["source_parent"] = sp
|
|
190
|
+
for key, ok, label in (("boxes", _valid_box, "ネットワークボックス"), ("stickies", _valid_sticky, "付箋"),
|
|
191
|
+
("dots", _valid_dot, "ドット")):
|
|
192
|
+
raw = ex.get(key, [])
|
|
193
|
+
if not isinstance(raw, list):
|
|
194
|
+
warns.append("%s の一覧が不正なため適用しませんでした" % label)
|
|
195
|
+
continue
|
|
196
|
+
good = [e for e in raw if ok(e)]
|
|
197
|
+
if len(good) != len(raw):
|
|
198
|
+
warns.append("不正な %s %d 件を適用しませんでした" % (label, len(raw) - len(good)))
|
|
199
|
+
out[key] = good
|
|
200
|
+
rm = ex.get("removed_nodes", [])
|
|
201
|
+
if isinstance(rm, list):
|
|
202
|
+
out["removed_nodes"] = [r for r in rm if isinstance(r, str) and 0 < len(r) <= 1024 and not r.startswith("/")
|
|
203
|
+
and all(seg and seg != ".." and seg != "." for seg in r.split("/")) and _plain(r)][:2000]
|
|
204
|
+
elif rm:
|
|
205
|
+
warns.append("削除するノードの一覧が不正なため適用しませんでした")
|
|
206
|
+
wi = ex.get("editable_wires", [])
|
|
207
|
+
if isinstance(wi, list):
|
|
208
|
+
out["editable_wires"] = [e for e in wi[:20000] if isinstance(e, dict) and isinstance(e.get("node"), str)
|
|
209
|
+
and e["node"] and _rel(e["node"]) and _int(e.get("idx"), 100000)
|
|
210
|
+
and (e.get("src") is None or _node_name(e.get("src"))) and _int(e.get("out", 0), 100000)]
|
|
211
|
+
elif wi:
|
|
212
|
+
warns.append("接続の一覧が不正なため適用しませんでした")
|
|
213
|
+
if ex.get("scene") is not None:
|
|
214
|
+
sc, w = sanitize_scene(ex["scene"])
|
|
215
|
+
out["scene"] = sc
|
|
216
|
+
warns += w
|
|
217
|
+
return out, warns
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def sanitize_scene(s) -> tuple[dict, list[str]]:
|
|
221
|
+
"""シーン設定を検証する。不正な項目は除き、警告を返す。"""
|
|
222
|
+
warns: list[str] = []
|
|
223
|
+
if not isinstance(s, dict):
|
|
224
|
+
return {}, ["シーン設定の形式が不正なため適用しませんでした"]
|
|
225
|
+
out: dict = {}
|
|
226
|
+
if _is_num(s.get("fps"), 0.001, 1000):
|
|
227
|
+
out["fps"] = float(s["fps"])
|
|
228
|
+
elif "fps" in s:
|
|
229
|
+
warns.append("シーン設定の fps が不正なため適用しませんでした")
|
|
230
|
+
for k in ("frame_range", "playback_range"):
|
|
231
|
+
v = s.get(k)
|
|
232
|
+
if _vec(v, 2, -1e7, 1e7) and v[0] <= v[1]:
|
|
233
|
+
out[k] = [float(v[0]), float(v[1])]
|
|
234
|
+
elif k in s:
|
|
235
|
+
warns.append("シーン設定の %s が不正なため適用しませんでした" % k)
|
|
236
|
+
if _is_num(s.get("frame")):
|
|
237
|
+
out["frame"] = float(s["frame"])
|
|
238
|
+
elif "frame" in s:
|
|
239
|
+
warns.append("シーン設定の frame が不正なため適用しませんでした")
|
|
240
|
+
g = s.get("globals", {})
|
|
241
|
+
if not isinstance(g, dict):
|
|
242
|
+
warns.append("グローバル変数の一覧が不正なため適用しませんでした")
|
|
243
|
+
g = {}
|
|
244
|
+
good = {}
|
|
245
|
+
for k, v in g.items():
|
|
246
|
+
why = scan.env_name_problem(k)
|
|
247
|
+
if why:
|
|
248
|
+
warns.append("グローバル変数 %s は設定しませんでした(%s)" % (str(k)[:40], why))
|
|
249
|
+
elif not isinstance(v, str) or len(v) > 4096 or not _plain(v):
|
|
250
|
+
warns.append("グローバル変数 %s の値が不正なため設定しませんでした" % k)
|
|
251
|
+
else:
|
|
252
|
+
good[k] = v
|
|
253
|
+
out["globals"] = good
|
|
254
|
+
return out, warns
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _plain(v: str) -> bool:
|
|
258
|
+
"""値に制御文字(改行など)を含まない。"""
|
|
259
|
+
return all(c >= " " and c != chr(127) for c in v)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def apply(base, ex: dict, shift=(0.0, 0.0), name_map=None) -> tuple[list, list]:
|
|
263
|
+
"""extras を base 配下に復元する。(作成した項目, 警告) を返す。"""
|
|
264
|
+
nm = name_map or {}
|
|
265
|
+
items: list = []
|
|
266
|
+
ex, warns = sanitize(ex)
|
|
267
|
+
sv = hou.Vector2(shift[0], shift[1])
|
|
268
|
+
|
|
269
|
+
def container(rel):
|
|
270
|
+
if not rel:
|
|
271
|
+
return base
|
|
272
|
+
parts = rel.split("/")
|
|
273
|
+
parts[0] = nm.get(parts[0], parts[0])
|
|
274
|
+
return base.node("/".join(parts))
|
|
275
|
+
|
|
276
|
+
def mapped(rel, name):
|
|
277
|
+
return nm.get(name, name) if not rel else name
|
|
278
|
+
|
|
279
|
+
def pos(rel, p):
|
|
280
|
+
v = hou.Vector2(p[0], p[1])
|
|
281
|
+
return v if rel else v + sv
|
|
282
|
+
|
|
283
|
+
for e in ex.get("boxes", []):
|
|
284
|
+
c = container(e["parent"])
|
|
285
|
+
if c is None:
|
|
286
|
+
warns.append("ネットワークボックス %s の親が見つからないため復元しませんでした" % e["name"])
|
|
287
|
+
continue
|
|
288
|
+
try:
|
|
289
|
+
b = c.createNetworkBox(e["name"])
|
|
290
|
+
except hou.Error:
|
|
291
|
+
b = c.createNetworkBox()
|
|
292
|
+
items.append(b)
|
|
293
|
+
members = [n for n in (c.node(mapped(e["parent"], m)) for m in e["members"]) if n is not None]
|
|
294
|
+
saved = [n.position() for n in members]
|
|
295
|
+
for n in members:
|
|
296
|
+
b.addNode(n)
|
|
297
|
+
b.setPosition(pos(e["parent"], e["pos"]))
|
|
298
|
+
for n, pp in zip(members, saved): # ボックス移動でメンバーが動くので戻す
|
|
299
|
+
n.setPosition(pp)
|
|
300
|
+
b.setSize(hou.Vector2(*e["size"]))
|
|
301
|
+
b.setColor(hou.Color(e["color"]))
|
|
302
|
+
b.setComment(e["comment"])
|
|
303
|
+
if e["minimized"]:
|
|
304
|
+
b.setMinimized(True)
|
|
305
|
+
for e in ex.get("stickies", []):
|
|
306
|
+
c = container(e["parent"])
|
|
307
|
+
if c is None:
|
|
308
|
+
warns.append("付箋 %s の親が見つからないため復元しませんでした" % e["name"])
|
|
309
|
+
continue
|
|
310
|
+
try:
|
|
311
|
+
s = c.createStickyNote(e["name"])
|
|
312
|
+
except hou.Error:
|
|
313
|
+
s = c.createStickyNote()
|
|
314
|
+
items.append(s)
|
|
315
|
+
s.setText(e["text"])
|
|
316
|
+
s.setPosition(pos(e["parent"], e["pos"]))
|
|
317
|
+
s.setSize(hou.Vector2(*e["size"]))
|
|
318
|
+
s.setColor(hou.Color(e["color"]))
|
|
319
|
+
s.setTextColor(hou.Color(e["text_color"]))
|
|
320
|
+
s.setTextSize(e["text_size"])
|
|
321
|
+
s.setDrawBackground(e["draw_background"])
|
|
322
|
+
if e["minimized"]:
|
|
323
|
+
s.setMinimized(True)
|
|
324
|
+
dots: dict = {}
|
|
325
|
+
for e in ex.get("dots", []):
|
|
326
|
+
c = container(e["parent"])
|
|
327
|
+
if c is None:
|
|
328
|
+
continue
|
|
329
|
+
d = c.createNetworkDot()
|
|
330
|
+
d.setPosition(pos(e["parent"], e["pos"]))
|
|
331
|
+
d.setColor(hou.Color(e["color"]))
|
|
332
|
+
d.setPinned(e["pinned"])
|
|
333
|
+
dots[(e["parent"], e["name"])] = (c, d)
|
|
334
|
+
items.append(d)
|
|
335
|
+
for e in ex.get("dots", []):
|
|
336
|
+
if (e["parent"], e["name"]) not in dots:
|
|
337
|
+
continue
|
|
338
|
+
c, d = dots[(e["parent"], e["name"])]
|
|
339
|
+
inp = e["input"]
|
|
340
|
+
try:
|
|
341
|
+
if inp and inp["kind"] == "node":
|
|
342
|
+
src = c.node(mapped(e["parent"], inp["name"]))
|
|
343
|
+
if src is not None:
|
|
344
|
+
d.setInput(0, src, inp["out"])
|
|
345
|
+
elif inp and (e["parent"], inp["name"]) in dots:
|
|
346
|
+
d.setInput(0, dots[(e["parent"], inp["name"])][1])
|
|
347
|
+
for o in e["outputs"]:
|
|
348
|
+
dst = c.node(mapped(e["parent"], o["node"]))
|
|
349
|
+
if dst is not None:
|
|
350
|
+
dst.setInput(o["idx"], d)
|
|
351
|
+
except hou.Error as exc:
|
|
352
|
+
warns.append("ドット %s の接続を復元できませんでした: %s" % (e["name"], exc))
|
|
353
|
+
d.setPosition(pos(e["parent"], e["pos"])) # 接続時に動くことがあるので再設定
|
|
354
|
+
return items, warns
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def all_vars() -> dict:
|
|
358
|
+
out = {}
|
|
359
|
+
for ln in hou.hscript("set -g")[0].splitlines():
|
|
360
|
+
m = re.match(r"^(\S+)\s*=\s?(.*)$", ln)
|
|
361
|
+
if m:
|
|
362
|
+
out[m.group(1)] = m.group(2)
|
|
363
|
+
return out
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def user_globals() -> dict:
|
|
367
|
+
out = {}
|
|
368
|
+
for k, v in all_vars().items():
|
|
369
|
+
if k in BUILTIN_VARS:
|
|
370
|
+
continue
|
|
371
|
+
try: # 一覧表示はバックスラッシュを二重に表示するので、実際の値を取り直す
|
|
372
|
+
v = hou.text.expandString("$" + k)
|
|
373
|
+
except hou.Error:
|
|
374
|
+
pass
|
|
375
|
+
out[k] = v
|
|
376
|
+
return out
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def set_var(name: str, value: str) -> None:
|
|
380
|
+
"""環境変数とグローバル hscript 変数を設定する。名前・値が不正なら ValueError。
|
|
381
|
+
値は hscript の文字列として解釈させない: 変数を空で作り(名前は検証済み)、値は hou.putenv で渡す。"""
|
|
382
|
+
why = scan.env_name_problem(name)
|
|
383
|
+
if why:
|
|
384
|
+
raise ValueError("変数名 %s は使えません: %s" % (str(name)[:40], why))
|
|
385
|
+
if not isinstance(value, str) or not _plain(value):
|
|
386
|
+
raise ValueError("変数 %s の値が不正です(文字列で、制御文字や改行を含まないこと)" % name)
|
|
387
|
+
hou.hscript("set -g %s = ''" % name) # 一覧(set -g)に載る hscript 変数を作る
|
|
388
|
+
hou.putenv(name, value) # hscript 変数の値
|
|
389
|
+
os.environ[name] = value # プロセスの環境変数(hscript は \ を解釈して値を変えるため揃える)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class VarJournal:
|
|
393
|
+
"""設定した変数の元の値を覚えておき、失敗時に戻す。"""
|
|
394
|
+
|
|
395
|
+
def __init__(self) -> None:
|
|
396
|
+
self.prev: dict = {}
|
|
397
|
+
|
|
398
|
+
def set(self, name: str, value: str) -> None:
|
|
399
|
+
if name not in self.prev:
|
|
400
|
+
self.prev[name] = (name in all_vars(), hou.getenv(name, None))
|
|
401
|
+
set_var(name, value)
|
|
402
|
+
|
|
403
|
+
def rollback(self) -> None:
|
|
404
|
+
for name, (listed, env) in self.prev.items():
|
|
405
|
+
try:
|
|
406
|
+
if listed and env is not None:
|
|
407
|
+
set_var(name, env)
|
|
408
|
+
else:
|
|
409
|
+
hou.hscript("set -u %s" % name)
|
|
410
|
+
hou.unsetenv(name)
|
|
411
|
+
os.environ.pop(name, None)
|
|
412
|
+
except Exception:
|
|
413
|
+
pass
|
|
414
|
+
self.prev = {}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def collect_scene() -> dict:
|
|
418
|
+
return {"fps": hou.fps(), "frame_range": list(hou.playbar.frameRange()),
|
|
419
|
+
"playback_range": list(hou.playbar.playbackRange()), "frame": hou.frame(),
|
|
420
|
+
"globals": user_globals()}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def apply_scene(s: dict, journal: "VarJournal | None" = None) -> list[str]:
|
|
424
|
+
"""検証済みの項目だけを適用する。警告を返す。"""
|
|
425
|
+
s, warns = sanitize_scene(s)
|
|
426
|
+
if "fps" in s:
|
|
427
|
+
hou.setFps(s["fps"])
|
|
428
|
+
if "frame_range" in s:
|
|
429
|
+
hou.playbar.setFrameRange(*s["frame_range"])
|
|
430
|
+
if "playback_range" in s:
|
|
431
|
+
hou.playbar.setPlaybackRange(*s["playback_range"])
|
|
432
|
+
for k, v in s.get("globals", {}).items():
|
|
433
|
+
if journal is not None:
|
|
434
|
+
journal.set(k, v)
|
|
435
|
+
else:
|
|
436
|
+
set_var(k, v)
|
|
437
|
+
if "frame" in s:
|
|
438
|
+
hou.setFrame(s["frame"])
|
|
439
|
+
return warns
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""HDA の同梱(書き出し側)とインストール(読み込み側)。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import glob
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
|
|
9
|
+
import hou
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _hfs() -> str:
|
|
13
|
+
return os.path.realpath(hou.text.expandString("$HFS")).lower()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _safe(name: str) -> str:
|
|
17
|
+
return re.sub(r"[^\w.\-]+", "_", name)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# スクリプトを含む HDA セクション(読み込み・作成時に自動実行され得るもの)
|
|
21
|
+
_SCRIPT_SECTIONS = frozenset(n.lower() for n in (
|
|
22
|
+
"OnCreated OnLoaded OnUpdated OnInputChanged OnNameChanged OnDeleted PreFirstCreate PostLastDelete "
|
|
23
|
+
"OnUnloaded OnInstall PythonModule ViewerStateModule ViewerHandleModule").split())
|
|
24
|
+
_SCRIPT_SUFFIXES = (".py", ".cmd", ".hscript", ".pypanel")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HdaRefused(Exception):
|
|
28
|
+
"""インストールしてはいけない HDA が含まれている。"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _template_callbacks(entries) -> bool:
|
|
32
|
+
for t in entries:
|
|
33
|
+
try:
|
|
34
|
+
if t.type() == hou.parmTemplateType.Folder or hasattr(t, "parmTemplates"):
|
|
35
|
+
if _template_callbacks(t.parmTemplates()):
|
|
36
|
+
return True
|
|
37
|
+
if t.scriptCallback():
|
|
38
|
+
return True
|
|
39
|
+
except Exception:
|
|
40
|
+
continue
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def script_info(d) -> tuple[list[str], bool]:
|
|
45
|
+
"""定義が持つスクリプト入りのセクション名と、パラメータのコールバックの有無。"""
|
|
46
|
+
names: list[str] = []
|
|
47
|
+
try:
|
|
48
|
+
for n in d.sections():
|
|
49
|
+
low = n.lower()
|
|
50
|
+
if low in _SCRIPT_SECTIONS or low.endswith(_SCRIPT_SUFFIXES):
|
|
51
|
+
names.append(n)
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
try:
|
|
55
|
+
cb = _template_callbacks(d.parmTemplateGroup().entries())
|
|
56
|
+
except Exception:
|
|
57
|
+
cb = False
|
|
58
|
+
return sorted(names), cb
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def collect_hdas(nodes, staging_dir: str) -> list[dict]:
|
|
62
|
+
"""nodes が使う非標準 HDA を staging_dir/hda へ書き出し、依存一覧を返す。"""
|
|
63
|
+
hfs = _hfs()
|
|
64
|
+
deps: list[dict] = []
|
|
65
|
+
seen: set = set()
|
|
66
|
+
copied: dict = {}
|
|
67
|
+
for n in nodes:
|
|
68
|
+
t = n.type()
|
|
69
|
+
d = t.definition()
|
|
70
|
+
key = t.nameWithCategory()
|
|
71
|
+
if d is None or key in seen:
|
|
72
|
+
continue
|
|
73
|
+
lib = d.libraryFilePath()
|
|
74
|
+
embedded = lib == "Embedded"
|
|
75
|
+
if not embedded and os.path.realpath(lib).lower().startswith(hfs):
|
|
76
|
+
continue
|
|
77
|
+
seen.add(key)
|
|
78
|
+
hda_dir = os.path.join(staging_dir, "hda")
|
|
79
|
+
os.makedirs(hda_dir, exist_ok=True)
|
|
80
|
+
if embedded:
|
|
81
|
+
fname = _safe(key) + ".hda"
|
|
82
|
+
d.copyToHDAFile(os.path.join(hda_dir, fname))
|
|
83
|
+
else:
|
|
84
|
+
if lib not in copied:
|
|
85
|
+
fname = os.path.basename(lib)
|
|
86
|
+
while os.path.exists(os.path.join(hda_dir, fname)):
|
|
87
|
+
fname = "_" + fname
|
|
88
|
+
shutil.copy2(lib, os.path.join(hda_dir, fname))
|
|
89
|
+
copied[lib] = fname
|
|
90
|
+
fname = copied[lib]
|
|
91
|
+
secs, cb = script_info(d)
|
|
92
|
+
deps.append({"type_name": key, "library": lib, "embedded": embedded, "bundled": "hda/" + fname,
|
|
93
|
+
"sections": secs, "callbacks": cb})
|
|
94
|
+
return deps
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def install_dir(hda_dir: str) -> list[str]:
|
|
98
|
+
"""hda_dir の HDA をインストールする。同じ定義が入っていれば何もしない。警告を返す。
|
|
99
|
+
Houdini 組み込みの(HDA ではない)ノード型と同名の定義があれば、何もインストールせず HdaRefused を投げる。"""
|
|
100
|
+
warns: list[str] = []
|
|
101
|
+
files = []
|
|
102
|
+
for pat in ("*.hda", "*.otl", "*.hdalc", "*.otllc"):
|
|
103
|
+
files += glob.glob(os.path.join(hda_dir, pat))
|
|
104
|
+
loaded: list[tuple[str, list]] = []
|
|
105
|
+
for f in sorted(files):
|
|
106
|
+
try:
|
|
107
|
+
defs = list(hou.hda.definitionsInFile(f))
|
|
108
|
+
except hou.Error as e:
|
|
109
|
+
warns.append("HDA %s を読めませんでした: %s" % (os.path.basename(f), e))
|
|
110
|
+
continue
|
|
111
|
+
for d in defs:
|
|
112
|
+
cur = hou.nodeType(d.nodeTypeCategory(), d.nodeTypeName())
|
|
113
|
+
if cur is not None and cur.definition() is None:
|
|
114
|
+
raise HdaRefused("HDA %s の %s は Houdini 組み込みのノード型と同名のためインストールできません"
|
|
115
|
+
% (os.path.basename(f), d.nodeTypeName()))
|
|
116
|
+
loaded.append((f, defs))
|
|
117
|
+
for f, defs in loaded:
|
|
118
|
+
need = False
|
|
119
|
+
for d in defs:
|
|
120
|
+
secs, cb = script_info(d)
|
|
121
|
+
if secs or cb:
|
|
122
|
+
warns.append("HDA %s はスクリプトを含みます: %s" % (
|
|
123
|
+
d.nodeTypeName(), ", ".join(secs + (["パラメータのコールバック"] if cb else []))))
|
|
124
|
+
cur = hou.nodeType(d.nodeTypeCategory(), d.nodeTypeName())
|
|
125
|
+
cd = cur.definition() if cur is not None else None
|
|
126
|
+
if cd is None:
|
|
127
|
+
need = True
|
|
128
|
+
elif cd.version() == d.version() and cd.modificationTime() == d.modificationTime():
|
|
129
|
+
continue # 同じ定義がインストール済み
|
|
130
|
+
else:
|
|
131
|
+
warns.append("HDA %s は同名の別定義が既にあるため、既存の定義を使います" % d.nodeTypeName())
|
|
132
|
+
if need:
|
|
133
|
+
try:
|
|
134
|
+
hou.hda.installFile(f, change_oplibraries_file=False)
|
|
135
|
+
except hou.Error as e:
|
|
136
|
+
warns.append("HDA %s をインストールできませんでした: %s" % (os.path.basename(f), e))
|
|
137
|
+
return warns
|