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/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)
|