procfunc 0.34.0__tar.gz → 0.35.1__tar.gz

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 (110) hide show
  1. {procfunc-0.34.0/src/procfunc.egg-info → procfunc-0.35.1}/PKG-INFO +7 -4
  2. {procfunc-0.34.0 → procfunc-0.35.1}/pyproject.toml +7 -3
  3. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/__init__.py +1 -1
  4. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/codegen/codegen.py +77 -14
  5. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/codegen/repr.py +16 -15
  6. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/compute_graph/proxy.py +4 -0
  7. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/context.py +9 -0
  8. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/execute/execute.py +1 -1
  9. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/execute/realize.py +31 -12
  10. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/types.py +10 -3
  11. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/util/node_function.py +6 -0
  12. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/primitives/__init__.py +1 -0
  13. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/random.py +12 -0
  14. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/tracer/trace.py +16 -0
  15. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/transpiler/bpy_to_computegraph.py +45 -6
  16. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/transpiler/main.py +5 -1
  17. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/types.py +22 -7
  18. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/util/manifest.py +2 -1
  19. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/util/teardown.py +15 -1
  20. {procfunc-0.34.0 → procfunc-0.35.1/src/procfunc.egg-info}/PKG-INFO +7 -4
  21. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc.egg-info/SOURCES.txt +1 -0
  22. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc.egg-info/requires.txt +7 -3
  23. procfunc-0.35.1/tests/test_codegen.py +89 -0
  24. procfunc-0.35.1/tests/test_random.py +73 -0
  25. procfunc-0.35.1/tests/test_teardown.py +51 -0
  26. procfunc-0.34.0/tests/test_codegen.py +0 -18
  27. procfunc-0.34.0/tests/test_random.py +0 -29
  28. {procfunc-0.34.0 → procfunc-0.35.1}/LICENSE.md +0 -0
  29. {procfunc-0.34.0 → procfunc-0.35.1}/README.md +0 -0
  30. {procfunc-0.34.0 → procfunc-0.35.1}/setup.cfg +0 -0
  31. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/cli/__init__.py +0 -0
  32. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/cli/main.py +0 -0
  33. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/codegen/__init__.py +0 -0
  34. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/codegen/identifiers.py +0 -0
  35. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/color.py +0 -0
  36. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/compute_graph/__init__.py +0 -0
  37. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/compute_graph/compute_graph.py +0 -0
  38. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/compute_graph/node.py +0 -0
  39. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/compute_graph/operators_info.py +0 -0
  40. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/compute_graph/util.py +0 -0
  41. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/control/__init__.py +0 -0
  42. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/__init__.py +0 -0
  43. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/color.py +0 -0
  44. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/compositor.py +0 -0
  45. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/execute/construct_nodes.py +0 -0
  46. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/execute/construct_operator.py +0 -0
  47. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/execute/construct_special_cases.py +0 -0
  48. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/execute/construct_standard.py +0 -0
  49. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/execute/infer_runtime_data_type.py +0 -0
  50. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/execute/util.py +0 -0
  51. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/func.py +0 -0
  52. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/geo.py +0 -0
  53. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/manifest.json +0 -0
  54. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/math.py +0 -0
  55. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/shader.py +0 -0
  56. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/texture.py +0 -0
  57. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/util/__init__.py +0 -0
  58. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/util/bindings_util.py +0 -0
  59. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/nodes/util/bpy_node_info.py +0 -0
  60. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/__init__.py +0 -0
  61. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/_util.py +0 -0
  62. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/addons.py +0 -0
  63. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/attr.py +0 -0
  64. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/collection.py +0 -0
  65. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/curve.py +0 -0
  66. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/file.py +0 -0
  67. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/manifest.json +0 -0
  68. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/mesh.py +0 -0
  69. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/modifier.py +0 -0
  70. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/object.py +0 -0
  71. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/primitives/camera.py +0 -0
  72. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/primitives/curve.py +0 -0
  73. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/primitives/light.py +0 -0
  74. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/primitives/mesh.py +0 -0
  75. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/ops/uv.py +0 -0
  76. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/tracer/__init__.py +0 -0
  77. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/tracer/decorator.py +0 -0
  78. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/tracer/patch.py +0 -0
  79. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/tracer/proxy.py +0 -0
  80. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/transforms/__init__.py +0 -0
  81. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/transforms/cleanup.py +0 -0
  82. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/transforms/convert.py +0 -0
  83. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/transforms/distribution.py +0 -0
  84. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/transforms/extract_materials.py +0 -0
  85. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/transforms/infer_distribution.py +0 -0
  86. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/transforms/parameters.py +0 -0
  87. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/transforms/util.py +0 -0
  88. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/transpiler/__init__.py +0 -0
  89. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/transpiler/parse_default_values.py +0 -0
  90. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/transpiler/parse_special_cases.py +0 -0
  91. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/util/__init__.py +0 -0
  92. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/util/bpy_data.py +0 -0
  93. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/util/bpy_info.py +0 -0
  94. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/util/camera.py +0 -0
  95. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/util/keyframe.py +0 -0
  96. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/util/log.py +0 -0
  97. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc/util/pytree.py +0 -0
  98. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc.egg-info/dependency_links.txt +0 -0
  99. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc.egg-info/entry_points.txt +0 -0
  100. {procfunc-0.34.0 → procfunc-0.35.1}/src/procfunc.egg-info/top_level.txt +0 -0
  101. {procfunc-0.34.0 → procfunc-0.35.1}/tests/test_asset.py +0 -0
  102. {procfunc-0.34.0 → procfunc-0.35.1}/tests/test_bpy_data_cleanup.py +0 -0
  103. {procfunc-0.34.0 → procfunc-0.35.1}/tests/test_cli_transpile.py +0 -0
  104. {procfunc-0.34.0 → procfunc-0.35.1}/tests/test_codegen_matrix.py +0 -0
  105. {procfunc-0.34.0 → procfunc-0.35.1}/tests/test_compute_graph.py +0 -0
  106. {procfunc-0.34.0 → procfunc-0.35.1}/tests/test_node_function.py +0 -0
  107. {procfunc-0.34.0 → procfunc-0.35.1}/tests/test_ops.py +0 -0
  108. {procfunc-0.34.0 → procfunc-0.35.1}/tests/test_pytree.py +0 -0
  109. {procfunc-0.34.0 → procfunc-0.35.1}/tests/test_trace.py +0 -0
  110. {procfunc-0.34.0 → procfunc-0.35.1}/tests/test_transforms.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: procfunc
