procfunc 0.33.1__tar.gz → 0.35.0__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.
- {procfunc-0.33.1/src/procfunc.egg-info → procfunc-0.35.0}/PKG-INFO +3 -1
- {procfunc-0.33.1 → procfunc-0.35.0}/pyproject.toml +3 -1
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/__init__.py +1 -1
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/codegen/codegen.py +77 -14
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/codegen/repr.py +16 -15
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/color.py +8 -11
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/compute_graph/__init__.py +0 -1
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/compute_graph/proxy.py +4 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/compute_graph/util.py +96 -14
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/context.py +0 -40
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/control/__init__.py +2 -5
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/color.py +4 -4
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/execute/construct_standard.py +5 -9
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/execute/execute.py +1 -1
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/execute/realize.py +31 -12
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/execute/util.py +2 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/func.py +21 -17
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/geo.py +10 -55
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/manifest.json +13 -627
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/math.py +72 -4
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/shader.py +0 -39
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/texture.py +99 -97
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/util/bindings_util.py +0 -35
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/util/node_function.py +6 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/attr.py +6 -3
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/primitives/__init__.py +1 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/random.py +12 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/tracer/trace.py +16 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/transforms/extract_materials.py +38 -16
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/transpiler/bpy_to_computegraph.py +63 -20
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/transpiler/main.py +5 -1
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/transpiler/parse_special_cases.py +23 -6
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/types.py +22 -7
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/util/bpy_info.py +18 -16
- {procfunc-0.33.1 → procfunc-0.35.0/src/procfunc.egg-info}/PKG-INFO +3 -1
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc.egg-info/requires.txt +2 -0
- procfunc-0.35.0/tests/test_codegen.py +89 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/tests/test_ops.py +28 -0
- procfunc-0.35.0/tests/test_random.py +73 -0
- procfunc-0.33.1/tests/test_codegen.py +0 -18
- procfunc-0.33.1/tests/test_random.py +0 -29
- {procfunc-0.33.1 → procfunc-0.35.0}/LICENSE.md +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/README.md +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/setup.cfg +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/cli/__init__.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/cli/main.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/codegen/__init__.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/codegen/identifiers.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/compute_graph/compute_graph.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/compute_graph/node.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/compute_graph/operators_info.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/__init__.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/compositor.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/execute/construct_nodes.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/execute/construct_operator.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/execute/construct_special_cases.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/execute/infer_runtime_data_type.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/types.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/util/__init__.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/nodes/util/bpy_node_info.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/__init__.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/_util.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/addons.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/collection.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/curve.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/file.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/manifest.json +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/mesh.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/modifier.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/object.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/primitives/camera.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/primitives/curve.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/primitives/light.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/primitives/mesh.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/ops/uv.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/tracer/__init__.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/tracer/decorator.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/tracer/patch.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/tracer/proxy.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/transforms/__init__.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/transforms/cleanup.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/transforms/convert.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/transforms/distribution.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/transforms/infer_distribution.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/transforms/parameters.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/transforms/util.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/transpiler/__init__.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/transpiler/parse_default_values.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/util/__init__.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/util/bpy_data.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/util/camera.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/util/keyframe.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/util/log.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/util/manifest.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/util/pytree.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc/util/teardown.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc.egg-info/SOURCES.txt +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc.egg-info/dependency_links.txt +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc.egg-info/entry_points.txt +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/src/procfunc.egg-info/top_level.txt +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/tests/test_asset.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/tests/test_bpy_data_cleanup.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/tests/test_cli_transpile.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/tests/test_codegen_matrix.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/tests/test_compute_graph.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/tests/test_node_function.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/tests/test_pytree.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/tests/test_trace.py +0 -0
- {procfunc-0.33.1 → procfunc-0.35.0}/tests/test_transforms.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: procfunc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.35.0
|
|
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
|
|
@@ -16,6 +16,8 @@ Requires-Dist: pytest; extra == "dev"
|
|
|
16
16
|
Requires-Dist: pytest-xdist; extra == "dev"
|
|
17
17
|
Requires-Dist: ruff<0.16,>=0.15; extra == "dev"
|
|
18
18
|
Requires-Dist: ty; extra == "dev"
|
|
19
|
+
Requires-Dist: imageio; extra == "dev"
|
|
20
|
+
Requires-Dist: matplotlib; extra == "dev"
|
|
19
21
|
Provides-Extra: docs
|
|
20
22
|
Requires-Dist: sphinx; extra == "docs"
|
|
21
23
|
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
309
|
-
line += f" = {repr_value(
|
|
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
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
+
raise ValueError(
|
|
79
80
|
f"Procnode object {value} should never be treated as a raw value in codegen"
|
|
80
81
|
)
|
|
81
82
|
|
|
@@ -10,6 +10,8 @@ def hsv_color(
|
|
|
10
10
|
saturation: float | None = None,
|
|
11
11
|
value: float | None = None,
|
|
12
12
|
) -> t.Color:
|
|
13
|
+
if hsv is None and (hue is None or saturation is None or value is None):
|
|
14
|
+
raise ValueError("provide hsv=... or all of hue/saturation/value")
|
|
13
15
|
color = t.Color()
|
|
14
16
|
if hsv is not None:
|
|
15
17
|
color.hsv = hsv
|
|
@@ -27,12 +29,9 @@ def rgb_color(
|
|
|
27
29
|
b: float | None = None,
|
|
28
30
|
rgb: np.ndarray | None = None,
|
|
29
31
|
) -> t.Color:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
else:
|
|
34
|
-
color.r, color.g, color.b = r, g, b
|
|
35
|
-
return color
|
|
32
|
+
if rgb is None and (r is None or g is None or b is None):
|
|
33
|
+
raise ValueError("provide rgb=... or all of r/g/b")
|
|
34
|
+
return t.Color(rgb if rgb is not None else (r, g, b))
|
|
36
35
|
|
|
37
36
|
|
|
38
37
|
def _srgb_to_linearrgb(c):
|
|
@@ -44,14 +43,12 @@ def _srgb_to_linearrgb(c):
|
|
|
44
43
|
return ((c + 0.055) / 1.055) ** 2.4
|
|
45
44
|
|
|
46
45
|
|
|
47
|
-
def _hex_to_rgb(h: int
|
|
46
|
+
def _hex_to_rgb(h: int):
|
|
48
47
|
r = (h & 0xFF0000) >> 16
|
|
49
48
|
g = (h & 0x00FF00) >> 8
|
|
50
49
|
b = h & 0x0000FF
|
|
51
50
|
return tuple([_srgb_to_linearrgb(c / 0xFF) for c in (r, g, b)])
|
|
52
51
|
|
|
53
52
|
|
|
54
|
-
def hex_color(h: int
|
|
55
|
-
|
|
56
|
-
c.r, c.g, c.b = _hex_to_rgb(h, alpha)
|
|
57
|
-
return c
|
|
53
|
+
def hex_color(h: int) -> t.Color:
|
|
54
|
+
return t.Color(_hex_to_rgb(h))
|
|
@@ -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
|
|
|
@@ -3,10 +3,21 @@ import logging
|
|
|
3
3
|
from collections import defaultdict, deque
|
|
4
4
|
from typing import Any, Callable, Generator, Literal, TypeVar
|
|
5
5
|
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
6
8
|
from procfunc.util import pytree
|
|
7
9
|
|
|
8
10
|
from .compute_graph import ComputeGraph
|
|
9
|
-
from .node import
|
|
11
|
+
from .node import (
|
|
12
|
+
ConstantNode,
|
|
13
|
+
FunctionCallNode,
|
|
14
|
+
GetAttributeNode,
|
|
15
|
+
InputPlaceholderNode,
|
|
16
|
+
MethodCallNode,
|
|
17
|
+
Node,
|
|
18
|
+
ProceduralNode,
|
|
19
|
+
SubgraphCallNode,
|
|
20
|
+
)
|
|
10
21
|
|
|
11
22
|
logger = logging.getLogger(__name__)
|
|
12
23
|
|
|
@@ -40,6 +51,7 @@ def traverse_breadth_first(
|
|
|
40
51
|
|
|
41
52
|
visited = set()
|
|
42
53
|
frontier = deque((None, name, node) for name, node in graph.outputs.items())
|
|
54
|
+
visited.update(id(node) for _, _, node in frontier)
|
|
43
55
|
# logger.debug(f"{traverse_breadth_first.__name__} {graph.name} {len(frontier)=}")
|
|
44
56
|
|
|
45
57
|
def res(parent, name, child):
|
|
@@ -61,10 +73,6 @@ def traverse_breadth_first(
|
|
|
61
73
|
if not isinstance(node, Node):
|
|
62
74
|
continue
|
|
63
75
|
|
|
64
|
-
if id(node) in visited:
|
|
65
|
-
continue
|
|
66
|
-
visited.add(id(node))
|
|
67
|
-
|
|
68
76
|
yield res(parent, name, node)
|
|
69
77
|
|
|
70
78
|
children = list(pytree.PyTree(node.args).items()) + list(
|
|
@@ -75,6 +83,7 @@ def traverse_breadth_first(
|
|
|
75
83
|
continue
|
|
76
84
|
if id(arg) in visited:
|
|
77
85
|
continue
|
|
86
|
+
visited.add(id(arg))
|
|
78
87
|
frontier.append((node, key, arg))
|
|
79
88
|
|
|
80
89
|
|
|
@@ -201,22 +210,95 @@ def usages_per_node(
|
|
|
201
210
|
return dict(usages)
|
|
202
211
|
|
|
203
212
|
|
|
204
|
-
def
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
213
|
+
def _value_equal(a: Any, b: Any) -> bool:
|
|
214
|
+
"""Array-safe equality for non-node values (constants, attrs, defaults)."""
|
|
215
|
+
if a is b:
|
|
216
|
+
return True
|
|
217
|
+
if isinstance(a, np.ndarray) or isinstance(b, np.ndarray):
|
|
218
|
+
return np.array_equal(a, b)
|
|
219
|
+
if isinstance(a, dict) and isinstance(b, dict):
|
|
220
|
+
return a.keys() == b.keys() and all(_value_equal(a[k], b[k]) for k in a)
|
|
221
|
+
if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)):
|
|
222
|
+
return (
|
|
223
|
+
type(a) is type(b)
|
|
224
|
+
and len(a) == len(b)
|
|
225
|
+
and all(_value_equal(x, y) for x, y in zip(a, b))
|
|
226
|
+
)
|
|
227
|
+
try:
|
|
228
|
+
return bool(a == b)
|
|
229
|
+
except (ValueError, TypeError):
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _args_equal(a: Any, b: Any, memo: dict[tuple[int, int], bool]) -> bool:
|
|
234
|
+
"""Structural equality of an args/kwargs tree: same container shape, with
|
|
235
|
+
node-valued leaves compared recursively and other leaves by value."""
|
|
236
|
+
tree_a = pytree.PyTree(a)
|
|
237
|
+
tree_b = pytree.PyTree(b)
|
|
238
|
+
if tree_a.spec != tree_b.spec:
|
|
208
239
|
return False
|
|
209
|
-
for
|
|
210
|
-
|
|
240
|
+
for leaf_a, leaf_b in zip(tree_a.values(), tree_b.values()):
|
|
241
|
+
a_is_node = isinstance(leaf_a, Node)
|
|
242
|
+
b_is_node = isinstance(leaf_b, Node)
|
|
243
|
+
if a_is_node != b_is_node:
|
|
211
244
|
return False
|
|
212
|
-
if
|
|
213
|
-
if not
|
|
245
|
+
if a_is_node:
|
|
246
|
+
if not _nodes_equal(leaf_a, leaf_b, memo):
|
|
214
247
|
return False
|
|
215
|
-
elif
|
|
248
|
+
elif not _value_equal(leaf_a, leaf_b):
|
|
216
249
|
return False
|
|
217
250
|
return True
|
|
218
251
|
|
|
219
252
|
|
|
253
|
+
def _nodes_equal(node1: Node, node2: Node, memo: dict[tuple[int, int], bool]) -> bool:
|
|
254
|
+
if node1 is node2:
|
|
255
|
+
return True
|
|
256
|
+
if type(node1) is not type(node2):
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
key = (id(node1), id(node2))
|
|
260
|
+
cached = memo.get(key)
|
|
261
|
+
if cached is not None:
|
|
262
|
+
return cached
|
|
263
|
+
memo[key] = True # optimistic, breaks cycles in shared DAGs
|
|
264
|
+
|
|
265
|
+
result = True
|
|
266
|
+
if isinstance(node1, SubgraphCallNode):
|
|
267
|
+
result = graph_nodes_equal(node1.subgraph, node2.subgraph)
|
|
268
|
+
if result and isinstance(node1, FunctionCallNode):
|
|
269
|
+
result = node1.func is node2.func
|
|
270
|
+
if result and isinstance(node1, MethodCallNode):
|
|
271
|
+
result = node1.method_name == node2.method_name
|
|
272
|
+
if result and isinstance(node1, GetAttributeNode):
|
|
273
|
+
result = node1.attribute_name == node2.attribute_name
|
|
274
|
+
if result and isinstance(node1, ProceduralNode):
|
|
275
|
+
result = node1.node_type == node2.node_type and _value_equal(
|
|
276
|
+
node1.attrs, node2.attrs
|
|
277
|
+
)
|
|
278
|
+
if result and isinstance(node1, ConstantNode):
|
|
279
|
+
result = _value_equal(node1.value, node2.value)
|
|
280
|
+
if result and isinstance(node1, InputPlaceholderNode):
|
|
281
|
+
result = node1.input_name == node2.input_name and _value_equal(
|
|
282
|
+
node1.default_value, node2.default_value
|
|
283
|
+
)
|
|
284
|
+
if result:
|
|
285
|
+
result = _args_equal(node1.args, node2.args, memo) and _args_equal(
|
|
286
|
+
node1.kwargs, node2.kwargs, memo
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
memo[key] = result
|
|
290
|
+
return result
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def graph_nodes_equal(graph1: ComputeGraph, graph2: ComputeGraph) -> bool:
|
|
294
|
+
nodes1 = list(traverse_depth_first(graph1))
|
|
295
|
+
nodes2 = list(traverse_depth_first(graph2))
|
|
296
|
+
if len(nodes1) != len(nodes2):
|
|
297
|
+
return False
|
|
298
|
+
memo: dict[tuple[int, int], bool] = {}
|
|
299
|
+
return all(_nodes_equal(node1, node2, memo) for node1, node2 in zip(nodes1, nodes2))
|
|
300
|
+
|
|
301
|
+
|
|
220
302
|
def transform_nodetree(
|
|
221
303
|
root: Node,
|
|
222
304
|
transform_fn: Callable[[Node], Any],
|
|
@@ -13,24 +13,6 @@ class ProcfuncContext:
|
|
|
13
13
|
num_cpu_cores: int
|
|
14
14
|
current_trace_level: int | None # compared against to TraceLevel int values
|
|
15
15
|
|
|
16
|
-
warn_mode_avoid_normal_bump: Literal["ignore", "warn", "throw"]
|
|
17
|
-
"""
|
|
18
|
-
Set to 'throw' for strict usage. Using a normal map / bump map / BSDF vector input _can_ be useful,
|
|
19
|
-
but its always better to do it via the Displacement output of the shader (so that it at least can also be done as displacement)
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
warn_mode_avoid_implicit_vector: Literal["ignore", "warn", "throw"]
|
|
23
|
-
"""
|
|
24
|
-
Set to 'throw' to force the user to specify exactly what vector to sample.
|
|
25
|
-
This prevents materials from being incorrect when on a moving object, or ignoring uv coordinates
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
warn_mode_avoid_io_nodes: Literal["ignore", "warn", "throw"]
|
|
29
|
-
"""
|
|
30
|
-
Set to 'throw' to prevent floating Value Vector Color nodes, or strange output nodes like AOV in shader
|
|
31
|
-
Intent is to force nodegroups to be more functional - all inputs and outputs go through nodegroup interface
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
16
|
warn_mode_empty_geonodes: Literal["ignore", "warn", "throw"]
|
|
35
17
|
"""
|
|
36
18
|
Controls behavior when a geometry node graph produces no mesh geometry (e.g. unconnected inputs).
|
|
@@ -44,16 +26,10 @@ class ProcfuncContext:
|
|
|
44
26
|
|
|
45
27
|
def set_strict(self):
|
|
46
28
|
"""Set all warning modes to 'throw'"""
|
|
47
|
-
self.warn_mode_avoid_normal_bump = "throw"
|
|
48
|
-
self.warn_mode_avoid_implicit_vector = "throw"
|
|
49
|
-
self.warn_mode_avoid_io_nodes = "throw"
|
|
50
29
|
self.warn_mode_empty_geonodes = "throw"
|
|
51
30
|
|
|
52
31
|
def set_warn(self):
|
|
53
32
|
"""Set all warning modes to 'warn'"""
|
|
54
|
-
self.warn_mode_avoid_normal_bump = "warn"
|
|
55
|
-
self.warn_mode_avoid_implicit_vector = "warn"
|
|
56
|
-
self.warn_mode_avoid_io_nodes = "warn"
|
|
57
33
|
self.warn_mode_empty_geonodes = "warn"
|
|
58
34
|
|
|
59
35
|
|
|
@@ -61,27 +37,11 @@ class ProcfuncContext:
|
|
|
61
37
|
|
|
62
38
|
warn_modes = ["ignore", "warn", "throw"]
|
|
63
39
|
|
|
64
|
-
_warn_mode_avoid_normal_bump = os.environ.get(
|
|
65
|
-
"PROCFUNC_WARN_MODE_AVOID_NORMAL_BUMP", "ignore"
|
|
66
|
-
)
|
|
67
|
-
assert _warn_mode_avoid_normal_bump in warn_modes
|
|
68
|
-
|
|
69
|
-
warn_mode_avoid_implicit_vector = os.environ.get(
|
|
70
|
-
"PROCFUNC_WARN_MODE_AVOID_IMPLICIT_VECTOR", "ignore"
|
|
71
|
-
)
|
|
72
|
-
assert warn_mode_avoid_implicit_vector in warn_modes
|
|
73
|
-
|
|
74
|
-
warn_mode_avoid_io_nodes = os.environ.get("PROCFUNC_WARN_MODE_AVOID_IO_NODES", "ignore")
|
|
75
|
-
assert warn_mode_avoid_io_nodes in warn_modes
|
|
76
|
-
|
|
77
40
|
_warn_mode_empty_geonodes = os.environ.get("PROCFUNC_WARN_MODE_EMPTY_GEONODES", "warn")
|
|
78
41
|
assert _warn_mode_empty_geonodes in warn_modes
|
|
79
42
|
|
|
80
43
|
globals = ProcfuncContext(
|
|
81
44
|
num_cpu_cores=int(os.environ.get("PROCFUNC_NUM_CPU_CORES", 0)),
|
|
82
|
-
warn_mode_avoid_normal_bump=_warn_mode_avoid_normal_bump, # type: ignore[invalid-assignment]
|
|
83
|
-
warn_mode_avoid_implicit_vector=warn_mode_avoid_implicit_vector, # type: ignore[invalid-assignment]
|
|
84
|
-
warn_mode_avoid_io_nodes=warn_mode_avoid_io_nodes, # type: ignore[invalid-assignment]
|
|
85
45
|
warn_mode_empty_geonodes=_warn_mode_empty_geonodes, # type: ignore[invalid-assignment]
|
|
86
46
|
current_trace_level=None,
|
|
87
47
|
)
|
|
@@ -127,12 +127,9 @@ def choice(
|
|
|
127
127
|
) -> T:
|
|
128
128
|
"""
|
|
129
129
|
Args:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
options: list of callables to choose from
|
|
133
|
-
chosen: if not None, use this value instead of choosing randomly. Cannot be traced, but may appear in the output of the tracer.
|
|
130
|
+
choice_rng: random number generator
|
|
131
|
+
choice_options: list of (value, weight) tuples; weights are normalized to a probability distribution
|
|
134
132
|
chosen_idx: if not None, use this index instead of choosing randomly. Cannot be traced, but may appear in the output of the tracer.
|
|
135
|
-
**child_kwargs: keyword arguments to pass to the chosen callable
|
|
136
133
|
|
|
137
134
|
TODO: this function should really take args and kwargs as tuple & dict, not via expansion
|
|
138
135
|
but this requires our Node to handle cases with pytrees as inputs. Currently it would fail to execute the children if they are hidden in a pytree
|
|
@@ -39,7 +39,7 @@ def mix_rgb(
|
|
|
39
39
|
clamp_factor: bool = True,
|
|
40
40
|
) -> nt.ProcNode[pt.Color]:
|
|
41
41
|
"""
|
|
42
|
-
Uses a Mix Node with
|
|
42
|
+
Uses a Mix Shader Node with data_type='RGBA'.
|
|
43
43
|
|
|
44
44
|
NOTE: separated from float/vector mix() due to extra arguments
|
|
45
45
|
|
|
@@ -80,7 +80,7 @@ def combine_rgb(
|
|
|
80
80
|
blue: nt.SocketOrVal[float] = 0.0,
|
|
81
81
|
) -> nt.ProcNode[pt.Color]:
|
|
82
82
|
"""
|
|
83
|
-
Uses a CombineColor Shader Node.
|
|
83
|
+
Uses a CombineColor Shader Node in RGB mode.
|
|
84
84
|
|
|
85
85
|
See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/converter/combine_color.html
|
|
86
86
|
"""
|
|
@@ -97,7 +97,7 @@ def combine_hsv(
|
|
|
97
97
|
value: nt.SocketOrVal[float] = 0.0,
|
|
98
98
|
) -> nt.ProcNode[pt.Color]:
|
|
99
99
|
"""
|
|
100
|
-
Uses a CombineColor Shader Node.
|
|
100
|
+
Uses a CombineColor Shader Node in HSV mode.
|
|
101
101
|
|
|
102
102
|
See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/converter/combine_color.html
|
|
103
103
|
"""
|
|
@@ -115,7 +115,7 @@ def combine_hsl(
|
|
|
115
115
|
lightness: nt.SocketOrVal[float] = 0.0,
|
|
116
116
|
) -> nt.ProcNode[pt.Color]:
|
|
117
117
|
"""
|
|
118
|
-
Uses a
|
|
118
|
+
Uses a CombineColor Shader Node in HSL mode.
|
|
119
119
|
|
|
120
120
|
See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/converter/combine_color.html
|
|
121
121
|
"""
|
|
@@ -320,26 +320,22 @@ def _construct_procnode_standard(
|
|
|
320
320
|
for input_name, input_py in kwargs.items():
|
|
321
321
|
input_result = input_results[input_name]
|
|
322
322
|
if input_result is None:
|
|
323
|
-
# Strict-None policy: None means "leave disconnected" and is only
|
|
324
|
-
# allowed for sockets with no default_value attr at all (Geometry,
|
|
325
|
-
# Shader, Matrix, Virtual) - never rely on Blender's internal
|
|
326
|
-
# defaults, whose values may change across versions. A disabled or
|
|
327
|
-
# missing socket name is a binding bug and propagates as ValueError.
|
|
328
323
|
to_socket = get_input_socket_to_connect_to(
|
|
329
324
|
bl_node_tree, bl_node, input_name, None
|
|
330
325
|
)
|
|
331
|
-
# IMAGE default_value is a datablock pointer; None faithfully means "no image assigned"
|
|
332
326
|
if (
|
|
333
327
|
to_socket.is_multi_input
|
|
334
328
|
or not hasattr(to_socket, "default_value")
|
|
335
|
-
or to_socket.
|
|
329
|
+
or to_socket.hide_value
|
|
330
|
+
or to_socket.default_value is None
|
|
336
331
|
):
|
|
337
332
|
continue
|
|
338
333
|
raise ValueError(
|
|
339
334
|
f"Node {bl_node.name!r} input {to_socket.name!r} (socket type "
|
|
340
335
|
f"{to_socket.type}) received None. Explicitly pass a value; None is "
|
|
341
|
-
f"only allowed for
|
|
342
|
-
f"
|
|
336
|
+
f"only allowed for disconnected implicit-field sockets (hide_value), "
|
|
337
|
+
f"datablock pointers, or sockets with no default_value attribute "
|
|
338
|
+
f"(e.g. Geometry/Shader)."
|
|
343
339
|
)
|
|
344
340
|
to_socket = get_input_socket_to_connect_to(
|
|
345
341
|
bl_node_tree, bl_node, input_name, input_result
|