Use URL icon, add Tip: prefix, increase lower margin
[blender-addons-contrib.git] / mesh_edgetools.py
index fd46d9a3cb6224f41396b8070bd9ebc5f8eba926..e3bc3a3b6280d60ea02a03e4fdd8c515a5b72638 100644 (file)
 #       functionality, though it will sadly be a little clumsier to use due
 #       to Blender's selection limitations.
 #
-# Tasks:
-#   - Figure out how to do a GUI for "Shaft", especially for controlling radius?
+# Notes:
 #   - Buggy parts have been hidden behind bpy.app.debug.  Run Blender in debug
 #       to expose those.  Example: Shaft with more than two edges selected.
+#   - Some functions have started to crash, despite working correctly before.
+#       What could be causing that?  Blender bug?  Or coding bug?
 #
 # Paul "BrikBot" Marshall
 # Created: January 28, 2012
-# Last Modified: August 25, 2012
+# Last Modified: October 6, 2012
 # Homepage (blog): http://post.darkarsenic.com/
 #                       //blog.darkarsenic.com/
 #
-# Coded in IDLE, tested in Blender 2.63.
+# Coded in IDLE, tested in Blender 2.6.
 # Search for "@todo" to quickly find sections that need work.
 #
 # Remeber -
 # ^^ Maybe. . . . :P
 
 bl_info = {
-    'name': "EdgeTools",
-    'author': "Paul Marshall",
-    'version': (0, 8),
-    'blender': (2, 6, 3),
-    'location': "View3D > Toolbar and View3D > Specials (W-key)",
-    'warning': "",
-    'description': "CAD style edge manipulation tools",
-    'wiki_url': "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Modeling/EdgeTools",
-    'tracker_url': 'https://projects.blender.org/tracker/index.php?'\
-                   'func=detail&aid=31566',
-    'category': 'Mesh'}
+    "name": "EdgeTools",
+    "author": "Paul Marshall",
+    "version": (0, 8),
+    "blender": (2, 68, 0),
+    "location": "View3D > Toolbar and View3D > Specials (W-key)",
+    "warning": "",
+    "description": "CAD style edge manipulation tools",
+    "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
+        "Scripts/Modeling/EdgeTools",
+    "tracker_url": "",
+    "category": "Mesh"}
+
 
 import bpy, bmesh, mathutils
 from math import acos, pi, radians, sqrt, tan
@@ -94,6 +96,8 @@ from bpy.props import (BoolProperty,
                        FloatProperty,
                        EnumProperty)
 
+integrated = False
+
 # Quick an dirty method for getting the sign of a number:
 def sign(number):
     return (number > 0) - (number < 0)
@@ -103,7 +107,7 @@ def sign(number):
 #
 # Checks to see if two lines are parallel
 def is_parallel(v1, v2, v3, v4):
-    result = intersect_line_line(v1, v2, v3, v4) 
+    result = intersect_line_line(v1, v2, v3, v4)
     return result == None
 
 
