Skip to content
Snippets Groups Projects
mesh_edgetools.py 76.2 KiB
Newer Older
  • Learn to ignore specific revisions
  •         # If the two edges are parallel:
            if cos == None:
    
                self.report({'WARNING'},
                            "Selected lines are parallel: results may be unpredictable.")
    
                vectors.append(verts[0].co - verts[1].co)
                vectors.append(verts[0].co - verts[2].co)
                vectors.append(vectors[0].cross(vectors[1]))
                vectors.append(vectors[2].cross(vectors[0]))
                vectors.append(-vectors[3])
            else:
                # Warn the user if they have not chosen two planar edges:
                if not is_same_co(cos[0], cos[1]):
    
                    self.report({'WARNING'},
                                "Selected lines are not planar: results may be unpredictable.")
    
    
                # This makes the +/- behavior predictable:
                if (verts[0].co - cos[0]).length < (verts[1].co - cos[0]).length:
                    verts[0], verts[1] = verts[1], verts[0]
                if (verts[2].co - cos[0]).length < (verts[3].co - cos[0]).length:
                    verts[2], verts[3] = verts[3], verts[2]
    
                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]))
    
                # Possible directions:
                vectors.append(vectors[2].cross(vectors[0]))
                vectors.append(vectors[1].cross(vectors[2]))
    
            # Set the length:
            vectors[3].length = self.length
            vectors[4].length = self.length
    
            # Perform any additional rotations:
            matrix = Matrix.Rotation(radians(90 + self.angle), 3, vectors[2])
            vectors.append(matrix * -vectors[3]) # vectors[5]
            matrix = Matrix.Rotation(radians(90 - self.angle), 3, vectors[2])
            vectors.append(matrix * vectors[4]) # vectors[6]
            vectors.append(matrix * vectors[3]) # vectors[7]
            matrix = Matrix.Rotation(radians(90 + self.angle), 3, vectors[2])
            vectors.append(matrix * -vectors[4]) # vectors[8]
    
            # Perform extrusions and displacements:
            # There will be a total of 8 extrusions.  One for each vert of each edge.
            # It looks like an extrusion will add the new vert to the end of the verts
            # list and leave the rest in the same location.
            # ----------- EDIT -----------
    
            # It looks like I might be able to do this within "bpy.data" with the ".add"
    
            # function.
            # ------- BMESH UPDATE -------
            # BMesh uses ".new()"
    
            for v in range(len(verts)):
                vert = verts[v]
                if (v == 0 and self.vert1) or (v == 1 and self.vert2) or (v == 2 and self.vert3) or (v == 3 and self.vert4):
                    if self.pos:
                        new = bVerts.new()
                        new.co = vert.co - vectors[5 + (v // 2) + ((v % 2) * 2)]
                        bEdges.new((vert, new))
                    if self.neg:
                        new = bVerts.new()
                        new.co = vert.co + vectors[5 + (v // 2) + ((v % 2) * 2)]
                        bEdges.new((vert, new))
    
            bm.to_mesh(bpy.context.active_object.data)
            bpy.ops.object.editmode_toggle()
            return {'FINISHED'}
    
    
    # Usage:
    # Select an edge and a point or an edge and specify the radius (default is 1 BU)
    # You can select two edges but it might be unpredicatble which edge it revolves
    # around so you might have to play with the switch.
    class Shaft(bpy.types.Operator):
        bl_idname = "mesh.edgetools_shaft"
        bl_label = "Shaft"
        bl_description = "Create a shaft mesh around an axis"
        bl_options = {'REGISTER', 'UNDO'}
    
    
    
        # For tracking if the user has changed selection:
        last_edge = IntProperty(name = "Last Edge",
                                description = "Tracks if user has changed selected edge",
                                min = 0, max = 1,
                                default = 0)
        last_flip = False
    
        edge = IntProperty(name = "Edge",
                           description = "Edge to shaft around.",
                           min = 0, max = 1,
                           default = 0)
        flip = BoolProperty(name = "Flip Second Edge",
                            description = "Flip the percieved direction of the second edge.",
                            default = False)
        radius = FloatProperty(name = "Radius",
                               description = "Shaft Radius",
                               min = 0.0, max = 1024.0,
                               default = 1.0)
        start = FloatProperty(name = "Starting Angle",
                              description = "Angle to start the shaft at.",
                              min = -360.0, max = 360.0,
                              default = 0.0)
        finish = FloatProperty(name = "Ending Angle",
                               description = "Angle to end the shaft at.",
                               min = -360.0, max = 360.0,
                               default = 360.0)
        segments = IntProperty(name = "Shaft Segments",
                               description = "Number of sgements to use in the shaft.",
                               min = 1, max = 4096,
                               soft_max = 512,
                               default = 32)
    
    
        def draw(self, context):
            layout = self.layout
    
            if self.shaftType == 0:
                layout.prop(self, "edge")
                layout.prop(self, "flip")
            elif self.shaftType == 3:
                layout.prop(self, "radius")
            layout.prop(self, "segments")
            layout.prop(self, "start")
            layout.prop(self, "finish")
    
    
        @classmethod
        def poll(cls, context):
            ob = context.active_object
            return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
    
        def invoke(self, context, event):
    
            # Make sure these get reset each time we run:
            self.last_edge = 0
            self.edge = 0
    
    
        def execute(self, context):
            bpy.ops.object.editmode_toggle()
            bm = bmesh.new()
            bm.from_mesh(bpy.context.active_object.data)
            bm.normal_update()
    
            bFaces = bm.faces
            bEdges = bm.edges
            bVerts = bm.verts
    
            active = None
            edges = []
            verts = []
    
            # Pre-caclulated values:
    
            rotRange = [radians(self.start), radians(self.finish)]
            rads = radians((self.finish - self.start) / self.segments)
    
            numV = self.segments + 1
            numE = self.segments
    
            edges = [e for e in bEdges if e.select]
    
    
            # Robustness check: there should at least be one edge selected
            if len(edges) < 1:
                bpy.ops.object.editmode_toggle()
                self.report({'ERROR_INVALID_INPUT'},
                            "At least one edge must be selected.")
                return {'CANCELLED'}
    
                # default:
                edge = [0, 1]
                vert = [0, 1]
    
                # Edge selection:
                #
                # By default, we want to shaft around the last selected edge (it
                # will be the active edge).  We know we are using the default if
                # the user has not changed which edge is being shafted around (as
                # is tracked by self.last_edge).  When they are not the same, then
                # the user has changed selection.
                #
                # We then need to make sure that the active object really is an edge
                # (robustness check).
                #
                # Finally, if the active edge is not the inital one, we flip them
                # and have the GUI reflect that.
                if self.last_edge == self.edge:
                    if isinstance(bm.select_history.active, bmesh.types.BMEdge):
                        if bm.select_history.active != edges[edge[0]]:
                            self.last_edge, self.edge = edge[1], edge[1]
                            edge = [edge[1], edge[0]]
                    else:
                        bpy.ops.object.editmode_toggle()
                        self.report({'ERROR_INVALID_INPUT'},
                                    "Active geometry is not an edge.")
                        return {'CANCELLED'}
                elif self.edge == 1:
                    edge = [1, 0]
    
                verts.append(edges[edge[0]].verts[0])
                verts.append(edges[edge[0]].verts[1])
    
    
                    verts = [1, 0]
    
                verts.append(edges[edge[1]].verts[vert[0]])
                verts.append(edges[edge[1]].verts[vert[1]])
    
    
            # If there is more than one edge selected:
    
            # 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])
    
                    verts = []
                    for e in edges:
                        if verts.count(e.verts[0]) == 0:
                            verts.append(e.verts[0])
                        if verts.count(e.verts[1]) == 0:
                            verts.append(e.verts[1])
                else:
    
                    self.report({'ERROR_INVALID_INPUT'},
                                "Active geometry is not an edge.")
    
                verts.append(edges[0].verts[0])
                verts.append(edges[0].verts[1])
    
    
                for v in bVerts:
                    if v.select and verts.count(v) == 0:
                        verts.append(v)
                    v.select = False
                if len(verts) == 2:
                    self.shaftType = 3
                else:
                    self.shaftType = 2
    
            # The vector denoting the axis of rotation:
            if self.shaftType == 1:
                axis = active.verts[1].co - active.verts[0].co
            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.
    
    ##        matrices = []
    ##        for i in range(numV):
    ##            matrices.append(Matrix.Rotation((rads * i) + rotRange[0], 3, axis))
            matrices = [Matrix.Rotation((rads * i) + rotRange[0], 3, axis) for i in range(numV)]
    
    
            # New vertice coordinates:
            verts_out = []
    
            # If two edges were selected:
            #   - If the lines are not parallel, then it will create a cone-like shaft
            if self.shaftType == 0:
                for i in range(len(verts) - 2):
                    init_vec = distance_point_line(verts[i + 2].co, verts[0].co, verts[1].co)
                    co = init_vec + verts[i + 2].co
    
                    # 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))
            elif self.shaftType == 1:
                for i in verts:
                    init_vec = distance_point_line(i.co, active.verts[0].co, active.verts[1].co)
                    co = init_vec + i.co
    
                    # 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))
    
            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:
                verts_out = [(verts[i].co - (matrices[j] * init_vec)) for i in range(2) for j in range(numV)]
            # Else the above are not possible, so we will just use the edge:
            #   - The vector defined by the edge is the normal of the plane for the shaft
            #   - The shaft will have radius "radius".
            else:
                if is_axial(verts[0].co, verts[1].co) == None:
                    proj = (verts[1].co - verts[0].co)
                    proj[2] = 0
                    norm = proj.cross(verts[1].co - verts[0].co)
                    vec = norm.cross(verts[1].co - verts[0].co)
                    vec.length = self.radius
                elif is_axial(verts[0].co, verts[1].co) == 'Z':
                    vec = verts[0].co + Vector((0, 0, self.radius))
                else:
                    vec = verts[0].co + Vector((0, self.radius, 0))
                init_vec = distance_point_line(vec, verts[0].co, verts[1].co)
                # These will be rotated about the orgin so will need to be shifted:
                verts_out = [(verts[i].co - (matrices[j] * init_vec)) for i in range(2) for j in range(numV)]
    
            # We should have the coordinates for a bunch of new verts.  Now add the verts
            # and build the edges and then the faces.
    
            newVerts = []
    
            if self.shaftType == 1:
                # Vertices:
                for i in range(numV * len(verts)):
                    new = bVerts.new()
                    new.co = verts_out[i]
                    new.select = True
                    newVerts.append(new)
    
                # Edges:
                for i in range(numE):
                    for j in range(len(verts)):
                        e = bEdges.new((newVerts[i + (numV * j)], newVerts[i + (numV * j) + 1]))
                        e.select = True
                for i in range(numV):
                    for j in range(len(verts) - 1):
                        e = bEdges.new((newVerts[i + (numV * j)], newVerts[i + (numV * (j + 1))]))
                        e.select = True
    
                # Faces:
    
    ##            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):
                    new = bVerts.new()
                    new.co = verts_out[i]
                    new.select = True
                    newVerts.append(new)
    
                # Edges:
                for i in range(numE):
                    e = bEdges.new((newVerts[i], newVerts[i + 1]))
                    e.select = True
                    e = bEdges.new((newVerts[i + numV], newVerts[i + numV + 1]))
                    e.select = True
                for i in range(numV):
                    e = bEdges.new((newVerts[i], newVerts[i + numV]))
                    e.select = True
    
                # Faces:
                for i in range(numE):
                    f = bFaces.new((newVerts[i], newVerts[i + 1],
                                    newVerts[i + numV + 1], newVerts[i + numV]))
                    f.normal_update()
    
            bm.to_mesh(bpy.context.active_object.data)
            bpy.ops.object.editmode_toggle()
            return {'FINISHED'}
    
    
    # "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"
        bl_description = "Cuts edges at the plane defined by a selected face."
        bl_options = {'REGISTER', 'UNDO'}
    
    
        make_copy = BoolProperty(name = "Make Copy",
                                 description = "Make new vertices at intersection points instead of spliting the edge",
                                 default = False)
        rip = BoolProperty(name = "Rip",
                           description = "Split into two edges that DO NOT share an intersection vertice.",
                           default = False)
    
        pos = BoolProperty(name = "Positive",
                           description = "Remove the portion on the side of the face normal",
                           default = False)
        neg = BoolProperty(name = "Negative",
                           description = "Remove the portion on the side opposite of the face normal",
                           default = False)
    
        def draw(self, context):
            layout = self.layout
    
    
            layout.prop(self, "make_copy")
            if not self.make_copy:
                layout.prop(self, "rip")
                layout.label("Remove Side:")
                layout.prop(self, "pos")
                layout.prop(self, "neg")
    
    
    
        @classmethod
        def poll(cls, context):
            ob = context.active_object
            return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
    
        def invoke(self, context, event):
            return self.execute(context)
    
    
        def execute(self, context):
            bpy.ops.object.editmode_toggle()
            bm = bmesh.new()
            bm.from_mesh(context.active_object.data)
            bm.normal_update()
    
            # For easy access to verts, edges, and faces:
            bVerts = bm.verts
            bEdges = bm.edges
            bFaces = bm.faces
    
    
            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
                bm.select_history.active.select = False
            else:
                for f in bFaces:
                    if f.select:
                        face = f
                        normal = f.normal
                        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.")
    
            # 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:
    
                # @todo DEBUG TRACKER - DELETE WHEN FINISHED:
                if bpy.app.debug:
                    print(dbg)
                    dbg = dbg + 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.
    
                        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
                        # cutting plane:
    
                        if abs(d1 + d2) < abs(d1 - d2):
                            # Make the first vertice the positive vertice:
                            if d1 < d2:
                                v2, v1 = v1, v2
    
                            if self.make_copy:
                                new = bVerts.new()
                                new.co = intersection
    
                            elif self.rip:
                                newV1 = bVerts.new()
                                newV1.co = intersection
    
                                newE1 = bEdges.new((v1, newV1))
                                newE2 = bEdges.new((v2, newV2))
    
                                    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
                                e.select = False
                                new[0].select = False
                                if self.pos:
    
    
            bm.to_mesh(context.active_object.data)
            bpy.ops.object.editmode_toggle()
            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"
        bl_description = "Projects the selected vertices/edges onto the selected plane."
        bl_options = {'REGISTER', 'UNDO'}
    
        make_copy = BoolProperty(name = "Make Copy",
                                 description = "Make a duplicate of the vertices instead of moving it",
                                 default = False)
    
        def draw(self, context):
            layout = self.layout
            layout.prop(self, "make_copy")
    
        @classmethod
        def poll(cls, context):
            ob = context.active_object
            return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
    
        def invoke(self, context, event):
            return self.execute(context)
    
    
        def execute(self, context):
            bpy.ops.object.editmode_toggle()
            bm = bmesh.new()
            bm.from_mesh(context.active_object.data)
            bm.normal_update()
    
            bFaces = bm.faces
            bEdges = bm.edges
            bVerts = bm.verts
    
            fVerts = []
    
            # Find the selected face.  This will provide the plane to project onto:
    
            for f in bFaces:
                if f.select:
                    for v in f.verts:
                        fVerts.append(v)
                    normal = f.normal
                    f.select = False
                    break
    
            for v in bVerts:
                if v.select:
                    if v in fVerts:
                        v.select = False
                        continue
                    d = distance_point_to_plane(v.co, fVerts[0].co, normal)
                    if self.make_copy:
                        temp = v
                        v = bVerts.new()
                        v.co = temp.co
                    vector = normal
                    vector.length = abs(d)
                    v.co = v.co - (vector * sign(d))
                    v.select = False
    
            bm.to_mesh(context.active_object.data)
            bpy.ops.object.editmode_toggle()
            return {'FINISHED'}
    
    
    # Project_End is for projecting/extending an edge to meet a plane.
    # This is used be selecting a face to define the plane then all the edges.
    # The add-on will then move the vertices in the edge that is closest to the
    # plane to the coordinates of the intersection of the edge and the plane.
    class Project_End(bpy.types.Operator):
        bl_idname = "mesh.edgetools_project_end"
        bl_label = "Project (End Point)"
        bl_description = "Projects the vertice of the selected edges closest to a plane onto that plane."
        bl_options = {'REGISTER', 'UNDO'}
    
        make_copy = BoolProperty(name = "Make Copy",
                                 description = "Make a duplicate of the vertice instead of moving it",
                                 default = False)
        keep_length = BoolProperty(name = "Keep Edge Length",
                                   description = "Maintain edge lengths",
                                   default = False)
        use_force = BoolProperty(name = "Use opposite vertices",
                                 description = "Force the usage of the vertices at the other end of the edge",
                                 default = False)
        use_normal = BoolProperty(name = "Project along normal",
                                  description = "Use the plane's normal as the projection direction",
                                  default = False)
    
        def draw(self, context):
            layout = self.layout
    ##        layout.prop(self, "keep_length")
            if not self.keep_length:
                layout.prop(self, "use_normal")
    ##        else:
    ##            self.report({'ERROR_INVALID_INPUT'}, "Maintaining edge length not yet supported")
    ##            self.report({'WARNING'}, "Projection may result in unexpected geometry")
            layout.prop(self, "make_copy")
            layout.prop(self, "use_force")
    
    
        @classmethod
        def poll(cls, context):
            ob = context.active_object
            return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
    
        def invoke(self, context, event):
            return self.execute(context)
    
    
        def execute(self, context):
            bpy.ops.object.editmode_toggle()
            bm = bmesh.new()
            bm.from_mesh(context.active_object.data)
            bm.normal_update()
    
            bFaces = bm.faces
            bEdges = bm.edges
            bVerts = bm.verts
    
            fVerts = []
    
            # Find the selected face.  This will provide the plane to project onto:
            for f in bFaces:
                if f.select:
                    for v in f.verts:
                        fVerts.append(v)
                    normal = f.normal
                    f.select = False
                    break
    
            for e in bEdges:
                if e.select:
                    v1 = e.verts[0]
                    v2 = e.verts[1]
                    if v1 in fVerts or v2 in fVerts:
                        e.select = False
                        continue
                    intersection = intersect_line_plane(v1.co, v2.co, fVerts[0].co, normal)
                    if intersection != None:
                        # Use abs because we don't care what side of plane we're on:
                        d1 = distance_point_to_plane(v1.co, fVerts[0].co, normal)
                        d2 = distance_point_to_plane(v2.co, fVerts[0].co, normal)
                        # If d1 is closer than we use v1 as our vertice:
                        # "xor" with 'use_force':
                        if (abs(d1) < abs(d2)) is not self.use_force:
                            if self.make_copy:
                                v1 = bVerts.new()
                                v1.co = e.verts[0].co
                            if self.keep_length:
                                v1.co = intersection
                            elif self.use_normal:
                                vector = normal
                                vector.length = abs(d1)
                                v1.co = v1.co - (vector * sign(d1))
                            else:
                                v1.co = intersection
                        else:
                            if self.make_copy:
                                v2 = bVerts.new()
                                v2.co = e.verts[1].co
                            if self.keep_length:
                                v2.co = intersection
                            elif self.use_normal:
                                vector = normal
                                vector.length = abs(d2)
                                v2.co = v2.co - (vector * sign(d2))
                            else:
                                v2.co = intersection
                    e.select = False
    
            bm.to_mesh(context.active_object.data)
            bpy.ops.object.editmode_toggle()
            return {'FINISHED'}
    
    
    # Edge Fillet
    #
    # Blender currently does not have a CAD-style edge-based fillet function. This
    # is my atempt to create one.  It should take advantage of BMesh and the ngon
    # capabilities for non-destructive modeling, if possible.  This very well may
    # not result in nice quads and it will be up to the artist to clean up the mesh
    # back into quads if necessary.
    #
    # Assumptions:
    #   - Faces are planar. This should, however, do a check an warn otherwise.
    #
    # Developement Process:
    # Because this will eventaully prove to be a great big jumble of code and
    # various functionality, this is to provide an outline for the developement
    # and functionality wanted at each milestone.
    #   1) intersect_line_face: function to find the intersection point, if it
    #       exists, at which a line intersects a face.  The face does not have to
    #       be planar, and can be an ngon.  This will allow for a point to be placed
    #       on the actual mesh-face for non-planar faces.
    #   2) Minimal propagation, single edge: Filleting of a single edge without
    #       propagation of the fillet along "tangent" edges.
    #   3) Minimal propagation, multiple edges: Perform said fillet along/on
    #       multiple edges.
    #   4) "Tangency" detection code: because we have a mesh based geometry, this
    #       have to make an educated guess at what is actually supposed to be
    #       treated as tangent and what constitutes a sharp edge.  This should
    #       respect edges marked as sharp (does not propagate passed an
    #       intersecting edge that is marked as sharp).
    #   5) Tangent propagation, single edge: Filleting of a single edge using the
    #       above tangency detection code to continue the fillet to adjacent
    #       "tangent" edges.
    #   6) Tangent propagation, multiple edges: Same as above, but with multiple
    #       edges selected.  If multiple edges were selected along the same
    #       tangency path, only one edge will be filleted.  The others must be
    #       ignored/discarded.
    class Fillet(bpy.types.Operator):
        bl_idname = "mesh.edgetools_fillet"
        bl_label = "Edge Fillet"
        bl_description = "Fillet the selected edges."
        bl_options = {'REGISTER', 'UNDO'}
    
        radius = FloatProperty(name = "Radius",
                               description = "Radius of the edge fillet",
    
                               default = 0.5)
        prop = EnumProperty(name = "Propagation",
                            items = [("m", "Minimal", "Minimal edge propagation"),
                                     ("t", "Tangential", "Tangential edge propagation")],
                            default = "m")
    
        prop_fac = FloatProperty(name = "Propagation Factor",
                                 description = "Corner detection sensitivity factor for tangential propagation",
                                 min = 0.0, max = 100.0,
                                 default = 25.0)
        deg_seg = FloatProperty(name = "Degrees/Section",
                                description = "Approximate degrees per section",
                                min = 0.00001, max = 180.0,
                                default = 10.0)
    
        res = IntProperty(name = "Resolution",
                          description = "Resolution of the fillet",
    
                          default = 8)
    
        def draw(self, context):
            layout = self.layout
            layout.prop(self, "radius")
            layout.prop(self, "prop")
    
            if self.prop == "t":
                layout.prop(self, "prop_fac")
            layout.prop(self, "deg_seg")
    
        @classmethod
        def poll(cls, context):
            ob = context.active_object
            return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
    
        def invoke(self, context, event):
            return self.execute(context)
    
    
        def execute(self, context):
            bpy.ops.object.editmode_toggle()
            bm = bmesh.new()
            bm.from_mesh(bpy.context.active_object.data)
            bm.normal_update()
    
            bFaces = bm.faces
            bEdges = bm.edges
            bVerts = bm.verts
    
    
            # Robustness check: this does not support n-gons (at least for now)
            # because I have no idea how to handle them righ now.  If there is
            # an n-gon in the mesh, warn the user that results may be nuts because
            # of it.
            #
            # I'm not going to cause it to exit if there are n-gons, as they may
            # not be encountered.
            # @todo I would like this to be a confirmation dialoge of some sort
            # @todo I would REALLY like this to just handle n-gons. . . .
            for f in bFaces:
                if len(face.verts) > 4:
                    self.report({'WARNING'},
                                "Mesh contains n-gons which are not supported. Operation may fail.")
                    break
    
    
            # Robustness check: boundary and wire edges are not fillet-able.
    
            edges = [e for e in bEdges if e.select and not e.is_boundary and not e.is_wire]
    
            for e in edges:
    
                axis_points = fillet_axis(e, self.radius)
    
    
            bm.to_mesh(bpy.context.active_object.data)
            bpy.ops.object.editmode_toggle()
    
    # For testing the mess that is "intersect_line_face" for possible math errors.
    # This will NOT be directly exposed to end users: it will always require running
    # Blender in debug mode.
    # So far no errors have been found. Thanks to anyone who tests and reports bugs!
    class Intersect_Line_Face(bpy.types.Operator):
        bl_idname = "mesh.edgetools_ilf"
        bl_label = "ILF TEST"
        bl_description = "TEST ONLY: INTERSECT_LINE_FACE"
        bl_options = {'REGISTER', 'UNDO'}
    
        @classmethod
        def poll(cls, context):
            ob = context.active_object
            return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
    
        def invoke(self, context, event):
            return self.execute(context)
    
    
        def execute(self, context):
            # Make sure we really are in debug mode:
            if not bpy.app.debug:
                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)
            bm.normal_update()
    
            bFaces = bm.faces
            bEdges = bm.edges
            bVerts = bm.verts
    
            face = None
            for f in bFaces:
                if f.select:
                    face = f
                    break
    
            edge = None
            for e in bEdges:
                if e.select and not e in face.edges:
                    edge = e
                    break
    
            point = intersect_line_face(edge, face, True)
    
            if point != None:
                new = bVerts.new()
                new.co = point
            else:
                bpy.ops.object.editmode_toggle()
                self.report({'ERROR_INVALID_INPUT'}, "point was \"None\"")
                return {'CANCELLED'}
    
            bm.to_mesh(bpy.context.active_object.data)
            bpy.ops.object.editmode_toggle()
            return {'FINISHED'}
    
    
    
    class VIEW3D_MT_edit_mesh_edgetools(bpy.types.Menu):
        bl_label = "EdgeTools"
    
            layout.operator("mesh.edgetools_extend")
            layout.operator("mesh.edgetools_spline")
            layout.operator("mesh.edgetools_ortho")
            layout.operator("mesh.edgetools_shaft")
            layout.operator("mesh.edgetools_slice")
            layout.operator("mesh.edgetools_project")
            layout.operator("mesh.edgetools_project_end")
    
            if bpy.app.debug:
                ## Not ready for prime-time yet:
                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):
        self.layout.menu("VIEW3D_MT_edit_mesh_edgetools")
        self.layout.separator()
    
    
    # define classes for registration
    classes = [VIEW3D_MT_edit_mesh_edgetools,
        Extend,
        Spline,
        Ortho,
        Shaft,
        Slice,
        Project,
    
        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)
    
    
    # unregistering and removing menus
    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()