1 # SPDX-License-Identifier: GPL-2.0-or-later
9 from bpy
.types
import Operator
10 from bpy
.props
import (
16 from bpy
.app
.translations
import pgettext_tip
as tip_
21 def clean_float(value
: float, precision
: int = 0) -> str:
22 # Avoid scientific notation and strip trailing zeros: 0.000 -> 0.0
24 text
= f
"{value:.{precision}f}"
25 index
= text
.rfind(".")
29 head
, tail
= text
[:index
], text
[index
:]
30 tail
= tail
.rstrip("0")
36 def get_unit(unit_system
: str, unit
: str) -> tuple[float, str]:
37 # Returns unit length relative to meter and unit symbol
41 "KILOMETERS": (1000.0, "km"),
43 "CENTIMETERS": (0.01, "cm"),
44 "MILLIMETERS": (0.001, "mm"),
45 "MICROMETERS": (0.000001, "µm"),
48 "MILES": (1609.344, "mi"),
49 "FEET": (0.3048, "\'"),
50 "INCHES": (0.0254, "\""),
51 "THOU": (0.0000254, "thou"),
56 return units
[unit_system
][unit
]
58 fallback_unit
= "CENTIMETERS" if unit_system
== "METRIC" else "INCHES"
59 return units
[unit_system
][fallback_unit
]
66 class MESH_OT_print3d_info_volume(Operator
):
67 bl_idname
= "mesh.print3d_info_volume"
68 bl_label
= "3D-Print Info Volume"
69 bl_description
= "Report the volume of the active mesh"
71 def execute(self
, context
):
72 from . import mesh_helpers
75 unit
= scene
.unit_settings
76 scale
= 1.0 if unit
.system
== 'NONE' else unit
.scale_length
77 obj
= context
.active_object
79 bm
= mesh_helpers
.bmesh_copy_from_object(obj
, apply_modifiers
=True)
80 volume
= bm
.calc_volume()
83 if unit
.system
== 'NONE':
84 volume_fmt
= clean_float(volume
, 8)
86 length
, symbol
= get_unit(unit
.system
, unit
.length_unit
)
88 volume_unit
= volume
* (scale
** 3.0) / (length
** 3.0)
89 volume_str
= clean_float(volume_unit
, 4)
90 volume_fmt
= f
"{volume_str} {symbol}"
92 report
.update((tip_("Volume: {}³").format(volume_fmt
), None))
97 class MESH_OT_print3d_info_area(Operator
):
98 bl_idname
= "mesh.print3d_info_area"
99 bl_label
= "3D-Print Info Area"
100 bl_description
= "Report the surface area of the active mesh"
102 def execute(self
, context
):
103 from . import mesh_helpers
105 scene
= context
.scene
106 unit
= scene
.unit_settings
107 scale
= 1.0 if unit
.system
== 'NONE' else unit
.scale_length
108 obj
= context
.active_object
110 bm
= mesh_helpers
.bmesh_copy_from_object(obj
, apply_modifiers
=True)
111 area
= mesh_helpers
.bmesh_calc_area(bm
)
114 if unit
.system
== 'NONE':
115 area_fmt
= clean_float(area
, 8)
117 length
, symbol
= get_unit(unit
.system
, unit
.length_unit
)
119 area_unit
= area
* (scale
** 2.0) / (length
** 2.0)
120 area_str
= clean_float(area_unit
, 4)
121 area_fmt
= f
"{area_str} {symbol}"
123 report
.update((tip_("Area: {}²").format(area_fmt
), None))
131 def execute_check(self
, context
):
132 obj
= context
.active_object
135 self
.main_check(obj
, info
)
138 multiple_obj_warning(self
, context
)
143 def multiple_obj_warning(self
, context
):
144 if len(context
.selected_objects
) > 1:
145 self
.report({"INFO"}, "Multiple selected objects. Only the active one will be evaluated")
148 class MESH_OT_print3d_check_solid(Operator
):
149 bl_idname
= "mesh.print3d_check_solid"
150 bl_label
= "3D-Print Check Solid"
151 bl_description
= "Check for geometry is solid (has valid inside/outside) and correct normals"
154 def main_check(obj
, info
):
156 from . import mesh_helpers
158 bm
= mesh_helpers
.bmesh_copy_from_object(obj
, transform
=False, triangulate
=False)
160 edges_non_manifold
= array
.array('i', (i
for i
, ele
in enumerate(bm
.edges
) if not ele
.is_manifold
))
161 edges_non_contig
= array
.array(
163 (i
for i
, ele
in enumerate(bm
.edges
) if ele
.is_manifold
and (not ele
.is_contiguous
)),
167 (tip_("Non Manifold Edges: {}").format(
168 len(edges_non_manifold
)),
170 edges_non_manifold
)))
171 info
.append((tip_("Bad Contiguous Edges: {}").format(len(edges_non_contig
)), (bmesh
.types
.BMEdge
, edges_non_contig
)))
175 def execute(self
, context
):
176 return execute_check(self
, context
)
179 class MESH_OT_print3d_check_intersections(Operator
):
180 bl_idname
= "mesh.print3d_check_intersect"
181 bl_label
= "3D-Print Check Intersections"
182 bl_description
= "Check geometry for self intersections"
185 def main_check(obj
, info
):
186 from . import mesh_helpers
188 faces_intersect
= mesh_helpers
.bmesh_check_self_intersect_object(obj
)
189 info
.append((tip_("Intersect Face: {}").format(len(faces_intersect
)), (bmesh
.types
.BMFace
, faces_intersect
)))
191 def execute(self
, context
):
192 return execute_check(self
, context
)
195 class MESH_OT_print3d_check_degenerate(Operator
):
196 bl_idname
= "mesh.print3d_check_degenerate"
197 bl_label
= "3D-Print Check Degenerate"
199 "Check for degenerate geometry that may not print properly "
200 "(zero area faces, zero length edges)"
204 def main_check(obj
, info
):
206 from . import mesh_helpers
208 scene
= bpy
.context
.scene
209 print_3d
= scene
.print_3d
210 threshold
= print_3d
.threshold_zero
212 bm
= mesh_helpers
.bmesh_copy_from_object(obj
, transform
=False, triangulate
=False)
214 faces_zero
= array
.array('i', (i
for i
, ele
in enumerate(bm
.faces
) if ele
.calc_area() <= threshold
))
215 edges_zero
= array
.array('i', (i
for i
, ele
in enumerate(bm
.edges
) if ele
.calc_length() <= threshold
))
217 info
.append((tip_("Zero Faces: {}").format(len(faces_zero
)), (bmesh
.types
.BMFace
, faces_zero
)))
218 info
.append((tip_("Zero Edges: {}").format(len(edges_zero
)), (bmesh
.types
.BMEdge
, edges_zero
)))
222 def execute(self
, context
):
223 return execute_check(self
, context
)
226 class MESH_OT_print3d_check_distorted(Operator
):
227 bl_idname
= "mesh.print3d_check_distort"
228 bl_label
= "3D-Print Check Distorted Faces"
229 bl_description
= "Check for non-flat faces"
232 def main_check(obj
, info
):
234 from . import mesh_helpers
236 scene
= bpy
.context
.scene
237 print_3d
= scene
.print_3d
238 angle_distort
= print_3d
.angle_distort
240 bm
= mesh_helpers
.bmesh_copy_from_object(obj
, transform
=True, triangulate
=False)
243 faces_distort
= array
.array(
245 (i
for i
, ele
in enumerate(bm
.faces
) if mesh_helpers
.face_is_distorted(ele
, angle_distort
))
248 info
.append((tip_("Non-Flat Faces: {}").format(len(faces_distort
)), (bmesh
.types
.BMFace
, faces_distort
)))
252 def execute(self
, context
):
253 return execute_check(self
, context
)
256 class MESH_OT_print3d_check_thick(Operator
):
257 bl_idname
= "mesh.print3d_check_thick"
258 bl_label
= "3D-Print Check Thickness"
260 "Check geometry is above the minimum thickness preference "
261 "(relies on correct normals)"
265 def main_check(obj
, info
):
266 from . import mesh_helpers
268 scene
= bpy
.context
.scene
269 print_3d
= scene
.print_3d
271 faces_error
= mesh_helpers
.bmesh_check_thick_object(obj
, print_3d
.thickness_min
)
272 info
.append((tip_("Thin Faces: {}").format(len(faces_error
)), (bmesh
.types
.BMFace
, faces_error
)))
274 def execute(self
, context
):
275 return execute_check(self
, context
)
278 class MESH_OT_print3d_check_sharp(Operator
):
279 bl_idname
= "mesh.print3d_check_sharp"
280 bl_label
= "3D-Print Check Sharp"
281 bl_description
= "Check edges are below the sharpness preference"
284 def main_check(obj
, info
):
285 from . import mesh_helpers
287 scene
= bpy
.context
.scene
288 print_3d
= scene
.print_3d
289 angle_sharp
= print_3d
.angle_sharp
291 bm
= mesh_helpers
.bmesh_copy_from_object(obj
, transform
=True, triangulate
=False)
295 ele
.index
for ele
in bm
.edges
296 if ele
.is_manifold
and ele
.calc_face_angle_signed() > angle_sharp
299 info
.append((tip_("Sharp Edge: {}").format(len(edges_sharp
)), (bmesh
.types
.BMEdge
, edges_sharp
)))
302 def execute(self
, context
):
303 return execute_check(self
, context
)
306 class MESH_OT_print3d_check_overhang(Operator
):
307 bl_idname
= "mesh.print3d_check_overhang"
308 bl_label
= "3D-Print Check Overhang"
309 bl_description
= "Check faces don't overhang past a certain angle"
312 def main_check(obj
, info
):
313 from mathutils
import Vector
314 from . import mesh_helpers
316 scene
= bpy
.context
.scene
317 print_3d
= scene
.print_3d
318 angle_overhang
= (math
.pi
/ 2.0) - print_3d
.angle_overhang
320 if angle_overhang
== math
.pi
:
321 info
.append(("Skipping Overhang", ()))
324 bm
= mesh_helpers
.bmesh_copy_from_object(obj
, transform
=True, triangulate
=False)
327 z_down
= Vector((0, 0, -1.0))
328 z_down_angle
= z_down
.angle
330 # 4.0 ignores zero area faces
332 ele
.index
for ele
in bm
.faces
333 if z_down_angle(ele
.normal
, 4.0) < angle_overhang
336 info
.append((tip_("Overhang Face: {}").format(len(faces_overhang
)), (bmesh
.types
.BMFace
, faces_overhang
)))
339 def execute(self
, context
):
340 return execute_check(self
, context
)
343 class MESH_OT_print3d_check_all(Operator
):
344 bl_idname
= "mesh.print3d_check_all"
345 bl_label
= "3D-Print Check All"
346 bl_description
= "Run all checks"
349 MESH_OT_print3d_check_solid
,
350 MESH_OT_print3d_check_intersections
,
351 MESH_OT_print3d_check_degenerate
,
352 MESH_OT_print3d_check_distorted
,
353 MESH_OT_print3d_check_thick
,
354 MESH_OT_print3d_check_sharp
,
355 MESH_OT_print3d_check_overhang
,
358 def execute(self
, context
):
359 obj
= context
.active_object
362 for cls
in self
.check_cls
:
363 cls
.main_check(obj
, info
)
367 multiple_obj_warning(self
, context
)
372 class MESH_OT_print3d_clean_distorted(Operator
):
373 bl_idname
= "mesh.print3d_clean_distorted"
374 bl_label
= "3D-Print Clean Distorted"
375 bl_description
= "Tessellate distorted faces"
376 bl_options
= {'REGISTER', 'UNDO'}
378 angle
: FloatProperty(
380 description
="Limit for checking distorted faces",
382 default
=math
.radians(45.0),
384 max=math
.radians(180.0),
387 def execute(self
, context
):
388 from . import mesh_helpers
390 obj
= context
.active_object
391 bm
= mesh_helpers
.bmesh_from_object(obj
)
393 elems_triangulate
= [ele
for ele
in bm
.faces
if mesh_helpers
.face_is_distorted(ele
, self
.angle
)]
395 if elems_triangulate
:
396 bmesh
.ops
.triangulate(bm
, faces
=elems_triangulate
)
397 mesh_helpers
.bmesh_to_object(obj
, bm
)
399 self
.report({'INFO'}, tip_("Triangulated {} faces").format(len(elems_triangulate
)))
403 def invoke(self
, context
, event
):
404 print_3d
= context
.scene
.print_3d
405 self
.angle
= print_3d
.angle_distort
407 return self
.execute(context
)
410 class MESH_OT_print3d_clean_non_manifold(Operator
):
411 bl_idname
= "mesh.print3d_clean_non_manifold"
412 bl_label
= "3D-Print Clean Non-Manifold"
413 bl_description
= "Cleanup problems, like holes, non-manifold vertices and inverted normals"
414 bl_options
= {'REGISTER', 'UNDO'}
416 threshold
: FloatProperty(
417 name
="Merge Distance",
418 description
="Minimum distance between elements to merge",
423 description
="Number of sides in hole required to fill (zero fills all holes)",
427 def execute(self
, context
):
428 self
.context
= context
429 mode_orig
= context
.mode
431 self
.setup_environment()
432 bm_key_orig
= self
.elem_count(context
)
435 self
.delete_interior()
436 self
.remove_doubles(self
.threshold
)
437 self
.dissolve_degenerate(self
.threshold
)
438 self
.fix_non_manifold(context
, self
.sides
) # may take a while
439 self
.make_normals_consistently_outwards()
441 bm_key
= self
.elem_count(context
)
443 if mode_orig
!= 'EDIT_MESH':
444 bpy
.ops
.object.mode_set(mode
='OBJECT')
446 verts
= bm_key
[0] - bm_key_orig
[0]
447 edges
= bm_key
[1] - bm_key_orig
[1]
448 faces
= bm_key
[2] - bm_key_orig
[2]
450 self
.report({'INFO'}, tip_("Modified: {:+} vertices, {:+} edges, {:+} faces").format(verts
, edges
, faces
))
455 def elem_count(context
):
456 bm
= bmesh
.from_edit_mesh(context
.edit_object
.data
)
457 return len(bm
.verts
), len(bm
.edges
), len(bm
.faces
)
460 def setup_environment():
461 """set the mode as edit, select mode as vertices, and reveal hidden vertices"""
462 bpy
.ops
.object.mode_set(mode
='EDIT')
463 bpy
.ops
.mesh
.select_mode(type='VERT')
464 bpy
.ops
.mesh
.reveal()
467 def remove_doubles(threshold
):
468 """remove duplicate vertices"""
469 bpy
.ops
.mesh
.select_all(action
='SELECT')
470 bpy
.ops
.mesh
.remove_doubles(threshold
=threshold
)
474 """delete loose vertices/edges/faces"""
475 bpy
.ops
.mesh
.select_all(action
='SELECT')
476 bpy
.ops
.mesh
.delete_loose(use_verts
=True, use_edges
=True, use_faces
=True)
479 def delete_interior():
480 """delete interior faces"""
481 bpy
.ops
.mesh
.select_all(action
='DESELECT')
482 bpy
.ops
.mesh
.select_interior_faces()
483 bpy
.ops
.mesh
.delete(type='FACE')
486 def dissolve_degenerate(threshold
):
487 """dissolve zero area faces and zero length edges"""
488 bpy
.ops
.mesh
.select_all(action
='SELECT')
489 bpy
.ops
.mesh
.dissolve_degenerate(threshold
=threshold
)
492 def make_normals_consistently_outwards():
493 """have all normals face outwards"""
494 bpy
.ops
.mesh
.select_all(action
='SELECT')
495 bpy
.ops
.mesh
.normals_make_consistent()
498 def fix_non_manifold(cls
, context
, sides
):
499 """naive iterate-until-no-more approach for fixing manifolds"""
500 total_non_manifold
= cls
.count_non_manifold_verts(context
)
502 if not total_non_manifold
:
506 bm_key
= cls
.elem_count(context
)
507 bm_states
.add(bm_key
)
510 cls
.fill_non_manifold(sides
)
511 cls
.delete_newly_generated_non_manifold_verts()
513 bm_key
= cls
.elem_count(context
)
514 if bm_key
in bm_states
:
517 bm_states
.add(bm_key
)
520 def select_non_manifold_verts(
523 use_multi_face
=False,
524 use_non_contiguous
=False,
527 """select non-manifold vertices"""
528 bpy
.ops
.mesh
.select_non_manifold(
531 use_boundary
=use_boundary
,
532 use_multi_face
=use_multi_face
,
533 use_non_contiguous
=use_non_contiguous
,
538 def count_non_manifold_verts(cls
, context
):
539 """return a set of coordinates of non-manifold vertices"""
540 cls
.select_non_manifold_verts(use_wire
=True, use_boundary
=True, use_verts
=True)
542 bm
= bmesh
.from_edit_mesh(context
.edit_object
.data
)
543 return sum((1 for v
in bm
.verts
if v
.select
))
546 def fill_non_manifold(cls
, sides
):
547 """fill in any remnant non-manifolds"""
548 bpy
.ops
.mesh
.select_all(action
='SELECT')
549 bpy
.ops
.mesh
.fill_holes(sides
=sides
)
552 def delete_newly_generated_non_manifold_verts(cls
):
553 """delete any newly generated vertices from the filling repair"""
554 cls
.select_non_manifold_verts(use_wire
=True, use_verts
=True)
555 bpy
.ops
.mesh
.delete(type='VERT')
558 class MESH_OT_print3d_clean_thin(Operator
):
559 bl_idname
= "mesh.print3d_clean_thin"
560 bl_label
= "3D-Print Clean Thin"
561 bl_description
= "Ensure minimum thickness"
562 bl_options
= {'REGISTER', 'UNDO'}
564 def execute(self
, context
):
572 # ... helper function for info UI
574 class MESH_OT_print3d_select_report(Operator
):
575 bl_idname
= "mesh.print3d_select_report"
576 bl_label
= "3D-Print Select Report"
577 bl_description
= "Select the data associated with this report"
578 bl_options
= {'INTERNAL'}
583 bmesh
.types
.BMVert
: 'VERT',
584 bmesh
.types
.BMEdge
: 'EDGE',
585 bmesh
.types
.BMFace
: 'FACE',
589 bmesh
.types
.BMVert
: "verts",
590 bmesh
.types
.BMEdge
: "edges",
591 bmesh
.types
.BMFace
: "faces",
594 def execute(self
, context
):
595 obj
= context
.edit_object
597 _text
, data
= info
[self
.index
]
598 bm_type
, bm_array
= data
600 bpy
.ops
.mesh
.reveal()
601 bpy
.ops
.mesh
.select_all(action
='DESELECT')
602 bpy
.ops
.mesh
.select_mode(type=self
._type
_to
_mode
[bm_type
])
604 bm
= bmesh
.from_edit_mesh(obj
.data
)
605 elems
= getattr(bm
, MESH_OT_print3d_select_report
._type
_to
_attr
[bm_type
])[:]
609 elems
[i
].select_set(True)
611 # possible arrays are out of sync
612 self
.report({'WARNING'}, "Report is out of date, re-run check")
620 def _scale(scale
, report
=None, report_suffix
=""):
622 bpy
.ops
.transform
.resize(value
=(scale
,) * 3)
623 if report
is not None:
624 scale_fmt
= clean_float(scale
, 6)
625 report({'INFO'}, tip_("Scaled by {}{}").format(scale_fmt
, report_suffix
))
628 class MESH_OT_print3d_scale_to_volume(Operator
):
629 bl_idname
= "mesh.print3d_scale_to_volume"
630 bl_label
= "Scale to Volume"
631 bl_description
= "Scale edit-mesh or selected-objects to a set volume"
632 bl_options
= {'REGISTER', 'UNDO'}
634 volume_init
: FloatProperty(
637 volume
: FloatProperty(
644 def execute(self
, context
):
645 scale
= math
.pow(self
.volume
, 1 / 3) / math
.pow(self
.volume_init
, 1 / 3)
646 scale_fmt
= clean_float(scale
, 6)
647 self
.report({'INFO'}, tip_("Scaled by {}").format(scale_fmt
))
648 _scale(scale
, self
.report
)
651 def invoke(self
, context
, event
):
653 def calc_volume(obj
):
654 from . import mesh_helpers
656 bm
= mesh_helpers
.bmesh_copy_from_object(obj
, apply_modifiers
=True)
657 volume
= bm
.calc_volume(signed
=True)
661 if context
.mode
== 'EDIT_MESH':
662 volume
= calc_volume(context
.edit_object
)
664 volume
= sum(calc_volume(obj
) for obj
in context
.selected_editable_objects
if obj
.type == 'MESH')
667 self
.report({'WARNING'}, "Object has zero volume")
670 self
.volume_init
= self
.volume
= abs(volume
)
672 wm
= context
.window_manager
673 return wm
.invoke_props_dialog(self
)
676 class MESH_OT_print3d_scale_to_bounds(Operator
):
677 bl_idname
= "mesh.print3d_scale_to_bounds"
678 bl_label
= "Scale to Bounds"
679 bl_description
= "Scale edit-mesh or selected-objects to fit within a maximum length"
680 bl_options
= {'REGISTER', 'UNDO'}
682 length_init
: FloatProperty(
685 axis_init
: IntProperty(
688 length
: FloatProperty(
695 def execute(self
, context
):
696 scale
= self
.length
/ self
.length_init
697 axis
= "XYZ"[self
.axis_init
]
698 _scale(scale
, report
=self
.report
, report_suffix
=tip_(", Clamping {}-Axis").format(axis
))
701 def invoke(self
, context
, event
):
702 from mathutils
import Vector
704 def calc_length(vecs
):
705 return max(((max(v
[i
] for v
in vecs
) - min(v
[i
] for v
in vecs
)), i
) for i
in range(3))
707 if context
.mode
== 'EDIT_MESH':
708 length
, axis
= calc_length(
709 [Vector(v
) @ obj
.matrix_world
for obj
in [context
.edit_object
] for v
in obj
.bound_box
]
712 length
, axis
= calc_length(
714 Vector(v
) @ obj
.matrix_world
for obj
in context
.selected_editable_objects
715 if obj
.type == 'MESH'
716 for v
in obj
.bound_box
721 self
.report({'WARNING'}, "Object has zero bounds")
724 self
.length_init
= self
.length
= length
725 self
.axis_init
= axis
727 wm
= context
.window_manager
728 return wm
.invoke_props_dialog(self
)
731 class MESH_OT_print3d_align_to_xy(Operator
):
732 bl_idname
= "mesh.print3d_align_to_xy"
733 bl_label
= "Align (rotate) object to XY plane"
735 "Rotates entire object (not mesh) so the selected faces/vertices lie, on average, parallel to the XY plane "
736 "(it does not adjust Z location)"
738 bl_options
= {'REGISTER', 'UNDO'}
740 def execute(self
, context
):
741 # FIXME: Undo is inconsistent.
742 # FIXME: Would be nicer if rotate could pick some object-local axis.
744 from mathutils
import Vector
746 print_3d
= context
.scene
.print_3d
747 face_areas
= print_3d
.use_alignxy_face_area
749 self
.context
= context
750 mode_orig
= context
.mode
753 for obj
in context
.selected_objects
:
754 orig_loc
= obj
.location
.copy()
755 orig_scale
= obj
.scale
.copy()
757 # When in edit mode, do as the edit mode does.
758 if mode_orig
== 'EDIT_MESH':
759 bm
= bmesh
.from_edit_mesh(obj
.data
)
760 faces
= [f
for f
in bm
.faces
if f
.select
]
762 faces
= [p
for p
in obj
.data
.polygons
if p
.select
]
765 skip_invalid
.append(obj
.name
)
768 # Rotate object so average normal of selected faces points down.
769 normal
= Vector((0.0, 0.0, 0.0))
772 if mode_orig
== 'EDIT_MESH':
773 normal
+= (face
.normal
* face
.calc_area())
775 normal
+= (face
.normal
* face
.area
)
778 normal
+= face
.normal
779 normal
= normal
.normalized()
780 normal
.rotate(obj
.matrix_world
) # local -> world.
781 offset
= normal
.rotation_difference(Vector((0.0, 0.0, -1.0)))
782 offset
= offset
.to_matrix().to_4x4()
783 obj
.matrix_world
= offset
@ obj
.matrix_world
784 obj
.scale
= orig_scale
785 obj
.location
= orig_loc
787 if len(skip_invalid
) > 0:
788 for name
in skip_invalid
:
789 print(tip_("Align to XY: Skipping object {}. No faces selected").format(name
))
790 if len(skip_invalid
) == 1:
791 self
.report({'WARNING'}, tip_("Skipping object {}. No faces selected").format(skip_invalid
[0]))
793 self
.report({'WARNING'}, "Skipping some objects. No faces selected. See terminal")
796 def invoke(self
, context
, event
):
797 if context
.mode
not in {'EDIT_MESH', 'OBJECT'}:
799 return self
.execute(context
)
805 class MESH_OT_print3d_export(Operator
):
806 bl_idname
= "mesh.print3d_export"
807 bl_label
= "3D-Print Export"
808 bl_description
= "Export selected objects using 3D-Print settings"
810 def execute(self
, context
):
813 ret
= export
.write_mesh(context
, self
.report
)