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,299 @@
1
+ import argparse
2
+ import itertools
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Callable, Literal
6
+
7
+ import bpy
8
+ import pandas as pd
9
+
10
+ import procfunc as pf
11
+ from procfunc import compute_graph as cg
12
+ from procfunc import transforms as tr
13
+ from procfunc.nodes import NODE_OPERATOR_TABLE
14
+ from procfunc.util import pytree
15
+ from procfunc.util.teardown import skip_teardown_on_exit
16
+
17
+ from .bpy_to_computegraph import (
18
+ ParseMemo,
19
+ parse_material,
20
+ parse_node_tree,
21
+ parse_object,
22
+ )
23
+ from .codegen import default_func_resolution_map, to_python
24
+
25
+ logging.basicConfig(
26
+ format="[%(asctime)s.%(msecs)03d] [%(module)s] [%(levelname)s] | %(message)s",
27
+ datefmt="%H:%M:%S",
28
+ level=logging.INFO,
29
+ )
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ MODIFIERS_MANIFEST: pd.DataFrame | None = None
34
+ NODES_MANIFEST: pd.DataFrame | None = None
35
+
36
+
37
+ BPY_COLLECTIONS = [
38
+ "scene",
39
+ "object",
40
+ "material",
41
+ # "collections",
42
+ ]
43
+
44
+ RETURN_TYPES = {
45
+ "object": pf.MeshObject,
46
+ "material": pf.Material,
47
+ "collection": pf.Collection,
48
+ }
49
+
50
+
51
+ def get_parser():
52
+ parser = argparse.ArgumentParser()
53
+
54
+ parser.add_argument("input", type=Path)
55
+
56
+ parser.add_argument("--materials", type=str, default=[], nargs="+")
57
+ parser.add_argument("--objects", type=str, default=[], nargs="+")
58
+ parser.add_argument("--node_trees", type=str, default=[], nargs="+")
59
+
60
+ parser.add_argument("--output", type=Path, default=None)
61
+ parser.add_argument("--transforms", type=str, default=[], nargs="+")
62
+
63
+ parser.add_argument(
64
+ "--object_mode",
65
+ type=str,
66
+ choices=["monkey", "active", "named"],
67
+ default="monkey",
68
+ )
69
+ parser.add_argument("--no_version_comments", action="store_true")
70
+
71
+ parser.add_argument(
72
+ "-d",
73
+ "--debug",
74
+ action="store_const",
75
+ dest="loglevel",
76
+ const=logging.DEBUG,
77
+ default=logging.INFO,
78
+ )
79
+
80
+ parser.add_argument("--add_line_comments", action="store_true")
81
+ parser.add_argument("--include_object_materials", action="store_true")
82
+
83
+ return parser
84
+
85
+
86
+ def parse_target(
87
+ target: bpy.types.Object | bpy.types.Material | bpy.types.NodeTree,
88
+ memo: ParseMemo,
89
+ object_mode: Literal["monkey", "active", "named"] = "monkey",
90
+ include_set_material: bool = True,
91
+ ) -> cg.ComputeGraph:
92
+ match target:
93
+ # case pf.Scene:
94
+ # return parse_scene(bpy.context.scene, memo)
95
+ case bpy.types.Object():
96
+ return parse_object(
97
+ target,
98
+ memo,
99
+ object_mode=object_mode,
100
+ include_set_material=include_set_material,
101
+ )
102
+ case bpy.types.Material():
103
+ return parse_material(target, memo)
104
+ case bpy.types.NodeTree():
105
+ graph, _ = parse_node_tree(target, memo)
106
+ return graph
107
+ case x:
108
+ raise ValueError(f"Invalid {x=} {type(x)=}")
109
+
110
+
111
+ def _find_target_str(target: str, col: bpy.types.bpy_prop_collection) -> list:
112
+ if target.isdigit():
113
+ return [col[int(target)]]
114
+ elif "*" in target:
115
+ assert target.count("*") == 1
116
+ assert target.endswith("*") == 0
117
+ prefix = target.replace("*", "")
118
+ return [v for k, v in col.items() if k.startswith(prefix)]
119
+ else:
120
+ matches = [v for k, v in col.items() if k == target]
121
+ if len(matches) != 1:
122
+ available = " ".join(col.keys())
123
+ raise ValueError(
124
+ f"Found {len(matches)} targets for {target=} in {available=}"
125
+ )
126
+ return [matches[0]]
127
+
128
+
129
+ def transpile_targets(
130
+ targets: list[bpy.types.Object | bpy.types.Material | bpy.types.NodeTree],
131
+ transforms: list[Callable[[list[cg.ComputeGraph]], list[cg.ComputeGraph]]],
132
+ object_mode: Literal["monkey", "active", "named"] = "monkey",
133
+ add_version_comment: bool = True,
134
+ add_line_comments: bool = False,
135
+ include_set_material: bool = True,
136
+ ) -> str:
137
+ memo = ParseMemo()
138
+
139
+ result_graphs = [
140
+ parse_target(
141
+ target,
142
+ memo,
143
+ object_mode=object_mode,
144
+ include_set_material=include_set_material,
145
+ )
146
+ for target in targets
147
+ ]
148
+
149
+ for tfunc in transforms:
150
+ if logger.isEnabledFor(logging.DEBUG):
151
+ logger.debug(f"Applying transform {tfunc} to {result_graphs=}")
152
+ result_graphs = tfunc(result_graphs)
153
+
154
+ vec = cg.FunctionCallNode(pf.nodes.shader.coord, (), {})
155
+ vec = cg.GetAttributeNode(vec, "generated")
156
+
157
+ result_calls = []
158
+ for result_graph in result_graphs:
159
+ kwargs = {}
160
+ if result_graph.name.startswith("material_"):
161
+ kwargs["vector"] = vec
162
+
163
+ return_type = RETURN_TYPES.get(type(result_graph), None)
164
+ call_node = cg.SubgraphCallNode(
165
+ subgraph=result_graph,
166
+ args=(),
167
+ kwargs=kwargs,
168
+ metadata={"known_value_type": return_type},
169
+ )
170
+ result_calls.append(call_node)
171
+
172
+ toplevel_name = "toplevel_invoke"
173
+ graph = cg.ComputeGraph(
174
+ inputs=pytree.PyTree(()),
175
+ outputs=pytree.PyTree({"result": result_calls}),
176
+ name=toplevel_name,
177
+ metadata={},
178
+ )
179
+ assert isinstance(graph, cg.ComputeGraph), graph
180
+
181
+ func_resolution, import_lines = default_func_resolution_map(graph)
182
+ for oprow in NODE_OPERATOR_TABLE:
183
+ func_resolution[oprow.pf_func] = oprow.operator_type
184
+
185
+ python = to_python(
186
+ graph,
187
+ func_resolution=func_resolution,
188
+ import_lines=import_lines,
189
+ add_version_comment=add_version_comment,
190
+ add_line_comments=add_line_comments,
191
+ )
192
+
193
+ return python
194
+
195
+
196
+ def inject_into_python_file(python: list[str], file: Path, target_name: str):
197
+ """
198
+ If def <target_name>(): is found in the python file, replace it with the python code.
199
+ If not, add it to end of the file.
200
+ """
201
+
202
+ text = file.read_text()
203
+ lines = text.splitlines()
204
+
205
+ start_line = next(
206
+ (i for i, line in enumerate(lines) if line.startswith(f"def {target_name}")),
207
+ None,
208
+ )
209
+
210
+ if start_line is None:
211
+ lines.append
212
+
213
+
214
+ def _targets_from_args(
215
+ args: argparse.Namespace,
216
+ ) -> list[bpy.types.Object | bpy.types.Material | bpy.types.NodeTree]:
217
+ targets = []
218
+ targets += [
219
+ _find_target_str(target, bpy.data.materials) for target in args.materials
220
+ ]
221
+ for target in args.objects:
222
+ if target == "ACTIVE":
223
+ if bpy.context.active_object is None:
224
+ raise ValueError(
225
+ "--objects ACTIVE specified but no active object in scene"
226
+ )
227
+ targets.append([bpy.context.active_object])
228
+ else:
229
+ targets.append(_find_target_str(target, bpy.data.objects))
230
+ targets += [
231
+ _find_target_str(target, bpy.data.node_groups) for target in args.node_trees
232
+ ]
233
+
234
+ return list(itertools.chain.from_iterable(targets))
235
+
236
+
237
+ _transforms_map = {
238
+ "colors_to_hsv_definition": tr.map_graph_list(tr.colors_to_hsv_definition),
239
+ "infer_distribution_hypercube": (
240
+ lambda graphs: graphs + [tr.infer_distribution_hypercube(graphs)]
241
+ ),
242
+ "infer_nodegroup_distributions": (
243
+ lambda graphs: tr.infer_nodegroup_distributions(graphs) + graphs
244
+ ),
245
+ "extract_materials": tr.extract_materials_from_graphs,
246
+ # "infer_distribution_polytope": lambda graphs: graphs + [tr.infer_distribution_polytope(graphs)],
247
+ }
248
+
249
+
250
+ def main():
251
+ parser = get_parser()
252
+ args = parser.parse_args()
253
+
254
+ for name in logging.root.manager.loggerDict:
255
+ logging.getLogger(name).setLevel(args.loglevel)
256
+
257
+ pf.ops.file.load_blend(args.input)
258
+
259
+ targets = _targets_from_args(args)
260
+
261
+ if len(targets) == 0:
262
+ raise ValueError(
263
+ f"No targets found for {args.input} {args.materials=} {args.objects=} {args.node_trees=}"
264
+ )
265
+
266
+ logger.info(f"Found targets {[x.name for x in targets]}")
267
+
268
+ transforms = [_transforms_map[x] for x in args.transforms]
269
+ python = transpile_targets(
270
+ targets,
271
+ transforms,
272
+ object_mode=args.object_mode,
273
+ add_version_comment=not args.no_version_comments,
274
+ add_line_comments=args.add_line_comments,
275
+ include_set_material=args.include_object_materials,
276
+ )
277
+
278
+ match args.output:
279
+ case "print":
280
+ print(python)
281
+ case None:
282
+ pass
283
+ case x if Path(x).suffix == ".py":
284
+ x = Path(x)
285
+ x.parent.mkdir(parents=True, exist_ok=True)
286
+ print(f"Writing transpiled code to path {x}")
287
+ with x.open("w") as f:
288
+ f.write(python)
289
+ case x if ":" in x:
290
+ raise NotImplementedError("Not implemented")
291
+ module_path, target_name = x.split(":")
292
+ inject_into_python_file(python.splitlines(), Path(module_path), target_name)
293
+ case _:
294
+ raise ValueError(f"Invalid output type: {args.output}")
295
+
296
+
297
+ if __name__ == "__main__":
298
+ with skip_teardown_on_exit():
299
+ main()
procfunc/types.py ADDED
@@ -0,0 +1,380 @@
1
+ import logging
2
+ from collections import defaultdict
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Any, Callable, Generic, NamedTuple, TypeAlias, TypeVar
6
+
7
+ import bpy
8
+ import mathutils
9
+
10
+ from procfunc.util.pytree import register_pytree_container
11
+
12
+ T = TypeVar("T")
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ __all__ = [
17
+ "Asset",
18
+ "BlenderAsset",
19
+ "ObjectType",
20
+ "Object",
21
+ "CameraObject",
22
+ "MeshObject",
23
+ "CurveObject",
24
+ "EmptyObject",
25
+ "ArmatureObject",
26
+ "HairObject",
27
+ "LatticeObject",
28
+ "LightObject",
29
+ "LightProbeObject",
30
+ "MetaObject",
31
+ "Material",
32
+ "Texture",
33
+ "Image",
34
+ "Collection",
35
+ "VolumeObject",
36
+ "PointCloudObject",
37
+ "World",
38
+ "ValueRange",
39
+ ]
40
+
41
+ INVALIDATED_USAGE_ID = -100
42
+
43
+
44
+ @dataclass
45
+ class AssetUsageTable:
46
+ counts: dict[int, int] = field(default_factory=lambda: defaultdict(int))
47
+
48
+
49
+ _global_usage_table = AssetUsageTable()
50
+
51
+
52
+ def _bpy_data_col_for_asset(item: T) -> bpy.types.bpy_prop_collection:
53
+ match item.bl_rna.name:
54
+ case "Object":
55
+ return bpy.data.objects
56
+ case "Material":
57
+ return bpy.data.materials
58
+ case "Image Texture":
59
+ return bpy.data.textures
60
+ case _:
61
+ raise TypeError(
62
+ f"{BlenderAsset.__name__} doesnt yet support: {item} with type {item.bl_rna.name}"
63
+ )
64
+
65
+
66
+ class Asset(Generic[T]):
67
+ def item(self) -> T:
68
+ raise NotImplementedError("Subclasses of Asset must implement item()")
69
+
70
+
71
+ class BlenderAsset(Asset, Generic[T]):
72
+ """
73
+ A pythonic wrapper around a blender object,material,texture, or other bpy.data.stuffgoeshere member.
74
+
75
+ We reference count the underlying asset and delete it when no python references remain.
76
+ """
77
+
78
+ def __init__(self, item: T):
79
+ if isinstance(item, Asset):
80
+ raise ValueError(
81
+ f"Attempted to wrap {item=} in a second {self.__class__.__name__} wrapper. This is almost certainly not intended"
82
+ )
83
+
84
+ self._item = item
85
+ _global_usage_table.counts[id(item)] += 1
86
+ if logger.isEnabledFor(logging.DEBUG):
87
+ logger.debug(
88
+ f"Asset ref {id(self)} created for {self._item.name}. remaining refs: {_global_usage_table.counts[id(self._item)]}"
89
+ )
90
+
91
+ # self._dependencies: list[Asset] = []
92
+
93
+ def item(self) -> T:
94
+ if _global_usage_table.counts[id(self._item)] == INVALIDATED_USAGE_ID:
95
+ raise ValueError(
96
+ "Cant access .item() since asset has been explicitly invalidated by a previous operation"
97
+ )
98
+
99
+ return self._item
100
+
101
+
102
+ '''
103
+ def add_dependency(self, dependency: "Asset"):
104
+ self._dependencies.append(dependency)
105
+
106
+ def extend_dependencies(self, other: "Asset"):
107
+ self._dependencies.extend(other._dependencies)
108
+
109
+ def update(self, other: "Asset"):
110
+ """
111
+ Updates this asset to point to the same underlying item as other.
112
+ """
113
+
114
+ self._item = other._item
115
+ self._dependencies = other._dependencies
116
+ _global_usage_table.counts[id(self._item)] += 1
117
+
118
+ def invalidate(self):
119
+ self._item = None
120
+ self._dependencies = []
121
+ _global_usage_table.counts[id(self._item)] = INVALIDATED_USAGE_ID
122
+
123
+ def __del__(self):
124
+ # return
125
+
126
+ if self._item is None:
127
+ return
128
+ return # TODO
129
+ item_id = id(self._item)
130
+ count = _global_usage_table.counts.get(item_id, 0)
131
+
132
+ count -= 1
133
+
134
+ # new bad ref counter
135
+ if count < 0:
136
+ raise ValueError(
137
+ f"Asset ref {id(self)} reached negative ref count for item {self._item.name}"
138
+ )
139
+
140
+ _global_usage_table.counts[item_id] = count
141
+
142
+ if count == 0:
143
+ try:
144
+ bpy_col = _bpy_data_col_for_asset(self._item)
145
+ if self._item.name in bpy_col:
146
+ bpy_col.remove(self._item, do_unlink=True)
147
+ except ReferenceError:
148
+ logger.warning(
149
+ f"{self.__class__.__name__} __del__ failed - item already deleted"
150
+ )
151
+
152
+ del _global_usage_table.counts[item_id]
153
+
154
+ self._item = None
155
+ '''
156
+
157
+
158
+ class ObjectType(Enum):
159
+ ARMATURE = "ARMATURE"
160
+ CAMERA = "CAMERA"
161
+ CURVE = "CURVE"
162
+ EMPTY = "EMPTY"
163
+ FONT = "FONT"
164
+ HAIR = "HAIR"
165
+ LATTICE = "LATTICE"
166
+ LIGHT = "LIGHT"
167
+ LIGHT_PROBE = "LIGHT_PROBE"
168
+ MESH = "MESH"
169
+ META = "META"
170
+ POINTCLOUD = "POINTCLOUD"
171
+ SURFACE = "SURFACE"
172
+ VOLUME = "VOLUME"
173
+ # GPENCIL = "GPENCIL"
174
+ # GREASEPENCIL = "GREASEPENCIL"
175
+ # SPEAKER = "SPEAKER"
176
+
177
+
178
+ Vector: TypeAlias = mathutils.Vector
179
+ Color: TypeAlias = mathutils.Color
180
+ Euler: TypeAlias = mathutils.Euler
181
+ Quaternion: TypeAlias = mathutils.Quaternion
182
+ Matrix: TypeAlias = mathutils.Matrix
183
+ BVHTree: TypeAlias = mathutils.bvhtree.BVHTree
184
+
185
+ NodeGroup: TypeAlias = bpy.types.NodeGroup
186
+ Scene: TypeAlias = bpy.types.Scene
187
+ ViewLayer: TypeAlias = bpy.types.ViewLayer
188
+
189
+
190
+ class Object(BlenderAsset[bpy.types.Object]):
191
+ def __init__(self, obj: bpy.types.Object):
192
+ assert isinstance(obj, bpy.types.Object)
193
+ super().__init__(obj)
194
+
195
+ def __repr__(self):
196
+ return f"pf.{self.__class__.__name__}(bpy.data.objects[{self._item.name!r}])"
197
+
198
+ def clone(self):
199
+ new_obj = self._item.copy()
200
+ new_obj.data = self._item.data.copy()
201
+ bpy.context.collection.objects.link(new_obj)
202
+ return self.__class__(new_obj)
203
+
204
+
205
+ class CameraObject(Object):
206
+ def __init__(self, obj: bpy.types.Object):
207
+ assert obj.type == ObjectType.CAMERA.value
208
+ super().__init__(obj)
209
+
210
+
211
+ class MeshObject(Object):
212
+ def __init__(self, obj: bpy.types.Object):
213
+ assert obj.type == ObjectType.MESH.value
214
+ super().__init__(obj)
215
+
216
+ if len(obj.children) > 0:
217
+ raise ValueError(
218
+ f"MeshObject {obj.name} had children {obj.children}, but this is not allowed for {self.__class__.__name__}"
219
+ )
220
+ if obj.parent is not None:
221
+ logger.warning(f"MeshObject {obj.name} had a parent {obj.parent}")
222
+
223
+
224
+ class CurveObject(Object):
225
+ def __init__(self, obj: bpy.types.Object):
226
+ assert obj.type == ObjectType.CURVE.value
227
+ super().__init__(obj)
228
+
229
+
230
+ class EmptyObject(Object):
231
+ def __init__(self, obj: bpy.types.Object):
232
+ assert obj.type == ObjectType.EMPTY.value
233
+ super().__init__(obj)
234
+
235
+
236
+ class ArmatureObject(Object):
237
+ def __init__(self, obj: bpy.types.Object):
238
+ assert obj.type == ObjectType.ARMATURE.value
239
+ super().__init__(obj)
240
+
241
+
242
+ class HairObject(Object):
243
+ def __init__(self, obj: bpy.types.Object):
244
+ assert obj.type == ObjectType.HAIR.value
245
+ super().__init__(obj)
246
+
247
+
248
+ class LatticeObject(Object):
249
+ def __init__(self, obj: bpy.types.Object):
250
+ assert obj.type == ObjectType.LATTICE.value
251
+ super().__init__(obj)
252
+
253
+
254
+ class LightObject(Object):
255
+ def __init__(self, obj: bpy.types.Object):
256
+ assert obj.type == ObjectType.LIGHT.value
257
+ super().__init__(obj)
258
+
259
+
260
+ class LightProbeObject(Object):
261
+ def __init__(self, obj: bpy.types.Object):
262
+ assert obj.type == ObjectType.LIGHT_PROBE.value
263
+ super().__init__(obj)
264
+
265
+
266
+ class MetaObject(Object):
267
+ def __init__(self, obj: bpy.types.Object):
268
+ assert obj.type == ObjectType.META.value
269
+ super().__init__(obj)
270
+
271
+
272
+ @dataclass
273
+ class Material:
274
+ surface: Any = None
275
+ displacement: Any = None
276
+ volume: Any = None
277
+ _bpy_material: Any = field(default=None, init=False, repr=False)
278
+
279
+ def item(self) -> bpy.types.Material:
280
+ if self._bpy_material is None:
281
+ from procfunc.nodes.execute.execute import _build_bpy_material
282
+
283
+ self._bpy_material = _build_bpy_material(
284
+ surface=self.surface,
285
+ displacement=self.displacement,
286
+ volume=self.volume,
287
+ )
288
+ return self._bpy_material
289
+
290
+
291
+ class Texture(BlenderAsset[bpy.types.Texture]):
292
+ def __init__(self, tex: bpy.types.Texture):
293
+ assert isinstance(tex, bpy.types.Texture)
294
+ super().__init__(tex)
295
+
296
+
297
+ class Image(BlenderAsset[bpy.types.Image]):
298
+ def __init__(self, img: bpy.types.Image):
299
+ assert isinstance(img, bpy.types.Image)
300
+ super().__init__(img)
301
+
302
+
303
+ class Collection:
304
+ def __init__(
305
+ self, objects: "list[Object] | bpy.types.Collection", name: str = "collection"
306
+ ):
307
+ if isinstance(objects, bpy.types.Collection):
308
+ self._collection = objects
309
+ self._objects = [Object(o) for o in objects.objects]
310
+ else:
311
+ self._objects = objects
312
+ self._collection = bpy.data.collections.new(name=name)
313
+ for obj in objects:
314
+ self._collection.objects.link(obj.item())
315
+
316
+ def __repr__(self):
317
+ return f"pf.Collection(bpy.data.collections[{self._collection.name!r}])"
318
+
319
+ def map(
320
+ self,
321
+ fn: Callable[[Object], Object],
322
+ skip_none: bool = True,
323
+ ) -> "Collection":
324
+ objs = []
325
+ for obj in self._objects:
326
+ result = fn(obj)
327
+ if result is None and skip_none:
328
+ continue
329
+ objs.append(result)
330
+ return Collection(objs, name=self._collection.name + "_" + fn.__name__)
331
+
332
+ def __len__(self):
333
+ return len(self._objects)
334
+
335
+ def __iter__(self):
336
+ return iter(self._objects)
337
+
338
+ def item(self) -> bpy.types.Collection:
339
+ return self._collection
340
+
341
+
342
+ class VolumeObject(BlenderAsset[bpy.types.Volume]):
343
+ def __init__(self, vol: bpy.types.Volume):
344
+ assert isinstance(vol, bpy.types.Volume)
345
+ super().__init__(vol)
346
+
347
+
348
+ class PointCloudObject(BlenderAsset[bpy.types.PointCloud]):
349
+ def __init__(self, vol: bpy.types.PointCloud):
350
+ assert isinstance(vol, bpy.types.PointCloud)
351
+ super().__init__(vol)
352
+
353
+
354
+ class World(BlenderAsset[bpy.types.World]):
355
+ def __init__(self, world: bpy.types.World):
356
+ assert isinstance(world, bpy.types.World)
357
+ super().__init__(world)
358
+
359
+ def __del__(self):
360
+ pass # no cleanup for these
361
+
362
+
363
+ # TODO Font, PointCloud, Surface, Volume, Scene? World? ViewLayer?
364
+
365
+ TRangeType = TypeVar("TRangeType")
366
+
367
+
368
+ class ValueRange(NamedTuple, Generic[TRangeType]):
369
+ min: TRangeType | None
370
+ max: TRangeType | None
371
+
372
+
373
+ register_pytree_container(
374
+ Material,
375
+ flatten_func=lambda m: ([m.surface, m.displacement, m.volume], None),
376
+ unflatten_func=lambda vals, _: Material(
377
+ surface=vals[0], displacement=vals[1], volume=vals[2]
378
+ ),
379
+ names_func=lambda m: ["surface", "displacement", "volume"],
380
+ )
File without changes