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.
- procfunc/__init__.py +87 -0
- procfunc/color.py +57 -0
- procfunc/compute_graph/__init__.py +28 -0
- procfunc/compute_graph/compute_graph.py +115 -0
- procfunc/compute_graph/node.py +200 -0
- procfunc/compute_graph/operators_info.py +92 -0
- procfunc/compute_graph/proxy.py +173 -0
- procfunc/compute_graph/util.py +282 -0
- procfunc/context.py +115 -0
- procfunc/control.py +174 -0
- procfunc/nodes/__init__.py +66 -0
- procfunc/nodes/bindings_util.py +196 -0
- procfunc/nodes/bpy_node_info.py +280 -0
- procfunc/nodes/compositor.py +2242 -0
- procfunc/nodes/execute/construct_nodes.py +571 -0
- procfunc/nodes/execute/construct_special_cases.py +246 -0
- procfunc/nodes/execute/execute.py +548 -0
- procfunc/nodes/execute/infer_runtime_data_type.py +195 -0
- procfunc/nodes/execute/util.py +247 -0
- procfunc/nodes/func.py +1417 -0
- procfunc/nodes/geo.py +4240 -0
- procfunc/nodes/manifest.json +8769 -0
- procfunc/nodes/math.py +644 -0
- procfunc/nodes/node_function.py +160 -0
- procfunc/nodes/shader.py +2359 -0
- procfunc/nodes/types.py +347 -0
- procfunc/ops/__init__.py +35 -0
- procfunc/ops/_util.py +275 -0
- procfunc/ops/addons.py +59 -0
- procfunc/ops/attr.py +426 -0
- procfunc/ops/collection.py +90 -0
- procfunc/ops/curve.py +18 -0
- procfunc/ops/file.py +126 -0
- procfunc/ops/manifest.json +39149 -0
- procfunc/ops/mesh.py +1510 -0
- procfunc/ops/modifier.py +603 -0
- procfunc/ops/object.py +258 -0
- procfunc/ops/primitives/__init__.py +31 -0
- procfunc/ops/primitives/camera.py +45 -0
- procfunc/ops/primitives/curve.py +71 -0
- procfunc/ops/primitives/light.py +114 -0
- procfunc/ops/primitives/mesh.py +358 -0
- procfunc/ops/uv.py +271 -0
- procfunc/random.py +247 -0
- procfunc/tracer/__init__.py +43 -0
- procfunc/tracer/decorator.py +121 -0
- procfunc/tracer/patch.py +494 -0
- procfunc/tracer/proxy.py +127 -0
- procfunc/tracer/trace.py +222 -0
- procfunc/transforms/__init__.py +49 -0
- procfunc/transforms/cleanup.py +214 -0
- procfunc/transforms/convert.py +20 -0
- procfunc/transforms/distribution.py +191 -0
- procfunc/transforms/extract_materials.py +116 -0
- procfunc/transforms/infer_distribution.py +326 -0
- procfunc/transforms/parameters.py +15 -0
- procfunc/transforms/util.py +35 -0
- procfunc/transpiler/__init__.py +24 -0
- procfunc/transpiler/bpy_to_computegraph.py +1348 -0
- procfunc/transpiler/codegen.py +919 -0
- procfunc/transpiler/identifiers.py +595 -0
- procfunc/transpiler/main.py +299 -0
- procfunc/types.py +380 -0
- procfunc/util/__init__.py +0 -0
- procfunc/util/bpy_info.py +145 -0
- procfunc/util/camera.py +0 -0
- procfunc/util/keyframe.py +70 -0
- procfunc/util/log.py +96 -0
- procfunc/util/manifest.py +121 -0
- procfunc/util/pytree.py +343 -0
- procfunc/util/teardown.py +37 -0
- procfunc-0.30.0.dist-info/METADATA +120 -0
- procfunc-0.30.0.dist-info/RECORD +76 -0
- procfunc-0.30.0.dist-info/WHEEL +5 -0
- procfunc-0.30.0.dist-info/licenses/LICENSE.md +11 -0
- procfunc-0.30.0.dist-info/top_level.txt +1 -0
procfunc/nodes/types.py
ADDED
|
@@ -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
|
+
]
|
procfunc/ops/__init__.py
ADDED
|
@@ -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
|