1 # SPDX-FileCopyrightText: 2013-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
11 from bpy
.types
import Operator
12 from bpy
.props
import (
18 from bpy
.app
.translations
import pgettext_tip
as tip_
23 def clean_float(value
: float, precision
: int = 0) -> str:
24 # Avoid scientific notation and strip trailing zeros: 0.000 -> 0.0
26 text
= f
"{value:.{precision}f}"
27 index
= text
.rfind(".")
31 head
, tail
= text
[:index
], text
[index
:]
32 tail
= tail
.rstrip("0")
38 def get_unit(unit_system
: str, unit
: str) -> tuple[float, str]:
39 # Returns unit length relative to meter and unit symbol
43 "KILOMETERS": (1000.0, "km"),
45 "CENTIMETERS": (0.01, "cm"),
46 "MILLIMETERS": (0.001, "mm"),
47 "MICROMETERS": (0.000001, "µm"),
50 "MILES": (1609.344, "mi"),
51 "FEET": (0.3048, "\'"),
52 "INCHES": (0.0254, "\""),
53 "THOU": (0.0000254, "thou"),
58 return units
[unit_system
][unit
]
60 fallback_unit
= "CENTIMETERS" if unit_system
== "METRIC" else "INCHES"
61 return units
[unit_system
][fallback_unit
]
68 class MESH_OT_print3d_info_volume(Operator
):
69 bl_idname
= "mesh.print3d_info_volume"
70 bl_label
= "3D-Print Info Volume"
71 bl_description
= "Report the volume of the active mesh"
73 def execute(self
, context
):
74 from . import mesh_helpers
77 unit
= scene
.unit_settings
78 scale
= 1.0 if unit
.system
== 'NONE' else unit
.scale_length
79 obj
= context
.active_object
81 bm
= mesh_helpers
.bmesh_copy_from_object(obj
, apply_modifiers
=True)
82 volume
= bm
.calc_volume()
85 if unit
.system
== 'NONE':
86 volume_fmt
= clean_float(volume
, 8)
88 length
, symbol
= get_unit(unit
.system
, unit
.length_unit
)
90 volume_unit
= volume
* (scale
** 3.0) / (length
** 3.0)
91 volume_str
= clean_float(volume_unit
, 4)
92 volume_fmt
= f
"{volume_str} {symbol}"
94 report
.update((tip_("Volume: {}³").format(volume_fmt
), None))
99 class MESH_OT_print3d_info_area(Operator
):
100 bl_idname
= "mesh.print3d_info_area"
101 bl_label
= "3D-Print Info Area"
102 bl_description
= "Report the surface area of the active mesh"
104 def execute(self
, context
):
105 from . import mesh_helpers
107 scene
= context
.scene
108 unit
= scene
.unit_settings
109 scale
= 1.0 if unit
.system
== 'NONE' else unit
.scale_length
110 obj
= context
.active_object
112 bm
= mesh_helpers
.bmesh_copy_from_object(obj
, apply_modifiers
=True)
113 area
= mesh_helpers
.bmesh_calc_area(bm
)
116 if unit
.system
== 'NONE':
117 area_fmt
= clean_float(area
, 8)
119 length
, symbol
= get_unit(unit
.system
, unit
.length_unit
)
121 area_unit
= area
* (scale
** 2.0) / (length
** 2.0)
122 area_str
= clean_float(area_unit
, 4)
123 area_fmt
= f
"{area_str} {symbol}"
125 report
.update((tip_("Area: {}²").format(area_fmt
), None))
133 def execute_check(self
, context
):
134 obj
= context
.active_object
137 self
.main_check(obj
, info
)
140 multiple_obj_warning(self
, context
)
145 def multiple_obj_warning(self
, context
):
146 if len(context
.selected_objects
) > 1:
147 self
.report({"INFO"}, "Multiple selected objects. Only the active one will be evaluated")
150 class MESH_OT_print3d_check_solid(Operator
):
151 bl_idname
= "mesh.print3d_check_solid"
152 bl_label
= "3D-Print Check Solid"
153 bl_description
= "Check for geometry is solid (has valid inside/outside) and correct normals"
156 def main_check(obj
, info
):
158 from . import mesh_helpers
160 bm
= mesh_helpers
.bmesh_copy_from_object(obj
, transform
=False, triangulate
=False)
162 edges_non_manifold
= array
.array('i', (i
for i
, ele
in enumerate(bm
.edges
) if not ele
.is_manifold
))
163 edges_non_contig
= array
.array(
165 (i
for i
, ele
in enumerate(bm
.edges
) if ele
.is_manifold
and (not ele
.is_contiguous
)),
169 (tip_("Non Manifold Edges: {}").format(
170 len(edges_non_manifold
)),
172 edges_non_manifold
)))
173 info
.append((tip_("Bad Contiguous Edges: {}").format(len(edges_non_contig
)), (bmesh
.types
.BMEdge
, edges_non_contig
)))
177 def execute(self
, context
):
178 return execute_check(self
, context
)
181 class MESH_OT_print3d_check_intersections(Operator
):
182 bl_idname
= "mesh.print3d_check_intersect"
183 bl_label
= "3D-Print Check Intersections"
184 bl_description
= "Check geometry for self intersections"
187 def main_check(obj
, info
):
188 from . import mesh_helpers
190 faces_intersect
= mesh_helpers
.bmesh_check_self_intersect_object(obj
)
191 info
.append((tip_("Intersect Face: {}").format(len(faces_intersect
)), (bmesh
.types
.BMFace
, faces_intersect
)))
193 def execute(self
, context
):
194 return execute_check(self
, context
)
197 class MESH_OT_print3d_check_degenerate(Operator
):
198 bl_idname
= "mesh.print3d_check_degenerate"
199 bl_label
= "3D-Print Check Degenerate"
201 "Check for degenerate geometry that may not print properly "
202 "(zero area faces, zero length edges)"
206 def main_check(obj
, info
):
208 from . import mesh_helpers
210 scene
= bpy
.context
.scene
211 print_3d
= scene
.print_3d
212 threshold
= print_3d
.threshold_zero
214 bm
= mesh_helpers
.bmesh_copy_from_object(obj
, transform
=False, triangulate
=False)
216 faces_zero
= array
.array('i', (i
for i
, ele
in enumerate(bm
.faces
) if ele
.calc_area() <= threshold
))
217 edges_zero
= array
.array('i', (i
for i
, ele
in enumerate(bm
.edges
) if ele
.calc_length() <= threshold
))
219 info
.append((tip_("Zero Faces: {}").format(len(faces_zero
)), (bmesh
.types
.BMFace
, faces_zero
)))
220 info
.append((tip_("Zero Edges: {}").format(len(edges_zero
)), (bmesh
.types
.BMEdge
, edges_zero
)))
224 def execute(self
, context
):
225 return execute_check(self
, context
)
228 class MESH_OT_print3d_check_distorted(Operator
):
229 bl_idname
= "mesh.print3d_check_distort"
230 bl_label
= "3D-Print Check Distorted Faces"
231 bl_description
= "Check for non-flat faces"
234 def main_check(obj
, info
):
236 from . import mesh_helpers
238 scene
= bpy
.context
.scene
239 print_3d
= scene
.print_3d
240 angle_distort
= print_3d
.angle_distort
242 bm
= mesh_helpers
.bmesh_copy_from_object(obj
, transform
=True, triangulate
=False)
245 faces_distort
= array
.array(
247 (i
for i
, ele
in enumerate(bm
.faces
) if mesh_helpers
.face_is_distorted(ele
, angle_distort
))
250 info
.append((tip_("Non-Flat Faces: {}").format(len(faces_distort
)), (bmesh
.types
.BMFace
, faces_distort
)))
254 def execute(self
, context
):
255 return execute_check(self
, context
)
258 class MESH_OT_print3d_check_thick(Operator
):
259 bl_idname
= "mesh.print3d_check_thick"
260 bl_label
= "3D-Print Check Thickness"
262 "Check geometry is above the minimum thickness preference "
263 "(relies on correct normals)"
267 def main_check(obj
, info
):
268 from . import mesh_helpers
270 scene
= bpy
.context
.scene
271 print_3d
= scene
.print_3d
273 faces_error
= mesh_helpers
.bmesh_check_thick_object(obj
, print_3d
.thickness_min
)
274 info
.append((tip_("Thin Faces: {}").format(len(faces_error
)), (bmesh
.types
.BMFace
, faces_error
)))
276 def execute(self
, context
):
277 return execute_check(self
, context
)
280 class MESH_OT_print3d_check_sharp(Operator
):
281 bl_idname
= "mesh.print3d_check_sharp"
282 bl_label
= "3D-Print Check Sharp"
283 bl_description
= "Check edges are below the sharpness preference"
286 def main_check(obj
, info
):
287 from . import mesh_helpers
289 scene
= bpy
.context
.scene
290 print_3d
= scene
.print_3d
291 angle_sharp
= print_3d
.angle_sharp
293 bm
= mesh_helpers
.bmesh_copy_from_object(obj
, transform
=True, triangulate
=False)
297 ele
.index
for ele
in bm
.edges
298 if ele
.is_manifold
and ele
.calc_face_angle_signed() > angle_sharp
301 info
.append((tip_("Sharp Edge: {}").format(len(edges_sharp
)), (bmesh
.types
.BMEdge
, edges_sharp
)))
304 def execute(self
, context
):
305 return execute_check(self
, context
)
308 class MESH_OT_print3d_check_overhang(Operator
):
309 bl_idname
= "mesh.print3d_check_overhang"
310 bl_label
= "3D-Print Check Overhang"
311 bl_description
= "Check faces don't overhang past a certain angle"
314 def main_check(obj
, info
):
315 from mathutils
import Vector
316 from . import mesh_helpers
318 scene
= bpy
.context
.scene
319 print_3d
= scene
.print_3d
320 angle_overhang
= (math
.pi
/ 2.0) - print_3d
.angle_overhang
322 if angle_overhang
== math
.pi
:
323 info
.append(("Skipping Overhang", ()))
326 bm
= mesh_helpers
.bmesh_copy_from_object(obj
, transform
=True, triangulate
=False)
329 z_down
= Vector((0, 0, -1.0))
330 z_down_angle
= z_down
.angle
332 # 4.0 ignores zero area faces
334 ele
.index
for ele
in bm
.faces
335 if z_down_angle(ele
.normal
, 4.0) < angle_overhang
338 info
.append((tip_("Overhang Face: {}").format(len(faces_overhang
)), (bmesh
.types
.BMFace
, faces_overhang
)))
341 def execute(self
, context
):
342 return execute_check(self
, context
)
345 class MESH_OT_print3d_check_all(Operator
):
346 bl_idname
= "mesh.print3d_check_all"
347 bl_label
= "3D-Print Check All"
348 bl_description
= "Run all checks"
351 MESH_OT_print3d_check_solid
,
352 MESH_OT_print3d_check_intersections
,
353 MESH_OT_print3d_check_degenerate
,
354 MESH_OT_print3d_check_distorted
,
355 MESH_OT_print3d_check_thick
,
356 MESH_OT_print3d_check_sharp
,
357 MESH_OT_print3d_check_overhang
,
360 def execute(self
, context
):
361 obj
= context
.active_object
364 for cls
in self
.check_cls
:
365 cls
.main_check(obj
, info
)
369 multiple_obj_warning(self
, context
)
374 class MESH_OT_print3d_clean_distorted(Operator
):
375 bl_idname
= "mesh.print3d_clean_distorted"
376 bl_label
= "3D-Print Clean Distorted"
377 bl_description
= "Tessellate distorted faces"
378 bl_options
= {'REGISTER', 'UNDO'}
380 angle
: FloatProperty(
382 description
="Limit for checking distorted faces",
384 default
=math
.radians(45.0),
386 max=math
.radians(180.0),
389 def execute(self
, context
):
390 from . import mesh_helpers
392 obj
= context
.active_object
393 bm
= mesh_helpers
.bmesh_from_object(obj
)
395 elems_triangulate
= [ele
for ele
in bm
.faces
if mesh_helpers
.face_is_distorted(ele
, self
.angle
)]
397 if elems_triangulate
:
398 bmesh
.ops
.triangulate(bm
, faces
=elems_triangulate
)
399 mesh_helpers
.bmesh_to_object(obj
, bm
)
401 self
.report({'INFO'}, tip_("Triangulated {} faces").format(len(elems_triangulate
)))
405 def invoke(self
, context
, event
):
406 print_3d
= context
.scene
.print_3d
407 self
.angle
= print_3d
.angle_distort
409 return self
.execute(context
)
412 class MESH_OT_print3d_clean_non_manifold(Operator
):
413 bl_idname
= "mesh.print3d_clean_non_manifold"
414 bl_label
= "3D-Print Clean Non-Manifold"
415 bl_description
= "Cleanup problems, like holes, non-manifold vertices and inverted normals"
416 bl_options
= {'REGISTER', 'UNDO'}
418 threshold
: FloatProperty(
419 name
="Merge Distance",
420 description
="Minimum distance between elements to merge",
425 description
="Number of sides in hole required to fill (zero fills all holes)",
429 def execute(self
, context
):
430 self
.context
= context
431 mode_orig
= context
.mode
433 self
.setup_environment()
434 bm_key_orig
= self
.elem_count(context
)
437 self
.delete_interior()
438 self
.remove_doubles(self
.threshold
)
439 self
.dissolve_degenerate(self
.threshold
)
440 self
.fix_non_manifold(context
, self
.sides
) # may take a while
441 self
.make_normals_consistently_outwards()
443 bm_key
= self
.elem_count(context
)
445 if mode_orig
!= 'EDIT_MESH':
446 bpy
.ops
.object.mode_set(mode
='OBJECT')
448 verts
= bm_key
[0] - bm_key_orig
[0]
449 edges
= bm_key
[1] - bm_key_orig
[1]
450 faces
= bm_key
[2] - bm_key_orig
[2]
452 self
.report({'INFO'}, tip_("Modified: {:+} vertices, {:+} edges, {:+} faces").format(verts
, edges
, faces
))
457 def elem_count(context
):
458 bm
= bmesh
.from_edit_mesh(context
.edit_object
.data
)
459 return len(bm
.verts
), len(bm
.edges
), len(bm
.faces
)
462 def setup_environment():
463 """set the mode as edit, select mode as vertices, and reveal hidden vertices"""
464 bpy
.ops
.object.mode_set(mode
='EDIT')
465 bpy
.ops
.mesh
.select_mode(type='VERT')
466 bpy
.ops
.mesh
.reveal()
469 def remove_doubles(threshold
):
470 """remove duplicate vertices"""
471 bpy
.ops
.mesh
.select_all(action
='SELECT')
472 bpy
.ops
.mesh
.remove_doubles(threshold
=threshold
)
476 """delete loose vertices/edges/faces"""
477 bpy
.ops
.mesh
.select_all(action
='SELECT')
478 bpy
.ops
.mesh
.delete_loose(use_verts
=True, use_edges
=True, use_faces
=True)
481 def delete_interior():
482 """delete interior faces"""
483 bpy
.ops
.mesh
.select_all(action
='DESELECT')
484 bpy
.ops
.mesh
.select_interior_faces()
485 bpy
.ops
.mesh
.delete(type='FACE')
488 def dissolve_degenerate(threshold
):
489 """dissolve zero area faces and zero length edges"""
490 bpy
.ops
.mesh
.select_all(action
='SELECT')
491 bpy
.ops
.mesh
.dissolve_degenerate(threshold
=threshold
)
494 def make_normals_consistently_outwards():
495 """have all normals face outwards"""
496 bpy
.ops
.mesh
.select_all(action
='SELECT')
497 bpy
.ops
.mesh
.normals_make_consistent()
500 def fix_non_manifold(cls
, context
, sides
):
501 """naive iterate-until-no-more approach for fixing manifolds"""
502 total_non_manifold
= cls
.count_non_manifold_verts(context
)
504 if not total_non_manifold
:
508 bm_key
= cls
.elem_count(context
)
509 bm_states
.add(bm_key
)
512 cls
.fill_non_manifold(sides
)
513 cls
.delete_newly_generated_non_manifold_verts()
515 bm_key
= cls
.elem_count(context
)
516 if bm_key
in bm_states
:
519 bm_states
.add(bm_key
)
522 def select_non_manifold_verts(
525 use_multi_face
=False,
526 use_non_contiguous
=False,
529 """select non-manifold vertices"""
530 bpy
.ops
.mesh
.select_non_manifold(
533 use_boundary
=use_boundary
,
534 use_multi_face
=use_multi_face
,
535 use_non_contiguous
=use_non_contiguous
,
540 def count_non_manifold_verts(cls
, context
):
541 """return a set of coordinates of non-manifold vertices"""
542 cls
.select_non_manifold_verts(use_wire
=True, use_boundary
=True, use_verts
=True)
544 bm
= bmesh
.from_edit_mesh(context
.edit_object
.data
)
545 return sum((1 for v
in bm
.verts
if v
.select
))
548 def fill_non_manifold(cls
, sides
):
549 """fill in any remnant non-manifolds"""
550 bpy
.ops
.mesh
.select_all(action
='SELECT')
551 bpy
.ops
.mesh
.fill_holes(sides
=sides
)
554 def delete_newly_generated_non_manifold_verts(cls
):
555 """delete any newly generated vertices from the filling repair"""
556 cls
.select_non_manifold_verts(use_wire
=True, use_verts
=True)
557 bpy
.ops
.mesh
.delete(type='VERT')
560 class MESH_OT_print3d_clean_thin(Operator
):
561 bl_idname
= "mesh.print3d_clean_thin"
562 bl_label
= "3D-Print Clean Thin"
563 bl_description
= "Ensure minimum thickness"
564 bl_options
= {'REGISTER', 'UNDO'}
566 def execute(self
, context
):
574 # ... helper function for info UI
576 class MESH_OT_print3d_select_report(Operator
):
577 bl_idname
= "mesh.print3d_select_report"
578 bl_label
= "3D-Print Select Report"
579 bl_description
= "Select the data associated with this report"
580 bl_options
= {'INTERNAL'}
585 bmesh
.types
.BMVert
: 'VERT',
586 bmesh
.types
.BMEdge
: 'EDGE',
587 bmesh
.types
.BMFace
: 'FACE',
591 bmesh
.types
.BMVert
: "verts",
592 bmesh
.types
.BMEdge
: "edges",
593 bmesh
.types
.BMFace
: "faces",
596 def execute(self
, context
):
597 obj
= context
.edit_object
599 _text
, data
= info
[self
.index
]
600 bm_type
, bm_array
= data
602 bpy
.ops
.mesh
.reveal()
603 bpy
.ops
.mesh
.select_all(action
='DESELECT')
604 bpy
.ops
.mesh
.select_mode(type=self
._type
_to
_mode
[bm_type
])
606 bm
= bmesh
.from_edit_mesh(obj
.data
)
607 elems
= getattr(bm
, MESH_OT_print3d_select_report
._type
_to
_attr
[bm_type
])[:]
611 elems
[i
].select_set(True)
613 # possible arrays are out of sync
614 self
.report({'WARNING'}, "Report is out of date, re-run check")
622 def _scale(scale
, report
=None, report_suffix
=""):
624 bpy
.ops
.transform
.resize(value
=(scale
,) * 3)
625 if report
is not None:
626 scale_fmt
= clean_float(scale
, 6)
627 report({'INFO'}, tip_("Scaled by {}{}").format(scale_fmt
, report_suffix
))
630 class MESH_OT_print3d_scale_to_volume(Operator
):
631 bl_idname
= "mesh.print3d_scale_to_volume"
632 bl_label
= "Scale to Volume"
633 bl_description
= "Scale edit-mesh or selected-objects to a set volume"
634 bl_options
= {'REGISTER', 'UNDO'}
636 volume_init
: FloatProperty(
639 volume
: FloatProperty(
646 def execute(self
, context
):
647 scale
= math
.pow(self
.volume
, 1 / 3) / math
.pow(self
.volume_init
, 1 / 3)
648 scale_fmt
= clean_float(scale
, 6)
649 self
.report({'INFO'}, tip_("Scaled by {}").format(scale_fmt
))
650 _scale(scale
, self
.report
)
653 def invoke(self
, context
, event
):
655 def calc_volume(obj
):
656 from . import mesh_helpers
658 bm
= mesh_helpers
.bmesh_copy_from_object(obj
, apply_modifiers
=True)
659 volume
= bm
.calc_volume(signed
=True)
663 if context
.mode
== 'EDIT_MESH':
664 volume
= calc_volume(context
.edit_object
)
666 volume
= sum(calc_volume(obj
) for obj
in context
.selected_editable_objects
if obj
.type == 'MESH')
669 self
.report({'WARNING'}, "Object has zero volume")
672 self
.volume_init
= self
.volume
= abs(volume
)
674 wm
= context
.window_manager
675 return wm
.invoke_props_dialog(self
)
678 class MESH_OT_print3d_scale_to_bounds(Operator
):
679 bl_idname
= "mesh.print3d_scale_to_bounds"
680 bl_label
= "Scale to Bounds"
681 bl_description
= "Scale edit-mesh or selected-objects to fit within a maximum length"
682 bl_options
= {'REGISTER', 'UNDO'}
684 length_init
: FloatProperty(
687 axis_init
: IntProperty(
690 length
: FloatProperty(
697 def execute(self
, context
):
698 scale
= self
.length
/ self
.length_init
699 axis
= "XYZ"[self
.axis_init
]
700 _scale(scale
, report
=self
.report
, report_suffix
=tip_(", Clamping {}-Axis").format(axis
))
703 def invoke(self
, context
, event
):
704 from mathutils
import Vector
706 def calc_length(vecs
):
707 return max(((max(v
[i
] for v
in vecs
) - min(v
[i
] for v
in vecs
)), i
) for i
in range(3))
709 if context
.mode
== 'EDIT_MESH':
710 length
, axis
= calc_length(
711 [Vector(v
) @ obj
.matrix_world
for obj
in [context
.edit_object
] for v
in obj
.bound_box
]
714 length
, axis
= calc_length(
716 Vector(v
) @ obj
.matrix_world
for obj
in context
.selected_editable_objects
717 if obj
.type == 'MESH'
718 for v
in obj
.bound_box
723 self
.report({'WARNING'}, "Object has zero bounds")
726 self
.length_init
= self
.length
= length
727 self
.axis_init
= axis
729 wm
= context
.window_manager
730 return wm
.invoke_props_dialog(self
)
733 class MESH_OT_print3d_align_to_xy(Operator
):
734 bl_idname
= "mesh.print3d_align_to_xy"
735 bl_label
= "Align (rotate) object to XY plane"
737 "Rotates entire object (not mesh) so the selected faces/vertices lie, on average, parallel to the XY plane "
738 "(it does not adjust Z location)"
740 bl_options
= {'REGISTER', 'UNDO'}
742 def execute(self
, context
):
743 # FIXME: Undo is inconsistent.
744 # FIXME: Would be nicer if rotate could pick some object-local axis.
746 from mathutils
import Vector
748 print_3d
= context
.scene
.print_3d
749 face_areas
= print_3d
.use_alignxy_face_area
751 self
.context
= context
752 mode_orig
= context
.mode
755 for obj
in context
.selected_objects
:
756 orig_loc
= obj
.location
.copy()
757 orig_scale
= obj
.scale
.copy()
759 # When in edit mode, do as the edit mode does.
760 if mode_orig
== 'EDIT_MESH':
761 bm
= bmesh
.from_edit_mesh(obj
.data
)
762 faces
= [f
for f
in bm
.faces
if f
.select
]
764 faces
= [p
for p
in obj
.data
.polygons
if p
.select
]
767 skip_invalid
.append(obj
.name
)
770 # Rotate object so average normal of selected faces points down.
771 normal
= Vector((0.0, 0.0, 0.0))
774 if mode_orig
== 'EDIT_MESH':
775 normal
+= (face
.normal
* face
.calc_area())
777 normal
+= (face
.normal
* face
.area
)
780 normal
+= face
.normal
781 normal
= normal
.normalized()
782 normal
.rotate(obj
.matrix_world
) # local -> world.
783 offset
= normal
.rotation_difference(Vector((0.0, 0.0, -1.0)))
784 offset
= offset
.to_matrix().to_4x4()
785 obj
.matrix_world
= offset
@ obj
.matrix_world
786 obj
.scale
= orig_scale
787 obj
.location
= orig_loc
789 if len(skip_invalid
) > 0:
790 for name
in skip_invalid
:
791 print(tip_("Align to XY: Skipping object {}. No faces selected").format(name
))
792 if len(skip_invalid
) == 1:
793 self
.report({'WARNING'}, tip_("Skipping object {}. No faces selected").format(skip_invalid
[0]))
795 self
.report({'WARNING'}, "Skipping some objects. No faces selected. See terminal")
798 def invoke(self
, context
, event
):
799 if context
.mode
not in {'EDIT_MESH', 'OBJECT'}:
801 return self
.execute(context
)
807 class MESH_OT_print3d_export(Operator
):
808 bl_idname
= "mesh.print3d_export"
809 bl_label
= "3D-Print Export"
810 bl_description
= "Export selected objects using 3D-Print settings"
812 def execute(self
, context
):
815 ret
= export
.write_mesh(context
, self
.report
)