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.
Files changed (76) hide show
  1. procfunc/__init__.py +87 -0
  2. procfunc/color.py +57 -0
  3. procfunc/compute_graph/__init__.py +28 -0
  4. procfunc/compute_graph/compute_graph.py +115 -0
  5. procfunc/compute_graph/node.py +200 -0
  6. procfunc/compute_graph/operators_info.py +92 -0
  7. procfunc/compute_graph/proxy.py +173 -0
  8. procfunc/compute_graph/util.py +282 -0
  9. procfunc/context.py +115 -0
  10. procfunc/control.py +174 -0
  11. procfunc/nodes/__init__.py +66 -0
  12. procfunc/nodes/bindings_util.py +196 -0
  13. procfunc/nodes/bpy_node_info.py +280 -0
  14. procfunc/nodes/compositor.py +2242 -0
  15. procfunc/nodes/execute/construct_nodes.py +571 -0
  16. procfunc/nodes/execute/construct_special_cases.py +246 -0
  17. procfunc/nodes/execute/execute.py +548 -0
  18. procfunc/nodes/execute/infer_runtime_data_type.py +195 -0
  19. procfunc/nodes/execute/util.py +247 -0
  20. procfunc/nodes/func.py +1417 -0
  21. procfunc/nodes/geo.py +4240 -0
  22. procfunc/nodes/manifest.json +8769 -0
  23. procfunc/nodes/math.py +644 -0
  24. procfunc/nodes/node_function.py +160 -0
  25. procfunc/nodes/shader.py +2359 -0
  26. procfunc/nodes/types.py +347 -0
  27. procfunc/ops/__init__.py +35 -0
  28. procfunc/ops/_util.py +275 -0
  29. procfunc/ops/addons.py +59 -0
  30. procfunc/ops/attr.py +426 -0
  31. procfunc/ops/collection.py +90 -0
  32. procfunc/ops/curve.py +18 -0
  33. procfunc/ops/file.py +126 -0
  34. procfunc/ops/manifest.json +39149 -0
  35. procfunc/ops/mesh.py +1510 -0
  36. procfunc/ops/modifier.py +603 -0
  37. procfunc/ops/object.py +258 -0
  38. procfunc/ops/primitives/__init__.py +31 -0
  39. procfunc/ops/primitives/camera.py +45 -0
  40. procfunc/ops/primitives/curve.py +71 -0
  41. procfunc/ops/primitives/light.py +114 -0
  42. procfunc/ops/primitives/mesh.py +358 -0
  43. procfunc/ops/uv.py +271 -0
  44. procfunc/random.py +247 -0
  45. procfunc/tracer/__init__.py +43 -0
  46. procfunc/tracer/decorator.py +121 -0
  47. procfunc/tracer/patch.py +494 -0
  48. procfunc/tracer/proxy.py +127 -0
  49. procfunc/tracer/trace.py +222 -0
  50. procfunc/transforms/__init__.py +49 -0
  51. procfunc/transforms/cleanup.py +214 -0
  52. procfunc/transforms/convert.py +20 -0
  53. procfunc/transforms/distribution.py +191 -0
  54. procfunc/transforms/extract_materials.py +116 -0
  55. procfunc/transforms/infer_distribution.py +326 -0
  56. procfunc/transforms/parameters.py +15 -0
  57. procfunc/transforms/util.py +35 -0
  58. procfunc/transpiler/__init__.py +24 -0
  59. procfunc/transpiler/bpy_to_computegraph.py +1348 -0
  60. procfunc/transpiler/codegen.py +919 -0
  61. procfunc/transpiler/identifiers.py +595 -0
  62. procfunc/transpiler/main.py +299 -0
  63. procfunc/types.py +380 -0
  64. procfunc/util/__init__.py +0 -0
  65. procfunc/util/bpy_info.py +145 -0
  66. procfunc/util/camera.py +0 -0
  67. procfunc/util/keyframe.py +70 -0
  68. procfunc/util/log.py +96 -0
  69. procfunc/util/manifest.py +121 -0
  70. procfunc/util/pytree.py +343 -0
  71. procfunc/util/teardown.py +37 -0
  72. procfunc-0.30.0.dist-info/METADATA +120 -0
  73. procfunc-0.30.0.dist-info/RECORD +76 -0
  74. procfunc-0.30.0.dist-info/WHEEL +5 -0
  75. procfunc-0.30.0.dist-info/licenses/LICENSE.md +11 -0
  76. 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")