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,145 @@
1
+ import logging
2
+ import uuid
3
+ from enum import Enum
4
+ from typing import Any
5
+
6
+ import bpy
7
+ import numpy as np
8
+
9
+ from procfunc import types as t
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ # Attribute data type dimensions
15
+ DATATYPE_DIMS = {
16
+ "FLOAT": 1,
17
+ "INT": 1,
18
+ "INT8": 1,
19
+ "FLOAT_VECTOR": 3,
20
+ "FLOAT2": 2,
21
+ "FLOAT_COLOR": 4,
22
+ "BYTE_COLOR": 4,
23
+ "RGBA": 4,
24
+ "BOOLEAN": 1,
25
+ "INT32_2D": 2,
26
+ "QUATERNION": 4,
27
+ "FLOAT4X4": 16,
28
+ }
29
+
30
+ # Attribute data type field names for foreach_get/foreach_set
31
+ DATATYPE_FIELDS = {
32
+ "FLOAT": "value",
33
+ "INT": "value",
34
+ "INT8": "value",
35
+ "FLOAT_VECTOR": "vector",
36
+ "FLOAT2": "vector",
37
+ "FLOAT_COLOR": "color",
38
+ "BYTE_COLOR": "color",
39
+ "RGBA": "color",
40
+ "BOOLEAN": "value",
41
+ "INT32_2D": "value",
42
+ "QUATERNION": "value",
43
+ "FLOAT4X4": "value",
44
+ }
45
+
46
+ # Attribute data type to Python type mapping
47
+ DATATYPE_TO_PYTYPE = {
48
+ "INT": int,
49
+ "FLOAT": np.float32,
50
+ "FLOAT_VECTOR": np.float64,
51
+ "FLOAT_COLOR": np.float32,
52
+ "RGBA": np.float32,
53
+ "BOOLEAN": bool,
54
+ }
55
+
56
+ # Python type to attribute data type mapping
57
+ PYTYPE_DATATYPE_TABLE = [
58
+ (int, "INT"),
59
+ (np.dtype(np.int32), "INT"),
60
+ (float, "FLOAT"),
61
+ (np.dtype(np.float32), "FLOAT"),
62
+ (np.dtype(np.float64), "FLOAT"),
63
+ (bool, "BOOLEAN"),
64
+ (np.dtype(np.bool_), "BOOLEAN"),
65
+ (t.Color, "RGBA"),
66
+ (t.Vector, "FLOAT_VECTOR"),
67
+ (t.Euler, "FLOAT_VECTOR"),
68
+ (t.Quaternion, "FLOAT_VECTOR"),
69
+ (t.Matrix, "FLOAT_VECTOR"),
70
+ ]
71
+ PYTYPE_TO_DATATYPE = {k: v for k, v in PYTYPE_DATATYPE_TABLE}
72
+ DATATYPE_TO_PYTYPE = {v: k for k, v in PYTYPE_DATATYPE_TABLE}
73
+ DATATYPE_TO_PYTYPE["FLOAT2"] = np.float32
74
+ DATATYPE_TO_PYTYPE["INT8"] = np.int8
75
+ DATATYPE_TO_PYTYPE["INT32_2D"] = np.int32
76
+ DATATYPE_TO_PYTYPE["BYTE_COLOR"] = np.uint8
77
+ DATATYPE_TO_PYTYPE["QUATERNION"] = np.float32
78
+ DATATYPE_TO_PYTYPE["FLOAT4X4"] = np.float32
79
+
80
+ UNSUPPORTED_DATATYPES = {"STRING"}
81
+ UNSUPPORTED_DOMAINS = {"LAYER"}
82
+
83
+
84
+ class NodeGroupType(Enum):
85
+ GEOMETRY = "GeometryNodeGroup"
86
+ SHADER = "ShaderNodeGroup"
87
+ COMPOSITOR = "CompositorNodeGroup"
88
+ TEXTURE = "TextureNodeGroup"
89
+
90
+ @classmethod
91
+ def from_str(cls, s: str) -> "NodeGroupType | None":
92
+ try:
93
+ return cls(s)
94
+ except ValueError:
95
+ return None
96
+
97
+
98
+ class NodeTreeType(Enum):
99
+ GEOMETRY = "GeometryNodeTree"
100
+ SHADER = "ShaderNodeTree"
101
+ COMPOSITOR = "CompositorNodeTree"
102
+ TEXTURE = "TextureNodeTree"
103
+
104
+
105
+ NODETREE_TO_NODEGROUP = {
106
+ NodeTreeType.GEOMETRY: NodeGroupType.GEOMETRY,
107
+ NodeTreeType.SHADER: NodeGroupType.SHADER,
108
+ NodeTreeType.COMPOSITOR: NodeGroupType.COMPOSITOR,
109
+ NodeTreeType.TEXTURE: NodeGroupType.TEXTURE,
110
+ }
111
+
112
+ NODETREE_TYPE_TO_MAIN_OUTPUT = {
113
+ NodeTreeType.GEOMETRY: "GeometryNodeOutput",
114
+ NodeTreeType.SHADER: "ShaderNodeOutputMaterial",
115
+ NodeTreeType.COMPOSITOR: "CompositorNodeOutput",
116
+ NodeTreeType.TEXTURE: "TextureNodeOutput",
117
+ }
118
+
119
+ NODEGROUP_TYPE_TO_INPUT_NODE = {
120
+ NodeGroupType.GEOMETRY: "GeometryNodeInput",
121
+ NodeGroupType.SHADER: "ShaderNodeInput",
122
+ NodeGroupType.COMPOSITOR: "CompositorNodeInput",
123
+ NodeGroupType.TEXTURE: "TextureNodeInput",
124
+ }
125
+
126
+
127
+ def bpy_nocollide_data_name(
128
+ x: Any,
129
+ bpy_data: bpy.types.bpy_prop_collection,
130
+ retries: int = 30,
131
+ ) -> str:
132
+ name = uuid.uuid4().hex[:12]
133
+ if name not in bpy_data:
134
+ return name
135
+
136
+ for i in range(retries):
137
+ newname = f"{name}_{len(bpy_data) + i}"
138
+ if newname not in bpy_data:
139
+ return newname
140
+ else:
141
+ logger.warning(
142
+ f"Could not find a unique name for {x} in {bpy_data} after {retries=}"
143
+ )
144
+
145
+ return newname
File without changes
@@ -0,0 +1,70 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Generic, TypeVar
3
+
4
+ INTERPOLATION_TYPES = {
5
+ "LINEAR",
6
+ "BEZIER",
7
+ # TODO more
8
+ }
9
+
10
+
11
+ T = TypeVar("T")
12
+
13
+
14
+ @dataclass
15
+ class Keyframes(Generic[T]):
16
+ """
17
+ Represents keyframe animation data for a parameter.
18
+
19
+ Args:
20
+ keyframes: List of (frame, value) tuples where value can be any type
21
+ interpolation: Blender interpolation mode ('LINEAR', 'BEZIER', 'CONSTANT', etc.)
22
+ """
23
+
24
+ keyframes: list[tuple[int, T]]
25
+ interpolation: str = "LINEAR"
26
+
27
+ def __post_init__(self):
28
+ if not self.keyframes:
29
+ raise ValueError("Keyframes list cannot be empty")
30
+ if self.interpolation not in INTERPOLATION_TYPES:
31
+ raise ValueError(
32
+ f"Invalid interpolation mode: {self.interpolation}, "
33
+ f"current supported are {INTERPOLATION_TYPES}, adding more may be trivial"
34
+ )
35
+
36
+
37
+ def apply_keyframes(
38
+ target: Any,
39
+ data_path_name: str,
40
+ keyframes: Keyframes,
41
+ ):
42
+ """
43
+ Apply keyframe animation to a node socket.
44
+
45
+ Args:
46
+ target_socket: The Blender node socket to animate
47
+ keyframes: Keyframes object containing frame/value pairs and interpolation mode
48
+ """
49
+ if not hasattr(target, data_path_name):
50
+ raise ValueError(
51
+ f"Cannot apply keyframes to {target} - no {data_path_name} attribute"
52
+ )
53
+
54
+ setattr(target, data_path_name, keyframes.keyframes[0][1])
55
+
56
+ # Apply all keyframes
57
+ for frame, value in keyframes.keyframes:
58
+ setattr(target, data_path_name, value)
59
+ target.keyframe_insert(data_path=data_path_name, frame=frame)
60
+
61
+ # Set interpolation mode for all keyframes
62
+ if hasattr(target, "id_data") and hasattr(target.id_data, "animation_data"):
63
+ anim_data = target.id_data.animation_data
64
+ if anim_data and anim_data.action:
65
+ for fcurve in anim_data.action.fcurves:
66
+ # Check if this fcurve corresponds to our socket
67
+ socket_path = target.path_from_id()
68
+ if socket_path and socket_path in fcurve.data_path:
69
+ for keyframe in fcurve.keyframe_points:
70
+ keyframe.interpolation = keyframes.interpolation
procfunc/util/log.py ADDED
@@ -0,0 +1,96 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ from contextlib import contextmanager
5
+ from typing import Literal
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class Suppress:
11
+ def __enter__(self, logfile=os.devnull):
12
+ open(logfile, "w").close()
13
+ self.old = os.dup(1)
14
+ sys.stdout.flush()
15
+ os.close(1)
16
+ os.open(logfile, os.O_WRONLY)
17
+ self.level = logging.root.manager.disable
18
+ logging.disable(logging.CRITICAL)
19
+
20
+ def __exit__(self, type, value, traceback):
21
+ os.close(1)
22
+ os.dup(self.old)
23
+ os.close(self.old)
24
+ logging.disable(self.level)
25
+
26
+
27
+ class LogLevel:
28
+ def __init__(self, logger, level):
29
+ self.logger = logger
30
+ self.level = level
31
+ self.orig_level = None
32
+
33
+ def __enter__(self):
34
+ self.orig_level = self.logger.level
35
+ self.logger.setLevel(self.level)
36
+
37
+ def __exit__(self, *_):
38
+ self.logger.setLevel(self.orig_level)
39
+
40
+
41
+ def clamp_with_log(val, logger, name, level=logging.WARNING, min=None, max=None):
42
+ if min is not None and val < min:
43
+ logger.log(level, f"{name} had {val} but will be clamped to {min=}")
44
+ val = min
45
+ if max is not None and val > max:
46
+ logger.log(level, f"{name} had {val} but will be clamped to {max=}")
47
+ val = max
48
+ return val
49
+
50
+
51
+ def raise_error_or_warn(
52
+ msg: str,
53
+ mode: Literal["throw", "warn", "ignore"],
54
+ warning_logger: logging.Logger | None,
55
+ error_class: type = ValueError,
56
+ ):
57
+ if warning_logger is None:
58
+ warning_logger = logger
59
+
60
+ match mode:
61
+ case "throw":
62
+ raise error_class(msg)
63
+ case "warn":
64
+ warning_logger.warning(msg)
65
+ case "ignore":
66
+ pass
67
+ case _:
68
+ raise ValueError(f"Unknown mode: {mode}")
69
+
70
+
71
+ @contextmanager
72
+ def add_exception_context_msg(
73
+ prefix: str = "",
74
+ postfix: str = "",
75
+ unparseable_exception_throw: Literal["throw", "warn", "ignore"] = "throw",
76
+ ):
77
+ try:
78
+ yield
79
+ except Exception as e:
80
+ is_arg_msg = (
81
+ isinstance(e.args, tuple) and len(e.args) > 0 and isinstance(e.args[0], str)
82
+ )
83
+
84
+ orig_msg = e.args[0] if is_arg_msg else f"{type(e).__name__}: {str(e)}"
85
+
86
+ msg = orig_msg
87
+ if prefix:
88
+ msg = f"{prefix} {msg}"
89
+ if postfix and not msg.endswith(postfix):
90
+ msg = f"{msg} {postfix}"
91
+
92
+ if is_arg_msg:
93
+ e.args = (msg, *e.args[1:])
94
+ raise e from e
95
+ else:
96
+ raise ValueError(msg) from e
@@ -0,0 +1,121 @@
1
+ """Utilities for reading and filtering manifest CSV files."""
2
+
3
+ import importlib
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Any, Callable
7
+
8
+ import pandas as pd
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def module_path():
14
+ return Path(__file__).parent.parent
15
+
16
+
17
+ def filter_manifest(
18
+ manifest: pd.DataFrame,
19
+ filter: dict[str, str] | None = None,
20
+ exclude: dict[str, list[str]] | None = None,
21
+ require_nonempty: list[str] | None = None,
22
+ min_entries: int | None = None,
23
+ ) -> pd.DataFrame:
24
+ if filter is None:
25
+ filter = {}
26
+ if exclude is None:
27
+ exclude = {}
28
+ if require_nonempty is None:
29
+ require_nonempty = []
30
+
31
+ for k, v in filter.items():
32
+ assert k in manifest.columns, (
33
+ f"Filter {k}={v} did not match any columns in {manifest.columns}"
34
+ )
35
+
36
+ before_count = len(manifest)
37
+ manifest = manifest.dropna(subset=[k])
38
+ manifest = manifest[manifest[k] == v]
39
+ after_count = len(manifest)
40
+ logger.debug(
41
+ f"Filter {k}={v}: {before_count} -> {after_count} (dropped {before_count - after_count})"
42
+ )
43
+
44
+ for column, patterns in exclude.items():
45
+ if not isinstance(patterns, list):
46
+ patterns = [patterns]
47
+
48
+ before_count = len(manifest)
49
+ if manifest[column].dtype == "object":
50
+ mask = pd.Series([False] * len(manifest), index=manifest.index)
51
+ for pattern in patterns:
52
+ if isinstance(pattern, str):
53
+ mask |= manifest[column].str.contains(pattern, na=False)
54
+ else:
55
+ mask |= manifest[column] == pattern
56
+ else:
57
+ mask = manifest[column].isin(patterns)
58
+ manifest = manifest[~mask]
59
+ after_count = len(manifest)
60
+ logger.debug(
61
+ f"Exclude {column} patterns {patterns}: {before_count} -> {after_count} (dropped {before_count - after_count})"
62
+ )
63
+
64
+ if require_nonempty:
65
+ before_count = len(manifest)
66
+ manifest = manifest.dropna(subset=require_nonempty)
67
+ after_count = len(manifest)
68
+ logger.debug(
69
+ f"Require nonempty {require_nonempty}: {before_count} -> {after_count} (dropped {before_count - after_count})"
70
+ )
71
+
72
+ if min_entries is not None and len(manifest) < min_entries:
73
+ raise ValueError(
74
+ f"Expected at least {min_entries} entries, got {len(manifest)} "
75
+ f"with {filter=} and {exclude=} and {require_nonempty=}"
76
+ )
77
+
78
+ return manifest
79
+
80
+
81
+ def import_item(name: str) -> Any:
82
+ """
83
+ Find and import a function or class by its dotted module path.
84
+
85
+ Args:
86
+ name: Dotted module path like "mymodule.generators.objects.funcname"
87
+
88
+ Returns:
89
+ The imported function or class
90
+
91
+ Raises:
92
+ ModuleNotFoundError: If the module or attribute cannot be found
93
+ """
94
+ *path_parts, item_name = name.split(".")
95
+ try:
96
+ return importlib.import_module("." + item_name, ".".join(path_parts))
97
+ except Exception as e:
98
+ if not isinstance(e, ModuleNotFoundError):
99
+ raise e
100
+
101
+ mod = importlib.import_module(".".join(path_parts))
102
+ try:
103
+ return getattr(mod, item_name)
104
+ except AttributeError as e:
105
+ raise AttributeError(
106
+ f"Attribute {item_name} not found in module {mod.__name__}, {dir(mod)=}"
107
+ ) from e
108
+
109
+
110
+ def import_item_iterative(name: str, from_mod: Any | None = None) -> Callable:
111
+ first, *rest = name.split(".")
112
+
113
+ if len(rest) == 0:
114
+ assert from_mod is not None, f"No module specified for {name}"
115
+ return getattr(from_mod, name)
116
+ elif from_mod is None:
117
+ mod = importlib.import_module(first)
118
+ return import_item_iterative(".".join(rest), mod)
119
+ else:
120
+ mod = getattr(from_mod, first)
121
+ return import_item_iterative(".".join(rest), mod)