3
- Version: 0.34.0
3
+ Version: 0.35.1
4
4
  Summary: Function-Oriented Abstractions for Procedural 3D Generation in Python
5
5
  License-Expression: BSD-3-Clause
6
6
  Project-URL: Homepage, https://github.com/princeton-vl/procfunc
@@ -9,15 +9,18 @@ Requires-Python: <3.12,>=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE.md
11
11
  Requires-Dist: bpy==4.2.0
12
- Requires-Dist: numpy<2
13
- Requires-Dist: pandas
12
+ Requires-Dist: numpy<2,>=1.23.5
13
+ Requires-Dist: pandas>=1.5
14
+ Provides-Extra: test
15
+ Requires-Dist: pytest<10,>=7.2; extra == "test"
14
16
  Provides-Extra: dev
15
- Requires-Dist: pytest; extra == "dev"
17
+ Requires-Dist: pytest<10,>=7.2; extra == "dev"
16
18
  Requires-Dist: pytest-xdist; extra == "dev"
17
19
  Requires-Dist: ruff<0.16,>=0.15; extra == "dev"
18
20
  Requires-Dist: ty; extra == "dev"
19
21
  Requires-Dist: imageio; extra == "dev"
20
22
  Requires-Dist: matplotlib; extra == "dev"
23
+ Requires-Dist: coverage; extra == "dev"
21
24
  Provides-Extra: docs
22
25
  Requires-Dist: sphinx; extra == "docs"
23
26
  Requires-Dist: sphinx-rtd-theme; extra == "docs"
@@ -11,8 +11,8 @@ license = "BSD-3-Clause"
11
11
  requires-python = ">=3.11,<3.12"
12
12
  dependencies = [
13
13
  "bpy==4.2.0",
14
- "numpy<2",
15
- "pandas",
14
+ "numpy>=1.23.5,<2",
15
+ "pandas>=1.5",
16
16
  ]
