Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCampbell Barton <ideasman42@gmail.com>2013-03-24 18:30:17 +0400
committerCampbell Barton <ideasman42@gmail.com>2013-03-24 18:30:17 +0400
commit7e6e45d7a7c80798a21684c278334188cce0e501 (patch)
treee603b101870c061f79aabb8b2c6ea0ad61187a00 /object_print3d_utils
parent2b3a3f56e43bc3d6c7d1f7cda8a063319f0c6c36 (diff)
move print toolbox into trunk
[[Split portion of a mixed commit.]]
Diffstat (limited to 'object_print3d_utils')
-rw-r--r--object_print3d_utils/__init__.py147
-rw-r--r--object_print3d_utils/export.py159
-rw-r--r--object_print3d_utils/mesh_helpers.py323
-rw-r--r--object_print3d_utils/operators.py511
-rw-r--r--object_print3d_utils/readme.rst4
-rw-r--r--object_print3d_utils/report.py30
-rw-r--r--object_print3d_utils/todo.rst77
-rw-r--r--object_print3d_utils/ui.py128
8 files changed, 1379 insertions, 0 deletions
diff --git a/object_print3d_utils/__init__.py b/object_print3d_utils/__init__.py
new file mode 100644
index 00000000..3dbec458
--- /dev/null
+++ b/object_print3d_utils/__init__.py
@@ -0,0 +1,147 @@
+# ##### 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>
+
+bl_info = {
+ "name": "3D Print Toolbox",
+ "author": "Campbell Barton",
+ "blender": (2, 65, 0),
+ "location": "3D View > Toolbox",
+ "description": "Utilities for 3D printing",
+ "warning": "",
+ "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
+ "Scripts/Modeling/PrintToolbox",
+ "tracker_url": "",
+ "support": 'OFFICIAL',
+ "category": "Mesh"}
+
+
+if "bpy" in locals():
+ import imp
+ imp.reload(ui)
+ imp.reload(operators)
+else:
+ import bpy
+ from bpy.props import (StringProperty,
+ BoolProperty,
+ IntProperty,
+ FloatProperty,
+ FloatVectorProperty,
+ EnumProperty,
+ PointerProperty,
+ )
+ from bpy.types import (Operator,
+ AddonPreferences,
+ PropertyGroup,
+ )
+ from . import ui
+ from . import operators
+
+import math
+
+class Print3DSettings(PropertyGroup):
+ export_format = EnumProperty(
+ name="Format",
+ description="Format type to export to",
+ items=(('STL', "STL", ""),
+ ('PLY', "PLY", ""),
+ ('WRL', "VRML2", ""),
+ ('X3D', "X3D", ""),
+ ('OBJ', "OBJ", "")),
+ default='STL',
+ )
+ export_path = StringProperty(
+ name="Export Directory",
+ description="Path to directory where the files are created",
+ default="//", maxlen=1024, subtype="DIR_PATH",
+ )
+ thickness_min = FloatProperty(
+ name="Thickness",
+ description="Minimum thickness",
+ subtype='DISTANCE',
+ default=0.001, # 1mm
+ min=0.0, max=1.0,
+ )
+ threshold_zero = FloatProperty(
+ name="Threshold",
+ description="Limit for checking zero area/length",
+ default=0.0001,
+ precision=5,
+ min=0.0, max=0.2,
+ )
+ angle_distort = FloatProperty(
+ name="Angle",
+ description="Limit for checking distorted faces",
+ subtype='ANGLE',
+ default=math.radians(15.0),
+ min=0.0, max=math.radians(180.0),
+ )
+ angle_sharp = FloatProperty(
+ name="Angle",
+ subtype='ANGLE',
+ default=math.radians(160.0),
+ min=0.0, max=math.radians(180.0),
+ )
+ angle_overhang = FloatProperty(
+ name="Angle",
+ subtype='ANGLE',
+ default=math.radians(45.0),
+ min=0.0, max=math.radians(90.0),
+ )
+
+classes = (
+ ui.Print3DToolBarObject,
+ ui.Print3DToolBarMesh,
+
+ operators.Print3DInfoVolume,
+ operators.Print3DInfoArea,
+
+ operators.Print3DCheckDegenerate,
+ operators.Print3DCheckDistorted,
+ operators.Print3DCheckSolid,
+ operators.Print3DCheckIntersections,
+ operators.Print3DCheckThick,
+ operators.Print3DCheckSharp,
+ operators.Print3DCheckOverhang,
+ operators.Print3DCheckAll,
+
+ operators.Print3DCleanIsolated,
+ operators.Print3DCleanDistorted,
+ operators.Print3DCleanThin,
+
+ operators.Print3DSelectReport,
+
+ operators.Print3DExport,
+
+ Print3DSettings,
+ )
+
+
+def register():
+ for cls in classes:
+ bpy.utils.register_class(cls)
+
+ bpy.types.Scene.print_3d = PointerProperty(type=Print3DSettings)
+
+
+def unregister():
+ for cls in classes:
+ bpy.utils.unregister_class(cls)
+
+ del bpy.types.Scene.print_3d
diff --git a/object_print3d_utils/export.py b/object_print3d_utils/export.py
new file mode 100644
index 00000000..3a2b6677
--- /dev/null
+++ b/object_print3d_utils/export.py
@@ -0,0 +1,159 @@
+# ##### 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>
+
+# Export wrappers and integration with external tools.
+
+import bpy
+import os
+
+
+def write_mesh(context, info, report_cb):
+ scene = context.scene
+ print_3d = scene.print_3d
+
+ obj_base = scene.object_bases.active
+ obj = obj_base.object
+
+ export_format = print_3d.export_format
+
+ context_override = context.copy()
+
+ obj_base_tmp = None
+
+ # PLY can only export single mesh objects!
+ if export_format == 'PLY':
+ context_backup = context.copy()
+ bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
+
+ from . import mesh_helpers
+ obj_base_tmp = mesh_helpers.object_merge(context, context_override["selected_objects"])
+ context_override["active_object"] = obj_base_tmp.object
+ context_override["selected_bases"] = [obj_base_tmp]
+ context_override["selected_objects"] = [obj_base_tmp.object]
+ else:
+ if obj_base not in context_override["selected_bases"]:
+ context_override["selected_bases"].append(obj_base)
+ if obj not in context_override["selected_objects"]:
+ context_override["selected_objects"].append(obj)
+
+ export_path = bpy.path.abspath(print_3d.export_path)
+
+ # Create name 'export_path/blendname-objname'
+ # add the filename component
+ if bpy.data.is_saved:
+ name = os.path.basename(bpy.data.filepath)
+ name = os.path.splitext(name)[0]
+ else:
+ name = "untitled"
+ # add object name
+ name += "-%s" % bpy.path.clean_name(obj.name)
+
+ # first ensure the path is created
+ if export_path:
+ # this can fail with strange errors,
+ # if the dir cant be made then we get an error later.
+ try:
+ os.makedirs(export_path, exist_ok=True)
+ except:
+ import traceback
+ traceback.print_exc()
+
+ filepath = os.path.join(export_path, name)
+
+ # ensure addon is enabled
+ import addon_utils
+
+ def addon_ensure(addon_id):
+ # Enable the addon, dont change preferences.
+ default_state, loaded_state = addon_utils.check(addon_id)
+ if not loaded_state:
+ addon_utils.enable(addon_id, default_set=False)
+
+ if export_format == 'STL':
+ addon_ensure("io_mesh_stl")
+ filepath = bpy.path.ensure_ext(filepath, ".stl")
+ ret = bpy.ops.export_mesh.stl(
+ context_override,
+ filepath=filepath,
+ ascii=False,
+ use_mesh_modifiers=True,
+ )
+ elif export_format == 'PLY':
+ addon_ensure("io_mesh_ply")
+ filepath = bpy.path.ensure_ext(filepath, ".ply")
+ ret = bpy.ops.export_mesh.ply(
+ context_override,
+ filepath=filepath,
+ use_mesh_modifiers=True,
+ )
+ elif export_format == 'X3D':
+ addon_ensure("io_scene_x3d")
+ filepath = bpy.path.ensure_ext(filepath, ".x3d")
+ ret = bpy.ops.export_scene.x3d(
+ context_override,
+ filepath=filepath,
+ use_mesh_modifiers=True,
+ use_selection=True,
+ )
+ elif export_format == 'WRL':
+ addon_ensure("io_scene_vrml2")
+ filepath = bpy.path.ensure_ext(filepath, ".wrl")
+ ret = bpy.ops.export_scene.vrml2(
+ context_override,
+ filepath=filepath,
+ use_mesh_modifiers=True,
+ use_selection=True,
+ )
+ elif export_format == 'OBJ':
+ addon_ensure("io_scene_obj")
+ filepath = bpy.path.ensure_ext(filepath, ".obj")
+ ret = bpy.ops.export_scene.obj(
+ context_override,
+ filepath=filepath,
+ use_mesh_modifiers=True,
+ use_selection=True,
+ )
+ else:
+ assert(0)
+
+ if obj_base_tmp is not None:
+ obj = obj_base_tmp.object
+ mesh = obj.data
+ scene.objects.unlink(obj)
+ bpy.data.objects.remove(obj)
+ bpy.data.meshes.remove(mesh)
+ del obj_base_tmp, obj, mesh
+
+ # restore context
+ base = None
+ for base in context_backup["selected_bases"]:
+ base.select = True
+ del base
+ scene.objects.active = context_backup["active_object"]
+
+ if 'FINISHED' in ret:
+ info.append(("%r ok" % os.path.basename(filepath), None))
+
+ if report_cb is not None:
+ report_cb({'INFO'}, "Exported: %r" % filepath)
+ return True
+ else:
+ info.append(("%r fail" % os.path.basename(filepath), None))
+ return False
diff --git a/object_print3d_utils/mesh_helpers.py b/object_print3d_utils/mesh_helpers.py
new file mode 100644
index 00000000..82db96bf
--- /dev/null
+++ b/object_print3d_utils/mesh_helpers.py
@@ -0,0 +1,323 @@
+# ##### 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>
+
+# Generic helper functions, to be used by any modules.
+
+import bmesh
+import array
+
+
+def bmesh_copy_from_object(obj, transform=True, triangulate=True, apply_modifiers=False):
+ """
+ Returns a transformed, triangulated copy of the mesh
+ """
+
+ assert(obj.type == 'MESH')
+
+ if apply_modifiers and obj.modifiers:
+ import bpy
+ me = obj.to_mesh(bpy.context.scene, True, 'PREVIEW', calc_tessface=False)
+ bm = bmesh.new()
+ bm.from_mesh(me)
+ bpy.data.meshes.remove(me)
+ del bpy
+ else:
+ me = obj.data
+ if obj.mode == 'EDIT':
+ bm_orig = bmesh.from_edit_mesh(me)
+ bm = bm_orig.copy()
+ else:
+ bm = bmesh.new()
+ bm.from_mesh(me)
+
+ # TODO. remove all customdata layers.
+ # would save ram
+
+ if transform:
+ bm.transform(obj.matrix_world)
+
+ if triangulate:
+ bmesh.ops.triangulate(bm, faces=bm.faces, use_beauty=True)
+
+ return bm
+
+
+def bmesh_from_object(obj):
+ """
+ Object/Edit Mode get mesh, use bmesh_to_object() to write back.
+ """
+ me = obj.data
+ is_editmode = (obj.mode == 'EDIT')
+ if is_editmode:
+ bm = bmesh.from_edit_mesh(me)
+ else:
+ bm = bmesh.new()
+ bm.from_mesh(me)
+ return bm
+
+
+def bmesh_to_object(obj, bm):
+ """
+ Object/Edit Mode update the object.
+ """
+ me = obj.data
+ is_editmode = (obj.mode == 'EDIT')
+ if is_editmode:
+ bmesh.update_edit_mesh(me, True)
+ else:
+ bm.to_mesh(me)
+ # grr... cause an update
+ if me.vertices:
+ me.vertices[0].co[0] = me.vertices[0].co[0]
+
+
+def bmesh_calc_volume(bm):
+ """
+ Calculate the volume of a triangulated bmesh.
+ """
+ def tri_signed_volume(p1, p2, p3):
+ return p1.dot(p2.cross(p3)) / 6.0
+ return abs(sum((tri_signed_volume(*(v.co for v in f.verts))
+ for f in bm.faces)))
+
+
+def bmesh_calc_area(bm):
+ """
+ Calculate the surface area.
+ """
+ return sum(f.calc_area() for f in bm.faces)
+
+
+def bmesh_check_self_intersect_object(obj):
+ """
+ Check if any faces self intersect
+
+ returns an array of edge index values.
+ """
+ import bpy
+
+ # Heres what we do!
+ #
+ # * Take original Mesh.
+ # * Copy it and triangulate it (keeping list of original edge index values)
+ # * Move the BMesh into a temp Mesh.
+ # * Make a temp Object in the scene and assign the temp Mesh.
+ # * For every original edge - ray-cast on the object to find which intersect.
+ # * Report all edge intersections.
+
+ # Triangulate
+ bm = bmesh_copy_from_object(obj, transform=False, triangulate=False)
+ face_map_index_org = {f: i for i, f in enumerate(bm.faces)}
+ ret = bmesh.ops.triangulate(bm, faces=bm.faces, use_beauty=False)
+ face_map = ret["face_map"]
+ # map new index to original index
+ face_map_index = {i: face_map_index_org[face_map.get(f, f)] for i, f in enumerate(bm.faces)}
+ del face_map_index_org
+ del ret
+
+ # Create a real mesh (lame!)
+ scene = bpy.context.scene
+ me_tmp = bpy.data.meshes.new(name="~temp~")
+ bm.to_mesh(me_tmp)
+ bm.free()
+ obj_tmp = bpy.data.objects.new(name=me_tmp.name, object_data=me_tmp)
+ scene.objects.link(obj_tmp)
+ scene.update()
+ ray_cast = obj_tmp.ray_cast
+
+ faces_error = set()
+
+ EPS_NORMAL = 0.0001
+ EPS_CENTER = 0.00001 # should always be bigger
+
+ for ed in me_tmp.edges:
+ v1i, v2i = ed.vertices
+ v1 = me_tmp.vertices[v1i]
+ v2 = me_tmp.vertices[v2i]
+
+ # setup the edge with an offset
+ co_1 = v1.co.copy()
+ co_2 = v2.co.copy()
+ co_mid = (co_1 + co_2) * 0.5
+ no_mid = (v1.normal + v2.normal).normalized() * EPS_NORMAL
+ co_1 = co_1.lerp(co_mid, EPS_CENTER) + no_mid
+ co_2 = co_2.lerp(co_mid, EPS_CENTER) + no_mid
+
+ co, no, index = ray_cast(co_1, co_2)
+ if index != -1:
+ faces_error.add(face_map_index[index])
+
+ scene.objects.unlink(obj_tmp)
+ bpy.data.objects.remove(obj_tmp)
+ bpy.data.meshes.remove(me_tmp)
+
+ return array.array('i', faces_error)
+
+
+def bmesh_face_points_random(f, num_points=1, margin=0.05):
+ import random
+ from random import uniform
+ uniform_args = 0.0 + margin, 1.0 - margin
+
+ # for pradictable results
+ random.seed(f.index)
+
+ vecs = [v.co for v in f.verts]
+
+ for i in range(num_points):
+ u1 = uniform(*uniform_args)
+ u2 = uniform(*uniform_args)
+ u_tot = u1 + u2
+
+ if u_tot > 1.0:
+ u1 = 1.0 - u1
+ u2 = 1.0 - u2
+
+ side1 = vecs[1] - vecs[0]
+ side2 = vecs[2] - vecs[0]
+
+ yield vecs[0] + u1 * side1 + u2 * side2
+
+
+def bmesh_check_thick_object(obj, thickness):
+
+ import bpy
+
+ # Triangulate
+ bm = bmesh_copy_from_object(obj, transform=True, triangulate=False)
+ # map original faces to their index.
+ face_index_map_org = {f: i for i, f in enumerate(bm.faces)}
+ ret = bmesh.ops.triangulate(bm, faces=bm.faces, use_beauty=False)
+ face_map = ret["face_map"]
+ del ret
+ # old edge -> new mapping
+
+ # Convert new/old map to index dict.
+
+ # Create a real mesh (lame!)
+ scene = bpy.context.scene
+ me_tmp = bpy.data.meshes.new(name="~temp~")
+ bm.to_mesh(me_tmp)
+ # bm.free() # delay free
+ obj_tmp = bpy.data.objects.new(name=me_tmp.name, object_data=me_tmp)
+ scene.objects.link(obj_tmp)
+ scene.update()
+ ray_cast = obj_tmp.ray_cast
+
+ EPS_BIAS = 0.0001
+
+ faces_error = set()
+
+ bm_faces_new = bm.faces[:]
+
+ for f in bm_faces_new:
+ no = f.normal
+ no_sta = no * EPS_BIAS
+ no_end = no * thickness
+ for p in bmesh_face_points_random(f, num_points=6):
+ # Cast the ray backwards
+ p_a = p - no_sta
+ p_b = p - no_end
+
+ co, no, index = ray_cast(p_a, p_b)
+
+ if index != -1:
+ # Add the face we hit
+ for f_iter in (f, bm_faces_new[index]):
+ # if the face wasn't triangulated, just use existing
+ f_org = face_map.get(f_iter, f_iter)
+ f_org_index = face_index_map_org[f_org]
+ faces_error.add(f_org_index)
+
+ # finished with bm
+ bm.free()
+
+ scene.objects.unlink(obj_tmp)
+ bpy.data.objects.remove(obj_tmp)
+ bpy.data.meshes.remove(me_tmp)
+
+ return array.array('i', faces_error)
+
+
+
+def object_merge(context, objects):
+ """
+ Caller must remove.
+ """
+
+ import bpy
+
+ def cd_remove_all_but_active(seq):
+ tot = len(seq)
+ if tot > 1:
+ act = seq.active_index
+ for i in range(tot - 1, -1, -1):
+ if i != act:
+ seq.remove(seq[i])
+
+ scene = context.scene
+
+ # deselect all
+ for obj in scene.objects:
+ obj.select = False
+
+ # add empty object
+ mesh_base = bpy.data.meshes.new(name="~tmp~")
+ obj_base = bpy.data.objects.new(name="~tmp~", object_data=mesh_base)
+ base_base = scene.objects.link(obj_base)
+ scene.objects.active = obj_base
+ obj_base.select = True
+
+ # loop over all meshes
+ for obj in objects:
+ if obj.type != 'MESH':
+ continue
+
+ # convert each to a mesh
+ mesh_new = obj.to_mesh(scene=scene,
+ apply_modifiers=True,
+ settings='PREVIEW',
+ calc_tessface=False)
+
+ # remove non-active uvs/vcols
+ cd_remove_all_but_active(mesh_new.vertex_colors)
+ cd_remove_all_but_active(mesh_new.uv_textures)
+
+ # join into base mesh
+ obj_new = bpy.data.objects.new(name="~tmp-new~", object_data=mesh_new)
+ base_new = scene.objects.link(obj_new)
+ obj_new.matrix_world = obj.matrix_world
+
+ fake_context = context.copy()
+ fake_context["active_object"] = obj_base
+ fake_context["selected_editable_bases"] = [base_base, base_new]
+
+ bpy.ops.object.join(fake_context)
+ del base_new, obj_new
+
+ # remove object and its mesh, join does this
+ #~ scene.objects.unlink(obj_new)
+ #~ bpy.data.objects.remove(obj_new)
+
+ bpy.data.meshes.remove(mesh_new)
+
+ # return new object
+ return base_base
+
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'}
diff --git a/object_print3d_utils/readme.rst b/object_print3d_utils/readme.rst
new file mode 100644
index 00000000..70815d15
--- /dev/null
+++ b/object_print3d_utils/readme.rst
@@ -0,0 +1,4 @@
+object_print3d_utils
+====================
+
+3d Printing Addon for Blender 2.66 \ No newline at end of file
diff --git a/object_print3d_utils/report.py b/object_print3d_utils/report.py
new file mode 100644
index 00000000..a27986f0
--- /dev/null
+++ b/object_print3d_utils/report.py
@@ -0,0 +1,30 @@
+# ##### 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>
+
+# Report errors with the mesh.
+
+
+_data = []
+
+def update(*args):
+ _data[:] = args
+
+def info():
+ return tuple(_data)
diff --git a/object_print3d_utils/todo.rst b/object_print3d_utils/todo.rst
new file mode 100644
index 00000000..c2f78e29
--- /dev/null
+++ b/object_print3d_utils/todo.rst
@@ -0,0 +1,77 @@
+Useful 3D printing tools
+========================
+
+Statistics
+----------
+
+- volume
+- surface area
+ *(if you gold plate for eg, or use expensive paint :))*
+
+
+Checks
+------
+
+- *degenerate geometry*
+- - zero area faces
+- - zero length edges
+- - bow-tie quads.
+- *solid geometry*
+- - self intersections
+- - non-manifold
+- - non-contiguous normals (bad flipping)
+
+
+Mesh Cleanup
+------------
+
+- basics - stray verts, loose edges
+- degenerate faces, bow-tie quads
+
+
+Visualisation
+-------------
+
+- visualize areas of low wall thickness.
+- visualize overhangs (some printers have this as s limit).
+- areas of low wall thickness.
+- sharp/pointy surface.
+
+
+Utilities
+---------
+
+- add text on an object *(common tasks - lots of people want this to add a name to personalize items)*
+- Rig sizes (rings also common item to make)
+- others???
+
+
+Exporters
+---------
+
+- nice UI with format select and output paths for the print.
+ *no need to recode re-use existing exporters, maybe recode some in C if too slow.*
+
+
+Integration with toolplating
+----------------------------
+*(the thing that gets the model into printer commands)*
+
+- http://slic3r.org
+- https://github.com/makerbot/Miracle-Grue/blob/master/README.md
+- Use a sliver, like slicer, skeinforge, cura, kissslicer, netfabb, ....
+
+...not sure yet exactly how this would work, but we could have a `Print` button and it would send the file off and print :).
+
+
+Notes
+-----
+
+- Normals are important
+- Self intersections _can_ be ok.
+- Some printer software already prevents solid areas from taking too much space by filling with non-solid grid.
+ *(So we may not have to care about solid shapes so much)*
+
+- For extrusion printers like makerbots it is really hard to print "overhangs"...
+ because they build "from the bottom up" they can't for instance make an arm sticking out of a character sideways
+- Check on http://www.shapeways.com/tutorials/
diff --git a/object_print3d_utils/ui.py b/object_print3d_utils/ui.py
new file mode 100644
index 00000000..d8da9663
--- /dev/null
+++ b/object_print3d_utils/ui.py
@@ -0,0 +1,128 @@
+# ##### 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>
+
+# Interface for this addon.
+
+import bmesh
+from bpy.types import Panel
+from . import report
+
+class Print3DToolBar:
+ bl_label = "Print3D"
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'TOOLS'
+
+ _type_to_icon = {
+ bmesh.types.BMVert: 'VERTEXSEL',
+ bmesh.types.BMEdge: 'EDGESEL',
+ bmesh.types.BMFace: 'FACESEL',
+ }
+
+ @classmethod
+ def poll(cls, context):
+ obj = context.active_object
+ return (obj and obj.type == 'MESH')
+
+ @staticmethod
+ def draw_report(layout, context):
+ """Display Reports"""
+ info = report.info()
+ if info:
+ obj = context.edit_object
+
+ layout.label("Output:")
+ box = layout.box()
+ col = box.column(align=False)
+ # box.alert = True
+ for i, (text, data) in enumerate(info):
+ if obj and data and data[1]:
+ bm_type, bm_array = data
+ col.operator("mesh.print3d_select_report",
+ text=text,
+ icon=Print3DToolBar._type_to_icon[bm_type]).index = i
+ else:
+ col.label(text)
+
+ def draw(self, context):
+ layout = self.layout
+
+ scene = context.scene
+ print_3d = scene.print_3d
+ obj = context.object
+
+ # TODO, presets
+
+ row = layout.row()
+ row.label("Statistics:")
+ col = layout.column(align=True)
+ col.operator("mesh.print3d_info_volume", text="Volume")
+ col.operator("mesh.print3d_info_area", text="Area")
+
+ row = layout.row()
+ row.label("Checks:")
+ col = layout.column(align=True)
+ col.operator("mesh.print3d_check_solid", text="Solid")
+ col.operator("mesh.print3d_check_intersect", text="Intersections")
+ rowsub = col.row()
+ rowsub.operator("mesh.print3d_check_degenerate", text="Degenerate")
+ rowsub.prop(print_3d, "threshold_zero", text="")
+ rowsub = col.row()
+ rowsub.operator("mesh.print3d_check_distort", text="Distorted")
+ rowsub.prop(print_3d, "angle_distort", text="")
+ rowsub = col.row()
+ rowsub.operator("mesh.print3d_check_thick", text="Thickness")
+ rowsub.prop(print_3d, "thickness_min", text="")
+ rowsub = col.row()
+ rowsub.operator("mesh.print3d_check_sharp", text="Edge Sharp")
+ rowsub.prop(print_3d, "angle_sharp", text="")
+ rowsub = col.row()
+ rowsub.operator("mesh.print3d_check_overhang", text="Overhang")
+ rowsub.prop(print_3d, "angle_overhang", text="")
+ col = layout.column()
+ col.operator("mesh.print3d_check_all", text="Check All")
+
+ row = layout.row()
+ row.label("Cleanup:")
+ col = layout.column(align=True)
+ col.operator("mesh.print3d_clean_isolated", text="Isolated")
+ rowsub = col.row()
+ rowsub.operator("mesh.print3d_clean_distorted", text="Distorted")
+ rowsub.prop(print_3d, "angle_distort", text="")
+ # XXX TODO
+ # col.operator("mesh.print3d_clean_thin", text="Wall Thickness")
+
+ col = layout.column()
+ col.label("Export Directory:")
+ col.prop(print_3d, "export_path", text="")
+
+ rowsub = col.row(align=True)
+ rowsub.prop(print_3d, "export_format", text="")
+ rowsub.operator("mesh.print3d_export", text="", icon='EXPORT')
+
+ Print3DToolBar.draw_report(layout, context)
+
+# So we can have a panel in both object mode and editmode
+class Print3DToolBarObject(Panel, Print3DToolBar):
+ bl_idname = "MESH_PT_print3d_object"
+ bl_context = "objectmode"
+
+class Print3DToolBarMesh(Panel, Print3DToolBar):
+ bl_idname = "MESH_PT_print3d_mesh"
+ bl_context = "mesh_edit"