diff options
author | Campbell Barton <ideasman42@gmail.com> | 2013-03-24 18:30:17 +0400 |
---|---|---|
committer | Campbell Barton <ideasman42@gmail.com> | 2013-03-24 18:30:17 +0400 |
commit | 7e6e45d7a7c80798a21684c278334188cce0e501 (patch) | |
tree | e603b101870c061f79aabb8b2c6ea0ad61187a00 /object_print3d_utils/operators.py | |
parent | 2b3a3f56e43bc3d6c7d1f7cda8a063319f0c6c36 (diff) |
move print toolbox into trunk
[[Split portion of a mixed commit.]]
Diffstat (limited to 'object_print3d_utils/operators.py')
-rw-r--r-- | object_print3d_utils/operators.py | 511 |
1 files changed, 511 insertions, 0 deletions
diff --git a/object_print3d_utils/operators.py b/object_print3d_utils/operators.py new file mode 100644 index 00000000..b63226d2 --- /dev/null +++ b/object_print3d_utils/operators.py @@ -0,0 +1,511 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8-80 compliant> + +# All Operator + +import bpy +import bmesh +from bpy.types import Operator +from bpy.props import (StringProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + EnumProperty, + PointerProperty, + ) + +from . import mesh_helpers +from . import report + + +def clean_float(text): + # strip trailing zeros: 0.000 -> 0.0 + index = text.rfind(".") + if index != -1: + index += 2 + head, tail = text[:index], text[index:] + tail = tail.rstrip("0") + text = head + tail + return text + +# --------- +# Mesh Info + +class Print3DInfoVolume(Operator): + """Report the volume of the active mesh""" + bl_idname = "mesh.print3d_info_volume" + bl_label = "Print3D Info Volume" + + def execute(self, context): + scene = context.scene + unit = scene.unit_settings + scale = 1.0 if unit.system == 'NONE' else unit.scale_length + obj = context.active_object + + bm = mesh_helpers.bmesh_copy_from_object(obj, apply_modifiers=True) + volume = mesh_helpers.bmesh_calc_volume(bm) + bm.free() + + info = [] + info.append(("Volume: %s³" % clean_float("%.4f" % volume), + None)) + info.append(("%s cm³" % clean_float("%.4f" % ((volume * (scale * scale * scale)) / (0.01 * 0.01 * 0.01) )), + None)) + + report.update(*info) + return {'FINISHED'} + + +class Print3DInfoArea(Operator): + """Report the surface area of the active mesh""" + bl_idname = "mesh.print3d_info_area" + bl_label = "Print3D Info Area" + + def execute(self, context): + scene = context.scene + unit = scene.unit_settings + scale = 1.0 if unit.system == 'NONE' else unit.scale_length + obj = context.active_object + + bm = mesh_helpers.bmesh_copy_from_object(obj, apply_modifiers=True) + area = mesh_helpers.bmesh_calc_area(bm) + bm.free() + + info = [] + info.append(("Area: %s²" % clean_float("%.4f" % area), + None)) + info.append(("%s cm²" % clean_float("%.4f" % ((area * (scale * scale)) / (0.01 * 0.01))), + None)) + report.update(*info) + return {'FINISHED'} + + +# --------------- +# Geometry Checks + +def execute_check(self, context): + obj = context.active_object + + info = [] + self.main_check(obj, info) + report.update(*info) + + return {'FINISHED'} + + +class Print3DCheckSolid(Operator): + """Check for geometry is solid (has valid inside/outside) and correct normals""" + bl_idname = "mesh.print3d_check_solid" + bl_label = "Print3D Check Solid" + + @staticmethod + def main_check(obj, info): + import array + + bm = mesh_helpers.bmesh_copy_from_object(obj, transform=False, triangulate=False) + + edges_non_manifold = array.array('i', (i for i, ele in enumerate(bm.edges) + if not ele.is_manifold)) + edges_non_contig = array.array('i', (i for i, ele in enumerate(bm.edges) + if ele.is_manifold and (not ele.is_contiguous))) + + info.append(("Non Manifold Edge: %d" % len(edges_non_manifold), + (bmesh.types.BMEdge, edges_non_manifold))) + + info.append(("Bad Contig. Edges: %d" % len(edges_non_contig), + (bmesh.types.BMEdge, edges_non_contig))) + + bm.free() + + def execute(self, context): + return execute_check(self, context) + + + +class Print3DCheckIntersections(Operator): + """Check geometry for self intersections""" + bl_idname = "mesh.print3d_check_intersect" + bl_label = "Print3D Check Intersections" + + @staticmethod + def main_check(obj, info): + faces_intersect = mesh_helpers.bmesh_check_self_intersect_object(obj) + info.append(("Intersect Face: %d" % len(faces_intersect), + (bmesh.types.BMFace, faces_intersect))) + + def execute(self, context): + return execute_check(self, context) + + +class Print3DCheckDegenerate(Operator): + """Check for degenerate geometry that may not print properly """ \ + """(zero area faces, zero length edges)""" + bl_idname = "mesh.print3d_check_degenerate" + bl_label = "Print3D Check Degenerate" + + @staticmethod + def main_check(obj, info): + import array + scene = bpy.context.scene + print_3d = scene.print_3d + threshold = print_3d.threshold_zero + + bm = mesh_helpers.bmesh_copy_from_object(obj, transform=False, triangulate=False) + + faces_zero = array.array('i', (i for i, ele in enumerate(bm.faces) if ele.calc_area() <= threshold)) + edges_zero = array.array('i', (i for i, ele in enumerate(bm.edges) if ele.calc_length() <= threshold)) + + info.append(("Zero Faces: %d" % len(faces_zero), + (bmesh.types.BMFace, faces_zero))) + + info.append(("Zero Edges: %d" % len(edges_zero), + (bmesh.types.BMEdge, edges_zero))) + + bm.free() + + def execute(self, context): + return execute_check(self, context) + + +class Print3DCheckDistorted(Operator): + """Check for non-flat faces """ + bl_idname = "mesh.print3d_check_distort" + bl_label = "Print3D Check Distorted Faces" + + @staticmethod + def main_check(obj, info): + import array + + scene = bpy.context.scene + print_3d = scene.print_3d + angle_distort = print_3d.angle_distort + + def face_is_distorted(ele): + no = ele.normal + angle_fn = no.angle + for loop in ele.loops: + if angle_fn(loop.calc_normal(), 1000.0) > angle_distort: + return True + return False + + bm = mesh_helpers.bmesh_copy_from_object(obj, transform=True, triangulate=False) + bm.normal_update() + + faces_distort = array.array('i', (i for i, ele in enumerate(bm.faces) if face_is_distorted(ele))) + + info.append(("Non-Flat Faces: %d" % len(faces_distort), + (bmesh.types.BMFace, faces_distort))) + + bm.free() + + def execute(self, context): + return execute_check(self, context) + + +class Print3DCheckThick(Operator): + """Check geometry is above the minimum thickness preference """ \ + """(relies on correct normals)""" + bl_idname = "mesh.print3d_check_thick" + bl_label = "Print3D Check Thickness" + + @staticmethod + def main_check(obj, info): + scene = bpy.context.scene + print_3d = scene.print_3d + + faces_error = mesh_helpers.bmesh_check_thick_object(obj, print_3d.thickness_min) + + info.append(("Thin Faces: %d" % len(faces_error), + (bmesh.types.BMFace, faces_error))) + + + def execute(self, context): + return execute_check(self, context) + + +class Print3DCheckSharp(Operator): + """Check edges are below the sharpness preference""" + bl_idname = "mesh.print3d_check_sharp" + bl_label = "Print3D Check Sharp" + + @staticmethod + def main_check(obj, info): + scene = bpy.context.scene + print_3d = scene.print_3d + angle_sharp = print_3d.angle_sharp + + bm = mesh_helpers.bmesh_copy_from_object(obj, transform=True, triangulate=False) + bm.normal_update() + + edges_sharp = [ele.index for ele in bm.edges + if ele.is_manifold and ele.calc_face_angle() > angle_sharp] + + info.append(("Sharp Edge: %d" % len(edges_sharp), + (bmesh.types.BMEdge, edges_sharp))) + bm.free() + + def execute(self, context): + return execute_check(self, context) + + +class Print3DCheckOverhang(Operator): + """Check faces don't overhang past a certain angle""" + bl_idname = "mesh.print3d_check_overhang" + bl_label = "Print3D Check Overhang" + + @staticmethod + def main_check(obj, info): + import math + from mathutils import Vector + + scene = bpy.context.scene + print_3d = scene.print_3d + angle_overhang = (math.pi / 2.0) - print_3d.angle_overhang + + if angle_overhang == math.pi: + info.append(("Skipping Overhang", ())) + return + + bm = mesh_helpers.bmesh_copy_from_object(obj, transform=True, triangulate=False) + bm.normal_update() + + z_down = Vector((0, 0, -1.0)) + z_down_angle = z_down.angle + + faces_overhang = [ele.index for ele in bm.faces + if z_down_angle(ele.normal) < angle_overhang] + + info.append(("Overhang Face: %d" % len(faces_overhang), + (bmesh.types.BMFace, faces_overhang))) + bm.free() + + def execute(self, context): + return execute_check(self, context) + + +class Print3DCheckAll(Operator): + """Run all checks""" + bl_idname = "mesh.print3d_check_all" + bl_label = "Print3D Check All" + + check_cls = ( + Print3DCheckSolid, + Print3DCheckIntersections, + Print3DCheckDegenerate, + Print3DCheckDistorted, + Print3DCheckThick, + Print3DCheckSharp, + Print3DCheckOverhang, + ) + + def execute(self, context): + obj = context.active_object + + info = [] + for cls in self.check_cls: + cls.main_check(obj, info) + + report.update(*info) + + return {'FINISHED'} + + +class Print3DCleanIsolated(Operator): + """Cleanup isolated vertices and edges""" + bl_idname = "mesh.print3d_clean_isolated" + bl_label = "Print3D Clean Isolated " + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + obj = context.active_object + bm = mesh_helpers.bmesh_from_object(obj) + + info = [] + change = False + + def face_is_isolated(ele): + for loop in ele.loops: + loop_next = loop.link_loop_radial_next + if loop is not loop_next: + return False + return True + + def edge_is_isolated(ele): + return ele.is_wire + + def vert_is_isolated(ele): + return (not bool(ele.link_edges)) + + # --- face + elems_remove = [ele for ele in bm.faces if face_is_isolated(ele)] + remove = bm.faces.remove + for ele in elems_remove: + remove(ele) + change |= bool(elems_remove) + info.append(("Faces Removed: %d" % len(elems_remove), + None)) + del elems_remove + # --- edge + elems_remove = [ele for ele in bm.edges if edge_is_isolated(ele)] + remove = bm.edges.remove + for ele in elems_remove: + remove(ele) + change |= bool(elems_remove) + info.append(("Edge Removed: %d" % len(elems_remove), + None)) + del elems_remove + # --- vert + elems_remove = [ele for ele in bm.verts if vert_is_isolated(ele)] + remove = bm.verts.remove + for ele in elems_remove: + remove(ele) + change |= bool(elems_remove) + info.append(("Verts Removed: %d" % len(elems_remove), + None)) + del elems_remove + # --- + + report.update(*info) + + if change: + mesh_helpers.bmesh_to_object(obj, bm) + return {'FINISHED'} + else: + return {'CANCELLED'} + + +class Print3DCleanDistorted(Operator): + """Tessellate distorted faces""" + bl_idname = "mesh.print3d_clean_distorted" + bl_label = "Print3D Clean Distorted" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + scene = bpy.context.scene + print_3d = scene.print_3d + angle_distort = print_3d.angle_distort + + def face_is_distorted(ele): + no = ele.normal + angle_fn = no.angle + for loop in ele.loops: + if angle_fn(loop.calc_normal(), 1000.0) > angle_distort: + return True + return False + + obj = context.active_object + bm = mesh_helpers.bmesh_from_object(obj) + bm.normal_update() + elems_triangulate = [ele for ele in bm.faces if face_is_distorted(ele)] + + # edit + if elems_triangulate: + bmesh.ops.triangulate(bm, faces=elems_triangulate) + mesh_helpers.bmesh_to_object(obj, bm) + return {'FINISHED'} + else: + return {'CANCELLED'} + + +class Print3DCleanThin(Operator): + """Ensure minimum thickness""" + bl_idname = "mesh.print3d_clean_thin" + bl_label = "Print3D Clean Thin" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + TODO + + return {'FINISHED'} + + +# ------------- +# Select Report +# ... helper function for info UI + +class Print3DSelectReport(Operator): + """Select the data assosiated with this report""" + bl_idname = "mesh.print3d_select_report" + bl_label = "Print3D Select Report" + bl_options = {'INTERNAL'} + + index = IntProperty() + + _type_to_mode = { + bmesh.types.BMVert: 'VERT', + bmesh.types.BMEdge: 'EDGE', + bmesh.types.BMFace: 'FACE', + } + + _type_to_attr = { + bmesh.types.BMVert: "verts", + bmesh.types.BMEdge: "edges", + bmesh.types.BMFace: "faces", + } + + + def execute(self, context): + obj = context.edit_object + info = report.info() + text, data = info[self.index] + bm_type, bm_array = data + + bpy.ops.mesh.reveal() + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_mode(type=self._type_to_mode[bm_type]) + + bm = bmesh.from_edit_mesh(obj.data) + elems = getattr(bm, Print3DSelectReport._type_to_attr[bm_type])[:] + + try: + for i in bm_array: + elems[i].select_set(True) + except: + # possible arrays are out of sync + self.report({'WARNING'}, "Report is out of date, re-run check") + + # Perhaps this is annoying? but also handy! + bpy.ops.view3d.view_selected(use_all_regions=False) + + return {'FINISHED'} + + +# ------ +# Export + +class Print3DExport(Operator): + """Export active object using print3d settings""" + bl_idname = "mesh.print3d_export" + bl_label = "Print3D Export" + + def execute(self, context): + scene = bpy.context.scene + print_3d = scene.print_3d + from . import export + + info = [] + ret = export.write_mesh(context, info, self.report) + report.update(*info) + + if ret: + return {'FINISHED'} + else: + return {'CANCELLED'} |