@@ -286,7 +290,7 @@ def interpolate_line_line(p1_co, p1_dir, p2_co, p2_dir, segments, tension = 1,
 # A quad may not be planar.  Therefore the treated definition of the surface is
 # that the surface is composed of all lines bridging two other lines defined by
 # the given four points.  The lines do not "cross".
-# 
+#
 # The two lines in 3-space can defined as:
 #   ┌  ┐         ┌   ┐     ┌   ┐  ┌  ┐         ┌   ┐     ┌   ┐
 #   │x1│         │a11│     │b11│  │x2│         │a21│     │b21│
@@ -512,7 +516,7 @@ def intersect_line_face(edge, face, is_infinite = False, error = 0.000002):
         y = (1 - t3) * a32 + t3 * b32
         z = (1 - t3) * a33 + t3 * b33
         int_co = Vector((x, y, z))
-        
+
         if bpy.app.debug:
             print(int_co)
 
@@ -525,10 +529,15 @@ def intersect_line_face(edge, face, is_infinite = False, error = 0.000002):
         int_co = intersect_line_plane(edge.verts[0].co, edge.verts[1].co, p1, face.normal)
 
         # Only check if the triangle is not being treated as an infinite plane:
+        # Math based from http://paulbourke.net/geometry/linefacet/
         if int_co != None and not is_infinite:
             pA = p1 - int_co
             pB = p2 - int_co
             pC = p3 - int_co
+            # These must be unit vectors, else we risk a domain error:
+            pA.length = 1
+            pB.length = 1
+            pC.length = 1
             aAB = acos(pA.dot(pB))
             aBC = acos(pB.dot(pC))
             aCA = acos(pC.dot(pA))
@@ -554,7 +563,7 @@ def project_point_plane(pt, plane_co, plane_no):
     proj_co = intersect_line_plane(pt, pt + plane_no, plane_co, plane_no)
     proj_ve = proj_co - pt
     return (proj_ve, proj_co)
-    
+
 
 # ------------ FILLET/CHAMPHER HELPER METHODS -------------
 
@@ -597,7 +606,7 @@ def is_planar_edge(edge, error = 0.000002):
 # debuged.
 def fillet_axis(edge, radius):
     vectors = [None, None, None, None]
-    
+
     origin = Vector((0, 0, 0))
     axis = edge.verts[1].co - edge.verts[0].co
 
@@ -630,10 +639,10 @@ def fillet_axis(edge, radius):
     # Get the normal for face 0 and face 1:
     norm1 = edge.link_faces[0].normal
     norm2 = edge.link_faces[1].normal
-    
+
     # We need to find the angle between the two faces, then bisect it:
     theda = (pi - edge.calc_face_angle()) / 2
-    
+
     # We are dealing with a triangle here, and we will need the length
     # of its adjacent side.  The opposite is the radius:
     adj_len = radius / tan(theda)
@@ -646,7 +655,7 @@ def fillet_axis(edge, radius):
         vectors[i] = project_point_plane(vectors[i], origin, axis)[1]
         vectors[i].length = adj_len
         vectors[i] = vectors[i] + edge.verts[i % 2].co
-    
+
     # Compute fillet axis end points:
     v1 = intersect_line_line(vectors[0], vectors[0] + norm1, vectors[2], vectors[2] + norm2)[0]
     v2 = intersect_line_line(vectors[1], vectors[1] + norm1, vectors[3], vectors[3] + norm2)[0]
@@ -684,7 +693,7 @@ class Extend(bpy.types.Operator):
         layout.prop(self, "di1")
         layout.prop(self, "di2")
         layout.prop(self, "length")
-    
+
 
     @classmethod
     def poll(cls, context):
@@ -695,7 +704,7 @@ class Extend(bpy.types.Operator):
     def invoke(self, context, event):
         return self.execute(context)
 
-    
+
     def execute(self, context):
         bpy.ops.object.editmode_toggle()
         bm = bmesh.new()
@@ -712,7 +721,7 @@ class Extend(bpy.types.Operator):
             for e in edges:
                 vector = e.verts[0].co - e.verts[1].co
                 vector.length = self.length
-                
+
                 if self.di1:
                     v = bVerts.new()
                     if (vector[0] + vector[1] + vector[2]) < 0:
@@ -767,7 +776,7 @@ class Spline(bpy.types.Operator):
     bl_label = "Spline"
     bl_description = "Create a spline interplopation between two edges"
     bl_options = {'REGISTER', 'UNDO'}
-    
+
     alg = EnumProperty(name = "Spline Algorithm",
                        items = [('Blender', 'Blender', 'Interpolation provided through \"mathutils.geometry\"'),
                                 ('Hermite', 'C-Spline', 'C-spline interpolation'),
@@ -818,7 +827,7 @@ class Spline(bpy.types.Operator):
     def invoke(self, context, event):
         return self.execute(context)
 
-    
+
     def execute(self, context):
         bpy.ops.object.editmode_toggle()
         bm = bmesh.new()
@@ -827,7 +836,7 @@ class Spline(bpy.types.Operator):
 
         bEdges = bm.edges
         bVerts = bm.verts
-        
+
         seg = self.segments
         edges = [e for e in bEdges if e.select]
         verts = [edges[v // 2].verts[v % 2] for v in range(4)]
@@ -853,7 +862,7 @@ class Spline(bpy.types.Operator):
         else:
             v2 = verts[2]
             p2_co = verts[2].co
-            p2_dir = verts[3].co - verts[2].co 
+            p2_dir = verts[3].co - verts[2].co
         if self.ten2 < 0:
             p2_dir = -1 * p2_dir
             p2_dir.length = -self.ten2
@@ -896,7 +905,7 @@ class Spline(bpy.types.Operator):
 #
 # @todo Change method from a cross product to a rotation matrix to make the
 #   angle part work.
-#   --- todo completed Feb 4th, but still needs work ---
+#   --- todo completed 2/4/2012, but still needs work ---
 # @todo Figure out a way to make +/- predictable
 #   - Maybe use angel between edges and vector direction definition?
 #   --- TODO COMPLETED ON 2/9/2012 ---
@@ -953,7 +962,7 @@ class Ortho(bpy.types.Operator):
         row.prop(self, "neg")
         layout.prop(self, "angle")
         layout.prop(self, "length")
-    
+
     @classmethod
     def poll(cls, context):
         ob = context.active_object
@@ -963,7 +972,7 @@ class Ortho(bpy.types.Operator):
     def invoke(self, context, event):
         return self.execute(context)
 
-    
+
     def execute(self, context):
         bpy.ops.object.editmode_toggle()
         bm = bmesh.new()
@@ -1012,7 +1021,7 @@ class Ortho(bpy.types.Operator):
 
             vectors.append(verts[0].co - verts[1].co)
             vectors.append(verts[2].co - verts[3].co)
-            
+
             # Normal of the plane formed by vector1 and vector2:
             vectors.append(vectors[0].cross(vectors[1]))
 
@@ -1079,7 +1088,7 @@ class Shaft(bpy.types.Operator):
                             min = 0, max = 1,
                             default = 0)
     last_flip = False
-    
+
     edge = IntProperty(name = "Edge",
                        description = "Edge to shaft around.",
                        min = 0, max = 1,
@@ -1132,7 +1141,7 @@ class Shaft(bpy.types.Operator):
 
         return self.execute(context)
 
-    
+
     def execute(self, context):
         bpy.ops.object.editmode_toggle()
         bm = bmesh.new()
@@ -1195,7 +1204,7 @@ class Shaft(bpy.types.Operator):
                     return {'CANCELLED'}
             elif self.edge == 1:
                 edge = [1, 0]
-                    
+
             verts.append(edges[edge[0]].verts[0])
             verts.append(edges[edge[0]].verts[1])
 
@@ -1207,13 +1216,14 @@ class Shaft(bpy.types.Operator):
 
             self.shaftType = 0
         # If there is more than one edge selected:
-        # There are some issues with it ATM, so don't expose is it to normal users:
+        # There are some issues with it ATM, so don't expose is it to normal users
+        # @todo Fix edge connection ordering issue
         elif len(edges) > 2 and bpy.app.debug:
             if isinstance(bm.select_history.active, bmesh.types.BMEdge):
                 active = bm.select_history.active
                 edges.remove(active)
                 # Get all the verts:
-                edges = order_joined_edges(edges[0])
+                edges = order_joined_edges(edges[0])
                 verts = []
                 for e in edges:
                     if verts.count(e.verts[0]) == 0:
@@ -1245,8 +1255,8 @@ class Shaft(bpy.types.Operator):
         else:
             axis = verts[1].co - verts[0].co
 
-        # We will need a series of rotation matrices.  We could use one which would be
-        # faster but also might cause propagation of error.
+        # We will need a series of rotation matrices.  We could use one which
+        # would be faster but also might cause propagation of error.
 ##        matrices = []
 ##        for i in range(numV):
 ##            matrices.append(Matrix.Rotation((rads * i) + rotRange[0], 3, axis))
@@ -1271,7 +1281,7 @@ class Shaft(bpy.types.Operator):
                 # These will be rotated about the orgin so will need to be shifted:
                 for j in range(numV):
                     verts_out.append(co - (matrices[j] * init_vec))
-        # Else if a line and a point was selected:    
+        # Else if a line and a point was selected:
         elif self.shaftType == 2:
             init_vec = distance_point_line(verts[2].co, verts[0].co, verts[1].co)
             # These will be rotated about the orgin so will need to be shifted:
@@ -1318,12 +1328,12 @@ class Shaft(bpy.types.Operator):
                     e.select = True
 
             # Faces:
-            # There is a problem with this right now:
-            for i in range(len(edges)):
-                for j in range(numE):
-                    f = bFaces.new((newVerts[i], newVerts[i + 1],
-                                    newVerts[i + (numV * j) + 1], newVerts[i + (numV * j)]))
-                    f.normal_update()
+            # There is a problem with this right now
+##            for i in range(len(edges)):
+##                for j in range(numE):
+##                    f = bFaces.new((newVerts[i], newVerts[i + 1],
+##                                    newVerts[i + (numV * j) + 1], newVerts[i + (numV * j)]))
+##                    f.normal_update()
         else:
             # Vertices:
             for i in range(numV * 2):
@@ -1354,6 +1364,8 @@ class Shaft(bpy.types.Operator):
 
 
 # "Slices" edges crossing a plane defined by a face.
+# @todo Selecting a face as the cutting plane will cause Blender to crash when
+#   using "Rip".
 class Slice(bpy.types.Operator):
     bl_idname = "mesh.edgetools_slice"
     bl_label = "Slice"
@@ -1393,7 +1405,7 @@ class Slice(bpy.types.Operator):
     def invoke(self, context, event):
         return self.execute(context)
 
-    
+
     def execute(self, context):
         bpy.ops.object.editmode_toggle()
         bm = bmesh.new()
@@ -1409,6 +1421,12 @@ class Slice(bpy.types.Operator):
         normal = None
 
         # Find the selected face.  This will provide the plane to project onto:
+        #   - First check to use the active face.  This allows users to just
+        #       select a bunch of faces with the last being the cutting plane.
+        #       This is try and make the tool act more like a built-in Blender
+        #       function.
+        #   - If that fails, then use the first found selected face in the BMesh
+        #       face list.
         if isinstance(bm.select_history.active, bmesh.types.BMFace):
             face = bm.select_history.active
             normal = bm.select_history.active.normal
@@ -1421,28 +1439,55 @@ class Slice(bpy.types.Operator):
                     f.select = False
                     break
 
+        # If we don't find a selected face, we have problem.  Exit:
         if face == None:
             bpy.ops.object.editmode_toggle()
             self.report({'ERROR_INVALID_INPUT'},
                         "You must select a face as the cutting plane.")
             return {'CANCELLED'}
+        # Warn the user if they are using an n-gon.  We can work with it, but it
+        # might lead to some odd results.
         elif len(face.verts) > 4 and not is_face_planar(face):
             self.report({'WARNING'},
                         "Selected face is an n-gon.  Results may be unpredictable.")
 
+        # @todo DEBUG TRACKER - DELETE WHEN FINISHED:
+        dbg = 0
+        if bpy.app.debug:
+            print(len(bEdges))
+
+        # Iterate over the edges:
         for e in bEdges:
+            # @todo DEBUG TRACKER - DELETE WHEN FINISHED:
+            if bpy.app.debug:
+                print(dbg)
+                dbg = dbg + 1
+
+            # Get the end verts on the edge:
             v1 = e.verts[0]
             v2 = e.verts[1]
+
+            # Make sure that verts are not a part of the cutting plane:
             if e.select and (v1 not in face.verts and v2 not in face.verts):
                 if len(face.verts) < 5:  # Not an n-gon
                     intersection = intersect_line_face(e, face, True)
                 else:
                     intersection = intersect_line_plane(v1.co, v2.co, face.verts[0].co, normal)
 
+                # More debug info - I think this can stay.
+                if bpy.app.debug:
+                    print("Intersection", end = ': ')
+                    print(intersection)
+
+                # If an intersection exists find the distance of each of the end
+                # points from the plane, with "positive" being in the direction
+                # of the cutting plane's normal.  If the points are on opposite
+                # side of the plane, then it intersects and we need to cut it.
                 if intersection != None:
                     d1 = distance_point_to_plane(v1.co, face.verts[0].co, normal)
                     d2 = distance_point_to_plane(v2.co, face.verts[0].co, normal)
-                    # If they have different signs, then the edge crosses the plane:
+                    # If they have different signs, then the edge crosses the
+                    # cutting plane:
                     if abs(d1 + d2) < abs(d1 - d2):
                         # Make the first vertice the positive vertice:
                         if d1 < d2:
@@ -1450,20 +1495,31 @@ class Slice(bpy.types.Operator):
                         if self.make_copy:
                             new = bVerts.new()
                             new.co = intersection
+                            new.select = True
                         elif self.rip:
                             newV1 = bVerts.new()
                             newV1.co = intersection
+
+                            if bpy.app.debug:
+                                print("newV1 created", end = '; ')
+
                             newV2 = bVerts.new()
                             newV2.co = intersection
+
                             if bpy.app.debug:
-                                print("New vertices were successfully created")
+                                print("newV2 created", end = '; ')
+
                             newE1 = bEdges.new((v1, newV1))
                             newE2 = bEdges.new((v2, newV2))
+
                             if bpy.app.debug:
-                                print("New edges were successfully created")
+                                print("new edges created", end = '; ')
+
                             bEdges.remove(e)
+
                             if bpy.app.debug:
-                                print("Old edge successfully removed")
+                                print("old edge removed.")
+                                print("We're done with this edge.")
                         else:
                             new = list(bmesh.utils.edge_split(e, v1, 0.5))
                             new[1].co = intersection
@@ -1479,6 +1535,8 @@ class Slice(bpy.types.Operator):
         return {'FINISHED'}
 
 
+# This projects the selected edges onto the selected plane.  This projects both
+# points on the selected edge.
 class Project(bpy.types.Operator):
     bl_idname = "mesh.edgetools_project"
     bl_label = "Project"
@@ -1516,6 +1574,7 @@ class Project(bpy.types.Operator):
         fVerts = []
 
         # Find the selected face.  This will provide the plane to project onto:
+        # @todo Check first for an active face
         for f in bFaces:
             if f.select:
                 for v in f.verts:
@@ -1726,7 +1785,7 @@ class Fillet(bpy.types.Operator):
         layout.prop(self, "deg_seg")
         layout.prop(self, "res")
 
-    
+
     @classmethod
     def poll(cls, context):
         ob = context.active_object
@@ -1768,7 +1827,7 @@ class Fillet(bpy.types.Operator):
 
         for e in edges:
             axis_points = fillet_axis(e, self.radius)
-            
+
 
         bm.to_mesh(bpy.context.active_object.data)
         bpy.ops.object.editmode_toggle()
@@ -1801,7 +1860,7 @@ class Intersect_Line_Face(bpy.types.Operator):
             self.report({'ERROR_INVALID_INPUT'},
                         "This is for debugging only: you should not be able to run this!")
             return {'CANCELLED'}
-        
+
         bpy.ops.object.editmode_toggle()
         bm = bmesh.new()
         bm.from_mesh(bpy.context.active_object.data)
@@ -1840,10 +1899,11 @@ class Intersect_Line_Face(bpy.types.Operator):
 
 class VIEW3D_MT_edit_mesh_edgetools(bpy.types.Menu):
     bl_label = "EdgeTools"
-    
+
     def draw(self, context):
+        global integrated
         layout = self.layout
-        
+
         layout.operator("mesh.edgetools_extend")
         layout.operator("mesh.edgetools_spline")
         layout.operator("mesh.edgetools_ortho")
@@ -1856,6 +1916,12 @@ class VIEW3D_MT_edit_mesh_edgetools(bpy.types.Menu):
             layout.operator("mesh.edgetools_fillet")
             ## For internal testing ONLY:
             layout.operator("mesh.edgetools_ilf")
+        # If TinyCAD VTX exists, add it to the menu.
+        # @todo This does not work.
+        if integrated and bpy.app.debug:
+            layout.operator(EdgeIntersections.bl_idname, text="Edges V Intersection").mode = -1
+            layout.operator(EdgeIntersections.bl_idname, text="Edges T Intersection").mode = 0
+            layout.operator(EdgeIntersections.bl_idname, text="Edges X Intersection").mode = 1
 
 
 def menu_func(self, context):
@@ -1878,13 +1944,22 @@ classes = [VIEW3D_MT_edit_mesh_edgetools,
 
 # registering and menu integration
 def register():
-    if int(bpy.app.build_revision[0:5]) < 44800:
-        print("Error in Edgetools:")
-        print("This version of Blender does not support the necessary BMesh API.")
-        print("Please download Blender 2.63 or newer.")
-        return {'ERROR'}
+    global integrated
+
     for c in classes:
         bpy.utils.register_class(c)
+
+    # I would like this script to integrate the TinyCAD VTX menu options into
+    # the edge tools menu if it exists.  This should make the UI a little nicer
+    # for users.
+    # @todo Remove TinyCAD VTX menu entries and add them too EdgeTool's menu
+    import inspect, os.path
+
+    path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
+    if os.path.isfile(path + "\mesh_edge_intersection_tools.py"):
+        print("EdgeTools UI integration test - TinyCAD VTX Found")
+        integrated = True
+
     bpy.types.VIEW3D_MT_edit_mesh_specials.prepend(menu_func)
 
 
@@ -1892,9 +1967,10 @@ def register():
 def unregister():
     for c in classes:
         bpy.utils.unregister_class(c)
+
     bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func)
 
 
 if __name__ == "__main__":
     register()
-    
+