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/mesh.py ADDED
@@ -0,0 +1,1510 @@
1
+ from dataclasses import asdict, dataclass
2
+ from typing import Literal, Tuple, Unpack
3
+
4
+ import bpy
5
+ import numpy as np
6
+
7
+ import procfunc as pf
8
+ from procfunc import types as t
9
+ from procfunc.ops._util import (
10
+ execute_mesh_op,
11
+ execute_object_op,
12
+ extract_edge_mask,
13
+ extract_face_mask,
14
+ extract_vertex_mask,
15
+ )
16
+
17
+ TProportionalEditFalloff = Literal[
18
+ "SMOOTH",
19
+ "SPHERE",
20
+ "ROOT",
21
+ "INVERSE_SQUARE",
22
+ "SHARP",
23
+ "LINEAR",
24
+ "CONSTANT",
25
+ "RANDOM",
26
+ ]
27
+
28
+
29
+ @pf.tracer.primitive(mutates=["mutates_obj"])
30
+ def transform_apply(
31
+ mutates_obj: t.MeshObject,
32
+ location: bool = True,
33
+ rotation: bool = True,
34
+ scale: bool = True,
35
+ ):
36
+ execute_object_op(
37
+ bpy.ops.object.transform_apply,
38
+ objs=mutates_obj,
39
+ location=location,
40
+ rotation=rotation,
41
+ scale=scale,
42
+ )
43
+
44
+
45
+ @pf.tracer.primitive(mutates=["mutates_obj"])
46
+ def transform(
47
+ mutates_obj: t.MeshObject,
48
+ location: t.Vector | None = None,
49
+ rotation_euler: t.Vector | t.Euler | None = None,
50
+ scale: t.Vector | None = None,
51
+ ):
52
+ obj = mutates_obj.item()
53
+
54
+ if location is not None:
55
+ obj.location += t.Vector(location)
56
+ if rotation_euler is not None:
57
+ obj.rotation_mode = "QUATERNION"
58
+ obj.rotation_quaternion = obj.rotation_quaternion @ t.Quaternion(rotation_euler)
59
+ if scale is not None:
60
+ obj.scale = obj.scale * t.Vector(scale)
61
+
62
+ transform_apply(
63
+ mutates_obj,
64
+ location=location is not None,
65
+ rotation=rotation_euler is not None,
66
+ scale=scale is not None,
67
+ )
68
+
69
+
70
+ @pf.tracer.primitive(mutates=["mutates_obj"])
71
+ def delete_geometry(
72
+ mutates_obj: t.MeshObject,
73
+ vertex_mask: np.ndarray | None = None,
74
+ edge_mask: np.ndarray | None = None,
75
+ face_mask: np.ndarray | None = None,
76
+ type: Literal["VERT", "EDGE", "FACE", "EDGE_FACE", "ONLY_FACE"] = "VERT",
77
+ ) -> None:
78
+ """Based on bpy.ops.mesh.delete"""
79
+ execute_mesh_op(
80
+ bpy.ops.mesh.delete,
81
+ mutates_obj,
82
+ vertex_mask=vertex_mask,
83
+ edge_mask=edge_mask,
84
+ face_mask=face_mask,
85
+ type=type,
86
+ )
87
+
88
+
89
+ @dataclass
90
+ class ProportionalEditProperties:
91
+ falloff: TProportionalEditFalloff | None = None
92
+ size: float = 1.0
93
+ connected: bool = False
94
+ projected: bool = False
95
+
96
+
97
+ @pf.tracer.primitive(mutates=["mutates_obj"])
98
+ def extrude_edges(
99
+ mutates_obj: t.MeshObject,
100
+ edge_mask: np.ndarray | None = None,
101
+ use_normal_flip: bool = False,
102
+ mirror: bool = False,
103
+ value: Tuple[float, float, float] = (0.0, 0.0, 0.0),
104
+ orient_type: Literal[
105
+ "GLOBAL", "LOCAL", "NORMAL", "GIMBAL", "VIEW", "CURSOR"
106
+ ] = "GLOBAL",
107
+ constraint_axis: Tuple[bool, bool, bool] = (False, False, False),
108
+ **proportional_edit_kwargs: Unpack[ProportionalEditProperties],
109
+ ) -> None:
110
+ """
111
+ Extrude individual edges and move
112
+
113
+ Based on bpy.ops.mesh.extrude_edges_move
114
+
115
+ Args:
116
+ edge_mask: Boolean array selecting edges to extrude.
117
+ """
118
+
119
+ proportional_edit = ProportionalEditProperties(**proportional_edit_kwargs)
120
+
121
+ execute_mesh_op(
122
+ bpy.ops.mesh.extrude_edges_move,
123
+ mutates_obj,
124
+ edge_mask=edge_mask,
125
+ MESH_OT_extrude_edges_indiv={
126
+ "use_normal_flip": use_normal_flip,
127
+ "mirror": mirror,
128
+ },
129
+ TRANSFORM_OT_translate={
130
+ "value": value,
131
+ "orient_type": orient_type,
132
+ "constraint_axis": constraint_axis,
133
+ "mirror": mirror,
134
+ "use_proportional_edit": proportional_edit.falloff is not None,
135
+ "proportional_edit_falloff": proportional_edit.falloff or "SMOOTH",
136
+ "proportional_size": proportional_edit.size,
137
+ "use_proportional_connected": proportional_edit.connected,
138
+ "use_proportional_projected": proportional_edit.projected,
139
+ },
140
+ )
141
+
142
+
143
+ def region_to_loop(
144
+ obj: t.MeshObject,
145
+ face_mask: np.ndarray | None = None,
146
+ ) -> np.ndarray:
147
+ """
148
+ Select boundary edges of face regions
149
+
150
+ Based on bpy.ops.mesh.region_to_loop
151
+
152
+ Args:
153
+ face_mask: Boolean array selecting face regions to convert to loops.
154
+
155
+ Returns:
156
+ Boolean array selecting edges that were converted to loops.
157
+ """
158
+ execute_mesh_op(
159
+ bpy.ops.mesh.region_to_loop,
160
+ obj,
161
+ face_mask=face_mask,
162
+ )
163
+
164
+ return extract_edge_mask(obj)
165
+
166
+
167
+ # Conversion functions moved to ops.object.py
168
+
169
+
170
+ @pf.tracer.primitive(mutates=["mutates_obj"])
171
+ def bridge_edge_loops(
172
+ mutates_obj: t.MeshObject,
173
+ edge_mask: np.ndarray | None = None,
174
+ type: Literal["SINGLE", "PAIRS", "FAN"] = "SINGLE",
175
+ use_merge: bool = False,
176
+ merge_factor: float = 0.5,
177
+ twist_offset: int = 0,
178
+ number_cuts: int = 0,
179
+ interpolation: Literal["PATH", "SURFACE"] = "PATH",
180
+ smoothness: float = 1.0,
181
+ profile_shape_factor: float = 0.0,
182
+ profile_shape: Literal[
183
+ "SMOOTH", "SPHERE", "ROOT", "INVERSE_SQUARE", "SHARP", "LINEAR"
184
+ ] = "SMOOTH",
185
+ ) -> None:
186
+ """
187
+ Create faces between two edge loops
188
+
189
+ Based on bpy.ops.mesh.bridge_edge_loops
190
+
191
+ Args:
192
+ edge_mask: Boolean array selecting edges to bridge between.
193
+ """
194
+ execute_mesh_op(
195
+ bpy.ops.mesh.bridge_edge_loops,
196
+ mutates_obj,
197
+ edge_mask=edge_mask,
198
+ type=type,
199
+ use_merge=use_merge,
200
+ merge_factor=merge_factor,
201
+ twist_offset=twist_offset,
202
+ number_cuts=number_cuts,
203
+ interpolation=interpolation,
204
+ smoothness=smoothness,
205
+ profile_shape_factor=profile_shape_factor,
206
+ profile_shape=profile_shape,
207
+ )
208
+
209
+
210
+ @pf.tracer.primitive(mutates=["mutates_obj"])
211
+ def normals_make_consistent(
212
+ mutates_obj: t.MeshObject,
213
+ face_mask: np.ndarray | None = None,
214
+ inside: bool = False,
215
+ ) -> None:
216
+ """
217
+ Make face normals point outside or inside
218
+
219
+ Based on bpy.ops.mesh.normals_make_consistent
220
+
221
+ Args:
222
+ face_mask: Boolean array selecting faces to make consistent. If None, operates on entire mesh.
223
+ """
224
+ execute_mesh_op(
225
+ bpy.ops.mesh.normals_make_consistent,
226
+ mutates_obj,
227
+ face_mask=face_mask,
228
+ inside=inside,
229
+ )
230
+
231
+
232
+ @pf.tracer.primitive(mutates=["mutates_obj"])
233
+ def remove_doubles(
234
+ mutates_obj: t.MeshObject,
235
+ vertex_mask: np.ndarray | None = None,
236
+ edge_mask: np.ndarray | None = None,
237
+ face_mask: np.ndarray | None = None,
238
+ threshold: float = 0.0001,
239
+ use_unselected: bool = False,
240
+ use_sharp_edge_from_normals: bool = False,
241
+ ) -> None:
242
+ """
243
+ Remove duplicate vertices
244
+
245
+ Based on bpy.ops.mesh.remove_doubles
246
+
247
+ Args:
248
+ vertex_mask: Boolean array selecting vertices to check for duplicates. If None, operates on entire mesh.
249
+ """
250
+ execute_mesh_op(
251
+ bpy.ops.mesh.remove_doubles,
252
+ mutates_obj,
253
+ vertex_mask=vertex_mask,
254
+ edge_mask=edge_mask,
255
+ face_mask=face_mask,
256
+ threshold=threshold,
257
+ use_unselected=use_unselected,
258
+ use_sharp_edge_from_normals=use_sharp_edge_from_normals,
259
+ )
260
+
261
+
262
+ @pf.tracer.primitive(mutates=["mutates_obj"])
263
+ def quads_convert_to_tris(
264
+ mutates_obj: t.MeshObject,
265
+ face_mask: np.ndarray | None = None,
266
+ # quad_method: Literal[
267
+ # "BEAUTY", "FIXED", "FIXED_ALTERNATE", "SHORTEST_DIAGONAL"
268
+ # ] = "BEAUTY",
269
+ # ngon_method: Literal["BEAUTY", "CLIP"] = "BEAUTY",
270
+ ) -> None:
271
+ """
272
+ Convert quad faces to triangular faces
273
+
274
+ Based on bpy.ops.mesh.quads_convert_to_tris
275
+
276
+ Args:
277
+ face_mask: Boolean array selecting faces to convert. If None, operates on entire mesh.
278
+
279
+ Note: quad_method and ngon_method not currently included, they are never used in infinigen
280
+ but could be re-added if useful
281
+
282
+ """
283
+ execute_mesh_op(
284
+ bpy.ops.mesh.quads_convert_to_tris,
285
+ mutates_obj,
286
+ face_mask=face_mask,
287
+ quad_method="BEAUTY",
288
+ ngon_method="BEAUTY",
289
+ )
290
+
291
+
292
+ @pf.tracer.primitive(mutates=["mutates_obj"])
293
+ def separate_mask(
294
+ mutates_obj: t.MeshObject,
295
+ vertex_mask: np.ndarray | None = None,
296
+ edge_mask: np.ndarray | None = None,
297
+ face_mask: np.ndarray | None = None,
298
+ ) -> t.MeshObject:
299
+ """
300
+ Separate selected geometry into a new mesh
301
+
302
+ Based on bpy.ops.mesh.separate
303
+
304
+ Args:
305
+ vertex_mask: Boolean array selecting vertices to separate.
306
+ edge_mask: Boolean array selecting edges to separate.
307
+ face_mask: Boolean array selecting faces to separate.
308
+
309
+ Note: we dont currently support the type="MATERIAL" option, please extract this mask explicitly and pass it in.
310
+ """
311
+ execute_mesh_op(
312
+ bpy.ops.mesh.separate,
313
+ mutates_obj,
314
+ vertex_mask=vertex_mask,
315
+ edge_mask=edge_mask,
316
+ face_mask=face_mask,
317
+ type="SELECTED",
318
+ )
319
+
320
+ assert len(bpy.context.selected_objects) == 2, (
321
+ f"{mutates_obj.item().name=} {list(bpy.context.selected_objects)}"
322
+ )
323
+ result_obj = bpy.context.selected_objects[1]
324
+ assert result_obj is not mutates_obj.item(), (
325
+ f"{mutates_obj.item().name=} {result_obj.name=} {bpy.data.objects.keys()}"
326
+ )
327
+ return t.MeshObject(result_obj)
328
+
329
+
330
+ @pf.tracer.primitive(mutates=["mutates_obj"])
331
+ def separate_loose(
332
+ mutates_obj: t.MeshObject,
333
+ ) -> list[t.MeshObject]:
334
+ """
335
+ Separate loose mesh islands into new objects
336
+
337
+ Based on bpy.ops.mesh.separate
338
+
339
+ """
340
+ execute_mesh_op(
341
+ bpy.ops.mesh.separate,
342
+ mutates_obj,
343
+ type="LOOSE",
344
+ )
345
+
346
+ return [t.MeshObject(o) for o in bpy.context.selected_objects]
347
+
348
+
349
+ @pf.tracer.primitive(mutates=["mutates_obj"])
350
+ def fill_grid(
351
+ mutates_obj: t.MeshObject,
352
+ edge_mask: np.ndarray,
353
+ span: int = 1,
354
+ offset: int = 0,
355
+ use_interp_simple: bool = False,
356
+ ) -> None:
357
+ """
358
+ Fill grid from two edge loops
359
+
360
+ Based on bpy.ops.mesh.fill_grid
361
+
362
+ Args:
363
+ edge_mask: Boolean array selecting edge loops to fill between.
364
+ """
365
+ execute_mesh_op(
366
+ bpy.ops.mesh.fill_grid,
367
+ mutates_obj,
368
+ edge_mask=edge_mask,
369
+ span=span,
370
+ offset=offset,
371
+ use_interp_simple=use_interp_simple,
372
+ )
373
+
374
+
375
+ @pf.tracer.primitive(mutates=["mutates_obj"])
376
+ def edge_face_add(
377
+ mutates_obj: t.MeshObject,
378
+ edge_mask: np.ndarray,
379
+ ) -> None:
380
+ """
381
+ Add an edge or face to selected
382
+
383
+ Based on bpy.ops.mesh.edge_face_add
384
+
385
+ Args:
386
+ edge_mask: Boolean array selecting edges to add faces to.
387
+ """
388
+ execute_mesh_op(
389
+ bpy.ops.mesh.edge_face_add,
390
+ mutates_obj,
391
+ edge_mask=edge_mask,
392
+ )
393
+
394
+
395
+ @pf.tracer.primitive(mutates=["mutates_obj"])
396
+ def duplicate(
397
+ mutates_obj: t.MeshObject,
398
+ vertex_mask: np.ndarray | None = None,
399
+ edge_mask: np.ndarray | None = None,
400
+ face_mask: np.ndarray | None = None,
401
+ ):
402
+ """
403
+ Duplicate selected faces
404
+
405
+ Args:
406
+ mutate_obj: MeshObject providing source and destination geometry
407
+ vertex_mask: If enabled, duplicate these vertices
408
+ edge_mask: If enabled, duplicate these edges
409
+ face_mask: If enabled, duplicate these faces
410
+
411
+ Returns:
412
+ mask over vertices edges or faces, depending on which mask was provided
413
+ """
414
+ execute_mesh_op(
415
+ bpy.ops.mesh.duplicate,
416
+ mutates_obj,
417
+ vertex_mask=vertex_mask,
418
+ edge_mask=edge_mask,
419
+ face_mask=face_mask,
420
+ )
421
+
422
+ if vertex_mask is not None:
423
+ return extract_vertex_mask(mutates_obj)
424
+ if edge_mask is not None:
425
+ return extract_edge_mask(mutates_obj)
426
+ if face_mask is not None:
427
+ return extract_face_mask(mutates_obj)
428
+ return None
429
+
430
+
431
+ @pf.tracer.primitive(mutates=["mutates_obj"])
432
+ def extrude_faces(
433
+ mutates_obj: t.MeshObject,
434
+ face_mask: np.ndarray | None = None,
435
+ use_normal_flip: bool = False,
436
+ use_dissolve_ortho_edges: bool = False,
437
+ mirror: bool = False,
438
+ value: Tuple[float, float, float] = (0.0, 0.0, 0.0),
439
+ orient_type: Literal[
440
+ "GLOBAL", "LOCAL", "NORMAL", "GIMBAL", "VIEW", "CURSOR"
441
+ ] = "GLOBAL",
442
+ constraint_axis: Tuple[bool, bool, bool] = (False, False, False),
443
+ **proportional_edit_kwargs: Unpack[ProportionalEditProperties],
444
+ ) -> None:
445
+ """
446
+ Extrude region and move result
447
+
448
+ Based on bpy.ops.mesh.extrude_region_move
449
+
450
+ Args:
451
+ face_mask: Boolean array selecting faces to extrude. If None, operates on entire mesh.
452
+ """
453
+ proportional_edit = ProportionalEditProperties(**proportional_edit_kwargs)
454
+ execute_mesh_op(
455
+ bpy.ops.mesh.extrude_region_move,
456
+ mutates_obj,
457
+ face_mask=face_mask,
458
+ MESH_OT_extrude_region={
459
+ "use_normal_flip": use_normal_flip,
460
+ "use_dissolve_ortho_edges": use_dissolve_ortho_edges,
461
+ "mirror": mirror,
462
+ },
463
+ TRANSFORM_OT_translate={
464
+ "value": value,
465
+ "orient_type": orient_type,
466
+ "constraint_axis": constraint_axis,
467
+ "mirror": mirror,
468
+ "use_proportional_edit": proportional_edit.falloff is not None,
469
+ "proportional_edit_falloff": proportional_edit.falloff or "SMOOTH",
470
+ "proportional_size": proportional_edit.size,
471
+ "use_proportional_connected": proportional_edit.connected,
472
+ "use_proportional_projected": proportional_edit.projected,
473
+ },
474
+ )
475
+
476
+
477
+ @pf.tracer.primitive(mutates=["mutates_obj"])
478
+ def subdivide(
479
+ mutates_obj: t.MeshObject,
480
+ vertex_mask: np.ndarray | None = None,
481
+ edge_mask: np.ndarray | None = None,
482
+ face_mask: np.ndarray | None = None,
483
+ number_cuts: int = 1,
484
+ smoothness: float = 0.0,
485
+ ngon: bool = True,
486
+ quadcorner: Literal["STRAIGHT_CUT", "INNER_VERT", "PATH", "FAN"] = "STRAIGHT_CUT",
487
+ fractal: float = 0.0,
488
+ fractal_along_normal: float = 0.0,
489
+ seed: int = 0,
490
+ ) -> None:
491
+ """
492
+ Subdivide selected edges
493
+
494
+ Based on bpy.ops.mesh.subdivide
495
+
496
+ Args:
497
+ face_mask: Boolean array selecting faces to subdivide. If None, operates on entire mesh.
498
+ """
499
+ execute_mesh_op(
500
+ bpy.ops.mesh.subdivide,
501
+ mutates_obj,
502
+ vertex_mask=vertex_mask,
503
+ edge_mask=edge_mask,
504
+ face_mask=face_mask,
505
+ number_cuts=number_cuts,
506
+ smoothness=smoothness,
507
+ ngon=ngon,
508
+ quadcorner=quadcorner,
509
+ fractal=fractal,
510
+ fractal_along_normal=fractal_along_normal,
511
+ seed=seed,
512
+ )
513
+
514
+
515
+ @pf.tracer.primitive(mutates=["mutates_obj"])
516
+ def unsubdivide(
517
+ mutates_obj: t.MeshObject,
518
+ iterations: int = 2,
519
+ ) -> None:
520
+ """
521
+ Un-subdivide selected edges and faces
522
+
523
+ Based on bpy.ops.mesh.unsubdivide
524
+
525
+ Args:
526
+ iterations: Number of times to un-subdivide.
527
+ """
528
+ execute_mesh_op(
529
+ bpy.ops.mesh.unsubdivide,
530
+ mutates_obj,
531
+ iterations=iterations,
532
+ )
533
+
534
+
535
+ @pf.tracer.primitive(mutates=["mutates_obj"])
536
+ def inset(
537
+ mutates_obj: t.MeshObject,
538
+ face_mask: np.ndarray | None = None,
539
+ use_boundary: bool = True,
540
+ use_even_offset: bool = True,
541
+ use_relative_offset: bool = False,
542
+ use_edge_rail: bool = False,
543
+ thickness: float = 0.0,
544
+ depth: float = 0.0,
545
+ use_outset: bool = False,
546
+ use_select_inset: bool = False,
547
+ use_individual: bool = False,
548
+ use_interpolate: bool = True,
549
+ ) -> np.ndarray:
550
+ """
551
+ Inset new faces into selected faces
552
+
553
+ Based on bpy.ops.mesh.inset
554
+
555
+ # TODO: use_select_inset as np.array output
556
+
557
+ Args:
558
+ face_mask: Boolean array selecting faces to inset. If None, operates on entire mesh.
559
+
560
+ Returns:
561
+ Boolean array selecting faces that were inset.
562
+ """
563
+
564
+ execute_mesh_op(
565
+ bpy.ops.mesh.inset,
566
+ mutates_obj,
567
+ face_mask=face_mask,
568
+ use_boundary=use_boundary,
569
+ use_even_offset=use_even_offset,
570
+ use_relative_offset=use_relative_offset,
571
+ use_edge_rail=use_edge_rail,
572
+ thickness=thickness,
573
+ depth=depth,
574
+ use_outset=use_outset,
575
+ use_select_inset=use_select_inset,
576
+ use_individual=use_individual,
577
+ use_interpolate=use_interpolate,
578
+ )
579
+
580
+ return extract_face_mask(mutates_obj)
581
+
582
+
583
+ @pf.tracer.primitive(mutates=["mutates_obj"])
584
+ def inset_individual(
585
+ mutates_obj: t.MeshObject,
586
+ face_mask: np.ndarray | None = None,
587
+ use_boundary: bool = True,
588
+ use_even_offset: bool = True,
589
+ use_relative_offset: bool = False,
590
+ use_edge_rail: bool = False,
591
+ thickness: float = 0.0,
592
+ depth: float = 0.0,
593
+ use_outset: bool = False,
594
+ use_interpolate: bool = True,
595
+ ):
596
+ execute_mesh_op(
597
+ bpy.ops.mesh.inset,
598
+ mutates_obj,
599
+ face_mask=face_mask,
600
+ use_boundary=use_boundary,
601
+ use_even_offset=use_even_offset,
602
+ use_relative_offset=use_relative_offset,
603
+ use_edge_rail=use_edge_rail,
604
+ thickness=thickness,
605
+ depth=depth,
606
+ use_outset=use_outset,
607
+ use_select_inset=False,
608
+ use_individual=True,
609
+ use_interpolate=use_interpolate,
610
+ )
611
+
612
+ return extract_face_mask(mutates_obj)
613
+
614
+
615
+ @pf.tracer.primitive(mutates=["mutates_obj"])
616
+ def bisect(
617
+ mutates_obj: t.MeshObject,
618
+ face_mask: np.ndarray | None = None,
619
+ plane_co: Tuple[float, float, float] = (0.0, 0.0, 0.0),
620
+ plane_no: Tuple[float, float, float] = (1.0, 0.0, 0.0),
621
+ use_fill: bool = False,
622
+ clear_inner: bool = False,
623
+ clear_outer: bool = False,
624
+ threshold: float = 0.0001,
625
+ flip: bool = False,
626
+ ) -> None:
627
+ """
628
+ Cut geometry along a plane
629
+
630
+ Based on bpy.ops.mesh.bisect
631
+
632
+ TODO: add edge_mask ?
633
+
634
+ NOTE: xstart, xend, ystart, yend are not supported as these relate to UI input, please manually specify the plane_c
635
+
636
+ Args:
637
+ mutates_obj: MeshObject to bisect
638
+ face_mask: Boolean array selecting faces to bisect. If None, operates on entire mesh.
639
+ plane_co: Location of the plane
640
+ plane_no: Normal of the plane
641
+ """
642
+ execute_mesh_op(
643
+ bpy.ops.mesh.bisect,
644
+ mutates_obj,
645
+ face_mask=face_mask,
646
+ plane_co=plane_co,
647
+ plane_no=plane_no,
648
+ use_fill=use_fill,
649
+ clear_inner=clear_inner,
650
+ clear_outer=clear_outer,
651
+ threshold=threshold,
652
+ flip=flip,
653
+ )
654
+
655
+
656
+ @pf.tracer.primitive(mutates=["mutates_obj"])
657
+ def convex_hull(
658
+ mutates_obj: t.MeshObject,
659
+ vertex_mask: np.ndarray | None = None,
660
+ delete_unused: bool = True,
661
+ use_existing_faces: bool = True,
662
+ make_holes: bool = False,
663
+ join_triangles: bool = True,
664
+ face_threshold: float = 0.698132,
665
+ shape_threshold: float = 0.698132,
666
+ uvs: bool = False,
667
+ vcols: bool = False,
668
+ seam: bool = False,
669
+ sharp: bool = False,
670
+ materials: bool = False,
671
+ ) -> None:
672
+ """
673
+ Enclose selected vertices in a convex hull
674
+
675
+ Based on bpy.ops.mesh.convex_hull
676
+
677
+ Args:
678
+ vertex_mask: Boolean array selecting vertices for hull computation. If None, operates on entire mesh.
679
+ """
680
+ bpy.context.view_layer.objects.active = mutates_obj.item()
681
+ execute_mesh_op(
682
+ bpy.ops.mesh.convex_hull,
683
+ mutates_obj,
684
+ vertex_mask=vertex_mask,
685
+ delete_unused=delete_unused,
686
+ use_existing_faces=use_existing_faces,
687
+ make_holes=make_holes,
688
+ join_triangles=join_triangles,
689
+ face_threshold=face_threshold,
690
+ shape_threshold=shape_threshold,
691
+ uvs=uvs,
692
+ vcols=vcols,
693
+ seam=seam,
694
+ sharp=sharp,
695
+ materials=materials,
696
+ )
697
+
698
+
699
+ @dataclass
700
+ class BevelProperties:
701
+ offset: float = 0.1
702
+ offset_pct: float = 0.1
703
+ offset_type: Literal["OFFSET", "WIDTH", "DEPTH", "PERCENT", "ABSOLUTE"] = "OFFSET"
704
+ profile_type: Literal["SUPERELLIPSE", "CUSTOM"] = "SUPERELLIPSE"
705
+ segments: int = 1
706
+ profile: float = 0.5
707
+ clamp_overlap: bool = False
708
+ loop_slide: bool = True
709
+ mark_seam: bool = False
710
+ mark_sharp: bool = False
711
+ material: int = -1
712
+ harden_normals: bool = False
713
+ face_strength_mode: Literal["NONE", "NEW", "AFFECTED", "ALL"] = "NONE"
714
+ miter_outer: Literal["SHARP", "PATCH", "ARC"] = "SHARP"
715
+ miter_inner: Literal["SHARP", "ARC"] = "SHARP"
716
+ spread: float = 0.1
717
+ vmesh_method: Literal["ADJ", "CUTOFF"] = "ADJ"
718
+
719
+
720
+ @pf.tracer.primitive(mutates=["mutates_obj"])
721
+ def bevel_vertices(
722
+ mutates_obj: t.MeshObject,
723
+ vertex_mask: np.ndarray | None = None,
724
+ **kwargs: Unpack[BevelProperties],
725
+ ) -> None:
726
+ """
727
+ Cut into selected items at an angle to create bevel or chamfer
728
+
729
+ Based on bpy.ops.mesh.bevel
730
+
731
+ Args:
732
+ vertex_mask: Boolean array selecting vertices to bevel. If None, operates on entire mesh.
733
+ """
734
+ bevel = BevelProperties(**kwargs)
735
+ execute_mesh_op(
736
+ bpy.ops.mesh.bevel,
737
+ mutates_obj,
738
+ vertex_mask=vertex_mask,
739
+ affect="VERTICES",
740
+ **asdict(bevel),
741
+ )
742
+
743
+
744
+ @pf.tracer.primitive(mutates=["mutates_obj"])
745
+ def bevel_edges(
746
+ mutates_obj: t.MeshObject,
747
+ edge_mask: np.ndarray | None = None,
748
+ **kwargs: Unpack[BevelProperties],
749
+ ) -> None:
750
+ """
751
+ Cut into selected items at an angle to create bevel or chamfer
752
+
753
+ Based on bpy.ops.mesh.bevel
754
+
755
+ Args:
756
+ edge_mask: Boolean array selecting edges to bevel. If None, operates on entire mesh.
757
+ """
758
+ bevel = BevelProperties(**kwargs)
759
+ execute_mesh_op(
760
+ bpy.ops.mesh.bevel,
761
+ mutates_obj,
762
+ edge_mask=edge_mask,
763
+ affect="EDGES",
764
+ **asdict(bevel),
765
+ )
766
+
767
+
768
+ @pf.tracer.primitive(mutates=["mutates_obj"])
769
+ def select_loose(
770
+ mutates_obj: t.MeshObject,
771
+ vertex_mask: np.ndarray | None = None,
772
+ edge_mask: np.ndarray | None = None,
773
+ face_mask: np.ndarray | None = None,
774
+ extend: bool = False,
775
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
776
+ """
777
+ Select loose geometry.
778
+
779
+ Based on bpy.ops.mesh.select_loose
780
+
781
+ Returns:
782
+ Tuple of (vertex_mask, edge_mask, face_mask) of the resulting selection.
783
+ """
784
+ execute_mesh_op(
785
+ bpy.ops.mesh.select_loose,
786
+ mutates_obj,
787
+ vertex_mask=vertex_mask,
788
+ edge_mask=edge_mask,
789
+ face_mask=face_mask,
790
+ empty_mask_mode="execute",
791
+ extend=extend,
792
+ )
793
+ return (
794
+ extract_vertex_mask(mutates_obj),
795
+ extract_edge_mask(mutates_obj),
796
+ extract_face_mask(mutates_obj),
797
+ )
798
+
799
+
800
+ @pf.tracer.primitive(mutates=["mutates_obj"])
801
+ def select_more(
802
+ mutates_obj: t.MeshObject,
803
+ vertex_mask: np.ndarray | None = None,
804
+ edge_mask: np.ndarray | None = None,
805
+ face_mask: np.ndarray | None = None,
806
+ use_face_step: bool = True,
807
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
808
+ """
809
+ Select more vertices, edges or faces connected to current selection.
810
+
811
+ Based on bpy.ops.mesh.select_more
812
+
813
+ Returns:
814
+ Tuple of (vertex_mask, edge_mask, face_mask) of the resulting selection.
815
+ """
816
+ execute_mesh_op(
817
+ bpy.ops.mesh.select_more,
818
+ mutates_obj,
819
+ vertex_mask=vertex_mask,
820
+ edge_mask=edge_mask,
821
+ face_mask=face_mask,
822
+ empty_mask_mode="execute",
823
+ use_face_step=use_face_step,
824
+ )
825
+ return (
826
+ extract_vertex_mask(mutates_obj),
827
+ extract_edge_mask(mutates_obj),
828
+ extract_face_mask(mutates_obj),
829
+ )
830
+
831
+
832
+ @pf.tracer.primitive(mutates=["mutates_obj"])
833
+ def loop_multi_select(
834
+ mutates_obj: t.MeshObject,
835
+ vertex_mask: np.ndarray | None = None,
836
+ edge_mask: np.ndarray | None = None,
837
+ face_mask: np.ndarray | None = None,
838
+ ring: bool = False,
839
+ ) -> np.ndarray:
840
+ """
841
+ Select a loop of connected edges by connection type.
842
+
843
+ Based on bpy.ops.mesh.loop_multi_select
844
+
845
+ Args:
846
+ ring: If True, select edge rings instead of edge loops.
847
+
848
+ Returns:
849
+ Boolean edge mask of the resulting selection.
850
+ """
851
+ execute_mesh_op(
852
+ bpy.ops.mesh.loop_multi_select,
853
+ mutates_obj,
854
+ vertex_mask=vertex_mask,
855
+ edge_mask=edge_mask,
856
+ face_mask=face_mask,
857
+ empty_mask_mode="execute",
858
+ ring=ring,
859
+ )
860
+ return extract_edge_mask(mutates_obj)
861
+
862
+
863
+ @pf.tracer.primitive(mutates=["mutates_obj"])
864
+ def fill(
865
+ mutates_obj: t.MeshObject,
866
+ edge_mask: np.ndarray | None = None,
867
+ use_beauty: bool = True,
868
+ ) -> None:
869
+ """
870
+ Fill a selected edge loop with faces
871
+
872
+ Based on bpy.ops.mesh.fill
873
+
874
+ Args:
875
+ edge_mask: Boolean array selecting edge loop to fill.
876
+ """
877
+ execute_mesh_op(
878
+ bpy.ops.mesh.fill,
879
+ mutates_obj,
880
+ edge_mask=edge_mask,
881
+ use_beauty=use_beauty,
882
+ )
883
+
884
+
885
+ @pf.tracer.primitive(mutates=["mutates_obj"])
886
+ def poke(
887
+ mutates_obj: t.MeshObject,
888
+ face_mask: np.ndarray | None = None,
889
+ offset: float = 0.0,
890
+ use_relative_offset: bool = False,
891
+ center_mode: Literal["MEDIAN_WEIGHTED", "MEDIAN", "BOUNDS"] = "MEDIAN_WEIGHTED",
892
+ ) -> None:
893
+ """
894
+ Split selected faces into individual triangles
895
+
896
+ Based on bpy.ops.mesh.poke
897
+
898
+ Args:
899
+ face_mask: Boolean array selecting faces to poke.
900
+ """
901
+ execute_mesh_op(
902
+ bpy.ops.mesh.poke,
903
+ mutates_obj,
904
+ face_mask=face_mask,
905
+ offset=offset,
906
+ use_relative_offset=use_relative_offset,
907
+ center_mode=center_mode,
908
+ )
909
+
910
+
911
+ @pf.tracer.primitive(mutates=["mutates_obj"])
912
+ def flip_normals(
913
+ mutates_obj: t.MeshObject,
914
+ face_mask: np.ndarray | None = None,
915
+ only_clnors: bool = False,
916
+ ) -> None:
917
+ """
918
+ Flip the direction of selected faces' normals
919
+
920
+ Based on bpy.ops.mesh.flip_normals
921
+
922
+ Args:
923
+ face_mask: Boolean array selecting faces to flip normals. If None, operates on entire mesh.
924
+ """
925
+ execute_mesh_op(
926
+ bpy.ops.mesh.flip_normals,
927
+ mutates_obj,
928
+ face_mask=face_mask,
929
+ only_clnors=only_clnors,
930
+ )
931
+
932
+
933
+ @pf.tracer.primitive(mutates=["mutates_obj"])
934
+ def extrude_faces_shrink_fatten(
935
+ mutates_obj: t.MeshObject,
936
+ use_normal_flip: bool = False,
937
+ use_dissolve_ortho_edges: bool = False,
938
+ mirror: bool = False,
939
+ value: float = 0.0,
940
+ use_even_offset: bool = False,
941
+ snap: bool = False,
942
+ use_accurate: bool = False,
943
+ face_mask: np.ndarray | None = None,
944
+ **proportional_edit_kwargs: Unpack[ProportionalEditProperties],
945
+ ) -> None:
946
+ """
947
+ Extrude region and shrink/fatten
948
+
949
+ Based on bpy.ops.mesh.extrude_region_shrink_fatten
950
+
951
+ Args:
952
+ face_mask: Boolean array selecting faces to extrude. If None, operates on entire mesh.
953
+ """
954
+ proportional_edit = ProportionalEditProperties(**proportional_edit_kwargs)
955
+ execute_mesh_op(
956
+ bpy.ops.mesh.extrude_region_shrink_fatten,
957
+ mutates_obj,
958
+ face_mask=face_mask,
959
+ MESH_OT_extrude_region={
960
+ "use_normal_flip": use_normal_flip,
961
+ "use_dissolve_ortho_edges": use_dissolve_ortho_edges,
962
+ "mirror": mirror,
963
+ },
964
+ TRANSFORM_OT_shrink_fatten={
965
+ "value": value,
966
+ "use_even_offset": use_even_offset,
967
+ "mirror": mirror,
968
+ "use_proportional_edit": proportional_edit.falloff is not None,
969
+ "proportional_edit_falloff": proportional_edit.falloff or "SMOOTH",
970
+ "proportional_size": proportional_edit.size,
971
+ "use_proportional_connected": proportional_edit.connected,
972
+ "use_proportional_projected": proportional_edit.projected,
973
+ "snap": snap,
974
+ "use_accurate": use_accurate,
975
+ },
976
+ )
977
+
978
+
979
+ @pf.tracer.primitive(mutates=["mutates_obj"])
980
+ def tris_convert_to_quads(
981
+ mutates_obj: t.MeshObject,
982
+ face_threshold: float = 0.698132,
983
+ shape_threshold: float = 0.698132,
984
+ uvs: bool = False,
985
+ vcols: bool = False,
986
+ seam: bool = False,
987
+ sharp: bool = False,
988
+ materials: bool = False,
989
+ face_mask: np.ndarray | None = None,
990
+ ) -> None:
991
+ """
992
+ Convert triangles to quads
993
+
994
+ Based on bpy.ops.mesh.tris_convert_to_quads
995
+
996
+ Args:
997
+ face_mask: Boolean array selecting faces to convert. If None, operates on entire mesh.
998
+ """
999
+ execute_mesh_op(
1000
+ bpy.ops.mesh.tris_convert_to_quads,
1001
+ mutates_obj,
1002
+ face_mask=face_mask,
1003
+ face_threshold=face_threshold,
1004
+ shape_threshold=shape_threshold,
1005
+ uvs=uvs,
1006
+ vcols=vcols,
1007
+ seam=seam,
1008
+ sharp=sharp,
1009
+ materials=materials,
1010
+ )
1011
+
1012
+
1013
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1014
+ def merge(
1015
+ mutates_obj: t.MeshObject,
1016
+ type: Literal["CENTER", "CURSOR", "COLLAPSE"] = "CENTER",
1017
+ uvs: bool = False,
1018
+ vertex_mask: np.ndarray | None = None,
1019
+ edge_mask: np.ndarray | None = None,
1020
+ face_mask: np.ndarray | None = None,
1021
+ ) -> None:
1022
+ """
1023
+ Merge selected vertices
1024
+
1025
+ Based on bpy.ops.mesh.merge
1026
+
1027
+ Args:
1028
+ vertex_mask: Boolean array selecting vertices to merge. At most one can be provided. If all are None, operates on entire mesh.
1029
+ edge_mask: Boolean array selecting edges to merge. At most one can be provided. If all are None, operates on entire mesh.
1030
+ face_mask: Boolean array selecting faces to merge. At most one can be provided. If all are None, operates on entire mesh.
1031
+
1032
+ Note: Only one of vertex_mask, edge_mask, or face_mask should be provided.
1033
+ """
1034
+ execute_mesh_op(
1035
+ bpy.ops.mesh.merge,
1036
+ mutates_obj,
1037
+ vertex_mask=vertex_mask,
1038
+ edge_mask=edge_mask,
1039
+ face_mask=face_mask,
1040
+ type=type,
1041
+ uvs=uvs,
1042
+ )
1043
+
1044
+
1045
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1046
+ def mark_sharp(
1047
+ mutates_obj: t.MeshObject,
1048
+ clear: bool = False,
1049
+ use_verts: bool = False,
1050
+ edge_mask: np.ndarray | None = None,
1051
+ ) -> None:
1052
+ """
1053
+ Mark selected edges as sharp
1054
+
1055
+ Based on bpy.ops.mesh.mark_sharp
1056
+
1057
+ Args:
1058
+ edge_mask: Boolean array selecting edges to mark/unmark as sharp. If None, operates on entire mesh.
1059
+ """
1060
+ execute_mesh_op(
1061
+ bpy.ops.mesh.mark_sharp,
1062
+ mutates_obj,
1063
+ edge_mask=edge_mask,
1064
+ clear=clear,
1065
+ use_verts=use_verts,
1066
+ )
1067
+
1068
+
1069
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1070
+ def dissolve_limited(
1071
+ mutates_obj: t.MeshObject,
1072
+ angle_limit: float = 0.0872665,
1073
+ use_dissolve_boundaries: bool = False,
1074
+ delimit: set[Literal["NORMAL", "MATERIAL", "SEAM", "SHARP", "UV"]] = {"NORMAL"},
1075
+ edge_mask: np.ndarray | None = None,
1076
+ ) -> None:
1077
+ """
1078
+ Dissolve selected edges and faces limited by the angle of adjacent faces
1079
+
1080
+ Based on bpy.ops.mesh.dissolve_limited
1081
+
1082
+ Args:
1083
+ edge_mask: Boolean array selecting edges to dissolve. If None, operates on entire mesh.
1084
+ """
1085
+ execute_mesh_op(
1086
+ bpy.ops.mesh.dissolve_limited,
1087
+ mutates_obj,
1088
+ angle_limit=angle_limit,
1089
+ use_dissolve_boundaries=use_dissolve_boundaries,
1090
+ delimit=delimit,
1091
+ edge_mask=edge_mask,
1092
+ )
1093
+
1094
+
1095
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1096
+ def extrude_vertices(
1097
+ mutates_obj: t.MeshObject,
1098
+ vertex_mask: np.ndarray,
1099
+ mirror: bool = False,
1100
+ value: Tuple[float, float, float] = (0.0, 0.0, 0.0),
1101
+ orient_type: Literal[
1102
+ "GLOBAL", "LOCAL", "NORMAL", "GIMBAL", "VIEW", "CURSOR"
1103
+ ] = "GLOBAL",
1104
+ constraint_axis: Tuple[bool, bool, bool] = (False, False, False),
1105
+ **kwargs: Unpack[ProportionalEditProperties],
1106
+ ) -> None:
1107
+ """
1108
+ Extrude individual vertices and move
1109
+
1110
+ Based on bpy.ops.mesh.extrude_vertices_move
1111
+
1112
+ Args:
1113
+ vertex_mask: Boolean array selecting vertices to extrude. If None, operates on entire mesh.
1114
+ """
1115
+ proportional_edit = ProportionalEditProperties(**kwargs)
1116
+
1117
+ execute_mesh_op(
1118
+ bpy.ops.mesh.extrude_vertices_move,
1119
+ mutates_obj,
1120
+ vertex_mask=vertex_mask,
1121
+ MESH_OT_extrude_verts_indiv={
1122
+ "mirror": mirror,
1123
+ },
1124
+ TRANSFORM_OT_translate={
1125
+ "value": value,
1126
+ "orient_type": orient_type,
1127
+ "constraint_axis": constraint_axis,
1128
+ "mirror": mirror,
1129
+ "use_proportional_edit": proportional_edit.falloff is not None,
1130
+ "proportional_edit_falloff": proportional_edit.falloff or "SMOOTH",
1131
+ "proportional_size": proportional_edit.size,
1132
+ "use_proportional_connected": proportional_edit.connected,
1133
+ "use_proportional_projected": proportional_edit.projected,
1134
+ },
1135
+ )
1136
+
1137
+
1138
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1139
+ def fill_holes(
1140
+ mutates_obj: t.MeshObject,
1141
+ sides: int = 4,
1142
+ ) -> None:
1143
+ """
1144
+ Fill in holes (boundary edge loops)
1145
+
1146
+ Based on bpy.ops.mesh.fill_holes
1147
+ """
1148
+ execute_mesh_op(
1149
+ bpy.ops.mesh.fill_holes,
1150
+ mutates_obj,
1151
+ sides=sides,
1152
+ )
1153
+
1154
+
1155
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1156
+ def spin(
1157
+ mutates_obj: t.MeshObject,
1158
+ vertex_mask: np.ndarray | None = None,
1159
+ steps: int = 12,
1160
+ dupli: bool = False,
1161
+ angle: float = 1.5708,
1162
+ use_auto_merge: bool = True,
1163
+ use_normal_flip: bool = False,
1164
+ center: Tuple[float, float, float] = (0.0, 0.0, 0.0),
1165
+ axis: Tuple[float, float, float] = (0.0, 0.0, 1.0),
1166
+ ) -> None:
1167
+ """
1168
+ Extrude selected vertices in a circle around the cursor in indicated viewport
1169
+
1170
+ Based on bpy.ops.mesh.spin
1171
+
1172
+ Args:
1173
+ vertex_mask: Boolean array selecting vertices to extrude in spin. If None, operates on entire mesh.
1174
+ """
1175
+ execute_mesh_op(
1176
+ bpy.ops.mesh.spin,
1177
+ mutates_obj,
1178
+ steps=steps,
1179
+ dupli=dupli,
1180
+ angle=angle,
1181
+ use_auto_merge=use_auto_merge,
1182
+ use_normal_flip=use_normal_flip,
1183
+ center=center,
1184
+ axis=axis,
1185
+ vertex_mask=vertex_mask,
1186
+ )
1187
+
1188
+
1189
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1190
+ def vertices_smooth(
1191
+ mutates_obj: t.MeshObject,
1192
+ vertex_mask: np.ndarray | None = None,
1193
+ factor: float = 0.5,
1194
+ repeat: int = 1,
1195
+ xaxis: bool = True,
1196
+ yaxis: bool = True,
1197
+ zaxis: bool = True,
1198
+ ) -> None:
1199
+ """
1200
+ Flatten angles of selected vertices
1201
+
1202
+ Based on bpy.ops.mesh.vertices_smooth
1203
+
1204
+ Args:
1205
+ vertex_mask: Boolean array selecting vertices to smooth. If None, operates on entire mesh.
1206
+ """
1207
+ execute_mesh_op(
1208
+ bpy.ops.mesh.vertices_smooth,
1209
+ mutates_obj,
1210
+ factor=factor,
1211
+ repeat=repeat,
1212
+ xaxis=xaxis,
1213
+ yaxis=yaxis,
1214
+ zaxis=zaxis,
1215
+ wait_for_input=False,
1216
+ vertex_mask=vertex_mask,
1217
+ )
1218
+
1219
+
1220
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1221
+ def split_nonplanar_faces(
1222
+ mutates_obj: t.MeshObject,
1223
+ face_mask: np.ndarray | None = None,
1224
+ angle_limit_rad: float = 0.0872665,
1225
+ ):
1226
+ """
1227
+ Split nonplanar faces into new faces
1228
+ """
1229
+ execute_mesh_op(
1230
+ bpy.ops.mesh.vert_connect_nonplanar,
1231
+ mutates_obj,
1232
+ face_mask=face_mask,
1233
+ angle_limit=angle_limit_rad,
1234
+ )
1235
+
1236
+
1237
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1238
+ def edges_select_sharp(
1239
+ mutates_obj: t.MeshObject,
1240
+ sharpness: float = 0.523599,
1241
+ ) -> np.ndarray:
1242
+ """
1243
+ Select all sharp enough edges
1244
+
1245
+ Based on bpy.ops.mesh.edges_select_sharp
1246
+ """
1247
+ execute_mesh_op(
1248
+ bpy.ops.mesh.edges_select_sharp,
1249
+ mutates_obj,
1250
+ sharpness=sharpness,
1251
+ edge_mask=np.zeros(len(mutates_obj.item().data.edges), dtype=bool),
1252
+ empty_mask_mode="execute",
1253
+ )
1254
+ return extract_edge_mask(mutates_obj)
1255
+
1256
+
1257
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1258
+ def select_nth(
1259
+ mutates_obj: t.MeshObject,
1260
+ domain: Literal["VERT", "EDGE", "FACE"] = "FACE",
1261
+ skip: int = 1,
1262
+ nth: int = 1,
1263
+ offset: int = 0,
1264
+ ) -> np.ndarray:
1265
+ """
1266
+ Deselect every Nth element starting from the active vertex, edge or face.
1267
+
1268
+ Based on bpy.ops.mesh.select_nth
1269
+
1270
+ Parameters:
1271
+ domain: Which element type to operate on.
1272
+ skip: Number of deselected elements in the repetitive sequence.
1273
+ nth: Number of selected elements in the repetitive sequence.
1274
+ offset: Offset from the starting point.
1275
+
1276
+ Returns:
1277
+ Boolean mask of the selected elements after the operation.
1278
+ """
1279
+ if domain == "VERT":
1280
+ mask = np.ones(len(mutates_obj.item().data.vertices), dtype=bool)
1281
+ execute_mesh_op(
1282
+ bpy.ops.mesh.select_nth,
1283
+ mutates_obj,
1284
+ vertex_mask=mask,
1285
+ skip=skip,
1286
+ nth=nth,
1287
+ offset=offset,
1288
+ )
1289
+ return extract_vertex_mask(mutates_obj)
1290
+ elif domain == "EDGE":
1291
+ mask = np.ones(len(mutates_obj.item().data.edges), dtype=bool)
1292
+ execute_mesh_op(
1293
+ bpy.ops.mesh.select_nth,
1294
+ mutates_obj,
1295
+ edge_mask=mask,
1296
+ skip=skip,
1297
+ nth=nth,
1298
+ offset=offset,
1299
+ )
1300
+ return extract_edge_mask(mutates_obj)
1301
+ else:
1302
+ mask = np.ones(len(mutates_obj.item().data.polygons), dtype=bool)
1303
+ execute_mesh_op(
1304
+ bpy.ops.mesh.select_nth,
1305
+ mutates_obj,
1306
+ face_mask=mask,
1307
+ skip=skip,
1308
+ nth=nth,
1309
+ offset=offset,
1310
+ )
1311
+ return extract_face_mask(mutates_obj)
1312
+
1313
+
1314
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1315
+ def edge_split(
1316
+ mutates_obj: t.MeshObject,
1317
+ edge_mask: np.ndarray | None = None,
1318
+ type: Literal["EDGE", "VERT"] = "EDGE",
1319
+ ):
1320
+ execute_mesh_op(
1321
+ bpy.ops.mesh.edge_split,
1322
+ mutates_obj,
1323
+ edge_mask=edge_mask,
1324
+ type=type,
1325
+ )
1326
+
1327
+
1328
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1329
+ def symmetrize(
1330
+ mutates_obj: t.MeshObject,
1331
+ vertex_mask: np.ndarray | None = None,
1332
+ edge_mask: np.ndarray | None = None,
1333
+ face_mask: np.ndarray | None = None,
1334
+ direction: Literal[
1335
+ "NEGATIVE_X",
1336
+ "POSITIVE_X",
1337
+ "NEGATIVE_Y",
1338
+ "POSITIVE_Y",
1339
+ "NEGATIVE_Z",
1340
+ "POSITIVE_Z",
1341
+ ] = "NEGATIVE_X",
1342
+ threshold: int | float | None = 0.001,
1343
+ ):
1344
+ execute_mesh_op(
1345
+ bpy.ops.mesh.symmetrize,
1346
+ mutates_obj,
1347
+ vertex_mask=vertex_mask,
1348
+ edge_mask=edge_mask,
1349
+ face_mask=face_mask,
1350
+ direction=direction,
1351
+ threshold=threshold,
1352
+ )
1353
+
1354
+
1355
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1356
+ def subdivide_edgering(
1357
+ mutates_obj: t.MeshObject,
1358
+ edge_mask: np.ndarray | None = None,
1359
+ number_cuts: int = 10,
1360
+ interpolation: Literal["PATH", "SMOOTH", "SPHERE", "CREASE", "FIRE"] = "PATH",
1361
+ smoothness: float = 1.0,
1362
+ profile_shape_factor: float = 0.0,
1363
+ profile_shape: Literal["SMOOTH", "SPHERE"] = "SMOOTH",
1364
+ ):
1365
+ execute_mesh_op(
1366
+ bpy.ops.mesh.subdivide_edgering,
1367
+ mutates_obj,
1368
+ edge_mask=edge_mask,
1369
+ number_cuts=number_cuts,
1370
+ interpolation=interpolation,
1371
+ smoothness=smoothness,
1372
+ profile_shape_factor=profile_shape_factor,
1373
+ profile_shape=profile_shape,
1374
+ )
1375
+
1376
+
1377
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1378
+ def move(
1379
+ mutates_obj: t.MeshObject,
1380
+ value: Tuple[float, float, float] = (0.0, 0.0, 0.0),
1381
+ vertex_mask: np.ndarray | None = None,
1382
+ edge_mask: np.ndarray | None = None,
1383
+ face_mask: np.ndarray | None = None,
1384
+ orient_type: Literal[
1385
+ "GLOBAL", "LOCAL", "NORMAL", "GIMBAL", "VIEW", "CURSOR"
1386
+ ] = "GLOBAL",
1387
+ constraint_axis: Tuple[bool, bool, bool] = (False, False, False),
1388
+ mirror: bool = False,
1389
+ **proportional_edit_kwargs: Unpack[ProportionalEditProperties],
1390
+ ) -> None:
1391
+ """
1392
+ Move selected geometry
1393
+
1394
+ Based on bpy.ops.transform.translate
1395
+ """
1396
+ proportional_edit = ProportionalEditProperties(**proportional_edit_kwargs)
1397
+ execute_mesh_op(
1398
+ bpy.ops.transform.translate,
1399
+ mutates_obj,
1400
+ vertex_mask=vertex_mask,
1401
+ edge_mask=edge_mask,
1402
+ face_mask=face_mask,
1403
+ value=value,
1404
+ orient_type=orient_type,
1405
+ constraint_axis=constraint_axis,
1406
+ mirror=mirror,
1407
+ use_proportional_edit=proportional_edit.falloff is not None,
1408
+ proportional_edit_falloff=proportional_edit.falloff or "SMOOTH",
1409
+ proportional_size=proportional_edit.size,
1410
+ use_proportional_connected=proportional_edit.connected,
1411
+ use_proportional_projected=proportional_edit.projected,
1412
+ )
1413
+
1414
+
1415
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1416
+ def rotate(
1417
+ mutates_obj: t.MeshObject,
1418
+ value: float = 0.0,
1419
+ vertex_mask: np.ndarray | None = None,
1420
+ edge_mask: np.ndarray | None = None,
1421
+ face_mask: np.ndarray | None = None,
1422
+ orient_axis: Literal["X", "Y", "Z"] = "Z",
1423
+ orient_type: Literal[
1424
+ "GLOBAL", "LOCAL", "NORMAL", "GIMBAL", "VIEW", "CURSOR"
1425
+ ] = "GLOBAL",
1426
+ constraint_axis: Tuple[bool, bool, bool] = (False, False, False),
1427
+ mirror: bool = False,
1428
+ **proportional_edit_kwargs: Unpack[ProportionalEditProperties],
1429
+ ) -> None:
1430
+ """
1431
+ Rotate selected geometry
1432
+
1433
+ Based on bpy.ops.transform.rotate
1434
+ """
1435
+ proportional_edit = ProportionalEditProperties(**proportional_edit_kwargs)
1436
+ execute_mesh_op(
1437
+ bpy.ops.transform.rotate,
1438
+ mutates_obj,
1439
+ vertex_mask=vertex_mask,
1440
+ edge_mask=edge_mask,
1441
+ face_mask=face_mask,
1442
+ value=value,
1443
+ orient_axis=orient_axis,
1444
+ orient_type=orient_type,
1445
+ constraint_axis=constraint_axis,
1446
+ mirror=mirror,
1447
+ use_proportional_edit=proportional_edit.falloff is not None,
1448
+ proportional_edit_falloff=proportional_edit.falloff or "SMOOTH",
1449
+ proportional_size=proportional_edit.size,
1450
+ use_proportional_connected=proportional_edit.connected,
1451
+ use_proportional_projected=proportional_edit.projected,
1452
+ )
1453
+
1454
+
1455
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1456
+ def resize(
1457
+ mutates_obj: t.MeshObject,
1458
+ value: Tuple[float, float, float] = (1.0, 1.0, 1.0),
1459
+ vertex_mask: np.ndarray | None = None,
1460
+ edge_mask: np.ndarray | None = None,
1461
+ face_mask: np.ndarray | None = None,
1462
+ orient_type: Literal[
1463
+ "GLOBAL", "LOCAL", "NORMAL", "GIMBAL", "VIEW", "CURSOR"
1464
+ ] = "GLOBAL",
1465
+ constraint_axis: Tuple[bool, bool, bool] = (False, False, False),
1466
+ mirror: bool = False,
1467
+ **proportional_edit_kwargs: Unpack[ProportionalEditProperties],
1468
+ ) -> None:
1469
+ """
1470
+ Resize selected geometry
1471
+
1472
+ Based on bpy.ops.transform.resize
1473
+ """
1474
+ proportional_edit = ProportionalEditProperties(**proportional_edit_kwargs)
1475
+ execute_mesh_op(
1476
+ bpy.ops.transform.resize,
1477
+ mutates_obj,
1478
+ vertex_mask=vertex_mask,
1479
+ edge_mask=edge_mask,
1480
+ face_mask=face_mask,
1481
+ value=value,
1482
+ orient_type=orient_type,
1483
+ constraint_axis=constraint_axis,
1484
+ mirror=mirror,
1485
+ use_proportional_edit=proportional_edit.falloff is not None,
1486
+ proportional_edit_falloff=proportional_edit.falloff or "SMOOTH",
1487
+ proportional_size=proportional_edit.size,
1488
+ use_proportional_connected=proportional_edit.connected,
1489
+ use_proportional_projected=proportional_edit.projected,
1490
+ )
1491
+
1492
+
1493
+ @pf.tracer.primitive(mutates=["mutates_obj"])
1494
+ def dissolve_verts(
1495
+ mutates_obj: t.MeshObject,
1496
+ vertex_mask: np.ndarray | None = None,
1497
+ edge_mask: np.ndarray | None = None,
1498
+ face_mask: np.ndarray | None = None,
1499
+ use_face_split: bool = False,
1500
+ use_boundary_tear: bool = False,
1501
+ ):
1502
+ execute_mesh_op(
1503
+ bpy.ops.mesh.dissolve_verts,
1504
+ mutates_obj,
1505
+ vertex_mask=vertex_mask,
1506
+ edge_mask=edge_mask,
1507
+ face_mask=face_mask,
1508
+ use_face_split=use_face_split,
1509
+ use_boundary_tear=use_boundary_tear,
1510
+ )