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,347 @@
1
+ import copy
2
+ import inspect
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Any, Generic, TypeVar, Union
6
+
7
+ import bpy
8
+
9
+ from procfunc import compute_graph as cg
10
+ from procfunc import types as pt
11
+ from procfunc.compute_graph.operators_info import (
12
+ OPERATORS_TO_FUNCTIONS,
13
+ OperatorType,
14
+ )
15
+ from procfunc.util import pytree
16
+ from procfunc.util.manifest import module_path
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ INPUT_NODE_TYPE = "NodeGroupInput"
21
+ OUTPUT_NODE_TYPE = "NodeGroupOutput"
22
+ NODE_FUNCTION_INSTANCE_TYPE = "NodeFunctionInstance"
23
+
24
+ T = TypeVar("T")
25
+
26
+
27
+ PROCNODE_OPERATORS = {
28
+ OperatorType.ADD,
29
+ OperatorType.SUB,
30
+ OperatorType.MUL,
31
+ OperatorType.DIV,
32
+ OperatorType.POW,
33
+ OperatorType.MOD,
34
+ OperatorType.LESS_THAN,
35
+ OperatorType.LESS_THAN_EQUAL,
36
+ OperatorType.GREATER_THAN,
37
+ OperatorType.GREATER_THAN_EQUAL,
38
+ OperatorType.EQUAL,
39
+ OperatorType.NOT_EQUAL,
40
+ }
41
+
42
+
43
+ def _node_definition_metadata() -> tuple[str, int, str]:
44
+ """
45
+ Dig through this functions callstack to find user-space filename/line number, and the procfunc function name
46
+ """
47
+
48
+ procfunc_frame = inspect.currentframe().f_back # type: ignore
49
+ while (
50
+ module_path() in Path(procfunc_frame.f_back.f_code.co_filename).parents # type: ignore
51
+ ):
52
+ procfunc_frame = procfunc_frame.f_back # type: ignore
53
+ return (
54
+ procfunc_frame.f_back.f_code.co_filename, # type: ignore
55
+ procfunc_frame.f_back.f_lineno, # type: ignore
56
+ procfunc_frame.f_code.co_name, # type: ignore
57
+ )
58
+
59
+
60
+ def _has_unpreprocessed_inputs(node: cg.Node) -> bool:
61
+ any_args = any(isinstance(a, ProcNode) for a in node.args)
62
+ any_kwargs = any(isinstance(v, ProcNode) for v in node.kwargs.values())
63
+ return any_args or any_kwargs
64
+
65
+
66
+ class ProcNode(Generic[T]):
67
+ """
68
+ Result datatype for all functions that return shader nodes, geometry nodes or compositor nodes.
69
+
70
+ ProcNode stores the data necessary to construct a blender nodegroup upon later execution.
71
+
72
+ ProcNode defines dunders to allow concise construction of nodegraphs e.g. __getattr__ and __add__, which map to appropriate blender nodes.
73
+ """
74
+
75
+ def __init__(
76
+ self,
77
+ node: cg.Node,
78
+ known_value_type: type | None = None,
79
+ ):
80
+ self._node = node
81
+
82
+ if known_value_type is not None:
83
+ logger.debug(f"{self} using provided known_value_type={known_value_type}")
84
+ self._node.metadata["known_value_type"] = known_value_type
85
+
86
+ if _has_unpreprocessed_inputs(node):
87
+ raise ValueError(
88
+ f"{node=} has inputs which are ProcNode, "
89
+ f"these should have been unwrapped to cg.Node {node.args} {node.kwargs}"
90
+ )
91
+
92
+ self._node.metadata["definition"] = _node_definition_metadata()
93
+
94
+ def astype(self, dtype: type) -> "ProcNode":
95
+ """
96
+ Marks a node as having been converted to a different internal data type, similarly to np.astype
97
+
98
+ Currently this just adds runtime NodeType data to help subsequent type-inferred functions/operators
99
+ make a correct choice of data_type.
100
+
101
+ e.g noise.color + (0.5, 0.5, 0.5) fails but noise.color.astype(t.Vector) + (0.5, 0.5, 0.5) works,
102
+ because `+` is defined for Vector but not Color
103
+ """
104
+
105
+ node = copy.copy(self._node)
106
+ node.metadata = copy.copy(self._node.metadata)
107
+ node.metadata["known_value_type"] = dtype
108
+
109
+ logger.debug(f"{self}.astype() using provided known_value_type={dtype}")
110
+ return ProcNode(node)
111
+
112
+ def __repr__(self):
113
+ # NOTE: dont change this to be anything verbose, it may slow down system
114
+ # due to generating strings for debug logs (even if they arent actually printed)
115
+ return f"ProcNode({self.item()!r})"
116
+
117
+ @classmethod
118
+ def from_nodetype(
119
+ cls,
120
+ node_type: str,
121
+ inputs: dict[str, Any],
122
+ attrs: dict[str, Any],
123
+ ) -> "ProcNode":
124
+ def _unwrap(v: Any) -> cg.Node:
125
+ if isinstance(v, ProcNode):
126
+ return object.__getattribute__(v, "_node")
127
+ return v
128
+
129
+ inputs = pytree.PyTree(inputs).map(_unwrap).obj()
130
+
131
+ if any(isinstance(v, ProcNode) for v in attrs.values()):
132
+ raise ValueError(
133
+ f"Attrs {attrs} contains ProcNode, which is not allowed. Must specify a constant."
134
+ )
135
+
136
+ node = cg.ProceduralNode(node_type=node_type, attrs=attrs, kwargs=inputs)
137
+ node.metadata["definition"] = _node_definition_metadata()
138
+
139
+ return cls(node=node)
140
+
141
+ def item(self) -> cg.Node:
142
+ return object.__getattribute__(self, "_node")
143
+
144
+ def __post_init__(self):
145
+ for k, v in self.attrs.items():
146
+ if isinstance(v, (bpy.types.NodeInternal, ProcNode)):
147
+ raise ValueError(
148
+ f"Node {self.type} has a {k} attribute that is a Node, which is not allowed. Must specify a constant."
149
+ )
150
+
151
+ def _output_socket(self, name: str) -> "ProcNode":
152
+ node = cg.GetAttributeNode(source=self.item(), attribute_name=name)
153
+ return ProcNode(node)
154
+
155
+ def _procnode_operator(
156
+ self,
157
+ op: OperatorType,
158
+ lhs: "ProcNode[T]",
159
+ rhs: "ProcNode[T] | T",
160
+ reverse: bool = False,
161
+ ) -> "ProcNode[T]":
162
+ rhs_unwrap = rhs.item() if isinstance(rhs, ProcNode) else rhs
163
+
164
+ if reverse:
165
+ args = (rhs_unwrap, lhs.item())
166
+ else:
167
+ args = (lhs.item(), rhs_unwrap)
168
+
169
+ node = cg.FunctionCallNode(
170
+ func=OPERATORS_TO_FUNCTIONS[op],
171
+ args=args,
172
+ kwargs={},
173
+ metadata=None,
174
+ )
175
+ return ProcNode(node)
176
+
177
+ def _getattr_xyz(
178
+ self: "ProcNode[pt.Vector]",
179
+ name: str,
180
+ ) -> "ProcNode[float]":
181
+ sep = ProcNode.from_nodetype(
182
+ node_type="ShaderNodeSeparateXYZ",
183
+ inputs={"Vector": self},
184
+ attrs={},
185
+ )
186
+ return sep._output_socket(name)
187
+
188
+ @property
189
+ def x(self: "ProcNode[pt.Vector]") -> "ProcNode[float]":
190
+ return self._getattr_xyz(name="x")
191
+
192
+ @property
193
+ def y(self: "ProcNode[pt.Vector]") -> "ProcNode[float]":
194
+ return self._getattr_xyz(name="y")
195
+
196
+ @property
197
+ def z(self: "ProcNode[pt.Vector]") -> "ProcNode[float]":
198
+ return self._getattr_xyz(name="z")
199
+
200
+ def __add__(self, other: "ProcNode[T] | T | tuple") -> "ProcNode[T]":
201
+ return self._procnode_operator(OperatorType.ADD, self, other)
202
+
203
+ def __radd__(self, other: "ProcNode[T] | T | tuple") -> "ProcNode[T]":
204
+ return self._procnode_operator(OperatorType.ADD, self, other, reverse=True)
205
+
206
+ def __sub__(self, other: "ProcNode[T] | T | tuple") -> "ProcNode[T]":
207
+ return self._procnode_operator(OperatorType.SUB, self, other)
208
+
209
+ def __rsub__(self, other: "ProcNode[T] | T | tuple") -> "ProcNode[T]":
210
+ return self._procnode_operator(OperatorType.SUB, self, other, reverse=True)
211
+
212
+ def __mul__(self, other: "ProcNode[T] | T | tuple") -> "ProcNode[T]":
213
+ return self._procnode_operator(OperatorType.MUL, self, other)
214
+
215
+ def __rmul__(self, other: "ProcNode[T] | T | tuple") -> "ProcNode[T]":
216
+ return self._procnode_operator(OperatorType.MUL, self, other, reverse=True)
217
+
218
+ def __truediv__(self, other: "ProcNode[T] | T | tuple") -> "ProcNode[T]":
219
+ return self._procnode_operator(OperatorType.DIV, self, other)
220
+
221
+ def __rtruediv__(self, other: "ProcNode[T] | T | tuple") -> "ProcNode[T]":
222
+ return self._procnode_operator(OperatorType.DIV, self, other, reverse=True)
223
+
224
+ def __pow__(self, other: "ProcNode[T] | T") -> "ProcNode[T]":
225
+ return self._procnode_operator(OperatorType.POW, self, other)
226
+
227
+ def __rpow__(self, other: "ProcNode[T] | T") -> "ProcNode[T]":
228
+ return self._procnode_operator(OperatorType.POW, self, other, reverse=True)
229
+
230
+ def __mod__(self, other: "ProcNode[T] | T") -> "ProcNode[T]":
231
+ return self._procnode_operator(OperatorType.MOD, self, other)
232
+
233
+ def __rmod__(self, other: "ProcNode[T] | T") -> "ProcNode[T]":
234
+ return self._procnode_operator(OperatorType.MOD, self, other, reverse=True)
235
+
236
+ def __lt__(self, other: "ProcNode[T] | T") -> "ProcNode[T]":
237
+ return self._procnode_operator(OperatorType.LESS_THAN, self, other)
238
+
239
+ def __rlt__(self, other: "ProcNode[T] | T") -> "ProcNode[T]":
240
+ return self._procnode_operator(
241
+ OperatorType.LESS_THAN, self, other, reverse=True
242
+ )
243
+
244
+ def __le__(self, other: "ProcNode[T] | T") -> "ProcNode[T]":
245
+ return self._procnode_operator(OperatorType.LESS_THAN_EQUAL, self, other)
246
+
247
+ def __rle__(self, other: "ProcNode[T] | T") -> "ProcNode[T]":
248
+ return self._procnode_operator(
249
+ OperatorType.LESS_THAN_EQUAL, self, other, reverse=True
250
+ )
251
+
252
+ def __gt__(self, other: "ProcNode[T] | T") -> "ProcNode[T]":
253
+ return self._procnode_operator(OperatorType.GREATER_THAN, self, other)
254
+
255
+ def __rgt__(self, other: "ProcNode[T] | T") -> "ProcNode[T]":
256
+ return self._procnode_operator(
257
+ OperatorType.GREATER_THAN, self, other, reverse=True
258
+ )
259
+
260
+ def __ge__(self, other: "ProcNode[T] | T") -> "ProcNode[T]":
261
+ return self._procnode_operator(OperatorType.GREATER_THAN_EQUAL, self, other)
262
+
263
+ def __rge__(self, other: "ProcNode[T] | T") -> "ProcNode[T]":
264
+ return self._procnode_operator(
265
+ OperatorType.GREATER_THAN_EQUAL, self, other, reverse=True
266
+ )
267
+
268
+ def __eq__(self, other: "ProcNode[T] | T") -> "ProcNode[T]":
269
+ return self._procnode_operator(OperatorType.EQUAL, self, other)
270
+
271
+ def __req__(self, other: "ProcNode[T] | T") -> "ProcNode[T]":
272
+ return self._procnode_operator(OperatorType.EQUAL, self, other, reverse=True)
273
+
274
+ def __ne__(self, other: "ProcNode[T] | T") -> "ProcNode[T]":
275
+ return self._procnode_operator(OperatorType.NOT_EQUAL, self, other)
276
+
277
+ def __rne__(self, other: "ProcNode[T] | T") -> "ProcNode[T]":
278
+ return self._procnode_operator(
279
+ OperatorType.NOT_EQUAL, self, other, reverse=True
280
+ )
281
+
282
+
283
+ def node_definition_context_message(node: cg.Node):
284
+ metadata = object.__getattribute__(node, "metadata")
285
+ lineno_metadata = metadata.get("definition", None)
286
+ if lineno_metadata is None:
287
+ return ""
288
+ file, lineno, procfunc_name = lineno_metadata
289
+ return f" {procfunc_name}() call on {file}:{lineno} "
290
+
291
+
292
+ TSocketVal = TypeVar("TSocketVal")
293
+ SocketOrVal = Union[ProcNode[TSocketVal], TSocketVal]
294
+
295
+
296
+ class Instances:
297
+ pass
298
+
299
+
300
+ Points = Union[pt.MeshObject, pt.CurveObject]
301
+
302
+ Geometry = Union[pt.MeshObject, pt.CurveObject, Instances, pt.VolumeObject]
303
+
304
+ AnyShaderDataVal = Union[
305
+ pt.Vector,
306
+ pt.Color,
307
+ float,
308
+ ]
309
+
310
+ AnyDataVal = Union[AnyShaderDataVal, int, str, bool, pt.Matrix, pt.Quaternion]
311
+ """
312
+ Union of all types that are data-like in a geometrynodes context
313
+ IE pretty much all the geonodes sockettypes except specialones like Material or Object
314
+ """
315
+
316
+ AnyAssetVal = Union[
317
+ pt.Object,
318
+ pt.Collection,
319
+ pt.Material,
320
+ pt.Texture,
321
+ ]
322
+ """
323
+ Union of all types that are object-like in a geometrynodes context
324
+ """
325
+
326
+ AnyVal = Union[AnyDataVal, AnyAssetVal]
327
+
328
+
329
+ class Shader:
330
+ """
331
+ Used only for type-annotating nodes as returning a shader.
332
+
333
+ Anythnig that would be a green socket in a SHADER nodegraph should be ProcNode[Shader]
334
+ """
335
+
336
+ pass
337
+
338
+
339
+ __all__ = [
340
+ "ProcNode",
341
+ "Shader",
342
+ "SocketOrVal",
343
+ "AnyShaderDataVal",
344
+ "AnyDataVal",
345
+ "AnyAssetVal",
346
+ "node_definition_context_message",
347
+ ]
@@ -0,0 +1,35 @@
1
+ from pandas import read_json as _read_json
2
+
3
+ from procfunc.tracer import autowrap_module as _autowrap
4
+ from procfunc.util.manifest import module_path
5
+
6
+ from . import (
7
+ attr,
8
+ collection,
9
+ curve,
10
+ file,
11
+ mesh,
12
+ modifier,
13
+ object,
14
+ primitives,
15
+ uv,
16
+ )
17
+
18
+ _autowrap(collection)
19
+ _autowrap(attr)
20
+ _autowrap(curve)
21
+ _autowrap(primitives)
22
+
23
+ OPS_MANIFEST_PATH = module_path() / "ops" / "manifest.json"
24
+ OPS_MANIFEST = _read_json(OPS_MANIFEST_PATH)
25
+
26
+ __all__ = [
27
+ "collection",
28
+ "file",
29
+ "mesh",
30
+ "modifier",
31
+ "object",
32
+ "attr",
33
+ "curve",
34
+ "uv",
35
+ ]
procfunc/ops/_util.py ADDED
@@ -0,0 +1,275 @@
1
+ import logging
2
+ from typing import Any, Callable, Literal, TypeVar
3
+
4
+ import bpy
5
+ import numpy as np
6
+
7
+ from procfunc import types as t
8
+ from procfunc.util.bpy_info import bpy_nocollide_data_name
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def _parse_objs(
14
+ objs: t.Object | list[t.Object] | None, active: t.Object | None
15
+ ) -> tuple[list[bpy.types.Object], bpy.types.Object]:
16
+ if objs is None:
17
+ assert active is not None
18
+ objs = [active.item()]
19
+ elif isinstance(objs, t.Object):
20
+ objs = [objs.item()]
21
+ elif isinstance(objs, list):
22
+ objs = [obj.item() for obj in objs]
23
+ else:
24
+ raise TypeError(f"Unexpected type for objs: {type(objs)}")
25
+
26
+ if active is None:
27
+ assert len(objs) > 0
28
+ active = objs[0]
29
+ elif isinstance(active, t.Asset):
30
+ active = active.item()
31
+
32
+ if active not in objs:
33
+ objs.append(active)
34
+
35
+ assert active is not None
36
+ assert len(objs) > 0
37
+ assert not isinstance(active, t.Asset)
38
+
39
+ return objs, active
40
+
41
+
42
+ def _assert_op_finished(result: Any, name: str):
43
+ if (
44
+ not isinstance(result, set)
45
+ or len(result) != 1
46
+ or next(iter(result)) != "FINISHED"
47
+ ):
48
+ raise ValueError(f"{name} got status {result}, expected {'FINISHED'}")
49
+
50
+
51
+ def execute_object_op(
52
+ operator: Callable,
53
+ objs: t.Object | list[t.Object] | None = None,
54
+ active: t.Object | None = None,
55
+ description: str = "",
56
+ **kwargs: Any,
57
+ ):
58
+ assert not isinstance(active, bpy.types.Object)
59
+
60
+ kwargs = {k: v.item() if isinstance(v, t.Asset) else v for k, v in kwargs.items()}
61
+
62
+ objs, active = _parse_objs(objs, active)
63
+
64
+ # TODO: more efficient via temp_override?
65
+ bpy.ops.object.select_all(action="DESELECT")
66
+ bpy.context.view_layer.objects.active = active
67
+ bpy.ops.object.mode_set(mode="OBJECT")
68
+ for o in objs:
69
+ o.select_set(True)
70
+
71
+ result = operator(**kwargs)
72
+ _assert_op_finished(result, description)
73
+
74
+
75
+ def _apply_selection_masks(
76
+ obj: bpy.types.Object,
77
+ vertex_mask: np.ndarray | None = None,
78
+ edge_mask: np.ndarray | None = None,
79
+ face_mask: np.ndarray | None = None,
80
+ ):
81
+ """
82
+ Apply one selection mask of some domain to the object
83
+
84
+ If no masks are provided, all elements of the domain are selected.
85
+
86
+ NOTE: function must start AND end in EDIT mode
87
+ """
88
+
89
+ n_masks = sum(mask is not None for mask in [vertex_mask, edge_mask, face_mask])
90
+
91
+ if n_masks == 0:
92
+ logger.debug(f"No edit-masks provided for {obj.name}, selecting all")
93
+ bpy.ops.mesh.select_all(action="SELECT")
94
+ return
95
+ if n_masks > 1:
96
+ raise ValueError(
97
+ "Only one of vertex_mask, edge_mask, or face_mask can be provided"
98
+ )
99
+
100
+ bpy.ops.mesh.select_all(action="DESELECT")
101
+
102
+ # must select the type of mask we're applying, otherwise re-entering editmode will incorrectly
103
+ # convert face/edge masks into vertex masks, which is lossy.
104
+ select_type = (
105
+ "VERT"
106
+ if vertex_mask is not None
107
+ else "EDGE"
108
+ if edge_mask is not None
109
+ else "FACE"
110
+ )
111
+ bpy.ops.mesh.select_mode(type=select_type)
112
+
113
+ # foreach_set operations must run in object mode then switch back to edit mode to re-sync them
114
+ bpy.ops.object.mode_set(mode="OBJECT")
115
+
116
+ if vertex_mask is not None:
117
+ assert vertex_mask.shape == (len(obj.data.vertices),)
118
+ assert vertex_mask.dtype == bool
119
+ obj.data.vertices.foreach_set("select", vertex_mask)
120
+
121
+ if edge_mask is not None:
122
+ assert edge_mask.shape == (len(obj.data.edges),)
123
+ assert edge_mask.dtype == bool
124
+ obj.data.edges.foreach_set("select", edge_mask)
125
+
126
+ if face_mask is not None:
127
+ assert face_mask.shape == (len(obj.data.polygons),)
128
+ assert face_mask.dtype == bool
129
+ obj.data.polygons.foreach_set("select", face_mask)
130
+
131
+ bpy.ops.object.mode_set(mode="EDIT")
132
+
133
+
134
+ def extract_face_mask(
135
+ obj: t.MeshObject,
136
+ ) -> np.ndarray:
137
+ obj = obj.item()
138
+ face_mask = np.zeros(len(obj.data.polygons), dtype=bool)
139
+ obj.data.polygons.foreach_get("select", face_mask)
140
+ return face_mask
141
+
142
+
143
+ def extract_edge_mask(
144
+ obj: t.MeshObject,
145
+ ) -> np.ndarray:
146
+ obj = obj.item()
147
+ edge_mask = np.zeros(len(obj.data.edges), dtype=bool)
148
+ obj.data.edges.foreach_get("select", edge_mask)
149
+ return edge_mask
150
+
151
+
152
+ def extract_vertex_mask(
153
+ obj: t.MeshObject,
154
+ ) -> np.ndarray:
155
+ obj = obj.item()
156
+ vertex_mask = np.zeros(len(obj.data.vertices), dtype=bool)
157
+ obj.data.vertices.foreach_get("select", vertex_mask)
158
+ return vertex_mask
159
+
160
+
161
+ def execute_mesh_op(
162
+ operator: Callable,
163
+ obj: t.MeshObject,
164
+ vertex_mask: np.ndarray | None = None,
165
+ edge_mask: np.ndarray | None = None,
166
+ face_mask: np.ndarray | None = None,
167
+ empty_mask_mode: Literal["return", "execute", "error"] = "return",
168
+ description: str = "",
169
+ **kwargs: Any,
170
+ ):
171
+ obj = obj.item()
172
+
173
+ logger.debug(f"Executing {operator} on {obj.name=}")
174
+
175
+ empty_mask = next(
176
+ (
177
+ mask
178
+ for mask in [vertex_mask, edge_mask, face_mask]
179
+ if mask is not None and mask.sum() == 0
180
+ ),
181
+ None,
182
+ )
183
+ if empty_mask is not None and empty_mask_mode == "error":
184
+ raise ValueError(
185
+ f"Empty mask provided for {operator} on {obj.name=} with {empty_mask_mode=}"
186
+ )
187
+
188
+ # TODO: would rather do this with context overrides,
189
+ # but it messes up the active_object for cases like ops.mesh.separate()
190
+ bpy.ops.object.select_all(action="DESELECT")
191
+ bpy.context.view_layer.objects.active = obj
192
+ bpy.ops.object.mode_set(mode="OBJECT")
193
+ obj.select_set(True)
194
+
195
+ bpy.ops.object.mode_set(mode="EDIT")
196
+ _apply_selection_masks(obj, vertex_mask, edge_mask, face_mask)
197
+
198
+ try:
199
+ if empty_mask is not None:
200
+ if empty_mask_mode == "return":
201
+ return
202
+ elif empty_mask_mode == "execute":
203
+ pass
204
+ elif empty_mask_mode == "error":
205
+ raise ValueError(
206
+ f"Empty mask provided for {operator} on {obj.name=} with {empty_mask_mode=}"
207
+ )
208
+ else:
209
+ raise ValueError(f"Invalid empty_mask_mode: {empty_mask_mode}")
210
+
211
+ result = operator(**kwargs)
212
+ finally:
213
+ bpy.ops.object.mode_set(mode="OBJECT")
214
+
215
+ _assert_op_finished(result, description)
216
+
217
+ return result
218
+
219
+
220
+ TModifyObject = TypeVar("TModifyObject", t.MeshObject, t.CurveObject)
221
+
222
+
223
+ def modify(
224
+ obj: TModifyObject,
225
+ modifier_type: str,
226
+ setitem_keyvals: dict[str, Any] | None = None,
227
+ _skip_apply: bool = False,
228
+ **setattr_keyvals: Any,
229
+ ) -> TModifyObject:
230
+ if setitem_keyvals is None:
231
+ setitem_keyvals = {}
232
+
233
+ mod_name = bpy_nocollide_data_name(modifier_type, obj.item().modifiers)
234
+ modifier = obj.item().modifiers.new(mod_name, modifier_type)
235
+ if modifier is None:
236
+ raise ValueError(
237
+ "modifier.new() returned None, blender might not allow "
238
+ f"{modifier_type=} for {obj.item().type=}"
239
+ )
240
+ modifier.show_viewport = False
241
+
242
+ for key, value in setitem_keyvals.items():
243
+ assert not isinstance(value, t.Asset), (
244
+ "TODO may need to conver Assets to item for modifier[x] = y"
245
+ )
246
+ modifier[key] = value
247
+
248
+ objs = []
249
+ for key, value in setattr_keyvals.items():
250
+ if isinstance(value, t.Object):
251
+ objs.append(value)
252
+ if isinstance(value, t.Asset):
253
+ value = value.item()
254
+
255
+ if hasattr(modifier, key):
256
+ setattr(modifier, key, value)
257
+ else:
258
+ raise AttributeError(f"Modifier {modifier_type} has no attribute {key}")
259
+
260
+ if _skip_apply:
261
+ return obj
262
+
263
+ execute_object_op(
264
+ bpy.ops.object.modifier_apply,
265
+ objs=objs,
266
+ active=obj,
267
+ modifier=modifier.name,
268
+ )
269
+
270
+ if mod_name in obj.item().modifiers:
271
+ raise ValueError(
272
+ f"Modifier {modifier_type} failed to execute with {setitem_keyvals=} and {setattr_keyvals=}"
273
+ )
274
+
275
+ return obj