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
procfunc/ops/addons.py ADDED
@@ -0,0 +1,59 @@
1
+ import logging
2
+
3
+ import addon_utils
4
+ import bpy
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def require_blender_addon(addon: str, fail: str = "fatal", allow_online=False):
10
+ def report_fail(msg):
11
+ if fail == "warn":
12
+ logger.warning(
13
+ msg + ". Generation may crash at runtime if certain assets are used."
14
+ )
15
+ elif fail == "fatal":
16
+ raise ValueError(msg)
17
+ else:
18
+ raise ValueError(
19
+ f"{require_blender_addon.__name__} got unrecognized {fail=}"
20
+ )
21
+
22
+ long = f"bl_ext.blender_org.{addon}"
23
+
24
+ addon_present = long in bpy.context.preferences.addons.keys()
25
+
26
+ if addon_present:
27
+ logger.debug(f"Addon {addon} already present.")
28
+ return True
29
+
30
+ builtin_local_addons = set(a.__name__ for a in addon_utils.modules(refresh=True))
31
+
32
+ if (
33
+ (addon not in builtin_local_addons)
34
+ and (long not in builtin_local_addons)
35
+ and (not allow_online)
36
+ ):
37
+ report_fail(f"{addon=} not found and online install is disabled")
38
+
39
+ try:
40
+ if long in builtin_local_addons:
41
+ logger.info(
42
+ f"Addon {addon} already in blender local addons, attempt to enable it."
43
+ )
44
+ bpy.ops.preferences.addon_enable(module=long)
45
+ else:
46
+ bpy.ops.extensions.userpref_allow_online()
47
+ logger.info(f"Installing Add-on {addon}.")
48
+ bpy.ops.extensions.repo_sync(repo_index=0)
49
+ bpy.ops.extensions.package_install(
50
+ repo_index=0, pkg_id=addon, enable_on_install=True
51
+ )
52
+ bpy.ops.preferences.addon_enable(module=long)
53
+ except Exception as e:
54
+ report_fail(f"Failed to install {addon=} due to {e=}")
55
+
56
+ if long not in bpy.context.preferences.addons.keys():
57
+ report_fail(f"Attempted to install {addon=} but wasnt found after install")
58
+
59
+ return True
procfunc/ops/attr.py ADDED
@@ -0,0 +1,426 @@
1
+ from typing import Literal
2
+
3
+ import bpy
4
+ import numpy as np
5
+
6
+ from procfunc import types as t
7
+ from procfunc.util import bpy_info
8
+
9
+
10
+ def read_attribute(
11
+ obj: t.MeshObject | t.CurveObject,
12
+ key: str,
13
+ domain: Literal["POINT", "EDGE", "FACE", "CORNER"] | None = None,
14
+ ) -> np.ndarray:
15
+ """
16
+ Read attribute data into a numpy array.
17
+
18
+ Args:
19
+ obj: Blender object (required if key is provided)
20
+ key: Attribute name (required if obj is provided)
21
+ domain: Attribute domain - POINT, EDGE, FACE, or CORNER. If None, allow any domain
22
+ attr: Blender attribute object (alternative to obj+key)
23
+
24
+ Returns:
25
+ numpy array containing the attribute data
26
+ """
27
+
28
+ attr = obj.item().data.attributes[key]
29
+
30
+ if attr.data_type in bpy_info.UNSUPPORTED_DATATYPES:
31
+ raise TypeError(f"Attribute {key} has unsupported data type {attr.data_type}")
32
+ if attr.domain in bpy_info.UNSUPPORTED_DOMAINS:
33
+ raise TypeError(f"Attribute {key} has unsupported domain {attr.domain}")
34
+
35
+ if domain is not None and domain != attr.domain:
36
+ raise ValueError(
37
+ f"Attribute {key} has domain {attr.domain}, requested {domain}"
38
+ )
39
+
40
+ n = len(attr.data)
41
+
42
+ dim = bpy_info.DATATYPE_DIMS[attr.data_type]
43
+ field = bpy_info.DATATYPE_FIELDS[attr.data_type]
44
+ result_dtype = bpy_info.DATATYPE_TO_PYTYPE[attr.data_type]
45
+
46
+ data = np.empty(n * dim, dtype=result_dtype)
47
+ attr.data.foreach_get(field, data)
48
+
49
+ if dim > 1:
50
+ data = data.reshape(-1, dim)
51
+
52
+ return data
53
+
54
+
55
+ def get_attribute(
56
+ obj: t.MeshObject | t.CurveObject,
57
+ key: str,
58
+ domain: Literal["POINT", "EDGE", "FACE", "CORNER"] | None = None,
59
+ ) -> np.ndarray | None:
60
+ """
61
+ Get attribute data from a Blender object.
62
+
63
+ Args:
64
+ obj: Blender object to read from
65
+ key: Attribute name to read
66
+ domain: Attribute domain - POINT, EDGE, FACE, or CORNER
67
+
68
+ Returns:
69
+ numpy array of attribute data, or None if attribute doesn't exist
70
+ """
71
+
72
+ if key not in obj.item().data.attributes:
73
+ return None
74
+
75
+ return read_attribute(obj, key, domain)
76
+
77
+
78
+ def _promote_data_type_for_shape(data_type: str, shape: tuple[int, ...]) -> str:
79
+ match data_type, shape:
80
+ case "FLOAT", (_x, 3):
81
+ return "FLOAT_VECTOR"
82
+ case "FLOAT", (_x, 2):
83
+ return "FLOAT2"
84
+ case "INT", (_x, 2):
85
+ return "INT32_2D"
86
+ case _, (_,):
87
+ return data_type
88
+ case _:
89
+ raise ValueError(
90
+ f"{_promote_data_type_for_shape.__name__} does not currently support {data_type=} {shape=}. "
91
+ "Please contact the developers if you believe this should be added."
92
+ )
93
+
94
+
95
+ def write_attribute(
96
+ obj: t.MeshObject | t.CurveObject,
97
+ data: np.ndarray | bool | int | float,
98
+ key: str,
99
+ domain: Literal["POINT", "EDGE", "FACE", "CORNER"],
100
+ overwrite: bool = False,
101
+ ):
102
+ """
103
+ Write numpy array data to a Blender object attribute.
104
+ """
105
+
106
+ obj = obj.item()
107
+
108
+ if not overwrite and key in obj.data.attributes:
109
+ raise ValueError(
110
+ f"Attribute {key} already exists for {obj.name=}, aborting due to kwarg {overwrite=}"
111
+ )
112
+
113
+ expected_count = {
114
+ "POINT": len(obj.data.vertices),
115
+ "EDGE": len(obj.data.edges),
116
+ "FACE": len(obj.data.polygons),
117
+ }
118
+
119
+ if isinstance(data, (bool, int, float)):
120
+ if domain in expected_count:
121
+ data = np.full((expected_count[domain],), data, dtype=type(data))
122
+
123
+ data_type = bpy_info.PYTYPE_TO_DATATYPE.get(data.dtype)
124
+ if data_type is None:
125
+ raise ValueError(
126
+ f"{write_attribute.__name__} does not currently support {data.dtype}, "
127
+ f"understood dtype to bpy mappings are {bpy_info.PYTYPE_TO_DATATYPE.keys()}. "
128
+ "Please contact the developers if you believe this should be added."
129
+ )
130
+
131
+ data_type = _promote_data_type_for_shape(data_type, data.shape)
132
+ dim = bpy_info.DATATYPE_DIMS[data_type]
133
+
134
+ if domain in expected_count:
135
+ expected_shape = (
136
+ (expected_count[domain],) if dim == 1 else (expected_count[domain], dim)
137
+ )
138
+ if data.shape != expected_shape:
139
+ raise ValueError(
140
+ f"{write_attribute.__name__} expects data of shape {expected_shape} "
141
+ f"for {domain=} with {data_type=}, got {data.shape}"
142
+ )
143
+
144
+ field = bpy_info.DATATYPE_FIELDS.get(data_type)
145
+ if field is None:
146
+ raise ValueError(
147
+ f"{write_attribute.__name__} does not currently support {data_type}, allowed are {bpy_info.DATATYPE_FIELDS.keys()}"
148
+ )
149
+
150
+ if overwrite and key in obj.data.attributes:
151
+ attr = obj.data.attributes[key]
152
+ else:
153
+ attr = obj.data.attributes.new(key, data_type, domain)
154
+
155
+ try:
156
+ attr.data.foreach_set(field, data.reshape(-1))
157
+ except RuntimeError as e:
158
+ raise RuntimeError(
159
+ f"Blender failed to write attribute {key} for {obj.name=} with {data_type=} {domain=} from {data.shape=} {data.dtype=}"
160
+ ) from e
161
+
162
+
163
+ def _transform_points(points_N3: np.ndarray, matrix: np.ndarray) -> np.ndarray:
164
+ points_homogeneous = np.empty((points_N3.shape[0], 4), dtype=points_N3.dtype)
165
+ points_homogeneous[:, :3] = points_N3
166
+ points_homogeneous[:, 3] = 1
167
+ points = points_homogeneous @ matrix.T
168
+ return points[:, :3]
169
+
170
+
171
+ def local_to_world(obj: t.Object, points_N3: np.ndarray) -> np.ndarray:
172
+ mat = np.array(obj.item().matrix_world)
173
+ return _transform_points(points_N3, mat)
174
+
175
+
176
+ def world_to_local(obj: t.Object, points_N3: np.ndarray) -> np.ndarray:
177
+ mat = np.array(obj.item().matrix_world.inverted())
178
+ return _transform_points(points_N3, mat)
179
+
180
+
181
+ def vertex_positions(obj: t.MeshObject, global_coords: bool = False) -> np.ndarray:
182
+ """
183
+ Read vertex positions from a Blender object.
184
+
185
+ Args:
186
+ obj: Blender mesh object
187
+ global_coords: If True, return global coordinates, otherwise return local coordinates
188
+ """
189
+
190
+ pos = np.zeros(len(obj.item().data.vertices) * 3)
191
+ obj.item().data.vertices.foreach_get("co", pos)
192
+ pos = pos.reshape(-1, 3)
193
+ if global_coords:
194
+ pos = local_to_world(obj, pos)
195
+ return pos
196
+
197
+
198
+ def write_vertex_positions(
199
+ obj: t.MeshObject, pos: np.ndarray, global_coords: bool = False
200
+ ):
201
+ if pos.shape != (len(obj.item().data.vertices), 3):
202
+ raise ValueError(
203
+ f"{write_vertex_positions.__name__} expects pos to be of shape (N, 3), "
204
+ f"got {pos.shape} for {obj.item().name=} with {len(obj.item().data.vertices)=}"
205
+ )
206
+ if global_coords:
207
+ pos = world_to_local(obj, pos)
208
+ obj.item().data.vertices.foreach_set("co", pos.reshape(-1))
209
+
210
+
211
+ def edge_indices(obj: t.MeshObject) -> np.ndarray:
212
+ arr = np.zeros(len(obj.item().data.edges) * 2, dtype=int)
213
+ obj.item().data.edges.foreach_get("vertices", arr)
214
+ return arr.reshape(-1, 2)
215
+
216
+
217
+ def polygon_centers(obj: t.MeshObject) -> np.ndarray:
218
+ arr = np.zeros(len(obj.item().data.polygons) * 3)
219
+ obj.item().data.polygons.foreach_get("center", arr)
220
+ return arr.reshape(-1, 3)
221
+
222
+
223
+ def polygon_normals(obj: t.MeshObject) -> np.ndarray:
224
+ arr = np.zeros(len(obj.item().data.polygons) * 3)
225
+ obj.item().data.polygons.foreach_get("normal", arr)
226
+ return arr.reshape(-1, 3)
227
+
228
+
229
+ def polygon_areas(obj: t.MeshObject) -> np.ndarray:
230
+ arr = np.zeros(len(obj.item().data.polygons))
231
+ obj.item().data.polygons.foreach_get("area", arr)
232
+ return arr.reshape(-1)
233
+
234
+
235
+ def polygon_loop_totals(obj: t.MeshObject) -> np.ndarray:
236
+ arr = np.zeros(len(obj.item().data.polygons))
237
+ obj.item().data.polygons.foreach_get("loop_total", arr)
238
+ return arr.reshape(-1)
239
+
240
+
241
+ def polygon_vertex_indices(
242
+ obj: t.MeshObject,
243
+ vertex_per_polygon: int,
244
+ safe: bool = True,
245
+ ) -> np.ndarray:
246
+ """
247
+ Get polygon vertex indices from a Blender object.
248
+
249
+ Args:
250
+ obj: Object
251
+ vertex_per_polygon: Number of vertices per polygon in the mesh,
252
+ e.g. 3 if you expect only triangles, 4 if you expect quads, etc.
253
+
254
+ TODO: explicitly check / validate that all polygons have the expected number of vertices
255
+
256
+ Returns:
257
+ Array of shape (num_polygons, vertex_per_polygon)
258
+ """
259
+
260
+ assert vertex_per_polygon >= 3
261
+
262
+ if safe:
263
+ for polygon in obj.item().data.polygons:
264
+ if len(polygon.vertices) != vertex_per_polygon:
265
+ raise ValueError(
266
+ f"Polygon {polygon.index} has {len(polygon.vertices)} vertices, expected {vertex_per_polygon}"
267
+ )
268
+
269
+ arr = np.zeros(len(obj.item().data.polygons) * vertex_per_polygon, dtype=int)
270
+ obj.item().data.polygons.foreach_get("vertices", arr)
271
+ return arr.reshape(-1, vertex_per_polygon)
272
+
273
+
274
+ def uv_coords(obj: t.MeshObject, layer: int | None = None) -> np.ndarray:
275
+ uv_layer = (
276
+ obj.item().data.uv_layers[layer]
277
+ if isinstance(layer, int)
278
+ else obj.item().data.uv_layers.active
279
+ )
280
+ arr = np.zeros(len(obj.item().data.loops) * 2)
281
+ uv_layer.data.foreach_get("uv", arr)
282
+ return arr.reshape(-1, 2)
283
+
284
+
285
+ def write_uv_coords(obj: t.MeshObject, uv: np.ndarray, layer: int | None = None):
286
+ uv_layer = (
287
+ obj.item().data.uv_layers[layer]
288
+ if isinstance(layer, int)
289
+ else obj.item().data.uv_layers.active
290
+ )
291
+
292
+ if uv.shape != (len(obj.item().data.loops), 2):
293
+ raise ValueError(
294
+ f"{write_uv_coords.__name__} expects uv to be of shape (N, 2), "
295
+ f"got {uv.shape} for {obj.item().name=} with {len(obj.item().data.loops)=}"
296
+ )
297
+
298
+ uv_layer.data.foreach_set("uv", uv.reshape(-1))
299
+
300
+
301
+ def uv_coords_new(obj: t.MeshObject, name: str, do_init: bool = True):
302
+ obj.item().data.uv_layers.new(name=name, do_init=do_init)
303
+
304
+
305
+ def bbox_corners(
306
+ obj: t.Object,
307
+ global_coords: bool = True,
308
+ ) -> np.ndarray:
309
+ """
310
+ Get bounding box corners.
311
+
312
+ Args:
313
+ obj: Blender mesh object
314
+ global_coords: If True, return global coordinates, otherwise return local coordinates
315
+
316
+ Returns:
317
+ Nx3 array of vertex positions. For global_coords=True, returns actual world-space
318
+ vertex positions (tight bbox). For global_coords=False, returns the 8 local bound_box corners.
319
+ """
320
+
321
+ depsgraph = bpy.context.evaluated_depsgraph_get()
322
+ obj_eval = obj.item().evaluated_get(depsgraph)
323
+
324
+ if not global_coords:
325
+ return np.array(obj_eval.bound_box)
326
+
327
+ bbox = np.array(obj_eval.bound_box)
328
+ if global_coords:
329
+ mat = np.array(obj_eval.matrix_world)
330
+ ones = np.ones((bbox.shape[0], 1))
331
+ bbox = (mat @ np.hstack([bbox, ones]).T).T[:, :3]
332
+ return bbox
333
+
334
+
335
+ def bbox_min_max(
336
+ obj: t.Object,
337
+ global_coords: bool = True,
338
+ ) -> tuple[np.ndarray, np.ndarray]:
339
+ bbox = bbox_corners(obj, global_coords=global_coords)
340
+ return bbox.min(axis=0), bbox.max(axis=0)
341
+
342
+
343
+ def edge_lengths(obj: t.MeshObject) -> np.ndarray:
344
+ cos = vertex_positions(obj)[edge_indices(obj).reshape(-1)].reshape(-1, 2, 3)
345
+ return np.linalg.norm(cos[:, 1] - cos[:, 0], axis=-1)
346
+
347
+
348
+ def edge_centers(obj: t.MeshObject) -> np.ndarray:
349
+ cos = vertex_positions(obj)[edge_indices(obj).reshape(-1)].reshape(-1, 2, 3)
350
+ return (cos[:, 1] + cos[:, 0]) / 2
351
+
352
+
353
+ def edge_directions(obj: t.MeshObject) -> np.ndarray:
354
+ cos = vertex_positions(obj)[edge_indices(obj).reshape(-1)].reshape(-1, 2, 3)
355
+ diff = cos[:, 1] - cos[:, 0]
356
+ norms = np.linalg.norm(diff, axis=-1, keepdims=True)
357
+ norms = np.where(norms == 0, 1.0, norms)
358
+ return diff / norms
359
+
360
+
361
+ def loop_starts(obj: t.MeshObject) -> np.ndarray:
362
+ arr = np.zeros(len(obj.item().data.polygons), dtype=int)
363
+ obj.item().data.polygons.foreach_get("loop_start", arr)
364
+ return arr
365
+
366
+
367
+ def loop_totals(obj: t.MeshObject) -> np.ndarray:
368
+ arr = np.zeros(len(obj.item().data.polygons), dtype=int)
369
+ obj.item().data.polygons.foreach_get("loop_total", arr)
370
+ return arr
371
+
372
+
373
+ def loop_edge_indices(obj: t.MeshObject) -> np.ndarray:
374
+ arr = np.zeros(len(obj.item().data.loops), dtype=int)
375
+ obj.item().data.loops.foreach_get("edge_index", arr)
376
+ return arr.reshape(-1)
377
+
378
+
379
+ def loop_vertex_indices(obj: t.MeshObject) -> np.ndarray:
380
+ arr = np.zeros(len(obj.item().data.loops), dtype=int)
381
+ obj.item().data.loops.foreach_get("vertex_index", arr)
382
+ return arr.reshape(-1)
383
+
384
+
385
+ def material_index(obj: t.MeshObject) -> np.ndarray:
386
+ arr = np.zeros(len(obj.item().data.polygons), dtype=int)
387
+ obj.item().data.polygons.foreach_get("material_index", arr)
388
+ return arr
389
+
390
+
391
+ def write_material_index(
392
+ obj: t.MeshObject, index: int, face_mask: np.ndarray | None = None
393
+ ) -> None:
394
+ if face_mask is None:
395
+ face_mask = np.ones(len(obj.item().data.polygons), dtype=bool)
396
+ arr = np.where(face_mask, index, material_index(obj))
397
+ obj.item().data.polygons.foreach_set("material_index", arr)
398
+
399
+
400
+ __all__ = [
401
+ "get_attribute",
402
+ "read_attribute",
403
+ "write_attribute",
404
+ "local_to_world",
405
+ "world_to_local",
406
+ "vertex_positions",
407
+ "write_vertex_positions",
408
+ "edge_indices",
409
+ "polygon_centers",
410
+ "polygon_normals",
411
+ "polygon_areas",
412
+ "uv_coords",
413
+ "write_uv_coords",
414
+ "uv_coords_new",
415
+ "bbox_corners",
416
+ "bbox_min_max",
417
+ "edge_lengths",
418
+ "edge_centers",
419
+ "edge_directions",
420
+ "loop_starts",
421
+ "loop_totals",
422
+ "loop_edge_indices",
423
+ "loop_vertex_indices",
424
+ "material_index",
425
+ "write_material_index",
426
+ ]
@@ -0,0 +1,90 @@
1
+ from typing import Callable, List, Optional
2
+
3
+ import bpy
4
+
5
+ import procfunc as pf
6
+ from procfunc import types as t
7
+
8
+
9
+ def _new_collection(name: str, reuse: bool = True) -> t.Collection:
10
+ """Create a new collection or reuse existing one with the same name."""
11
+ if reuse and name in bpy.data.collections:
12
+ return t.Collection(bpy.data.collections[name])
13
+ else:
14
+ col = bpy.data.collections.new(name=name)
15
+ bpy.context.scene.collection.children.link(col)
16
+ return t.Collection(col)
17
+
18
+
19
+ def _unlink_from_all(obj: bpy.types.Object) -> None:
20
+ """Remove object from all collections."""
21
+ for c in list(bpy.data.collections) + [bpy.context.scene.collection]:
22
+ if obj.name in c.objects:
23
+ c.objects.unlink(obj)
24
+
25
+
26
+ def _link_to_collection(
27
+ objs: bpy.types.Object | List[bpy.types.Object],
28
+ collection: bpy.types.Collection | str,
29
+ exclusive: bool = True,
30
+ ) -> t.Collection:
31
+ """Link objects to a collection."""
32
+ if isinstance(collection, str):
33
+ collection = _new_collection(collection)
34
+
35
+ if isinstance(objs, bpy.types.Object):
36
+ objs = [objs]
37
+ else:
38
+ objs = list(objs)
39
+
40
+ for o in objs:
41
+ if exclusive:
42
+ _unlink_from_all(o)
43
+ collection.objects.link(o) # type: ignore
44
+
45
+ return collection
46
+
47
+
48
+ def _traverse_children(
49
+ obj: bpy.types.Object,
50
+ fn: Callable[[bpy.types.Object], None],
51
+ ):
52
+ """Recursively traverse object children."""
53
+ fn(obj)
54
+ for child in obj.children:
55
+ _traverse_children(child, fn)
56
+
57
+
58
+ @pf.tracer.primitive
59
+ def group_objects(
60
+ *args: t.Object | t.Object | List[t.Object | t.Object],
61
+ name: Optional[str] = None,
62
+ ) -> t.Collection:
63
+ """Create a collection containing the specified objects and their children.
64
+
65
+ Args:
66
+ *args: Objects or lists of objects to include
67
+ name: Name of the collection
68
+
69
+ Returns:
70
+ Collection containing all objects
71
+ """
72
+ if name is None:
73
+ name = "Generated Collection"
74
+
75
+ col = bpy.data.collections.new(name)
76
+ bpy.context.scene.collection.children.link(col)
77
+
78
+ for obj in args:
79
+ if obj is None:
80
+ continue
81
+ if isinstance(obj, list):
82
+ for o in obj:
83
+ if o is not None:
84
+ _traverse_children(o.item(), lambda o: _link_to_collection(o, col))
85
+ elif isinstance(obj, t.Object):
86
+ _traverse_children(obj.item(), lambda o: _link_to_collection(o, col))
87
+ else:
88
+ raise ValueError(f"Invalid object type: {type(obj)}")
89
+
90
+ return t.Collection(col)
procfunc/ops/curve.py ADDED
@@ -0,0 +1,18 @@
1
+ import bpy
2
+
3
+ import procfunc as pf
4
+ from procfunc import types as t
5
+
6
+
7
+ @pf.tracer.primitive(mutates=["mutates_obj"])
8
+ def subdivide(
9
+ mutates_obj: t.CurveObject,
10
+ number_cuts: int = 1,
11
+ ) -> None:
12
+ """
13
+ Subdivide selected segments
14
+
15
+ Based on bpy.ops.curve.subdivide
16
+ """
17
+ bpy.context.view_layer.objects.active = mutates_obj.item()
18
+ bpy.ops.curve.subdivide(number_cuts=number_cuts)