17
17
 
18
18
  [project.urls]
@@ -23,13 +23,17 @@ Repository = "https://github.com/princeton-vl/procfunc"
23
23
  procfunc = "procfunc.cli:cli"
24
24
 
25
25
  [project.optional-dependencies]
26
+ test = [
27
+ "pytest>=7.2,<10",
28
+ ]
26
29
  dev = [
27
- "pytest",
30
+ "pytest>=7.2,<10",
28
31
  "pytest-xdist",
29
32
  "ruff>=0.15,<0.16",
30
33
  "ty",
31
34
  "imageio",
32
35
  "matplotlib",
36
+ "coverage",
33
37
  ]
34
38
  docs = [
35
39
  "sphinx",
@@ -2,7 +2,7 @@
2
2
  # ensure this gets imported first so that mathutils etc is available even if later modules dont import bpy
3
3
  import bpy
4
4
 
5
- __version__ = "0.34.0"
5
+ __version__ = "0.35.1"
6
6
 
7
7
  from numpy.random import Generator as RNG
8
8
 
@@ -1,3 +1,4 @@
1
+ import ast
1
2
  import dataclasses
2
3
  import enum
3
4
  import inspect
@@ -5,7 +6,7 @@ import itertools
5
6
  import logging
6
7
  from collections import OrderedDict, defaultdict
7
8
  from pathlib import Path
8
- from typing import Any, Callable, Generator
9
+ from typing import Any, Callable, Generator, get_type_hints
9
10
 
10
11
  import numpy as np
11
12
 
@@ -24,6 +25,30 @@ logger = logging.getLogger(__name__)
24
25
 
25
26
  INDENT = " "
26
27
 
28
+ # UnaryOp included since ** binds tighter than unary minus: -2.0 ** x == -(2.0 ** x)
29
+ _PAREN_SENSITIVE_NODES = (ast.BinOp, ast.UnaryOp, ast.Compare, ast.BoolOp, ast.IfExp)
30
+
31
+
32
+ def _has_enclosing_parens(expr: str) -> bool:
33
+ if not (expr.startswith("(") and expr.endswith(")")):
34
+ return False
35
+ depth = 0
36
+ for i, char in enumerate(expr):
37
+ depth += (char == "(") - (char == ")")
38
+ if depth == 0:
39
+ return i == len(expr) - 1
40
+ return False
41
+
42
+
43
+ def _needs_parens(expr: str) -> bool:
44
+ if _has_enclosing_parens(expr):
45
+ return False
46
+ try:
47
+ parsed = ast.parse(expr, mode="eval")
48
+ except SyntaxError:
49
+ return " " in expr
50
+ return isinstance(parsed.body, _PAREN_SENSITIVE_NODES)
51
+
27
52
 
28
53
  def indent_lines(lines: list[str], indent: str = INDENT) -> list[str]:
29
54
  return [indent + line for line in lines]
@@ -52,7 +77,7 @@ def _repr_inp(
52
77
  expr = expr[0]
53
78
  assert isinstance(expr, str)
54
79
 
55
- if " " in expr and extra_parens and expr[0] != "(" and expr[-1] != ")":
80
+ if extra_parens and _needs_parens(expr):
56
81
  return f"({expr})"
57
82
  else:
58
83
  return expr
@@ -146,7 +171,8 @@ def _repr_function_call(
146
171
  if not isinstance(target, cg.Node):
147
172
  raise ValueError(f"Method call {node=} has non-node target {target=}")
148
173
  func = None
149
- func_str = f"{_repr_inp(target, scope_expressions)}.{method_name}"
174
+ target_expr = _repr_inp(target, scope_expressions, extra_parens=True)
175
+ func_str = f"{target_expr}.{method_name}"
150
176
  case cg.SubgraphCallNode(subgraph=subgraph):
151
177
  func = None
152
178
  func_str = scope_expressions.get(id(subgraph))
@@ -176,6 +202,22 @@ def _repr_function_call(
176
202
  return [f"{func_str}({', '.join(arg_reprs)})"]
177
203
 
178
204
 
205
+ # ==/!= only match Blender Compare for exact dtypes; float/vector stay epsilon-tolerant.
206
+ _EPSILON_EQUALITY_TEMPLATES = frozenset(
207
+ OPERATOR_TEMPLATES[op] for op in (OperatorType.EQUAL, OperatorType.NOT_EQUAL)
208
+ )
209
+
210
+
211
+ def _is_exact_compare_operand(value: Any) -> bool:
212
+ if isinstance(value, (cg.Node, cg.Proxy)):
213
+ return False
214
+ if isinstance(value, str):
215
+ return True
216
+ if isinstance(value, bool):
217
+ return True
218
+ return isinstance(value, int) and not isinstance(value, float)
219
+
220
+
179
221
  def _operator_call_operands(
180
222
  node: cg.FunctionCallNode,
181
223
  template: str,
@@ -196,13 +238,21 @@ def _operator_call_operands(
196
238
  if any(name not in bound.arguments for name in operand_names):
197
239
  return None
198
240
 
241
+ operands = [bound.arguments[name] for name in operand_names]
242
+
243
+ # a default epsilon stays tolerant unlike exact ==/!=, so require exact operands
244
+ if template in _EPSILON_EQUALITY_TEMPLATES and not all(
245
+ _is_exact_compare_operand(v) for v in operands
246
+ ):
247
+ return None
248
+
199
249
  for name, value in bound.arguments.items():
200
250
  if name in operand_names:
201
251
  continue
202
252
  if not _kwarg_matches_default(sig, name, value):
203
253
  return None
204
254
 
205
- return [bound.arguments[name] for name in operand_names]
255
+ return operands
206
256
 
207
257
 
208
258
  def _repr_operator_call(
@@ -244,7 +294,7 @@ def _codegen_for_node(
244
294
  else:
245
295
  return _repr_function_call(node, scope_expressions)
246
296
  case cg.MethodCallNode() if node.method_name == "__getitem__":
247
- callee_expr = _repr_inp(node.args[0], scope_expressions)
297
+ callee_expr = _repr_inp(node.args[0], scope_expressions, extra_parens=True)
248
298
  idx_expr = _repr_inp(node.args[1], scope_expressions)
249
299
  return [f"{callee_expr}[{idx_expr}]"]
250
300
  case cg.MethodCallNode():
@@ -280,7 +330,7 @@ def _codegen_graph_inputs(
280
330
  ) -> list[str]:
281
331
  args = sorted(
282
332
  list(graph.inputs.values()),
283
- key=lambda x: x.kwargs.get("default_value", None) is not None,
333
+ key=lambda x: "default_value" in x.kwargs,
284
334
  )
285
335
 
286
336
  func_name = func_name or graph.name
@@ -305,8 +355,8 @@ def _codegen_graph_inputs(
305
355
  else f"{name}"
306
356
  )
307
357
 
308
- if (default := node.kwargs.get("default_value")) is not None:
309
- line += f" = {repr_value(default)}"
358
+ if "default_value" in node.kwargs:
359
+ line += f" = {repr_value(node.kwargs['default_value'])}"
310
360
 
311
361
  args_lines.append(line + ",")
312
362
 
@@ -315,18 +365,31 @@ def _codegen_graph_inputs(
315
365
  return [f"def {func_name}("] + indent_lines(args_lines) + [end_statement]
316
366
 
317
367
 
368
+ def _namedtuple_field_type(node, annotation) -> str:
369
+ vt = node.metadata.get("known_value_type", None)
370
+ if vt is None:
371
+ vt = annotation
372
+ if vt is None:
373
+ return "Any"
374
+ try:
375
+ return repr_type(vt)
376
+ except Exception:
377
+ return "Any"
378
+
379
+
318
380
  def _codegen_namedtuple_def(outputs: pytree.PyTree):
319
381
  tupletype = outputs.toplevel_type()
382
+ try:
383
+ annotations = get_type_hints(tupletype)
384
+ except Exception:
385
+ annotations = getattr(tupletype, "__annotations__", {})
320
386
 
321
387
  type_lines = []
322
388
  for name, node in outputs.items():
323
389
  if node is None:
324
390
  continue
325
- vt = node.metadata.get("known_value_type", None)
326
- if vt is None:
327
- type_lines.append(f"{name}: Any")
328
- else:
329
- type_lines.append(f"{name}: {repr_type(vt)}")
391
+ field_type = _namedtuple_field_type(node, annotations.get(name))
392
+ type_lines.append(f"{name}: {field_type}")
330
393
 
331
394
  return [f"class {tupletype.__name__}(NamedTuple):"] + indent_lines(type_lines)
332
395
 
@@ -840,7 +903,7 @@ def to_python(
840
903
  add_line_comments: bool = False,
841
904
  ) -> str:
842
905
  code_lines = []
843
- code_lines.append("from typing import NamedTuple, Annotated")
906
+ code_lines.append("from typing import Any, NamedTuple, Annotated")
844
907
  code_lines.append("import numpy as np")
845
908
  code_lines.append("import bpy")
846
909
  # code_lines.append("import logging; logging.basicConfig(level=logging.DEBUG)")
@@ -2,6 +2,7 @@ import dataclasses
2
2
  import enum
3
3
  import logging
4
4
  import math
5
+ import types
5
6
  from pathlib import Path
6
7
  from typing import Any, Union, get_args, get_origin
7
8
 
@@ -20,12 +21,22 @@ def repr_type(x: Any) -> str:
20
21
  if isinstance(x, str):
21
22
  return x
22
23
 
23
- if x.__name__ == "NoneType":
24
- return "None"
25
-
26
24
  origin = get_origin(x)
27
25
  args = get_args(x)
28
26
 
27
+ if origin is Union or origin is types.UnionType:
28
+ first_args = get_args(args[0])
29
+ if (
30
+ get_origin(args[0]) is nt.ProcNode
31
+ and first_args
32
+ and first_args[0] is args[1]
33
+ ):
34
+ return f"t.SocketOrVal[{repr_type(first_args[0])}]"
35
+ return " | ".join([repr_type(a) for a in args])
36
+
37
+ if x.__name__ == "NoneType":
38
+ return "None"
39
+
29
40
  if x.__name__ == "ProcNode":
30
41
  if len(args) == 1:
31
42
  return f"pf.ProcNode[{repr_type(args[0])}]"
@@ -42,16 +53,6 @@ def repr_type(x: Any) -> str:
42
53
  if x.__module__ == "builtins":
43
54
  return x.__name__
44
55
 
45
- origin = get_origin(x)
46
- args = get_args(x)
47
-
48
- if origin is Union:
49
- args_0 = get_args(args[0])
50
- if get_origin(args[0]) is nt.ProcNode and args_0[0] is args[1]:
51
- return f"t.SocketOrVal[{repr_type(args_0[0])}]"
52
- else:
53
- return " | ".join([repr_type(a) for a in args])
54
-
55
56
  if getattr(x, "__module__", None) == "procfunc.nodes.types":
56
57
  return f"t.{x.__name__}"
57
58
 
@@ -70,12 +71,12 @@ def repr_value(value: Any) -> str:
70
71
  value = value.__wrapped__
71
72
 
72
73
  if isinstance(value, cg.Proxy):
73
- logger.warning(
74
+ raise ValueError(
74
75
  f"Proxy object {value} should never appear as a raw value in codegen - "
75
76
  f"its underlying node {value.node} was not resolved to a variable"
76
77
  )
77
78
  if isinstance(value, nt.ProcNode):
78
- logger.warning(
79
+ raise ValueError(
79
80
  f"Procnode object {value} should never be treated as a raw value in codegen"
80
81
  )
81
82
 
@@ -15,6 +15,10 @@ class Proxy(Generic[T]):
15
15
 
16
16
  node: Node
17
17
 
18
+ # Tell numpy to defer (ndarray + Proxy -> Proxy.__radd__) instead of probing the
19
+ # Proxy as an array, which would fabricate a bogus __array_struct__ via __getattr__.
20
+ __array_ufunc__ = None
21
+
18
22
  def __repr__(self):
19
23
  return f"Proxy({self.node!r})"
20
24
 
@@ -19,6 +19,13 @@ class ProcfuncContext:
19
19
  'ignore' silently returns an empty mesh, 'warn' logs a warning, 'throw' raises an error.
20
20
  """
21
21
 
22
+ record_node_definitions: bool = False
23
+ """
24
+ Record the user-space file/line/function that constructed each ProcNode, used only to
25
+ enrich node-instantiation error messages. Walks the stack once per node, which can
26
+ dominate build time in node-heavy callers, so off by default.
27
+ """
28
+
22
29
  def __post_init__(self):
23
30
  """Initialize computed fields after dataclass creation."""
24
31
  if self.num_cpu_cores <= 0:
@@ -44,6 +51,8 @@ globals = ProcfuncContext(
44
51
  num_cpu_cores=int(os.environ.get("PROCFUNC_NUM_CPU_CORES", 0)),
45
52
  warn_mode_empty_geonodes=_warn_mode_empty_geonodes, # type: ignore[invalid-assignment]
46
53
  current_trace_level=None,
54
+ record_node_definitions=os.environ.get("PROCFUNC_RECORD_NODE_DEFINITIONS", "0")
55
+ == "1",
47
56
  )
48
57
 
49
58
 
@@ -67,7 +67,7 @@ def to_environment(
67
67
  return pt.World(world)
68
68
 
69
69
 
70
- @tracer_primitive
70
+ @tracer_primitive(mutates=["light"])
71
71
  def to_light(
72
72
  light: pt.LightObject,
73
73
  surface: nt.ProcNode[nt.Shader],
@@ -5,13 +5,13 @@ import bpy
5
5
  from procfunc import compute_graph as cg
6
6
  from procfunc import types as pt
7
7
  from procfunc.nodes import types as nt
8
- from procfunc.nodes.util.bpy_node_info import NodeGroupType, SocketType
8
+ from procfunc.nodes.util.bpy_node_info import SocketType
9
9
  from procfunc.ops._util import modify
10
10
  from procfunc.ops.primitives.mesh import mesh_single_vertex
11
- from procfunc.util import pytree
12
11
  from procfunc.util.bpy_info import bpy_nocollide_data_name
13
12
 
14
- from .construct_nodes import as_nodegroup, instantiate_nodegroup
13
+ from . import construct_standard
14
+ from .construct_nodes import construct_procnode_to_bpy, instantiate_nodegroup
15
15
 
16
16
  logger = logging.getLogger(__name__)
17
17
 
@@ -81,11 +81,38 @@ def nodegroup_to_output(
81
81
  return output_node
82
82
 
83
83
 
84
+ def construct_outputs_to_output_node(
85
+ node_tree: bpy.types.NodeTree,
86
+ outputs: dict[str, cg.Node],
87
+ output_node_type: str,
88
+ ) -> bpy.types.Node:
89
+ output_node = node_tree.nodes.new(output_node_type)
90
+ cache = {}
91
+ for key, node in outputs.items():
92
+ res = construct_procnode_to_bpy(node, node_tree, cache)
93
+ if isinstance(res, bpy.types.Node):
94
+ res = construct_standard._get_primary_output_socket(node, res)
95
+ construct_standard.connect_single_input(
96
+ node_tree, output_node.inputs[key.capitalize()], res
97
+ )
98
+ return output_node
99
+
100
+
84
101
  def build_bpy_material(
85
102
  surface: nt.ProcNode[nt.Shader] | None = None,
86
103
  displacement: nt.ProcNode[pt.Vector] | None = None,
87
104
  volume: nt.ProcNode[nt.Shader] | None = None,
88
105
  ) -> bpy.types.Material:
106
+ for key, val in {
107
+ "surface": surface,
108
+ "displacement": displacement,
109
+ "volume": volume,
110
+ }.items():
111
+ if val is not None and not isinstance(val, (nt.ProcNode, cg.Node)):
112
+ raise TypeError(
113
+ f"Material {key} must be a ProcNode or None, got {type(val)}: {val!r}"
114
+ )
115
+
89
116
  # optimization: a constant zero displacement has no effect, so drop it and
90
117
  # leave the output socket disconnected rather than emitting a dead subgraph
91
118
  if displacement is not None and pt.is_zero_displacement(displacement):
@@ -107,15 +134,7 @@ def build_bpy_material(
107
134
  mnt = material.node_tree
108
135
  mnt.nodes.clear()
109
136
 
110
- outputs = pytree.PyTree(outputs)
111
- graph = cg.ComputeGraph(
112
- inputs=pytree.PyTree({}),
113
- outputs=outputs,
114
- name="to_material",
115
- metadata={},
116
- )
117
- body = as_nodegroup(graph, NodeGroupType.SHADER)
118
- nodegroup_to_output(mnt, body, "ShaderNodeOutputMaterial", list(outputs.names()))
137
+ construct_outputs_to_output_node(mnt, outputs, "ShaderNodeOutputMaterial")
119
138
 
120
139
  return material
121
140
 
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
  from typing import Any, Generic, TypeVar, Union
6
6
 
7
7
  from procfunc import compute_graph as cg
8
+ from procfunc import context
8
9
  from procfunc import types as pt
9
10
  from procfunc.compute_graph.operators_info import (
10
11
  OPERATORS_TO_FUNCTIONS,
@@ -79,7 +80,8 @@ class ProcNode(Generic[T]):
79
80
  f"these should have been unwrapped to cg.Node {node.args} {node.kwargs}"
80
81
  )
81
82
 
82
- self._node.metadata["definition"] = _node_definition_metadata()
83
+ if context.globals.record_node_definitions:
84
+ self._node.metadata["definition"] = _node_definition_metadata()
83
85
 
84
86
  def astype(self, dtype: type) -> "ProcNode":
85
87
  node = copy.copy(self._node)
@@ -114,7 +116,8 @@ class ProcNode(Generic[T]):
114
116
  )
115
117
 
116
118
  node = cg.ProceduralNode(node_type=node_type, attrs=attrs, kwargs=inputs)
117
- node.metadata["definition"] = _node_definition_metadata()
119
+ if context.globals.record_node_definitions:
120
+ node.metadata["definition"] = _node_definition_metadata()
118
121
 
119
122
  return cls(node=node)
120
123
 
@@ -262,7 +265,11 @@ def node_definition_context_message(node: cg.Node):
262
265
  metadata = object.__getattribute__(node, "metadata")
263
266
  lineno_metadata = metadata.get("definition", None)
264
267
  if lineno_metadata is None:
265
- return ""
268
+ return (
269
+ " (set PROCFUNC_RECORD_NODE_DEFINITIONS=1 or "
270
+ "override_globals(record_node_definitions=True) to see the "
271
+ "file/line that defined this node) "
272
+ )
266
273
  file, lineno, procfunc_name = lineno_metadata
267
274
  return f" {procfunc_name}() call on {file}:{lineno} "
268
275
 
@@ -144,6 +144,12 @@ def _subgraph_call_procnode(func: Callable, subgraph: cg.ComputeGraph, *args, **
144
144
  def node_function(func: Callable):
145
145
  @functools.wraps(func)
146
146
  def node_function_wrapper(*args, **kwargs):
147
+ active = pf.context.globals.current_trace_level
148
+ if active is not None and active >= TraceLevel.NODEGROUPS.value:
149
+ from procfunc.tracer.trace import capture_as_function_call
150
+
151
+ return capture_as_function_call(node_function_wrapper, args, kwargs)
152
+
147
153
  subgraph = _execute_procnode_func_to_computegraph(func)
148
154
  subgraph.metadata["operations"] = [
149
155
  (node_function, {"func": func}),
@@ -20,6 +20,7 @@ from .mesh import (
20
20
  mesh_cone,
21
21
  mesh_cube,
22
22
  mesh_cylinder,
23
+ mesh_from_numpy,
23
24
  mesh_grid,
24
25
  mesh_icosphere,
25
26
  mesh_line,
@@ -75,6 +75,7 @@ def clip_gaussian(
75
75
  )
76
76
 
77
77
 
78
+ @pf.tracer.primitive(allow_exec=True)
78
79
  def wrap_gaussian(
79
80
  rng: np.random.Generator,
80
81
  mean: float,
@@ -92,6 +93,7 @@ def wrap_gaussian(
92
93
  return x
93
94
 
94
95
 
96
+ @pf.tracer.primitive(allow_exec=True)
95
97
  def exponential(
96
98
  rng: np.random.Generator,
97
99
  scale: float,
@@ -99,6 +101,7 @@ def exponential(
99
101
  return rng.exponential(scale)
100
102
 
101
103
 
104
+ @pf.tracer.primitive(allow_exec=True)
102
105
  def log_uniform(
103
106
  rng: np.random.Generator,
104
107
  low: float,
@@ -108,6 +111,7 @@ def log_uniform(
108
111
  return np.exp(rng.uniform(np.log(low), np.log(high), size=size))
109
112
 
110
113
 
114
+ @pf.tracer.primitive(allow_exec=True)
111
115
  def log_normal(
112
116
  rng: np.random.Generator,
113
117
  mean: float,
@@ -117,6 +121,7 @@ def log_normal(
117
121
  return np.exp(rng.normal(np.log(mean), std, size=size))
118
122
 
119
123
 
124
+ @pf.tracer.primitive(allow_exec=True)
120
125
  def spherical_sample(
121
126
  rng: np.random.Generator,
122
127
  min_elevation: float | None = None,
@@ -137,6 +142,7 @@ def spherical_sample(
137
142
  )
138
143
 
139
144
 
145
+ @pf.tracer.primitive(allow_exec=True)
140
146
  def mixture_of_gaussian(
141
147
  rng: np.random.Generator,
142
148
  means: np.ndarray,
@@ -161,6 +167,7 @@ def mixture_of_gaussian(
161
167
  return res
162
168
 
163
169
 
170
+ @pf.tracer.primitive(allow_exec=True)
164
171
  def beta(
165
172
  rng: np.random.Generator,
166
173
  a: float,
@@ -169,6 +176,7 @@ def beta(
169
176
  return rng.beta(a, b)
170
177
 
171
178
 
179
+ @pf.tracer.primitive(allow_exec=True)
172
180
  def poisson(
173
181
  rng: np.random.Generator,
174
182
  lam: float,
@@ -176,6 +184,7 @@ def poisson(
176
184
  return rng.poisson(lam)
177
185
 
178
186
 
187
+ @pf.tracer.primitive(allow_exec=True)
179
188
  def triangular(
180
189
  rng: np.random.Generator,
181
190
  low: float,
@@ -185,6 +194,7 @@ def triangular(
185
194
  return rng.triangular(low, mode, high)
186
195
 
187
196
 
197
+ @pf.tracer.primitive(allow_exec=True)
188
198
  def gamma(
189
199
  rng: np.random.Generator,
190
200
  shape: float,
@@ -193,6 +203,7 @@ def gamma(
193
203
  return rng.gamma(shape, scale)
194
204
 
195
205
 
206
+ @pf.tracer.primitive(allow_exec=True)
196
207
  def binomial(
197
208
  rng: np.random.Generator,
198
209
  n: int,
@@ -201,6 +212,7 @@ def binomial(
201
212
  return rng.binomial(n, p)
202
213
 
203
214
 
215
+ @pf.tracer.primitive(allow_exec=True)
204
216
  def geometric(
205
217
  rng: np.random.Generator,
206
218
  p: float,
@@ -139,6 +139,22 @@ def _map_args(
139
139
  return res
140
140
 
141
141
 
142
+ def capture_as_function_call(func: Callable, args: tuple, kwargs: dict) -> cg.Proxy:
143
+ """Record a call as a single FunctionCallNode leaf without executing func's body.
144
+
145
+ The Proxy's attribute/item access becomes graph getattrs, so multi-output results
146
+ (e.g. a NamedTuple) work through it. Codegen re-emits this as an imported call.
147
+ """
148
+
149
+ def _unwrap(v):
150
+ return v.node if isinstance(v, cg.Proxy) else v
151
+
152
+ node_args = tuple(pytree.PyTree(a).map(_unwrap).obj() for a in args)
153
+ node_kwargs = {k: pytree.PyTree(v).map(_unwrap).obj() for k, v in kwargs.items()}
154
+ node = cg.FunctionCallNode(func=func, args=node_args, kwargs=node_kwargs)
155
+ return cg.Proxy(node)
156
+
157
+
142
158
  def trace(
143
159
  func: Callable,
144
160
  trace_level: TraceLevel = TraceLevel.GENERATORS,