Skip to content
Snippets Groups Projects
mesh_looptools.py 165 KiB
Newer Older
  • Learn to ignore specific revisions
  •             if interpolation == 'cubic':
                    ax, bx, cx, dx, tx = splines[i][n][0]
                    x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
                    ay, by, cy, dy, ty = splines[i][n][1]
                    y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
                    az, bz, cz, dz, tz = splines[i][n][2]
                    z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
                    change.append([p, mathutils.Vector([x,y,z])])
                else: # interpolation == 'linear'
                    a, d, t, u = splines[i][n]
                    if u == 0:
                        u = 1e-8
                    change.append([p, ((m-t)/u)*d + a])
        for c in change:
            move.append([c[0], (bm_mod.verts[c[0]].co + c[1]) / 2])
    
        return(move)
    
    
    ##########################################
    ####### Space functions ##################
    ##########################################
    
    # calculate relative positions compared to first knot
    def space_calculate_t(bm_mod, knots):
        tknots = []
        loc_prev = False
        len_total = 0
        for k in knots:
            loc = mathutils.Vector(bm_mod.verts[k].co[:])
            if not loc_prev:
                loc_prev = loc
            len_total += (loc - loc_prev).length
            tknots.append(len_total)
            loc_prev = loc
        amount = len(knots)
        t_per_segment = len_total / (amount - 1)
        tpoints = [i * t_per_segment for i in range(amount)]
    
        return(tknots, tpoints)
    
    
    # change the location of the points to their place on the spline
    def space_calculate_verts(bm_mod, interpolation, tknots, tpoints, points,
    splines):
        move = []
        for p in points:
            m = tpoints[points.index(p)]
            if m in tknots:
                n = tknots.index(m)
            else:
                t = tknots[:]
                t.append(m)
                t.sort()
                n = t.index(m) - 1
            if n > len(splines) - 1:
                n = len(splines) - 1
            elif n < 0:
                n = 0
    
            if interpolation == 'cubic':
                ax, bx, cx, dx, tx = splines[n][0]
                x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
                ay, by, cy, dy, ty = splines[n][1]
                y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
                az, bz, cz, dz, tz = splines[n][2]
                z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
                move.append([p, mathutils.Vector([x,y,z])])
            else: # interpolation == 'linear'
                a, d, t, u = splines[n]
                move.append([p, ((m-t)/u)*d + a])
    
        return(move)
    
    
    ##########################################
    ####### Operators ########################
    ##########################################
    
    # bridge operator
    class Bridge(bpy.types.Operator):
        bl_idname = 'mesh.looptools_bridge'
        bl_label = "Bridge / Loft"
        bl_description = "Bridge two, or loft several, loops of vertices"
        bl_options = {'REGISTER', 'UNDO'}
    
        cubic_strength = bpy.props.FloatProperty(name = "Strength",
            description = "Higher strength results in more fluid curves",
            default = 1.0,
            soft_min = -3.0,
            soft_max = 3.0)
        interpolation = bpy.props.EnumProperty(name = "Interpolation mode",
            items = (('cubic', "Cubic", "Gives curved results"),
                ('linear', "Linear", "Basic, fast, straight interpolation")),
            description = "Interpolation mode: algorithm used when creating "\
                "segments",
            default = 'cubic')
        loft = bpy.props.BoolProperty(name = "Loft",
            description = "Loft multiple loops, instead of considering them as "\
                "a multi-input for bridging",
            default = False)
        loft_loop = bpy.props.BoolProperty(name = "Loop",
            description = "Connect the first and the last loop with each other",
            default = False)
        min_width = bpy.props.IntProperty(name = "Minimum width",
            description = "Segments with an edge smaller than this are merged "\
                "(compared to base edge)",
            default = 0,
            min = 0,
            max = 100,
            subtype = 'PERCENTAGE')
        mode = bpy.props.EnumProperty(name = "Mode",
            items = (('basic', "Basic", "Fast algorithm"), ('shortest',
                "Shortest edge", "Slower algorithm with better vertex matching")),
            description = "Algorithm used for bridging",
            default = 'shortest')
        remove_faces = bpy.props.BoolProperty(name = "Remove faces",
            description = "Remove faces that are internal after bridging",
            default = True)
        reverse = bpy.props.BoolProperty(name = "Reverse",
            description = "Manually override the direction in which the loops "\
                          "are bridged. Only use if the tool gives the wrong " \
                          "result",
            default = False)
        segments = bpy.props.IntProperty(name = "Segments",
            description = "Number of segments used to bridge the gap "\
                "(0 = automatic)",
            default = 1,
            min = 0,
            soft_max = 20)
        twist = bpy.props.IntProperty(name = "Twist",
            description = "Twist what vertices are connected to each other",
            default = 0)
    
        @classmethod
        def poll(cls, context):
            ob = context.active_object
            return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
        def draw(self, context):
            layout = self.layout
            #layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
    
            # top row
            col_top = layout.column(align=True)
            row = col_top.row(align=True)
            col_left = row.column(align=True)
            col_right = row.column(align=True)
            col_right.active = self.segments != 1
            col_left.prop(self, "segments")
            col_right.prop(self, "min_width", text="")
            # bottom row
            bottom_left = col_left.row()
            bottom_left.active = self.segments != 1
            bottom_left.prop(self, "interpolation", text="")
            bottom_right = col_right.row()
            bottom_right.active = self.interpolation == 'cubic'
            bottom_right.prop(self, "cubic_strength")
            # boolean properties
            col_top.prop(self, "remove_faces")
            if self.loft:
                col_top.prop(self, "loft_loop")
    
            # override properties
            col_top.separator()
            row = layout.row(align = True)
            row.prop(self, "twist")
            row.prop(self, "reverse")
    
        def invoke(self, context, event):
            # load custom settings
            context.window_manager.looptools.bridge_loft = self.loft
            settings_load(self)
            return self.execute(context)
    
        def execute(self, context):
            # initialise
            global_undo, object, bm = initialise()
            edge_faces, edgekey_to_edge, old_selected_faces, smooth = \
                bridge_initialise(bm, self.interpolation)
            settings_write(self)
    
            # check cache to see if we can save time
            input_method = bridge_input_method(self.loft, self.loft_loop)
            cached, single_loops, loops, derived, mapping = cache_read("Bridge",
                object, bm, input_method, False)
            if not cached:
                # get loops
                loops = bridge_get_input(bm)
                if loops:
                    # reorder loops if there are more than 2
                    if len(loops) > 2:
                        if self.loft:
                            loops = bridge_sort_loops(bm, loops, self.loft_loop)
                        else:
                            loops = bridge_match_loops(bm, loops)
    
            # saving cache for faster execution next time
            if not cached:
                cache_write("Bridge", object, bm, input_method, False, False,
                    loops, False, False)
    
            if loops:
                # calculate new geometry
                vertices = []
                faces = []
                max_vert_index = len(bm.verts)-1
                for i in range(1, len(loops)):
                    if not self.loft and i%2 == 0:
                        continue
                    lines = bridge_calculate_lines(bm, loops[i-1:i+1],
                        self.mode, self.twist, self.reverse)
                    vertex_normals = bridge_calculate_virtual_vertex_normals(bm,
                        lines, loops[i-1:i+1], edge_faces, edgekey_to_edge)
                    segments = bridge_calculate_segments(bm, lines,
                        loops[i-1:i+1], self.segments)
                    new_verts, new_faces, max_vert_index = \
                        bridge_calculate_geometry(bm, lines, vertex_normals,
                        segments, self.interpolation, self.cubic_strength,
                        self.min_width, max_vert_index)
                    if new_verts:
                        vertices += new_verts
                    if new_faces:
                        faces += new_faces
                # make sure faces in loops that aren't used, aren't removed
                if self.remove_faces and old_selected_faces:
                    bridge_save_unused_faces(bm, old_selected_faces, loops)
                # create vertices
                if vertices:
                    bridge_create_vertices(bm, vertices)
                # create faces
                if faces:
    
                    new_faces = bridge_create_faces(object, bm, faces, self.twist)
                    old_selected_faces = [i for i, face in enumerate(bm.faces) \
                        if face.index in old_selected_faces] # updating list
                    bridge_select_new_faces(new_faces, smooth)
    
                # edge-data could have changed, can't use cache next run
                if faces and not vertices:
                    cache_delete("Bridge")
                # delete internal faces
                if self.remove_faces and old_selected_faces:
                    bridge_remove_internal_faces(bm, old_selected_faces)
                # make sure normals are facing outside
    
                bmesh.update_edit_mesh(object.data, tessface=False,
                    destructive=True)
    
                bpy.ops.mesh.normals_make_consistent()
    
            # cleaning up
            terminate(global_undo)
    
            return{'FINISHED'}
    
    
    # circle operator
    class Circle(bpy.types.Operator):
        bl_idname = "mesh.looptools_circle"
        bl_label = "Circle"
        bl_description = "Move selected vertices into a circle shape"
        bl_options = {'REGISTER', 'UNDO'}
    
        custom_radius = bpy.props.BoolProperty(name = "Radius",
            description = "Force a custom radius",
            default = False)
        fit = bpy.props.EnumProperty(name = "Method",
            items = (("best", "Best fit", "Non-linear least squares"),
                ("inside", "Fit inside","Only move vertices towards the center")),
            description = "Method used for fitting a circle to the vertices",
            default = 'best')
        flatten = bpy.props.BoolProperty(name = "Flatten",
            description = "Flatten the circle, instead of projecting it on the " \
                "mesh",
            default = True)
        influence = bpy.props.FloatProperty(name = "Influence",
            description = "Force of the tool",
            default = 100.0,
            min = 0.0,
            max = 100.0,
            precision = 1,
            subtype = 'PERCENTAGE')
        radius = bpy.props.FloatProperty(name = "Radius",
            description = "Custom radius for circle",
            default = 1.0,
            min = 0.0,
            soft_max = 1000.0)
        regular = bpy.props.BoolProperty(name = "Regular",
            description = "Distribute vertices at constant distances along the " \
                "circle",
            default = True)
    
        @classmethod
        def poll(cls, context):
            ob = context.active_object
            return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
        def draw(self, context):
            layout = self.layout
            col = layout.column()
    
            col.prop(self, "fit")
            col.separator()
    
            col.prop(self, "flatten")
            row = col.row(align=True)
            row.prop(self, "custom_radius")
            row_right = row.row(align=True)
            row_right.active = self.custom_radius
            row_right.prop(self, "radius", text="")
            col.prop(self, "regular")
            col.separator()
    
            col.prop(self, "influence")
    
        def invoke(self, context, event):
            # load custom settings
            settings_load(self)
            return self.execute(context)
    
        def execute(self, context):
            # initialise
            global_undo, object, bm = initialise()
            settings_write(self)
            # check cache to see if we can save time
            cached, single_loops, loops, derived, mapping = cache_read("Circle",
                object, bm, False, False)
            if cached:
                derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
            else:
                # find loops
                derived, bm_mod, single_vertices, single_loops, loops = \
                    circle_get_input(object, bm, context.scene)
                mapping = get_mapping(derived, bm, bm_mod, single_vertices,
                    False, loops)
                single_loops, loops = circle_check_loops(single_loops, loops,
                    mapping, bm_mod)
    
            # saving cache for faster execution next time
            if not cached:
                cache_write("Circle", object, bm, False, False, single_loops,
                    loops, derived, mapping)
    
            move = []
            for i, loop in enumerate(loops):
                # best fitting flat plane
                com, normal = calculate_plane(bm_mod, loop)
                # if circular, shift loop so we get a good starting vertex
                if loop[1]:
                    loop = circle_shift_loop(bm_mod, loop, com)
                # flatten vertices on plane
                locs_2d, p, q = circle_3d_to_2d(bm_mod, loop, com, normal)
                # calculate circle
                if self.fit == 'best':
                    x0, y0, r = circle_calculate_best_fit(locs_2d)
                else: # self.fit == 'inside'
                    x0, y0, r = circle_calculate_min_fit(locs_2d)
                # radius override
                if self.custom_radius:
                    r = self.radius / p.length
                # calculate positions on circle
                if self.regular:
                    new_locs_2d = circle_project_regular(locs_2d[:], x0, y0, r)
                else:
                    new_locs_2d = circle_project_non_regular(locs_2d[:], x0, y0, r)
                # take influence into account
                locs_2d = circle_influence_locs(locs_2d, new_locs_2d,
                    self.influence)
                # calculate 3d positions of the created 2d input
                move.append(circle_calculate_verts(self.flatten, bm_mod,
                    locs_2d, com, p, q, normal))
                # flatten single input vertices on plane defined by loop
                if self.flatten and single_loops:
                    move.append(circle_flatten_singles(bm_mod, com, p, q,
                        normal, single_loops[i]))
    
            # move vertices to new locations
            move_verts(object, bm, mapping, move, -1)
    
            # cleaning up
            if derived:
                bm_mod.free()
            terminate(global_undo)
    
            return{'FINISHED'}
    
    
    # curve operator
    class Curve(bpy.types.Operator):
        bl_idname = "mesh.looptools_curve"
        bl_label = "Curve"
        bl_description = "Turn a loop into a smooth curve"
        bl_options = {'REGISTER', 'UNDO'}
    
        boundaries = bpy.props.BoolProperty(name = "Boundaries",
            description = "Limit the tool to work within the boundaries of the "\
                "selected vertices",
            default = False)
        influence = bpy.props.FloatProperty(name = "Influence",
            description = "Force of the tool",
            default = 100.0,
            min = 0.0,
            max = 100.0,
            precision = 1,
            subtype = 'PERCENTAGE')
        interpolation = bpy.props.EnumProperty(name = "Interpolation",
            items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
                ("linear", "Linear", "Simple and fast linear algorithm")),
            description = "Algorithm used for interpolation",
            default = 'cubic')
        regular = bpy.props.BoolProperty(name = "Regular",
            description = "Distribute vertices at constant distances along the" \
                "curve",
            default = True)
        restriction = bpy.props.EnumProperty(name = "Restriction",
            items = (("none", "None", "No restrictions on vertex movement"),
                ("extrude", "Extrude only","Only allow extrusions (no "\
                    "indentations)"),
                ("indent", "Indent only", "Only allow indentation (no "\
                    "extrusions)")),
            description = "Restrictions on how the vertices can be moved",
            default = 'none')
    
        @classmethod
        def poll(cls, context):
            ob = context.active_object
            return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
        def draw(self, context):
            layout = self.layout
            col = layout.column()
    
            col.prop(self, "interpolation")
            col.prop(self, "restriction")
            col.prop(self, "boundaries")
            col.prop(self, "regular")
            col.separator()
    
            col.prop(self, "influence")
    
        def invoke(self, context, event):
            # load custom settings
            settings_load(self)
            return self.execute(context)
    
        def execute(self, context):
            # initialise
            global_undo, object, bm = initialise()
            settings_write(self)
            # check cache to see if we can save time
            cached, single_loops, loops, derived, mapping = cache_read("Curve",
                object, bm, False, self.boundaries)
            if cached:
                derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
            else:
                # find loops
                derived, bm_mod, loops = curve_get_input(object, bm,
                    self.boundaries, context.scene)
                mapping = get_mapping(derived, bm, bm_mod, False, True, loops)
                loops = check_loops(loops, mapping, bm_mod)
            verts_selected = [v.index for v in bm_mod.verts if v.select \
                and not v.hide]
    
            # saving cache for faster execution next time
            if not cached:
                cache_write("Curve", object, bm, False, self.boundaries, False,
                    loops, derived, mapping)
    
            move = []
            for loop in loops:
                knots, points = curve_calculate_knots(loop, verts_selected)
                pknots = curve_project_knots(bm_mod, verts_selected, knots,
                    points, loop[1])
                tknots, tpoints = curve_calculate_t(bm_mod, knots, points,
                    pknots, self.regular, loop[1])
                splines = calculate_splines(self.interpolation, bm_mod,
                    tknots, knots)
                move.append(curve_calculate_vertices(bm_mod, knots, tknots,
                    points, tpoints, splines, self.interpolation,
                    self.restriction))
    
            # move vertices to new locations
            move_verts(object, bm, mapping, move, self.influence)
    
            if derived:
                bm_mod.free()
            terminate(global_undo)
    
            return{'FINISHED'}
    
    
    # flatten operator
    class Flatten(bpy.types.Operator):
        bl_idname = "mesh.looptools_flatten"
        bl_label = "Flatten"
        bl_description = "Flatten vertices on a best-fitting plane"
        bl_options = {'REGISTER', 'UNDO'}
    
        influence = bpy.props.FloatProperty(name = "Influence",
            description = "Force of the tool",
            default = 100.0,
            min = 0.0,
            max = 100.0,
            precision = 1,
            subtype = 'PERCENTAGE')
        plane = bpy.props.EnumProperty(name = "Plane",
            items = (("best_fit", "Best fit", "Calculate a best fitting plane"),
                ("normal", "Normal", "Derive plane from averaging vertex "\
                "normals"),
                ("view", "View", "Flatten on a plane perpendicular to the "\
                "viewing angle")),
            description = "Plane on which vertices are flattened",
            default = 'best_fit')
        restriction = bpy.props.EnumProperty(name = "Restriction",
            items = (("none", "None", "No restrictions on vertex movement"),
                ("bounding_box", "Bounding box", "Vertices are restricted to "\
                "movement inside the bounding box of the selection")),
            description = "Restrictions on how the vertices can be moved",
            default = 'none')
    
        @classmethod
        def poll(cls, context):
            ob = context.active_object
            return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
        def draw(self, context):
            layout = self.layout
            col = layout.column()
    
            col.prop(self, "plane")
            #col.prop(self, "restriction")
            col.separator()
    
            col.prop(self, "influence")
    
        def invoke(self, context, event):
            # load custom settings
            settings_load(self)
            return self.execute(context)
    
        def execute(self, context):
            # initialise
            global_undo, object, bm = initialise()
            settings_write(self)
            # check cache to see if we can save time
            cached, single_loops, loops, derived, mapping = cache_read("Flatten",
                object, bm, False, False)
            if not cached:
                # order input into virtual loops
                loops = flatten_get_input(bm)
                loops = check_loops(loops, mapping, bm)
    
            # saving cache for faster execution next time
            if not cached:
                cache_write("Flatten", object, bm, False, False, False, loops,
                    False, False)
    
            move = []
            for loop in loops:
                # calculate plane and position of vertices on them
                com, normal = calculate_plane(bm, loop, method=self.plane,
                    object=object)
                to_move = flatten_project(bm, loop, com, normal)
                if self.restriction == 'none':
                    move.append(to_move)
                else:
                    move.append(to_move)
            move_verts(object, bm, False, move, self.influence)
    
            # cleaning up
            terminate(global_undo)
    
    Bart Crouch's avatar
    Bart Crouch committed
    # gstretch operator
    class GStretch(bpy.types.Operator):
        bl_idname = "mesh.looptools_gstretch"
        bl_label = "Gstretch"
        bl_description = "Stretch selected vertices to Grease Pencil stroke"
        bl_options = {'REGISTER', 'UNDO'}
    
        conversion = bpy.props.EnumProperty(name = "Conversion",
            items = (("distance", "Distance", "Set the distance between vertices "\
                "of the converted grease pencil stroke"),
                ("limit_vertices", "Limit vertices", "Set the minimum and maximum "\
                "number of vertices that converted GP strokes will have"),
                ("vertices", "Exact vertices", "Set the exact number of vertices "\
                "that converted grease pencil strokes will have. Short strokes "\
                "with few points may contain less vertices than this number."),
                ("none", "No simplification", "Convert each grease pencil point "\
                "to a vertex")),
            description = "If grease pencil strokes are converted to geometry, "\
                "use this simplification method",
            default = 'limit_vertices')
        conversion_distance = bpy.props.FloatProperty(name = "Distance",
            description = "Absolute distance between vertices along the converted "\
                "grease pencil stroke",
            default = 0.1,
            min = 0.000001,
            soft_min = 0.01,
            soft_max = 100)
        conversion_max = bpy.props.IntProperty(name = "Max Vertices",
            description = "Maximum number of vertices grease pencil strokes will "\
                "have, when they are converted to geomtery",
            default = 32,
            min = 3,
            soft_max = 500,
            update = gstretch_update_min)
        conversion_min = bpy.props.IntProperty(name = "Min Vertices",
            description = "Minimum number of vertices grease pencil strokes will "\
                "have, when they are converted to geomtery",
            default = 8,
            min = 3,
            soft_max = 500,
            update = gstretch_update_max)
        conversion_vertices = bpy.props.IntProperty(name = "Vertices",
            description = "Number of vertices grease pencil strokes will "\
                "have, when they are converted to geometry. If strokes have less "\
                "points than required, the 'Spread evenly' method is used.",
            default = 32,
            min = 3,
            soft_max = 500)
    
    Bart Crouch's avatar
    Bart Crouch committed
        delete_strokes = bpy.props.BoolProperty(name="Delete strokes",
            description = "Remove Grease Pencil strokes if they have been used "\
    
                "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
    
    Bart Crouch's avatar
    Bart Crouch committed
        influence = bpy.props.FloatProperty(name = "Influence",
            description = "Force of the tool",
            default = 100.0,
            min = 0.0,
            max = 100.0,
            precision = 1,
            subtype = 'PERCENTAGE')
        method = bpy.props.EnumProperty(name = "Method",
            items = (("project", "Project", "Project vertices onto the stroke, "\
                "using vertex normals and connected edges"),
                ("irregular", "Spread", "Distribute vertices along the full "\
                "stroke, retaining relative distances between the vertices"),
                ("regular", "Spread evenly", "Distribute vertices at regular "\
                "distances along the full stroke")),
            description = "Method of distributing the vertices over the Grease "\
                "Pencil stroke",
            default = 'regular')
    
    Bart Crouch's avatar
    Bart Crouch committed
        @classmethod
        def poll(cls, context):
            ob = context.active_object
    
            return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
    Bart Crouch's avatar
    Bart Crouch committed
        def draw(self, context):
            layout = self.layout
            col = layout.column()
    
    Bart Crouch's avatar
    Bart Crouch committed
            col.prop(self, "method")
    
            col.prop(self, "delete_strokes")
            col.separator()
    
            col_conv = col.column(align=True)
            col_conv.prop(self, "conversion", text="")
            if self.conversion == 'distance':
                col_conv.prop(self, "conversion_distance")
            elif self.conversion == 'limit_vertices':
                row = col_conv.row(align=True)
                row.prop(self, "conversion_min", text="Min")
                row.prop(self, "conversion_max", text="Max")
            elif self.conversion == 'vertices':
                col_conv.prop(self, "conversion_vertices")
    
    Bart Crouch's avatar
    Bart Crouch committed
            col.separator()
    
    Bart Crouch's avatar
    Bart Crouch committed
            col.prop(self, "influence")
    
    Bart Crouch's avatar
    Bart Crouch committed
        def invoke(self, context, event):
    
            # flush cached strokes
            if 'Gstretch' in looptools_cache:
                looptools_cache['Gstretch']['single_loops'] = []
    
    Bart Crouch's avatar
    Bart Crouch committed
            # load custom settings
            settings_load(self)
            return self.execute(context)
    
    Bart Crouch's avatar
    Bart Crouch committed
        def execute(self, context):
            # initialise
            global_undo, object, bm = initialise()
            settings_write(self)
    
    Bart Crouch's avatar
    Bart Crouch committed
            # check cache to see if we can save time
    
            cached, safe_strokes, loops, derived, mapping = cache_read("Gstretch",
    
                object, bm, False, False)
    
    Bart Crouch's avatar
    Bart Crouch committed
            if cached:
    
                if safe_strokes:
                    strokes = gstretch_safe_to_true_strokes(safe_strokes)
                # cached strokes were flushed (see operator's invoke function)
                elif object.grease_pencil:
                    strokes = gstretch_get_strokes(object)
                else:
    
                    derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
    
                    strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
    
    Bart Crouch's avatar
    Bart Crouch committed
                derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
            else:
                # find loops
                derived, bm_mod, loops = get_connected_input(object, bm,
                    context.scene, input='selected')
                mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
                loops = check_loops(loops, mapping, bm_mod)
    
                # get strokes
                if object.grease_pencil:
                    strokes = gstretch_get_strokes(object)
                else:
                    strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
    
    Bart Crouch's avatar
    Bart Crouch committed
            # saving cache for faster execution next time
            if not cached:
    
                if strokes:
                    safe_strokes = gstretch_true_to_safe_strokes(strokes)
                else:
                    safe_strokes = []
    
                cache_write("Gstretch", object, bm, False, False,
    
                    safe_strokes, loops, derived, mapping)
    
    
    Bart Crouch's avatar
    Bart Crouch committed
            # pair loops and strokes
            ls_pairs = gstretch_match_loops_strokes(loops, strokes, object, bm_mod)
            ls_pairs = gstretch_align_pairs(ls_pairs, object, bm_mod, self.method)
    
    Bart Crouch's avatar
    Bart Crouch committed
            move = []
    
            if not loops:
                # no selected geometry, convert GP to verts
                if strokes:
                    move.append(gstretch_create_verts(object, bm, strokes,
                        self.method, self.conversion, self.conversion_distance,
                        self.conversion_max, self.conversion_min,
                        self.conversion_vertices))
                    for stroke in strokes:
                        gstretch_erase_stroke(stroke, context)
            elif ls_pairs:
    
    Bart Crouch's avatar
    Bart Crouch committed
                for (loop, stroke) in ls_pairs:
                    move.append(gstretch_calculate_verts(loop, stroke, object,
                        bm_mod, self.method))
                    if self.delete_strokes:
    
                        if type(stroke) != bpy.types.GPencilStroke:
                            # in case of cached fake stroke, get the real one
                            if object.grease_pencil:
                                strokes = gstretch_get_strokes(object)
                                ls_pairs = gstretch_match_loops_strokes(loops,
                                    strokes, object, bm_mod)
                                ls_pairs = gstretch_align_pairs(ls_pairs, object,
                                    bm_mod, self.method)
                                for (l, s) in ls_pairs:
                                    if l == loop:
                                        stroke = s
                                        break
    
    Bart Crouch's avatar
    Bart Crouch committed
                        gstretch_erase_stroke(stroke, context)
    
    Bart Crouch's avatar
    Bart Crouch committed
            # move vertices to new locations
    
            bmesh.update_edit_mesh(object.data, tessface=True,
                    destructive=True)
    
    Bart Crouch's avatar
    Bart Crouch committed
            move_verts(object, bm, mapping, move, self.influence)
    
    Bart Crouch's avatar
    Bart Crouch committed
            if derived:
                bm_mod.free()
            terminate(global_undo)
    
    Bart Crouch's avatar
    Bart Crouch committed
            return{'FINISHED'}
    
    
    
    # relax operator
    class Relax(bpy.types.Operator):
        bl_idname = "mesh.looptools_relax"
        bl_label = "Relax"
        bl_description = "Relax the loop, so it is smoother"
        bl_options = {'REGISTER', 'UNDO'}
    
        input = bpy.props.EnumProperty(name = "Input",
            items = (("all", "Parallel (all)", "Also use non-selected "\
                    "parallel loops as input"),
                ("selected", "Selection","Only use selected vertices as input")),
            description = "Loops that are relaxed",
            default = 'selected')
        interpolation = bpy.props.EnumProperty(name = "Interpolation",
            items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
                ("linear", "Linear", "Simple and fast linear algorithm")),
            description = "Algorithm used for interpolation",
            default = 'cubic')
        iterations = bpy.props.EnumProperty(name = "Iterations",
            items = (("1", "1", "One"),
                ("3", "3", "Three"),
                ("5", "5", "Five"),
                ("10", "10", "Ten"),
                ("25", "25", "Twenty-five")),
            description = "Number of times the loop is relaxed",
            default = "1")
        regular = bpy.props.BoolProperty(name = "Regular",
            description = "Distribute vertices at constant distances along the" \
                "loop",
            default = True)
    
        @classmethod
        def poll(cls, context):
            ob = context.active_object
            return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
        def draw(self, context):
            layout = self.layout
            col = layout.column()
    
            col.prop(self, "interpolation")
            col.prop(self, "input")
            col.prop(self, "iterations")
            col.prop(self, "regular")
    
        def invoke(self, context, event):
            # load custom settings
            settings_load(self)
            return self.execute(context)
    
        def execute(self, context):
            # initialise
            global_undo, object, bm = initialise()
            settings_write(self)
            # check cache to see if we can save time
            cached, single_loops, loops, derived, mapping = cache_read("Relax",
                object, bm, self.input, False)
            if cached:
                derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
            else:
                # find loops
                derived, bm_mod, loops = get_connected_input(object, bm,
                    context.scene, self.input)
                mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
                loops = check_loops(loops, mapping, bm_mod)
            knots, points = relax_calculate_knots(loops)
    
            # saving cache for faster execution next time
            if not cached:
                cache_write("Relax", object, bm, self.input, False, False, loops,
                    derived, mapping)
    
            for iteration in range(int(self.iterations)):
                # calculate splines and new positions
                tknots, tpoints = relax_calculate_t(bm_mod, knots, points,
                    self.regular)
                splines = []
                for i in range(len(knots)):
                    splines.append(calculate_splines(self.interpolation, bm_mod,
                        tknots[i], knots[i]))
                move = [relax_calculate_verts(bm_mod, self.interpolation,
                    tknots, knots, tpoints, points, splines)]
                move_verts(object, bm, mapping, move, -1)
    
            # cleaning up
            if derived:
                bm_mod.free()
            terminate(global_undo)
    
            return{'FINISHED'}
    
    
    # space operator
    class Space(bpy.types.Operator):
        bl_idname = "mesh.looptools_space"
        bl_label = "Space"
        bl_description = "Space the vertices in a regular distrubtion on the loop"
        bl_options = {'REGISTER', 'UNDO'}
    
        influence = bpy.props.FloatProperty(name = "Influence",
            description = "Force of the tool",
            default = 100.0,
            min = 0.0,
            max = 100.0,
            precision = 1,
            subtype = 'PERCENTAGE')
        input = bpy.props.EnumProperty(name = "Input",
            items = (("all", "Parallel (all)", "Also use non-selected "\
                    "parallel loops as input"),
                ("selected", "Selection","Only use selected vertices as input")),
            description = "Loops that are spaced",
            default = 'selected')
        interpolation = bpy.props.EnumProperty(name = "Interpolation",
            items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
                ("linear", "Linear", "Vertices are projected on existing edges")),
            description = "Algorithm used for interpolation",
            default = 'cubic')
    
        @classmethod
        def poll(cls, context):
            ob = context.active_object
            return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
        def draw(self, context):
            layout = self.layout
            col = layout.column()
    
            col.prop(self, "interpolation")
            col.prop(self, "input")
            col.separator()
    
            col.prop(self, "influence")
    
        def invoke(self, context, event):
            # load custom settings
            settings_load(self)
            return self.execute(context)
    
        def execute(self, context):
            # initialise
            global_undo, object, bm = initialise()
            settings_write(self)
            # check cache to see if we can save time
            cached, single_loops, loops, derived, mapping = cache_read("Space",
                object, bm, self.input, False)
            if cached:
                derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
            else:
                # find loops
                derived, bm_mod, loops = get_connected_input(object, bm,
                    context.scene, self.input)
                mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
                loops = check_loops(loops, mapping, bm_mod)
    
            # saving cache for faster execution next time
            if not cached:
                cache_write("Space", object, bm, self.input, False, False, loops,
                    derived, mapping)
    
            move = []
            for loop in loops:
                # calculate splines and new positions
                if loop[1]: # circular
                    loop[0].append(loop[0][0])
                tknots, tpoints = space_calculate_t(bm_mod, loop[0][:])
                splines = calculate_splines(self.interpolation, bm_mod,
                    tknots, loop[0][:])
                move.append(space_calculate_verts(bm_mod, self.interpolation,
                    tknots, tpoints, loop[0][:-1], splines))
            # move vertices to new locations
            move_verts(object, bm, mapping, move, self.influence)
    
            # cleaning up
            if derived:
                bm_mod.free()
            terminate(global_undo)
    
            return{'FINISHED'}
    
    
    ##########################################
    ####### GUI and registration #############
    ##########################################
    
    # menu containing all tools
    class VIEW3D_MT_edit_mesh_looptools(bpy.types.Menu):
        bl_label = "LoopTools"
    
        def draw(self, context):
            layout = self.layout
    
    Campbell Barton's avatar
    Campbell Barton committed
            layout.operator("mesh.looptools_bridge", text="Bridge").loft = False
    
            layout.operator("mesh.looptools_circle")
            layout.operator("mesh.looptools_curve")
            layout.operator("mesh.looptools_flatten")
    
    Bart Crouch's avatar
    Bart Crouch committed
            layout.operator("mesh.looptools_gstretch")
    
    Campbell Barton's avatar
    Campbell Barton committed
            layout.operator("mesh.looptools_bridge", text="Loft").loft = True
    
            layout.operator("mesh.looptools_relax")
            layout.operator("mesh.looptools_space")
    
    
    # panel containing all tools
    class VIEW3D_PT_tools_looptools(bpy.types.Panel):
        bl_space_type = 'VIEW_3D'
        bl_region_type = 'TOOLS'
    
        bl_category = 'Tools'
    
        bl_context = "mesh_edit"
        bl_label = "LoopTools"
    
    Brendon Murphy's avatar
    Brendon Murphy committed
        bl_options = {'DEFAULT_CLOSED'}
    
    
        def draw(self, context):
            layout = self.layout
            col = layout.column(align=True)
            lt = context.window_manager.looptools
    
            split = col.split(percentage=0.15, align=True)
    
            if lt.display_bridge:
                split.prop(lt, "display_bridge", text="", icon='DOWNARROW_HLT')
            else:
                split.prop(lt, "display_bridge", text="", icon='RIGHTARROW')
    
    Campbell Barton's avatar
    Campbell Barton committed
            split.operator("mesh.looptools_bridge", text="Bridge").loft = False
    
            # bridge - settings
            if lt.display_bridge:
                box = col.column(align=True).box().column()
                #box.prop(self, "mode")
    
                # top row
                col_top = box.column(align=True)
                row = col_top.row(align=True)
                col_left = row.column(align=True)
                col_right = row.column(align=True)
                col_right.active = lt.bridge_segments != 1
                col_left.prop(lt, "bridge_segments")