procfunc 0.30.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.
- procfunc/__init__.py +87 -0
- procfunc/color.py +57 -0
- procfunc/compute_graph/__init__.py +28 -0
- procfunc/compute_graph/compute_graph.py +115 -0
- procfunc/compute_graph/node.py +200 -0
- procfunc/compute_graph/operators_info.py +92 -0
- procfunc/compute_graph/proxy.py +173 -0
- procfunc/compute_graph/util.py +282 -0
- procfunc/context.py +115 -0
- procfunc/control.py +174 -0
- procfunc/nodes/__init__.py +66 -0
- procfunc/nodes/bindings_util.py +196 -0
- procfunc/nodes/bpy_node_info.py +280 -0
- procfunc/nodes/compositor.py +2242 -0
- procfunc/nodes/execute/construct_nodes.py +571 -0
- procfunc/nodes/execute/construct_special_cases.py +246 -0
- procfunc/nodes/execute/execute.py +548 -0
- procfunc/nodes/execute/infer_runtime_data_type.py +195 -0
- procfunc/nodes/execute/util.py +247 -0
- procfunc/nodes/func.py +1417 -0
- procfunc/nodes/geo.py +4240 -0
- procfunc/nodes/manifest.json +8769 -0
- procfunc/nodes/math.py +644 -0
- procfunc/nodes/node_function.py +160 -0
- procfunc/nodes/shader.py +2359 -0
- procfunc/nodes/types.py +347 -0
- procfunc/ops/__init__.py +35 -0
- procfunc/ops/_util.py +275 -0
- procfunc/ops/addons.py +59 -0
- procfunc/ops/attr.py +426 -0
- procfunc/ops/collection.py +90 -0
- procfunc/ops/curve.py +18 -0
- procfunc/ops/file.py +126 -0
- procfunc/ops/manifest.json +39149 -0
- procfunc/ops/mesh.py +1510 -0
- procfunc/ops/modifier.py +603 -0
- procfunc/ops/object.py +258 -0
- procfunc/ops/primitives/__init__.py +31 -0
- procfunc/ops/primitives/camera.py +45 -0
- procfunc/ops/primitives/curve.py +71 -0
- procfunc/ops/primitives/light.py +114 -0
- procfunc/ops/primitives/mesh.py +358 -0
- procfunc/ops/uv.py +271 -0
- procfunc/random.py +247 -0
- procfunc/tracer/__init__.py +43 -0
- procfunc/tracer/decorator.py +121 -0
- procfunc/tracer/patch.py +494 -0
- procfunc/tracer/proxy.py +127 -0
- procfunc/tracer/trace.py +222 -0
- procfunc/transforms/__init__.py +49 -0
- procfunc/transforms/cleanup.py +214 -0
- procfunc/transforms/convert.py +20 -0
- procfunc/transforms/distribution.py +191 -0
- procfunc/transforms/extract_materials.py +116 -0
- procfunc/transforms/infer_distribution.py +326 -0
- procfunc/transforms/parameters.py +15 -0
- procfunc/transforms/util.py +35 -0
- procfunc/transpiler/__init__.py +24 -0
- procfunc/transpiler/bpy_to_computegraph.py +1348 -0
- procfunc/transpiler/codegen.py +919 -0
- procfunc/transpiler/identifiers.py +595 -0
- procfunc/transpiler/main.py +299 -0
- procfunc/types.py +380 -0
- procfunc/util/__init__.py +0 -0
- procfunc/util/bpy_info.py +145 -0
- procfunc/util/camera.py +0 -0
- procfunc/util/keyframe.py +70 -0
- procfunc/util/log.py +96 -0
- procfunc/util/manifest.py +121 -0
- procfunc/util/pytree.py +343 -0
- procfunc/util/teardown.py +37 -0
- procfunc-0.30.0.dist-info/METADATA +120 -0
- procfunc-0.30.0.dist-info/RECORD +76 -0
- procfunc-0.30.0.dist-info/WHEEL +5 -0
- procfunc-0.30.0.dist-info/licenses/LICENSE.md +11 -0
- procfunc-0.30.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1348 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
from collections import namedtuple
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
Callable,
|
|
8
|
+
Literal,
|
|
9
|
+
TypeVar,
|
|
10
|
+
Union,
|
|
11
|
+
get_args,
|
|
12
|
+
get_origin,
|
|
13
|
+
get_type_hints,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
import bpy
|
|
17
|
+
import idprop.types
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
import procfunc as pf
|
|
22
|
+
from procfunc import compute_graph as cg
|
|
23
|
+
from procfunc import types as t
|
|
24
|
+
from procfunc.nodes import NODES_MANIFEST, bpy_node_info
|
|
25
|
+
from procfunc.nodes import bpy_node_info as bni
|
|
26
|
+
from procfunc.nodes import types as nt
|
|
27
|
+
from procfunc.nodes.execute.util import get_active_sockets, normalize_socket_type
|
|
28
|
+
from procfunc.ops import OPS_MANIFEST
|
|
29
|
+
from procfunc.transpiler import identifiers
|
|
30
|
+
from procfunc.util import bpy_info, log, manifest, pytree
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
_NODES_MANIFEST_INDEXED = NODES_MANIFEST.set_index("bpy_name")
|
|
35
|
+
_OPS_MANIFEST_INDEXED = OPS_MANIFEST.set_index("bpy_name")
|
|
36
|
+
|
|
37
|
+
# TODO replace with nodes/types.py or bpy_info.py?
|
|
38
|
+
SUBCOMPONENT_TYPES = (
|
|
39
|
+
bpy.types.Material,
|
|
40
|
+
bpy.types.Object,
|
|
41
|
+
bpy.types.Collection,
|
|
42
|
+
bpy.types.Image,
|
|
43
|
+
bpy.types.Texture,
|
|
44
|
+
)
|
|
45
|
+
MODE_ATTRS = [
|
|
46
|
+
"mode",
|
|
47
|
+
"data_type",
|
|
48
|
+
"operation",
|
|
49
|
+
"rotation_type",
|
|
50
|
+
"feature",
|
|
51
|
+
"distribute_method",
|
|
52
|
+
]
|
|
53
|
+
IGNORE_ATTRS = ["color_mapping", "texture_mapping", "active_item", "capture_items"]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def handle_specialcase_math(_node: bpy.types.Node, cg_node: cg.Node) -> cg.Node:
|
|
57
|
+
if cg_node.kwargs.pop("use_clamp", False):
|
|
58
|
+
# our math funcs wont support inline clamp, so we add an extra node when needed
|
|
59
|
+
cg_node = cg.FunctionCallNode(
|
|
60
|
+
func=pf.nodes.math.clamp, args=(cg_node,), kwargs={}
|
|
61
|
+
)
|
|
62
|
+
return cg_node
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def handle_specialcase_color_ramp(node: bpy.types.Node, cg_node: cg.Node) -> cg.Node:
|
|
66
|
+
cg_node.kwargs.pop("color_ramp", None)
|
|
67
|
+
cg_node.kwargs["interpolation"] = node.color_ramp.interpolation
|
|
68
|
+
cg_node.kwargs["points"] = [
|
|
69
|
+
(round(point.position, 3), tuple(round(x, 3) for x in point.color))
|
|
70
|
+
for point in node.color_ramp.elements
|
|
71
|
+
]
|
|
72
|
+
return cg_node
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def handle_specialcase_value(node: bpy.types.Node, cg_node: cg.Node) -> cg.Node:
|
|
76
|
+
cg_node.kwargs["value"] = _repr_default_value(
|
|
77
|
+
node.outputs[0].default_value, node.outputs[0].type
|
|
78
|
+
)
|
|
79
|
+
return cg_node
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def handle_specialcase_vector_rotate(node: bpy.types.Node, cg_node: cg.Node) -> cg.Node:
|
|
83
|
+
angle = cg_node.kwargs.pop("angle", None)
|
|
84
|
+
|
|
85
|
+
match node.rotation_type:
|
|
86
|
+
case "X_AXIS":
|
|
87
|
+
cg_node.kwargs["rotation"] = cg.FunctionCallNode(
|
|
88
|
+
pf.nodes.func.combine_xyz, args=(angle, 0, 0), kwargs={}
|
|
89
|
+
)
|
|
90
|
+
case "Y_AXIS":
|
|
91
|
+
cg_node.kwargs["rotation"] = cg.FunctionCallNode(
|
|
92
|
+
pf.nodes.func.combine_xyz, args=(0, angle, 0), kwargs={}
|
|
93
|
+
)
|
|
94
|
+
case "Z_AXIS":
|
|
95
|
+
cg_node.kwargs["rotation"] = cg.FunctionCallNode(
|
|
96
|
+
pf.nodes.func.combine_xyz, args=(0, 0, angle), kwargs={}
|
|
97
|
+
)
|
|
98
|
+
case "EULER_XYZ" | "AXIS_ANGLE":
|
|
99
|
+
pass
|
|
100
|
+
case _:
|
|
101
|
+
raise ValueError(f"Unknown rotation type {node.rotation_type}")
|
|
102
|
+
|
|
103
|
+
return cg_node
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def handle_specialcase_curve(node: bpy.types.Node, cg_node: cg.Node) -> cg.Node:
|
|
107
|
+
cg_node.kwargs.pop("mapping", None)
|
|
108
|
+
|
|
109
|
+
def _repr_point(point):
|
|
110
|
+
return tuple(round(p, 4) for p in point.location)
|
|
111
|
+
|
|
112
|
+
curves = [
|
|
113
|
+
np.array([_repr_point(point) for point in curve.points])
|
|
114
|
+
for curve in node.mapping.curves
|
|
115
|
+
]
|
|
116
|
+
if len(curves) > 1:
|
|
117
|
+
cg_node.kwargs["curves"] = curves
|
|
118
|
+
else:
|
|
119
|
+
cg_node.kwargs["curve"] = curves[0]
|
|
120
|
+
|
|
121
|
+
invalid_handle = next(
|
|
122
|
+
(
|
|
123
|
+
point.handle_type
|
|
124
|
+
for point in node.mapping.curves[0].points
|
|
125
|
+
if point.handle_type != "AUTO"
|
|
126
|
+
),
|
|
127
|
+
None,
|
|
128
|
+
)
|
|
129
|
+
if invalid_handle:
|
|
130
|
+
logger.warning(
|
|
131
|
+
f"{node.name=} had curve handle {invalid_handle=}, currently only AUTO is supported. "
|
|
132
|
+
"Please use a different handle, or contact the developers to add support for it"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return cg_node
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
SPECIAL_CASE_NODES: Callable[[bpy.types.Node, cg.Node], cg.Node] = {
|
|
139
|
+
"ShaderNodeMath": handle_specialcase_math,
|
|
140
|
+
"ShaderNodeValToRGB": handle_specialcase_color_ramp,
|
|
141
|
+
"ShaderNodeVectorRotate": handle_specialcase_vector_rotate,
|
|
142
|
+
# curves share handler
|
|
143
|
+
"ShaderNodeFloatCurve": handle_specialcase_curve,
|
|
144
|
+
"ShaderNodeRGBCurve": handle_specialcase_curve,
|
|
145
|
+
"ShaderNodeVectorCurve": handle_specialcase_curve,
|
|
146
|
+
# values with .outputs[0].default_value can share handler
|
|
147
|
+
"ShaderNodeValue": handle_specialcase_value,
|
|
148
|
+
"ShaderNodeRGB": handle_specialcase_value,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class InvalidNodeGraph(Exception):
|
|
153
|
+
def __init__(self, message: str, nodes: list[bpy.types.Node]):
|
|
154
|
+
super().__init__(message)
|
|
155
|
+
self.nodes = nodes
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class ParseMemo:
|
|
160
|
+
nodes: dict[tuple[int, str], cg.Node] = field(default_factory=dict)
|
|
161
|
+
"""
|
|
162
|
+
(str, str) key is (node_tree.name, node.name)
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
links: dict[tuple[int, str, str], cg.Node] = field(default_factory=dict)
|
|
166
|
+
"""
|
|
167
|
+
(str, str, str) key is (node_tree.name, node.name, from_socket.identifier)
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
compute_graphs: dict[int, tuple[cg.ComputeGraph, dict[str, cg.Node]]] = field(
|
|
171
|
+
default_factory=dict
|
|
172
|
+
)
|
|
173
|
+
"""
|
|
174
|
+
int key is id(node_tree)
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
assets: dict[tuple[type, str], cg.ComputeGraph] = field(default_factory=dict)
|
|
178
|
+
"""
|
|
179
|
+
str key is asset.name
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _find_node_blidname(
|
|
184
|
+
node_tree: bpy.types.NodeTree, bl_idname: str
|
|
185
|
+
) -> list[bpy.types.Node]:
|
|
186
|
+
return [node for node in node_tree.nodes if node.bl_idname == bl_idname]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def parse_texture(tex: t.Texture, memo: ParseMemo) -> cg.Node:
|
|
190
|
+
assert tex.use_nodes
|
|
191
|
+
raise NotImplementedError("Texture not implemented")
|
|
192
|
+
# node_tree = parse_node_tree(tex.node_tree, memo)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _target_attrs(node: bpy.types.Node) -> dict[str, Any]:
|
|
196
|
+
attr_vals = {}
|
|
197
|
+
for k in dir(node):
|
|
198
|
+
if k.startswith("_"):
|
|
199
|
+
continue
|
|
200
|
+
elif k in bpy_node_info.SPECIAL_CASE_ATTR_NAMES:
|
|
201
|
+
attr_vals[k] = None
|
|
202
|
+
elif k not in bpy_node_info.UNIVERSAL_ATTR_NAMES:
|
|
203
|
+
attr_vals[k] = getattr(node, k)
|
|
204
|
+
|
|
205
|
+
return attr_vals
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _bpy_node_defaults(
|
|
209
|
+
node_tree: bpy.types.NodeTree,
|
|
210
|
+
node: bpy.types.Node,
|
|
211
|
+
attr_keys: list[str],
|
|
212
|
+
) -> dict[str, Any]:
|
|
213
|
+
temp_default_node = node_tree.nodes.new(node.bl_idname)
|
|
214
|
+
if node.bl_idname.endswith("NodeGroup"):
|
|
215
|
+
temp_default_node.node_tree = node.node_tree
|
|
216
|
+
if hasattr(node, "operation"):
|
|
217
|
+
# assert "operation" not in attr_keys, (node, attr_keys)
|
|
218
|
+
temp_default_node.operation = node.operation
|
|
219
|
+
|
|
220
|
+
attr_defaults = {}
|
|
221
|
+
for k in attr_keys:
|
|
222
|
+
if hasattr(temp_default_node, k):
|
|
223
|
+
val = getattr(temp_default_node, k)
|
|
224
|
+
# copy mathutils types before removing the node to avoid dangling pointer segfaults
|
|
225
|
+
if hasattr(val, "copy"):
|
|
226
|
+
val = val.copy()
|
|
227
|
+
attr_defaults[k] = val
|
|
228
|
+
|
|
229
|
+
node_tree.nodes.remove(temp_default_node)
|
|
230
|
+
|
|
231
|
+
return attr_defaults
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _remove_banned_attrs(
|
|
235
|
+
attrs: dict[str, Any],
|
|
236
|
+
blender_attr_vals: dict[str, Any],
|
|
237
|
+
):
|
|
238
|
+
for k in IGNORE_ATTRS:
|
|
239
|
+
res = attrs.pop(k, None)
|
|
240
|
+
if (
|
|
241
|
+
res is not None
|
|
242
|
+
and k not in ["capture_items", "active_item"]
|
|
243
|
+
and res != blender_attr_vals[k]
|
|
244
|
+
):
|
|
245
|
+
logger.warning(
|
|
246
|
+
f"Ignoring {k}={res} which had been changed from its default value {blender_attr_vals[k]!r}"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _parse_getattr(
|
|
251
|
+
res: cg.Node,
|
|
252
|
+
link: bpy.types.NodeLink,
|
|
253
|
+
):
|
|
254
|
+
target = link.from_socket.name
|
|
255
|
+
func_spec, _ = _node_to_spec(
|
|
256
|
+
link.from_node.bl_idname, _target_attrs(link.from_node)
|
|
257
|
+
)
|
|
258
|
+
if func_spec is not None and isinstance(func_spec["output_names_map"], dict):
|
|
259
|
+
target = func_spec["output_names_map"].get(target, target)
|
|
260
|
+
target = identifiers.bpy_name_to_pythonid(target)
|
|
261
|
+
|
|
262
|
+
res = cg.GetAttributeNode(attribute_name=target, source=res)
|
|
263
|
+
return res
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _create_link_impl_node(
|
|
267
|
+
node_tree: bpy.types.NodeTree,
|
|
268
|
+
link: bpy.types.NodeLink,
|
|
269
|
+
memo: ParseMemo,
|
|
270
|
+
) -> cg.Node:
|
|
271
|
+
if link.from_node.bl_idname in ["NodeGroupInput", "NodeGroupOutput"]:
|
|
272
|
+
key = (id(link.from_node), link.from_socket.identifier)
|
|
273
|
+
raise ValueError(
|
|
274
|
+
f"{parse_link.__name__} {node_tree.name=} {key} {link.from_node.name} {link.from_socket.name} -> {link.to_node.name} {link.to_socket.name} "
|
|
275
|
+
f"has {link.from_node.bl_idname=}, which should have been avoided from parsing via {len(memo.links)=}"
|
|
276
|
+
)
|
|
277
|
+
elif link.from_node.bl_idname == "NodeReroute":
|
|
278
|
+
if len(link.from_node.inputs[0].links) == 0:
|
|
279
|
+
raise ValueError(
|
|
280
|
+
f"Node {link.from_node.bl_idname} {link.from_node.name=} in {node_tree.name=} has no inputs"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# pass through to_socket to avoid potentially having wrong type inference
|
|
284
|
+
res = parse_link(node_tree, link.from_node.inputs[0].links[0], memo)
|
|
285
|
+
assert res is not None, link
|
|
286
|
+
return res
|
|
287
|
+
elif link.from_node.bl_idname == "ShaderNodeSeparateXYZ":
|
|
288
|
+
inp_vec = link.from_node.inputs[0]
|
|
289
|
+
if len(inp_vec.links) == 0:
|
|
290
|
+
# WARN: creates multiple constants without memoizing
|
|
291
|
+
default_value = _repr_default_value(inp_vec.default_value, inp_vec.type)
|
|
292
|
+
source = cg.FunctionCallNode(
|
|
293
|
+
func=pf.nodes.func.constant,
|
|
294
|
+
args=(default_value,),
|
|
295
|
+
kwargs={},
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
# skips over `link.from_node` since we are relying on the ProcNode getattr() to do that, rather than an explicit functioncall
|
|
299
|
+
source = parse_link(node_tree, inp_vec.links[0], memo)
|
|
300
|
+
|
|
301
|
+
return cg.GetAttributeNode(
|
|
302
|
+
attribute_name=link.from_socket.name.lower(),
|
|
303
|
+
source=source,
|
|
304
|
+
)
|
|
305
|
+
else:
|
|
306
|
+
res = parse_node(node_tree, link.from_node, memo)
|
|
307
|
+
assert res is not None, link
|
|
308
|
+
|
|
309
|
+
outsockets = get_active_sockets(link.from_node.outputs)
|
|
310
|
+
assert len(outsockets) > 0
|
|
311
|
+
if len(outsockets) > 1:
|
|
312
|
+
res = _parse_getattr(res, link)
|
|
313
|
+
assert res is not None, link
|
|
314
|
+
|
|
315
|
+
assert res is not None, link
|
|
316
|
+
return res
|
|
317
|
+
|
|
318
|
+
raise ValueError("Impossible")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def parse_link(
|
|
322
|
+
node_tree: bpy.types.NodeTree,
|
|
323
|
+
link: bpy.types.NodeLink,
|
|
324
|
+
memo: ParseMemo,
|
|
325
|
+
) -> cg.Node:
|
|
326
|
+
"""
|
|
327
|
+
Create a cg.Node for the from_node, and prefix it with a GET_ATTRIBUTE
|
|
328
|
+
if this is necessary to disambiguate multiple outputs
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
key = (node_tree.session_uid, link.from_node.name, link.from_socket.identifier)
|
|
332
|
+
|
|
333
|
+
if link_node := memo.links.get(key):
|
|
334
|
+
assert link_node is not None, key
|
|
335
|
+
res = link_node
|
|
336
|
+
else:
|
|
337
|
+
res = _create_link_impl_node(node_tree, link, memo)
|
|
338
|
+
memo.links[key] = res
|
|
339
|
+
|
|
340
|
+
# make all implicit blender typeconversions into explicit .astype calls
|
|
341
|
+
if link.from_socket.type != link.to_socket.type:
|
|
342
|
+
to_socket_type = bpy_node_info.SocketType(
|
|
343
|
+
normalize_socket_type(link.to_socket.bl_idname)
|
|
344
|
+
)
|
|
345
|
+
to_py_type = bpy_node_info.SOCKET_TYPE_TO_PYTHON_TYPE[to_socket_type]
|
|
346
|
+
|
|
347
|
+
assert to_py_type is not None, to_socket_type
|
|
348
|
+
|
|
349
|
+
logger.debug(
|
|
350
|
+
f"Adding explicit astype({to_py_type}) for {link.from_node.name}.{link.from_socket.name} -> "
|
|
351
|
+
f"{link.to_node.name}.{link.to_socket.name}"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
res = cg.MethodCallNode(
|
|
355
|
+
callee=res,
|
|
356
|
+
method_name="astype",
|
|
357
|
+
args=(),
|
|
358
|
+
kwargs={"dtype": to_py_type},
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
assert res is not None, link
|
|
362
|
+
|
|
363
|
+
return res
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _repr_default_value(value: Any, socket_type: str) -> Any:
|
|
367
|
+
if isinstance(value, SUBCOMPONENT_TYPES):
|
|
368
|
+
return value.name
|
|
369
|
+
elif socket_type == "RGBA":
|
|
370
|
+
return pf.Color([round(x, 6) for x in value[:3]])
|
|
371
|
+
elif isinstance(
|
|
372
|
+
value, (bpy.types.bpy_prop_array, t.Vector, t.Euler, t.Quaternion, t.Matrix)
|
|
373
|
+
):
|
|
374
|
+
return tuple(round(v, 6) for v in value)
|
|
375
|
+
elif isinstance(value, idprop.types.IDPropertyArray):
|
|
376
|
+
return tuple(round(v, 6) for v in value)
|
|
377
|
+
elif isinstance(value, float):
|
|
378
|
+
return round(value, 6)
|
|
379
|
+
else:
|
|
380
|
+
return value
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _create_link_input(
|
|
384
|
+
node_tree: bpy.types.NodeTree,
|
|
385
|
+
socket: bpy.types.NodeSocket,
|
|
386
|
+
memo: ParseMemo,
|
|
387
|
+
is_toplevel: bool,
|
|
388
|
+
func_default: Any | None = None,
|
|
389
|
+
) -> cg.Node | list[cg.Node] | None:
|
|
390
|
+
if len(socket.links) > 1:
|
|
391
|
+
return [parse_link(node_tree, l, memo) for l in socket.links]
|
|
392
|
+
|
|
393
|
+
if len(socket.links) == 1:
|
|
394
|
+
return parse_link(node_tree, socket.links[0], memo)
|
|
395
|
+
|
|
396
|
+
if not hasattr(socket, "default_value"):
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
if isinstance(socket.default_value, SUBCOMPONENT_TYPES) and not is_toplevel:
|
|
400
|
+
logger.warning(
|
|
401
|
+
(
|
|
402
|
+
f"Transpiler recommends against {type(socket.default_value)} as default_value but got {socket.default_value.name=} {is_toplevel=}"
|
|
403
|
+
f"please use a typed socket connected from the toplevel nodegroup inputs instead. This makes your subcomponent user-configurable ",
|
|
404
|
+
[socket],
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
if isinstance(socket.default_value, bpy.types.Material):
|
|
409
|
+
mat_graph = parse_material(socket.default_value, memo)
|
|
410
|
+
return cg.SubgraphCallNode(subgraph=mat_graph, args=(), kwargs={})
|
|
411
|
+
|
|
412
|
+
if isinstance(socket.default_value, bpy.types.Object):
|
|
413
|
+
return t.MeshObject(socket.default_value)
|
|
414
|
+
|
|
415
|
+
if isinstance(socket.default_value, bpy.types.Collection):
|
|
416
|
+
return t.Collection(socket.default_value)
|
|
417
|
+
|
|
418
|
+
if not hasattr(socket, "default_value"):
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
res = _repr_default_value(getattr(socket, "default_value", None), socket.type)
|
|
422
|
+
|
|
423
|
+
if func_default is not None:
|
|
424
|
+
repr_func_default = _repr_default_value(func_default, socket.type)
|
|
425
|
+
if res == repr_func_default:
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
return res
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _create_inputs(
|
|
432
|
+
node_tree: bpy.types.NodeTree,
|
|
433
|
+
node: bpy.types.Node,
|
|
434
|
+
memo: ParseMemo,
|
|
435
|
+
func_defaults: dict[str, Any],
|
|
436
|
+
names: dict[str, str] | None = None,
|
|
437
|
+
is_toplevel: bool = False,
|
|
438
|
+
) -> dict[str, cg.Node]:
|
|
439
|
+
res = {}
|
|
440
|
+
|
|
441
|
+
if names is None:
|
|
442
|
+
names = {
|
|
443
|
+
socket.identifier: socket.name
|
|
444
|
+
for socket in node.inputs.values()
|
|
445
|
+
if socket.enabled and socket.name != ""
|
|
446
|
+
}
|
|
447
|
+
names = identifiers.dedup_names_with_suffix(names, first_use_suffix=True)
|
|
448
|
+
|
|
449
|
+
inputs = {}
|
|
450
|
+
for identifier, name in names.items():
|
|
451
|
+
socket = next(
|
|
452
|
+
(s for s in node.inputs.values() if s.identifier == identifier), None
|
|
453
|
+
)
|
|
454
|
+
assert socket is not None
|
|
455
|
+
|
|
456
|
+
name = identifiers.bpy_name_to_pythonid(name)
|
|
457
|
+
|
|
458
|
+
func_default_kwarg = func_defaults.get(name, None)
|
|
459
|
+
res = _create_link_input(
|
|
460
|
+
node_tree,
|
|
461
|
+
socket,
|
|
462
|
+
memo,
|
|
463
|
+
is_toplevel,
|
|
464
|
+
func_default=func_default_kwarg,
|
|
465
|
+
)
|
|
466
|
+
if res is None:
|
|
467
|
+
logger.debug(
|
|
468
|
+
f"Skipping argument for {name=} {socket.node.bl_idname=} {func_default_kwarg=}"
|
|
469
|
+
)
|
|
470
|
+
continue
|
|
471
|
+
inputs[name] = res
|
|
472
|
+
|
|
473
|
+
for name in inputs.keys():
|
|
474
|
+
if not identifiers.is_valid_snake_identifier(name):
|
|
475
|
+
raise ValueError(
|
|
476
|
+
f"Input name {name!r} is not a valid identifier. {node.bl_idname=}, {node.inputs.keys()=}"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
return inputs
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _find_manifest_func(
|
|
483
|
+
bpy_name: str,
|
|
484
|
+
mode_vals: dict[str, Any],
|
|
485
|
+
manifest_indexed: pd.DataFrame,
|
|
486
|
+
) -> dict | None:
|
|
487
|
+
if bpy_name not in manifest_indexed.index:
|
|
488
|
+
return None
|
|
489
|
+
|
|
490
|
+
candidates = manifest_indexed.loc[bpy_name]
|
|
491
|
+
if isinstance(candidates, pd.Series):
|
|
492
|
+
candidates = pd.DataFrame([candidates])
|
|
493
|
+
|
|
494
|
+
exploded = candidates["bpy_mode_args"].fillna({}).apply(pd.Series)
|
|
495
|
+
|
|
496
|
+
mask = pd.Series([True] * len(candidates), index=candidates.index)
|
|
497
|
+
for mode_attr, val in mode_vals.items():
|
|
498
|
+
# Behavior: if a mode_attr is not in the manifest, we can ignore it.
|
|
499
|
+
# if its in the manifest, but none match our val, then we can take a
|
|
500
|
+
|
|
501
|
+
if val is None:
|
|
502
|
+
continue
|
|
503
|
+
if mode_attr not in exploded.columns:
|
|
504
|
+
continue
|
|
505
|
+
match_mask = exploded[mode_attr] == val
|
|
506
|
+
if match_mask.sum() == 0:
|
|
507
|
+
match_mask = exploded[mode_attr].isna()
|
|
508
|
+
mask &= match_mask
|
|
509
|
+
|
|
510
|
+
if mask.sum() == 0:
|
|
511
|
+
raise ValueError(
|
|
512
|
+
f"{bpy_name=} had {len(candidates)=}, but filtering for {mode_vals=} eliminated them all"
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
if mask.sum() > 1:
|
|
516
|
+
options_for_modevals = {
|
|
517
|
+
k: list(exploded[k].unique()) if k in exploded.columns else None
|
|
518
|
+
for k in mode_vals.keys()
|
|
519
|
+
}
|
|
520
|
+
raise ValueError(
|
|
521
|
+
f"Found {mask.sum()} nodes with {bpy_name=} {mode_vals=} in manifest, expected exactly 1. "
|
|
522
|
+
f"Options for {mode_vals.keys()=} are {options_for_modevals}"
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
matches = candidates.loc[mask]
|
|
526
|
+
|
|
527
|
+
if len(matches) == 1:
|
|
528
|
+
return matches.iloc[0].to_dict()
|
|
529
|
+
else:
|
|
530
|
+
return None
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _node_to_spec(
|
|
534
|
+
bl_idname: str,
|
|
535
|
+
attrs: dict[str, Any],
|
|
536
|
+
) -> tuple[dict[str, Any] | None, dict]:
|
|
537
|
+
if bpy_info.NodeGroupType.from_str(bl_idname) is not None:
|
|
538
|
+
return None, attrs
|
|
539
|
+
|
|
540
|
+
mode_attr_vals = {k: attrs[k] for k in MODE_ATTRS if k in attrs}
|
|
541
|
+
|
|
542
|
+
func_row = _find_manifest_func(
|
|
543
|
+
bl_idname,
|
|
544
|
+
mode_attr_vals,
|
|
545
|
+
_NODES_MANIFEST_INDEXED,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
if func_row is None:
|
|
549
|
+
raise ValueError(f"Node {bl_idname} {mode_attr_vals=} had no manifest row")
|
|
550
|
+
elif func_row["name"] in ["LATER", "DECLINE"]:
|
|
551
|
+
raise ValueError(f"Node {bl_idname} {mode_attr_vals=} had {func_row['name']=}")
|
|
552
|
+
|
|
553
|
+
return func_row, attrs
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _map_inputs_with_arg_map(
|
|
557
|
+
inputs: dict[str, Any],
|
|
558
|
+
arg_names_map: dict[str, str],
|
|
559
|
+
) -> dict[str, Any]:
|
|
560
|
+
mapped_inputs = {}
|
|
561
|
+
for k, v in inputs.items():
|
|
562
|
+
if k in arg_names_map:
|
|
563
|
+
k = arg_names_map[k]
|
|
564
|
+
mapped_inputs[k] = v
|
|
565
|
+
|
|
566
|
+
return mapped_inputs
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def parse_standard_node(
|
|
570
|
+
node_tree: bpy.types.NodeTree,
|
|
571
|
+
node: bpy.types.Node,
|
|
572
|
+
memo: ParseMemo,
|
|
573
|
+
) -> cg.Node:
|
|
574
|
+
# note: read/write result into memo happens at parse_node level, not here
|
|
575
|
+
|
|
576
|
+
if node.bl_idname == "NodeGroupInput":
|
|
577
|
+
raise ValueError(
|
|
578
|
+
f"NodeGroupInput {node} {id(node)=} should have been pre-memo'd in parse_node_tree"
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
attrs = _target_attrs(node)
|
|
582
|
+
func_spec, attrs = _node_to_spec(node.bl_idname, attrs)
|
|
583
|
+
|
|
584
|
+
func = manifest.import_item_iterative(func_spec["name"].replace("pf.", "procfunc."))
|
|
585
|
+
func_sig = inspect.signature(func)
|
|
586
|
+
arg_names_map = func_spec.get("arg_names_map")
|
|
587
|
+
|
|
588
|
+
attr_defaults = _bpy_node_defaults(node_tree, node, list(attrs.keys()))
|
|
589
|
+
is_named_attr = node.bl_idname == "GeometryNodeInputNamedAttribute"
|
|
590
|
+
attrs = {
|
|
591
|
+
k: v
|
|
592
|
+
for k, v in attrs.items()
|
|
593
|
+
if v != attr_defaults[k] or (k == "data_type" and is_named_attr)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if arg_names_map is not None:
|
|
597
|
+
attrs = {arg_names_map.get(k, k): v for k, v in attrs.items()}
|
|
598
|
+
|
|
599
|
+
# we only want to remove MODE_ATTRS which were actually used to resolve the function
|
|
600
|
+
# (since presumably the restriction implied by these is already enforced by the new function signature)
|
|
601
|
+
resolve_mode_args = func_spec.get("bpy_mode_args")
|
|
602
|
+
if resolve_mode_args is not None:
|
|
603
|
+
for k, v in resolve_mode_args.items():
|
|
604
|
+
if k in attrs and k not in func_sig.parameters.keys():
|
|
605
|
+
attrs.pop(k)
|
|
606
|
+
|
|
607
|
+
# we will assume the data_types in an input .blend can always be inferred.
|
|
608
|
+
# it is the job of the .astype() insertion to preserve enough info for this
|
|
609
|
+
if "data_type" in attrs and node.bl_idname != "GeometryNodeInputNamedAttribute":
|
|
610
|
+
attrs.pop("data_type")
|
|
611
|
+
|
|
612
|
+
func_defaults = {
|
|
613
|
+
param.name: param.default
|
|
614
|
+
for param in func_sig.parameters.values()
|
|
615
|
+
if param.default is not param.empty
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
inputs = _create_inputs(node_tree, node, memo, func_defaults=func_defaults)
|
|
619
|
+
arg_names_map = func_spec["arg_names_map"]
|
|
620
|
+
if arg_names_map is not None:
|
|
621
|
+
inputs = _map_inputs_with_arg_map(inputs, arg_names_map)
|
|
622
|
+
|
|
623
|
+
if overlap := set(attrs.keys()).intersection(set(inputs.keys())):
|
|
624
|
+
raise ValueError(
|
|
625
|
+
f"Node {node.bl_idname} had keys {overlap=} between {attrs.keys()=} and {inputs.keys()=}, which is invalid"
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
cg_node = cg.FunctionCallNode(
|
|
629
|
+
func=func,
|
|
630
|
+
args=(),
|
|
631
|
+
kwargs={**attrs, **inputs},
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
cg_node_orig = cg_node
|
|
635
|
+
if handler := SPECIAL_CASE_NODES.get(node.bl_idname):
|
|
636
|
+
cg_node = handler(node, cg_node)
|
|
637
|
+
|
|
638
|
+
_remove_banned_attrs(cg_node.kwargs, attr_defaults)
|
|
639
|
+
|
|
640
|
+
signature = inspect.signature(func)
|
|
641
|
+
|
|
642
|
+
# Check if the function accepts **kwargs (VAR_KEYWORD)
|
|
643
|
+
has_var_keyword = any(
|
|
644
|
+
p.kind == inspect.Parameter.VAR_KEYWORD for p in signature.parameters.values()
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
# Only check for missing parameters if the function doesn't accept **kwargs
|
|
648
|
+
excess_kwargs = set(cg_node_orig.kwargs.keys()) - set(signature.parameters.keys())
|
|
649
|
+
if not has_var_keyword and excess_kwargs:
|
|
650
|
+
node_mode = getattr(node, "mode", None)
|
|
651
|
+
node_operation = getattr(node, "operation", None)
|
|
652
|
+
node_data_type = getattr(node, "data_type", None)
|
|
653
|
+
raise ValueError(
|
|
654
|
+
f"Codegen would attempt to call {func.__name__=} with {excess_kwargs} "
|
|
655
|
+
f"but these attributes do not exist in the procfunc signature, which had {list(signature.parameters.keys())} "
|
|
656
|
+
f"source node had {node.bl_idname} {node.inputs.keys()=} {node_mode=} {node_operation=} {node_data_type=} "
|
|
657
|
+
"Please contact the developers."
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
return cg_node
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def parse_nodegroup_call(
|
|
664
|
+
node_tree: bpy.types.NodeTree,
|
|
665
|
+
node: bpy.types.Node,
|
|
666
|
+
memo: ParseMemo,
|
|
667
|
+
) -> cg.Node:
|
|
668
|
+
assert hasattr(node, "node_tree"), f"Node {node.bl_idname} has no node_tree"
|
|
669
|
+
|
|
670
|
+
sockets = [socket for socket in node.inputs.values() if socket.enabled]
|
|
671
|
+
input_names = {socket.identifier: socket.name for socket in sockets}
|
|
672
|
+
input_names = identifiers.apply_panel_names_to_input_names(
|
|
673
|
+
node.node_tree, input_names, only_dedup=False
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
with log.add_exception_context_msg(f"While processing {node.name=}:"):
|
|
677
|
+
graph, _ = parse_node_tree(node.node_tree, memo)
|
|
678
|
+
|
|
679
|
+
func_defaults = {
|
|
680
|
+
name: value.kwargs.get("default_value", None)
|
|
681
|
+
for name, value in graph.inputs.items()
|
|
682
|
+
}
|
|
683
|
+
inputs = _create_inputs(
|
|
684
|
+
node_tree,
|
|
685
|
+
node,
|
|
686
|
+
memo,
|
|
687
|
+
func_defaults=func_defaults,
|
|
688
|
+
is_toplevel=False,
|
|
689
|
+
names=input_names,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
return cg.SubgraphCallNode(
|
|
693
|
+
subgraph=graph,
|
|
694
|
+
args=(),
|
|
695
|
+
kwargs=inputs,
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _parse_constant_node(node: bpy.types.Node) -> cg.Node:
|
|
700
|
+
attr_name = bpy_node_info.CONSTANT_NODES[node.bl_idname]
|
|
701
|
+
|
|
702
|
+
if attr_name == "DEFAULT_VALUE":
|
|
703
|
+
val = node.outputs[0].default_value
|
|
704
|
+
else:
|
|
705
|
+
val = getattr(node, attr_name)
|
|
706
|
+
|
|
707
|
+
return cg.ConstantNode(value=val)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def parse_node(
|
|
711
|
+
node_tree: bpy.types.NodeTree,
|
|
712
|
+
node: bpy.types.Node,
|
|
713
|
+
memo: ParseMemo,
|
|
714
|
+
) -> cg.Node:
|
|
715
|
+
memo_key = (node_tree.session_uid, node.name)
|
|
716
|
+
if node_node := memo.nodes.get(memo_key):
|
|
717
|
+
return node_node
|
|
718
|
+
|
|
719
|
+
# logger.debug(f"Parsing node {node_tree.name} {node.bl_idname}")
|
|
720
|
+
|
|
721
|
+
if bpy_info.NodeGroupType.from_str(node.bl_idname) is not None:
|
|
722
|
+
res = parse_nodegroup_call(node_tree, node, memo)
|
|
723
|
+
elif node.bl_idname == "NodeReroute":
|
|
724
|
+
raise ValueError(
|
|
725
|
+
f"Node {node.bl_idname} {node.name=} is a NodeReroute, which should have been folded away"
|
|
726
|
+
)
|
|
727
|
+
else:
|
|
728
|
+
res = parse_standard_node(node_tree, node, memo)
|
|
729
|
+
|
|
730
|
+
if node.label != "":
|
|
731
|
+
res.metadata["varname"] = identifiers.bpy_name_to_pythonid(node.label)
|
|
732
|
+
|
|
733
|
+
memo.nodes[memo_key] = res
|
|
734
|
+
return res
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _find_output_node(
|
|
738
|
+
node_tree: bpy.types.NodeTree,
|
|
739
|
+
):
|
|
740
|
+
node_tree_type = bpy_info.NodeTreeType(node_tree.bl_idname)
|
|
741
|
+
ng_type = bpy_info.NODETREE_TO_NODEGROUP[node_tree_type]
|
|
742
|
+
main_output_node_type = bpy_info.NODETREE_TYPE_TO_MAIN_OUTPUT[node_tree_type]
|
|
743
|
+
|
|
744
|
+
output_nodes_ng = _find_node_blidname(node_tree, "NodeGroupOutput")
|
|
745
|
+
output_nodes_ctx = _find_node_blidname(node_tree, main_output_node_type)
|
|
746
|
+
output_nodes = output_nodes_ng + output_nodes_ctx
|
|
747
|
+
|
|
748
|
+
if len(output_nodes) > 1:
|
|
749
|
+
raise ValueError(f"Found mutltiple {output_nodes=} for {node_tree=}")
|
|
750
|
+
if len(output_nodes) == 0:
|
|
751
|
+
idnames = set(node.bl_idname for node in node_tree.nodes)
|
|
752
|
+
raise ValueError(
|
|
753
|
+
f"No {main_output_node_type=} found "
|
|
754
|
+
f"for {node_tree.bl_idname} of type {ng_type} with {idnames}"
|
|
755
|
+
)
|
|
756
|
+
return output_nodes[0]
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def _name_from_socket_and_panels(
|
|
760
|
+
socket: bpy.types.NodeSocket,
|
|
761
|
+
panels: list[bpy.types.NodeSocket],
|
|
762
|
+
) -> str:
|
|
763
|
+
for panel in panels:
|
|
764
|
+
match = next(
|
|
765
|
+
(
|
|
766
|
+
psock
|
|
767
|
+
for psock in panel.interface_items.values()
|
|
768
|
+
if psock.identifier == socket.identifier
|
|
769
|
+
),
|
|
770
|
+
None,
|
|
771
|
+
)
|
|
772
|
+
if match is not None:
|
|
773
|
+
return identifiers.bpy_name_to_pythonid(panel.name + "_" + socket.name)
|
|
774
|
+
return identifiers.bpy_name_to_pythonid(socket.name)
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _infer_geometry_type(node: cg.Node, _depth: int = 0) -> type | None:
|
|
778
|
+
"""Infer concrete geometry type by inspecting the procfunc function that produces this node."""
|
|
779
|
+
if _depth > 10:
|
|
780
|
+
return None
|
|
781
|
+
|
|
782
|
+
if isinstance(node, cg.FunctionCallNode):
|
|
783
|
+
try:
|
|
784
|
+
hints = get_type_hints(node.func)
|
|
785
|
+
except Exception:
|
|
786
|
+
return None
|
|
787
|
+
return_type = hints.get("return")
|
|
788
|
+
if return_type is None:
|
|
789
|
+
return None
|
|
790
|
+
concrete = _extract_procnode_inner_type(return_type)
|
|
791
|
+
if concrete is not None:
|
|
792
|
+
return concrete
|
|
793
|
+
# Return type is generic (TypeVar) — recurse into geometry input args
|
|
794
|
+
for arg in list(node.args) + list(node.kwargs.values()):
|
|
795
|
+
if isinstance(arg, list):
|
|
796
|
+
for item in arg:
|
|
797
|
+
if isinstance(item, cg.Node):
|
|
798
|
+
result = _infer_geometry_type(item, _depth + 1)
|
|
799
|
+
if result is not None:
|
|
800
|
+
return result
|
|
801
|
+
elif isinstance(arg, cg.Node):
|
|
802
|
+
result = _infer_geometry_type(arg, _depth + 1)
|
|
803
|
+
if result is not None:
|
|
804
|
+
return result
|
|
805
|
+
return None
|
|
806
|
+
|
|
807
|
+
if isinstance(node, cg.GetAttributeNode):
|
|
808
|
+
source = node.args[0]
|
|
809
|
+
if not isinstance(source, cg.FunctionCallNode):
|
|
810
|
+
return None
|
|
811
|
+
try:
|
|
812
|
+
hints = get_type_hints(source.func)
|
|
813
|
+
except Exception:
|
|
814
|
+
return None
|
|
815
|
+
return_type = hints.get("return")
|
|
816
|
+
if return_type is None:
|
|
817
|
+
return None
|
|
818
|
+
if hasattr(return_type, "__annotations__"):
|
|
819
|
+
field_type = return_type.__annotations__.get(node.attribute_name)
|
|
820
|
+
if field_type is not None:
|
|
821
|
+
return _extract_procnode_inner_type(field_type)
|
|
822
|
+
return None
|
|
823
|
+
|
|
824
|
+
return None
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def _extract_procnode_inner_type(t: type) -> type | None:
|
|
828
|
+
origin = get_origin(t)
|
|
829
|
+
if origin is nt.ProcNode:
|
|
830
|
+
args = get_args(t)
|
|
831
|
+
if args and not isinstance(args[0], TypeVar):
|
|
832
|
+
return args[0]
|
|
833
|
+
return None
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def _socket_to_pf_type(
|
|
837
|
+
socket: bpy.types.NodeSocket,
|
|
838
|
+
is_output: bool,
|
|
839
|
+
interface: Any | None = None, # unsure type
|
|
840
|
+
use_socket_bounds: bool = False,
|
|
841
|
+
use_specialized_sockets: bool = False,
|
|
842
|
+
) -> type:
|
|
843
|
+
"""
|
|
844
|
+
Create a python typing str to represent the interface bounds of a blender _socket_to_pf_type
|
|
845
|
+
|
|
846
|
+
Args:
|
|
847
|
+
socket: The blender socket to create a python typing str for
|
|
848
|
+
interface: The nodegroup interface of the socket, if one exists
|
|
849
|
+
use_socket_bounds: Whether to use the bounds of the socket's interface.
|
|
850
|
+
Disabled by default since these are often accidentally / imprecisely filled by implementers
|
|
851
|
+
|
|
852
|
+
Returns:
|
|
853
|
+
A python typing str to represent the interface bounds of a blender socket
|
|
854
|
+
"""
|
|
855
|
+
|
|
856
|
+
type_str = normalize_socket_type(socket.bl_idname)
|
|
857
|
+
st = bpy_node_info.SocketType(type_str)
|
|
858
|
+
py_type = bpy_node_info.SOCKET_TYPE_TO_PYTHON_TYPE[st]
|
|
859
|
+
|
|
860
|
+
bounds = [None, None]
|
|
861
|
+
|
|
862
|
+
if use_socket_bounds and interface is not None:
|
|
863
|
+
if hasattr(interface, "min_value") and interface.min_value > -1000:
|
|
864
|
+
bounds[0] = interface.min_value
|
|
865
|
+
if hasattr(interface, "max_value") and interface.max_value < 1000:
|
|
866
|
+
bounds[1] = interface.max_value
|
|
867
|
+
elif use_specialized_sockets and type_str != socket.bl_idname:
|
|
868
|
+
match socket.bl_idname:
|
|
869
|
+
case "NodeSocketFloatFactor":
|
|
870
|
+
bounds = [0.0, 1.0]
|
|
871
|
+
case "NodeSocketVectorEuler":
|
|
872
|
+
bounds = [(0.0, 0.0, 0.0), (3.141592, 3.141592, 3.141592)]
|
|
873
|
+
case _:
|
|
874
|
+
logger.info(
|
|
875
|
+
f"No implemented bounds annot for special socket {socket.bl_idname}"
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
if bounds[0] is not None or bounds[1] is not None:
|
|
879
|
+
raise NotImplementedError("Range annotation current not supported")
|
|
880
|
+
# py_type = Annotated[float, t.ValueRange{tuple(bounds)}]
|
|
881
|
+
|
|
882
|
+
if py_type is None:
|
|
883
|
+
return nt.ProcNode
|
|
884
|
+
elif is_output:
|
|
885
|
+
return nt.ProcNode[py_type]
|
|
886
|
+
else:
|
|
887
|
+
return nt.SocketOrVal[py_type]
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _placeholder_for_graph_input(
|
|
891
|
+
socket: bpy.types.NodeSocket,
|
|
892
|
+
varname: str,
|
|
893
|
+
node_tree: bpy.types.NodeTree,
|
|
894
|
+
) -> cg.Node:
|
|
895
|
+
interface = node_tree.interface.items_tree[socket.name]
|
|
896
|
+
if interface.hide_value:
|
|
897
|
+
logger.warning(
|
|
898
|
+
f"{node_tree.name=} {socket.name=} has hide_value=True, "
|
|
899
|
+
"overwriting it to False or else current transpiler implementationl will break"
|
|
900
|
+
)
|
|
901
|
+
interface.hide_value = False
|
|
902
|
+
|
|
903
|
+
inner_type = _socket_to_pf_type(
|
|
904
|
+
socket,
|
|
905
|
+
is_output=False,
|
|
906
|
+
interface=interface,
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
raw_default = getattr(interface, "default_value", None)
|
|
910
|
+
if raw_default is None:
|
|
911
|
+
raw_default = getattr(socket, "default_value", None)
|
|
912
|
+
default_value = _repr_default_value(raw_default, socket.type)
|
|
913
|
+
|
|
914
|
+
norm_soc = normalize_socket_type(socket.bl_idname)
|
|
915
|
+
if default_value is None and norm_soc in [
|
|
916
|
+
"NodeSocketFloat",
|
|
917
|
+
"NodeSocketVector",
|
|
918
|
+
]:
|
|
919
|
+
raise ValueError(f"{socket.name=} has no default_value and is a {norm_soc=}")
|
|
920
|
+
|
|
921
|
+
node = cg.InputPlaceholderNode(
|
|
922
|
+
name=varname,
|
|
923
|
+
default_value=default_value,
|
|
924
|
+
metadata=dict(
|
|
925
|
+
known_value_type=inner_type,
|
|
926
|
+
varname=varname,
|
|
927
|
+
),
|
|
928
|
+
)
|
|
929
|
+
if default_value is not None:
|
|
930
|
+
node.kwargs["default_value"] = default_value
|
|
931
|
+
|
|
932
|
+
return node
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def _create_and_memoize_input_placeholders(
|
|
936
|
+
node_tree: bpy.types.NodeTree,
|
|
937
|
+
input_nodes: list[bpy.types.Node],
|
|
938
|
+
memo: ParseMemo,
|
|
939
|
+
) -> tuple[dict[str, cg.Node], dict[str, cg.Node]]:
|
|
940
|
+
"""
|
|
941
|
+
prefill memo for all output links of all input nodes
|
|
942
|
+
this is so that we never later recurse onto input nodes, since we dont actually want to create input nodes
|
|
943
|
+
the output identifiers of input nodes are defined by the function args instead.
|
|
944
|
+
|
|
945
|
+
also: create the placeholder nodes for the input nodes
|
|
946
|
+
"""
|
|
947
|
+
|
|
948
|
+
panels = [
|
|
949
|
+
socket
|
|
950
|
+
for socket in node_tree.interface.items_tree.values()
|
|
951
|
+
if socket.item_type == "PANEL"
|
|
952
|
+
]
|
|
953
|
+
|
|
954
|
+
placeholders = {}
|
|
955
|
+
id_to_node = {}
|
|
956
|
+
for input_node in input_nodes:
|
|
957
|
+
active_sockets = get_active_sockets(input_node.outputs)
|
|
958
|
+
for socket in active_sockets:
|
|
959
|
+
key = (node_tree.session_uid, input_node.name, socket.identifier)
|
|
960
|
+
if socket.identifier in id_to_node:
|
|
961
|
+
memo.links[key] = id_to_node[socket.identifier]
|
|
962
|
+
continue
|
|
963
|
+
|
|
964
|
+
varname = _name_from_socket_and_panels(socket, panels)
|
|
965
|
+
node = _placeholder_for_graph_input(socket, varname, node_tree)
|
|
966
|
+
|
|
967
|
+
if varname in placeholders:
|
|
968
|
+
raise ValueError(
|
|
969
|
+
f"Duplicate varname {varname} for {key=} in {input_node.outputs.keys()=} with existing {placeholders.keys()=}"
|
|
970
|
+
)
|
|
971
|
+
id_to_node[socket.identifier] = node
|
|
972
|
+
placeholders[varname] = node
|
|
973
|
+
memo.links[key] = node
|
|
974
|
+
|
|
975
|
+
# logger.debug(
|
|
976
|
+
# f"{_create_and_memoize_input_placeholders.__name__} {node_tree.name=} memoized {placeholders.keys()=}"
|
|
977
|
+
# )
|
|
978
|
+
|
|
979
|
+
return placeholders, id_to_node
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def parse_node_tree(
|
|
983
|
+
node_tree: bpy.types.NodeTree,
|
|
984
|
+
memo: ParseMemo,
|
|
985
|
+
) -> tuple[cg.ComputeGraph, dict[str, cg.Node]]:
|
|
986
|
+
"""
|
|
987
|
+
|
|
988
|
+
Note: recursive over nodes and node_trees which is not ideal. TODO convert to stack breadth-first
|
|
989
|
+
|
|
990
|
+
"""
|
|
991
|
+
|
|
992
|
+
assert node_tree.name != "Shader Nodetree", (
|
|
993
|
+
"nodetree name must be nondefault as we use it as a hash key"
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
memo_key = node_tree.session_uid
|
|
997
|
+
if res := memo.compute_graphs.get(memo_key):
|
|
998
|
+
return res
|
|
999
|
+
|
|
1000
|
+
cg_name = node_tree.name
|
|
1001
|
+
if cg_name.startswith("nodegroup_"):
|
|
1002
|
+
cg_name = cg_name.replace("nodegroup_", "")
|
|
1003
|
+
if cg_name.endswith(" (no gc)"): # comes from v1 to_nodegroup singleton=True
|
|
1004
|
+
cg_name = cg_name.replace(" (no gc)", "")
|
|
1005
|
+
cg_name = identifiers.bpy_name_to_pythonid(cg_name)
|
|
1006
|
+
|
|
1007
|
+
name_parts = cg_name.split("_")
|
|
1008
|
+
if name_parts[0].isdigit():
|
|
1009
|
+
cg_name = "_".join(name_parts[1:]) + "_" + name_parts[0]
|
|
1010
|
+
|
|
1011
|
+
if not identifiers.is_valid_snake_identifier(cg_name):
|
|
1012
|
+
raise ValueError(f"Invalid cg_name {cg_name} for {node_tree.name}")
|
|
1013
|
+
|
|
1014
|
+
input_nodes = _find_node_blidname(node_tree, "NodeGroupInput")
|
|
1015
|
+
output_node = _find_output_node(node_tree)
|
|
1016
|
+
inputs, id_to_node = _create_and_memoize_input_placeholders(
|
|
1017
|
+
node_tree, input_nodes, memo
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
outputs = {}
|
|
1021
|
+
for output_name, output_result_socket in output_node.inputs.items():
|
|
1022
|
+
if output_name == "":
|
|
1023
|
+
continue # nodegroups seem to have an empty socket with identifier __extend__, skip it
|
|
1024
|
+
if len(output_result_socket.links) == 0:
|
|
1025
|
+
continue
|
|
1026
|
+
if len(output_result_socket.links) > 1:
|
|
1027
|
+
raise ValueError(
|
|
1028
|
+
f"Node {node_tree.bl_idname} has multiple inputs for {output_name=} {output_result_socket.identifier=} "
|
|
1029
|
+
"Multi-output sockets not supported on nodegroup. please contact the developers."
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
# logger.debug(
|
|
1033
|
+
# f"Parsing link for {cg_name=} {output_name=} {output_result_socket.identifier=}"
|
|
1034
|
+
# )
|
|
1035
|
+
output_name = identifiers.bpy_name_to_pythonid(output_name)
|
|
1036
|
+
proc_node = parse_link(node_tree, output_result_socket.links[0], memo)
|
|
1037
|
+
if proc_node.metadata.get("known_value_type") is None:
|
|
1038
|
+
inferred = _infer_geometry_type(proc_node)
|
|
1039
|
+
if inferred is not None:
|
|
1040
|
+
vt = nt.ProcNode[inferred]
|
|
1041
|
+
logger.debug(
|
|
1042
|
+
f"Inferred known_value_type={vt} for {proc_node=} from function signature"
|
|
1043
|
+
)
|
|
1044
|
+
else:
|
|
1045
|
+
vt = _socket_to_pf_type(
|
|
1046
|
+
output_result_socket,
|
|
1047
|
+
is_output=True,
|
|
1048
|
+
)
|
|
1049
|
+
logger.debug(
|
|
1050
|
+
f"Setting known_value_type={vt} for {proc_node=} for {output_result_socket=}"
|
|
1051
|
+
)
|
|
1052
|
+
proc_node.metadata["known_value_type"] = vt
|
|
1053
|
+
if isinstance(proc_node, cg.InputPlaceholderNode):
|
|
1054
|
+
proc_node.default_value = None
|
|
1055
|
+
outputs[output_name] = proc_node
|
|
1056
|
+
|
|
1057
|
+
if len(outputs) > 1:
|
|
1058
|
+
assert " " not in cg_name, f"cg_name {cg_name} contains spaces"
|
|
1059
|
+
|
|
1060
|
+
# remove .001 suffixes
|
|
1061
|
+
typename = identifiers.snake_to_pascal(cg_name).rsplit(".", 1)[0] + "Result"
|
|
1062
|
+
output_type = namedtuple(typename, outputs.keys())
|
|
1063
|
+
output = output_type(**outputs)
|
|
1064
|
+
else:
|
|
1065
|
+
output = list(outputs.values())[0]
|
|
1066
|
+
|
|
1067
|
+
compute_graph = cg.ComputeGraph(
|
|
1068
|
+
inputs=pytree.PyTree(inputs),
|
|
1069
|
+
outputs=pytree.PyTree(output),
|
|
1070
|
+
name=cg_name,
|
|
1071
|
+
metadata={
|
|
1072
|
+
"is_node_function": True, # causes codegen to apply decorator
|
|
1073
|
+
},
|
|
1074
|
+
)
|
|
1075
|
+
logger.debug(
|
|
1076
|
+
f"Parsed node_tree {cg_name} with {len(inputs.keys())} inputs, {len(outputs.keys())} outputs "
|
|
1077
|
+
f"and {len(list(cg.traverse_depth_first(compute_graph)))} nodes"
|
|
1078
|
+
)
|
|
1079
|
+
|
|
1080
|
+
memo.compute_graphs[memo_key] = (compute_graph, id_to_node)
|
|
1081
|
+
return compute_graph, id_to_node
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def _parse_geomod_input(
|
|
1085
|
+
mod: bpy.types.Modifier,
|
|
1086
|
+
name: str,
|
|
1087
|
+
node_curr: cg.Node,
|
|
1088
|
+
memo: ParseMemo,
|
|
1089
|
+
) -> cg.Node | None:
|
|
1090
|
+
socket = mod.node_group.interface.items_tree[name]
|
|
1091
|
+
|
|
1092
|
+
if socket.socket_type == bni.SocketType.GEOMETRY.value:
|
|
1093
|
+
return node_curr
|
|
1094
|
+
|
|
1095
|
+
value = mod[socket.identifier]
|
|
1096
|
+
|
|
1097
|
+
if isinstance(value, bpy.types.Material):
|
|
1098
|
+
mat_graph = parse_material(value, memo)
|
|
1099
|
+
return cg.SubgraphCallNode(subgraph=mat_graph, args=(), kwargs={})
|
|
1100
|
+
if isinstance(value, bpy.types.Object):
|
|
1101
|
+
return t.MeshObject(value)
|
|
1102
|
+
if isinstance(value, bpy.types.Collection):
|
|
1103
|
+
return t.Collection(value)
|
|
1104
|
+
|
|
1105
|
+
datatype = bni.SOCKET_CLASS_TO_DATATYPE[socket.socket_type]
|
|
1106
|
+
dtype = bni.DATATYPE_TO_SOCKET_DTYPE[datatype].value
|
|
1107
|
+
return _repr_default_value(value, dtype)
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def parse_geo_modifier(
|
|
1111
|
+
obj: bpy.types.Object,
|
|
1112
|
+
node_curr: cg.Node,
|
|
1113
|
+
mod: bpy.types.Modifier,
|
|
1114
|
+
memo: ParseMemo,
|
|
1115
|
+
) -> cg.Node:
|
|
1116
|
+
# TODO: need to find and memoize the input geometries and input attribute assignments.
|
|
1117
|
+
|
|
1118
|
+
logger.info(f"Parsing geometry node modifier {mod.node_group.name}")
|
|
1119
|
+
|
|
1120
|
+
graph, id_to_node = parse_node_tree(mod.node_group, memo)
|
|
1121
|
+
|
|
1122
|
+
inputs = {}
|
|
1123
|
+
for name, soc in mod.node_group.interface.items_tree.items():
|
|
1124
|
+
if soc.in_out != "INPUT":
|
|
1125
|
+
continue
|
|
1126
|
+
if soc.identifier not in id_to_node:
|
|
1127
|
+
raise ValueError(
|
|
1128
|
+
f"Socket {soc.identifier=} {soc.name=} not found in {id_to_node.keys()}"
|
|
1129
|
+
)
|
|
1130
|
+
parsed_name = id_to_node[soc.identifier].metadata.get("varname", None)
|
|
1131
|
+
assert parsed_name is not None, id_to_node[soc.identifier]
|
|
1132
|
+
inputs[parsed_name] = _parse_geomod_input(mod, name, node_curr, memo)
|
|
1133
|
+
|
|
1134
|
+
node_curr = cg.SubgraphCallNode(subgraph=graph, args=(), kwargs=inputs)
|
|
1135
|
+
|
|
1136
|
+
geo_output_keys = [
|
|
1137
|
+
soc.name.lower()
|
|
1138
|
+
for soc in mod.node_group.interface.items_tree.values()
|
|
1139
|
+
if soc.in_out == "OUTPUT" and soc.socket_type == bni.SocketType.GEOMETRY.value
|
|
1140
|
+
]
|
|
1141
|
+
attribute_output_keys = [
|
|
1142
|
+
soc.name.lower()
|
|
1143
|
+
for soc in mod.node_group.interface.items_tree.values()
|
|
1144
|
+
if soc.in_out == "OUTPUT" and soc.socket_type != bni.SocketType.GEOMETRY.value
|
|
1145
|
+
]
|
|
1146
|
+
geo_output_getattrs = {
|
|
1147
|
+
k: cg.GetAttributeNode(source=node_curr, attribute_name=k)
|
|
1148
|
+
for k in geo_output_keys
|
|
1149
|
+
}
|
|
1150
|
+
attribute_output_getattrs = {
|
|
1151
|
+
k: cg.GetAttributeNode(source=node_curr, attribute_name=k)
|
|
1152
|
+
for k in attribute_output_keys
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
match len(geo_output_keys), len(attribute_output_keys):
|
|
1156
|
+
case 1, 0:
|
|
1157
|
+
return cg.FunctionCallNode(
|
|
1158
|
+
pf.nodes.to_mesh_object, args=(node_curr,), kwargs={}
|
|
1159
|
+
)
|
|
1160
|
+
case 1, _:
|
|
1161
|
+
return cg.FunctionCallNode(
|
|
1162
|
+
pf.nodes.to_mesh_object_with_attributes,
|
|
1163
|
+
kwargs={**geo_output_getattrs, **attribute_output_getattrs},
|
|
1164
|
+
)
|
|
1165
|
+
case _, _:
|
|
1166
|
+
return cg.FunctionCallNode(
|
|
1167
|
+
pf.nodes.to_objects_multi,
|
|
1168
|
+
args=(geo_output_getattrs, attribute_output_getattrs),
|
|
1169
|
+
)
|
|
1170
|
+
case _:
|
|
1171
|
+
raise ValueError(
|
|
1172
|
+
f"Expected 1 geo output and 0 or 1 attribute output, found {len(geo_output_keys)} and {len(attribute_output_keys)}"
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def parse_modifier(
|
|
1177
|
+
obj: bpy.types.Object,
|
|
1178
|
+
node_curr: cg.Node,
|
|
1179
|
+
mod: bpy.types.Modifier,
|
|
1180
|
+
memo: ParseMemo,
|
|
1181
|
+
) -> cg.Node:
|
|
1182
|
+
if mod.type == "NODES":
|
|
1183
|
+
return parse_geo_modifier(obj, node_curr, mod, memo)
|
|
1184
|
+
|
|
1185
|
+
mode_vals = {"type": mod.type, "operation": getattr(mod, "operation", None)}
|
|
1186
|
+
func_row = _find_manifest_func(
|
|
1187
|
+
"bpy.ops.object.modifier_add", mode_vals, _OPS_MANIFEST_INDEXED
|
|
1188
|
+
)
|
|
1189
|
+
if func_row is None:
|
|
1190
|
+
raise ValueError(f"Modifier {mod.type} {mode_vals=} not found in manifest")
|
|
1191
|
+
func_name = func_row["name"].replace("pf.", "procfunc.")
|
|
1192
|
+
func = manifest.import_item_iterative(func_name)
|
|
1193
|
+
|
|
1194
|
+
inputs = {
|
|
1195
|
+
k: getattr(mod, k)
|
|
1196
|
+
for k in inspect.signature(func).parameters.keys()
|
|
1197
|
+
if k != "mutates_obj" and hasattr(mod, k)
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
res = cg.FunctionCallNode(func=func, args=(node_curr,), kwargs=inputs)
|
|
1201
|
+
return cg.MutatedArgumentNode(original_node=node_curr, mutator_call_node=res)
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
def _replace_vector_inpnodes_as_arg(
|
|
1205
|
+
node_tree: bpy.types.NodeTree,
|
|
1206
|
+
memo: ParseMemo,
|
|
1207
|
+
) -> cg.Node:
|
|
1208
|
+
vector_input_nodes = _find_node_blidname(node_tree, "ShaderNodeTexCoord")
|
|
1209
|
+
vector_input_nodes += _find_node_blidname(node_tree, "ShaderNodeNewGeometry")
|
|
1210
|
+
|
|
1211
|
+
vector_links = [
|
|
1212
|
+
link
|
|
1213
|
+
for node in vector_input_nodes
|
|
1214
|
+
for output in node.outputs.values()
|
|
1215
|
+
for link in output.links
|
|
1216
|
+
]
|
|
1217
|
+
|
|
1218
|
+
vector_placeholder = cg.InputPlaceholderNode(
|
|
1219
|
+
name="vector",
|
|
1220
|
+
default_value=None,
|
|
1221
|
+
metadata={"known_value_type": pf.ProcNode[pf.Vector], "varname": "vector"},
|
|
1222
|
+
)
|
|
1223
|
+
|
|
1224
|
+
for node in vector_input_nodes:
|
|
1225
|
+
memo.nodes[(node_tree.session_uid, node.name)] = vector_placeholder
|
|
1226
|
+
|
|
1227
|
+
for link in vector_links:
|
|
1228
|
+
key = (node_tree.session_uid, link.from_node.name, link.from_socket.identifier)
|
|
1229
|
+
memo.links[key] = vector_placeholder
|
|
1230
|
+
|
|
1231
|
+
return vector_placeholder
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
_MATERIAL_OUTPUT_SOCKETS = ["Surface", "Displacement", "Volume"]
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def parse_material(
|
|
1238
|
+
mat: bpy.types.Material, memo: ParseMemo, coord_inp_as_arg: bool = False
|
|
1239
|
+
) -> cg.ComputeGraph:
|
|
1240
|
+
memo_key = (type(mat), mat.name)
|
|
1241
|
+
if mat_node := memo.assets.get(memo_key):
|
|
1242
|
+
return mat_node
|
|
1243
|
+
|
|
1244
|
+
node_tree = mat.node_tree
|
|
1245
|
+
|
|
1246
|
+
inputs_dict = {}
|
|
1247
|
+
|
|
1248
|
+
if coord_inp_as_arg:
|
|
1249
|
+
vector_placeholder = _replace_vector_inpnodes_as_arg(node_tree, memo)
|
|
1250
|
+
inputs_dict["vector"] = vector_placeholder
|
|
1251
|
+
|
|
1252
|
+
(output_node,) = _find_node_blidname(node_tree, "ShaderNodeOutputMaterial")
|
|
1253
|
+
|
|
1254
|
+
outputs_dict = {}
|
|
1255
|
+
for key in _MATERIAL_OUTPUT_SOCKETS:
|
|
1256
|
+
expect_type = pf.Vector if key == "Displacement" else pf.Shader
|
|
1257
|
+
if output_node.inputs[key].is_linked:
|
|
1258
|
+
res = parse_link(node_tree, output_node.inputs[key].links[0], memo)
|
|
1259
|
+
res.metadata["known_value_type"] = pf.ProcNode[expect_type]
|
|
1260
|
+
else:
|
|
1261
|
+
res = cg.ConstantNode(value=None)
|
|
1262
|
+
res.metadata["known_value_type"] = Union[pf.ProcNode[expect_type], None]
|
|
1263
|
+
outputs_dict[key.lower()] = res
|
|
1264
|
+
|
|
1265
|
+
func_name = identifiers.bpy_name_to_pythonid(mat.name)
|
|
1266
|
+
graph = cg.ComputeGraph(
|
|
1267
|
+
inputs=pytree.PyTree(inputs_dict),
|
|
1268
|
+
outputs=pytree.PyTree(t.Material(**outputs_dict)),
|
|
1269
|
+
name=func_name,
|
|
1270
|
+
metadata={}, # TODO
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
memo.assets[memo_key] = graph
|
|
1274
|
+
return graph
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def parse_primitive(obj: t.Object) -> cg.Node:
|
|
1278
|
+
return cg.FunctionCallNode(pf.ops.primitives.mesh_monkey, args=(), kwargs={})
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def parse_object(
|
|
1282
|
+
obj: bpy.types.Object,
|
|
1283
|
+
memo: ParseMemo,
|
|
1284
|
+
object_mode: Literal["monkey", "active", "named"] = "monkey",
|
|
1285
|
+
include_set_material: bool = True,
|
|
1286
|
+
) -> cg.ComputeGraph:
|
|
1287
|
+
memo_key = (type(obj), obj.name)
|
|
1288
|
+
if obj_node := memo.assets.get(memo_key):
|
|
1289
|
+
return obj_node
|
|
1290
|
+
|
|
1291
|
+
# TODO assert starting from single vertex?
|
|
1292
|
+
match object_mode:
|
|
1293
|
+
case "monkey":
|
|
1294
|
+
node_curr = cg.FunctionCallNode(
|
|
1295
|
+
pf.ops.primitives.mesh_monkey, args=(), kwargs={}
|
|
1296
|
+
)
|
|
1297
|
+
case "active":
|
|
1298
|
+
node_curr = cg.ConstantNode(
|
|
1299
|
+
value=cg.LiteralConstant("pf.MeshObject(bpy.context.active_object)")
|
|
1300
|
+
)
|
|
1301
|
+
case "named":
|
|
1302
|
+
node_curr = cg.ConstantNode(
|
|
1303
|
+
value=cg.LiteralConstant(
|
|
1304
|
+
f"pf.MeshObject(bpy.data.objects[{obj.name!r}])"
|
|
1305
|
+
)
|
|
1306
|
+
)
|
|
1307
|
+
case _:
|
|
1308
|
+
raise ValueError(f"Invalid object mode: {object_mode}")
|
|
1309
|
+
|
|
1310
|
+
coord = cg.FunctionCallNode(pf.nodes.shader.coord, args=(), kwargs={})
|
|
1311
|
+
coord = cg.GetAttributeNode(source=coord, attribute_name="generated")
|
|
1312
|
+
|
|
1313
|
+
if include_set_material:
|
|
1314
|
+
for mat in obj.material_slots:
|
|
1315
|
+
mat_graph = parse_material(mat.material, memo)
|
|
1316
|
+
mat_kwargs = (
|
|
1317
|
+
{"vector": coord} if "vector" in mat_graph.inputs.obj().keys() else {}
|
|
1318
|
+
)
|
|
1319
|
+
mat_call = cg.SubgraphCallNode(
|
|
1320
|
+
subgraph=mat_graph, args=(), kwargs=mat_kwargs
|
|
1321
|
+
)
|
|
1322
|
+
|
|
1323
|
+
node_curr = cg.FunctionCallNode(
|
|
1324
|
+
pf.ops.object.set_material,
|
|
1325
|
+
args=(node_curr,),
|
|
1326
|
+
kwargs={"material": mat_call},
|
|
1327
|
+
)
|
|
1328
|
+
|
|
1329
|
+
for mod in obj.modifiers:
|
|
1330
|
+
node_curr = parse_modifier(obj, node_curr, mod, memo)
|
|
1331
|
+
|
|
1332
|
+
name = "object_" + identifiers.bpy_name_to_pythonid(obj.name) + "_generate"
|
|
1333
|
+
graph = cg.ComputeGraph(
|
|
1334
|
+
inputs=pytree.PyTree({}),
|
|
1335
|
+
outputs=pytree.PyTree({"result": node_curr}),
|
|
1336
|
+
name=name,
|
|
1337
|
+
metadata={"func": parse_object, "object": obj.name},
|
|
1338
|
+
)
|
|
1339
|
+
|
|
1340
|
+
memo.assets[memo_key] = graph
|
|
1341
|
+
return graph
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def parse_scene(
|
|
1345
|
+
scene: bpy.types.Scene,
|
|
1346
|
+
memo: ParseMemo,
|
|
1347
|
+
) -> cg.Node:
|
|
1348
|
+
raise NotImplementedError("Scene not implemented")
|