Skip to content
Snippets Groups Projects
mesh_looptools.py 185 KiB
Newer Older
  • Learn to ignore specific revisions
  •     return(strokes)
    
    
    # convert a GP stroke into a list of points which can be stored in cache
    def gstretch_true_to_safe_strokes(strokes):
        safe_strokes = []
        for stroke in strokes:
            safe_strokes.append([p.co.copy() for p in stroke.points])
    
        return(safe_strokes)
    
    
    # force consistency in GUI, max value can never be lower than min value
    def gstretch_update_max(self, context):
        # called from operator settings (after execution)
        if 'conversion_min' in self.keys():
            if self.conversion_min > self.conversion_max:
                self.conversion_max = self.conversion_min
        # called from toolbar
        else:
            lt = context.window_manager.looptools
            if lt.gstretch_conversion_min > lt.gstretch_conversion_max:
                lt.gstretch_conversion_max = lt.gstretch_conversion_min
    
    
    # force consistency in GUI, min value can never be higher than max value
    def gstretch_update_min(self, context):
        # called from operator settings (after execution)
        if 'conversion_max' in self.keys():
            if self.conversion_max < self.conversion_min:
                self.conversion_min = self.conversion_max
        # called from toolbar
        else:
            lt = context.window_manager.looptools
            if lt.gstretch_conversion_max < lt.gstretch_conversion_min:
                lt.gstretch_conversion_min = lt.gstretch_conversion_max
    
    
    
    # ########################################
    # ##### Relax functions ##################
    # ########################################
    
    
    # create lists with knots and points, all correctly sorted
    def relax_calculate_knots(loops):
        all_knots = []
        all_points = []
        for loop, circular in loops:
            knots = [[], []]
            points = [[], []]
            if circular:
    
                if len(loop) % 2 == 1:  # odd
    
                    extend = [False, True, 0, 1, 0, 1]
    
                else:  # even
    
                    extend = [True, False, 0, 1, 1, 2]
            else:
    
                if len(loop) % 2 == 1:  # odd
    
                    extend = [False, False, 0, 1, 1, 2]
    
                else:  # even
    
                    extend = [False, False, 0, 1, 1, 2]
            for j in range(2):
                if extend[j]:
                    loop = [loop[-1]] + loop + [loop[0]]
    
                for i in range(extend[2 + 2 * j], len(loop), 2):
    
                    knots[j].append(loop[i])
    
                for i in range(extend[3 + 2 * j], len(loop), 2):
    
                    if loop[i] == loop[-1] and not circular:
                        continue
                    if len(points[j]) == 0:
                        points[j].append(loop[i])
                    elif loop[i] != points[j][0]:
                        points[j].append(loop[i])
                if circular:
                    if knots[j][0] != knots[j][-1]:
                        knots[j].append(knots[j][0])
            if len(points[1]) == 0:
                knots.pop(1)
                points.pop(1)
            for k in knots:
                all_knots.append(k)
            for p in points:
                all_points.append(p)
    
        return(all_knots, all_points)
    
    
    # calculate relative positions compared to first knot
    def relax_calculate_t(bm_mod, knots, points, regular):
        all_tknots = []
        all_tpoints = []
        for i in range(len(knots)):
            amount = len(knots[i]) + len(points[i])
    
            for j in range(amount):
    
                if j % 2 == 0:
                    mix.append([True, knots[i][round(j / 2)]])
                elif j == amount - 1:
    
                    mix.append([True, knots[i][-1]])
                else:
    
                    mix.append([False, points[i][int(j / 2)]])
    
            len_total = 0
            loc_prev = False
            tknots = []
            tpoints = []
            for m in mix:
                loc = mathutils.Vector(bm_mod.verts[m[1]].co[:])
                if not loc_prev:
                    loc_prev = loc
                len_total += (loc - loc_prev).length
                if m[0]:
                    tknots.append(len_total)
                else:
                    tpoints.append(len_total)
                loc_prev = loc
            if regular:
                tpoints = []
                for p in range(len(points[i])):
    
                    tpoints.append((tknots[p] + tknots[p + 1]) / 2)
    
            all_tknots.append(tknots)
            all_tpoints.append(tpoints)
    
        return(all_tknots, all_tpoints)
    
    
    # change the location of the points to their place on the spline
    def relax_calculate_verts(bm_mod, interpolation, tknots, knots, tpoints,
    points, splines):
        change = []
        move = []
        for i in range(len(knots)):
            for p in points[i]:
                m = tpoints[i][points[i].index(p)]
                if m in tknots[i]:
                    n = tknots[i].index(m)
                else:
                    t = tknots[i][:]
                    t.append(m)
                    t.sort()
    
                    n = t.index(m) - 1
    
                if n > len(splines[i]) - 1:
                    n = len(splines[i]) - 1
                elif n < 0:
                    n = 0
    
                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])
    
    # ########################################
    # ##### 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])
    
    # ########################################
    # ##### Operators ########################
    # ########################################
    
    class Bridge(Operator):
    
        bl_idname = 'mesh.looptools_bridge'
        bl_label = "Bridge / Loft"
        bl_description = "Bridge two, or loft several, loops of vertices"
        bl_options = {'REGISTER', 'UNDO'}
    
    florianfelix's avatar
    florianfelix committed
        cubic_strength: FloatProperty(
    
            name="Strength",
            description="Higher strength results in more fluid curves",
            default=1.0,
            soft_min=-3.0,
            soft_max=3.0
            )
    
    florianfelix's avatar
    florianfelix committed
        interpolation: 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'
            )
    
    florianfelix's avatar
    florianfelix committed
        loft: BoolProperty(
    
            name="Loft",
            description="Loft multiple loops, instead of considering them as "
                        "a multi-input for bridging",
            default=False
            )
    
    florianfelix's avatar
    florianfelix committed
        loft_loop: BoolProperty(
    
            name="Loop",
            description="Connect the first and the last loop with each other",
            default=False
            )
    
    florianfelix's avatar
    florianfelix committed
        min_width: 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'
            )
    
    florianfelix's avatar
    florianfelix committed
        mode: EnumProperty(
    
            name="Mode",
            items=(('basic', "Basic", "Fast algorithm"),
                   ('shortest', "Shortest edge", "Slower algorithm with better vertex matching")),
            description="Algorithm used for bridging",
            default='shortest'
            )
    
    florianfelix's avatar
    florianfelix committed
        remove_faces: BoolProperty(
    
            name="Remove faces",
            description="Remove faces that are internal after bridging",
            default=True
            )
    
    florianfelix's avatar
    florianfelix committed
        reverse: 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
            )
    
    florianfelix's avatar
    florianfelix committed
        segments: IntProperty(
    
            name="Segments",
            description="Number of segments used to bridge the gap (0=automatic)",
            default=1,
            min=0,
            soft_max=20
            )
    
    florianfelix's avatar
    florianfelix committed
        twist: 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
    
            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:
    
                    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, loop_triangles=False,
    
                bpy.ops.mesh.normals_make_consistent()
    
    class Circle(Operator):
    
        bl_idname = "mesh.looptools_circle"
        bl_label = "Circle"
        bl_description = "Move selected vertices into a circle shape"
        bl_options = {'REGISTER', 'UNDO'}
    
    florianfelix's avatar
    florianfelix committed
        custom_radius: BoolProperty(
    
            name="Radius",
            description="Force a custom radius",
            default=False
            )
    
    florianfelix's avatar
    florianfelix committed
        fit: 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'
            )
    
    florianfelix's avatar
    florianfelix committed
        flatten: BoolProperty(
    
            name="Flatten",
            description="Flatten the circle, instead of projecting it on the mesh",
            default=True
            )
    
    florianfelix's avatar
    florianfelix committed
        influence: FloatProperty(
    
            name="Influence",
            description="Force of the tool",
            default=100.0,
            min=0.0,
            max=100.0,
            precision=1,
            subtype='PERCENTAGE'
            )
    
    florianfelix's avatar
    florianfelix committed
        lock_x: BoolProperty(
    
            name="Lock X",
            description="Lock editing of the x-coordinate",
            default=False
            )
    
    florianfelix's avatar
    florianfelix committed
        lock_y: BoolProperty(
    
            name="Lock Y",
            description="Lock editing of the y-coordinate",
            default=False
            )
    
    florianfelix's avatar
    florianfelix committed
        lock_z: BoolProperty(name="Lock Z",
    
            description="Lock editing of the z-coordinate",
            default=False
            )
    
    florianfelix's avatar
    florianfelix committed
        radius: FloatProperty(
    
            name="Radius",
            description="Custom radius for circle",
            default=1.0,
            min=0.0,
            soft_max=1000.0
            )
    
    florianfelix's avatar
    florianfelix committed
        regular: 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_move = col.column(align=True)
            row = col_move.row(align=True)
            if self.lock_x:
    
                row.prop(self, "lock_x", text="X", icon='LOCKED')
    
                row.prop(self, "lock_x", text="X", icon='UNLOCKED')
    
            if self.lock_y:
    
                row.prop(self, "lock_y", text="Y", icon='LOCKED')
    
                row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
    
            if self.lock_z:
    
                row.prop(self, "lock_z", text="Z", icon='LOCKED')
    
                row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
    
            col_move.prop(self, "influence")
    
        def invoke(self, context, event):
            # load custom settings
            settings_load(self)
            return self.execute(context)
    
        def execute(self, context):
            # 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, False)
    
            else:
                # find loops
                derived, bm_mod, single_vertices, single_loops, loops = \
    
                    circle_get_input(object, bm)
    
                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
    
            if self.lock_x or self.lock_y or self.lock_z:
                lock = [self.lock_x, self.lock_y, self.lock_z]
            else:
                lock = False
            move_verts(object, bm, mapping, move, lock, -1)
    
            # cleaning up
            if derived:
                bm_mod.free()
    
    class Curve(Operator):
    
        bl_idname = "mesh.looptools_curve"
        bl_label = "Curve"
        bl_description = "Turn a loop into a smooth curve"
        bl_options = {'REGISTER', 'UNDO'}
    
    florianfelix's avatar
    florianfelix committed
        boundaries: BoolProperty(
    
            name="Boundaries",
            description="Limit the tool to work within the boundaries of the selected vertices",
            default=False
            )
    
    florianfelix's avatar
    florianfelix committed
        influence: FloatProperty(
    
            name="Influence",
            description="Force of the tool",
            default=100.0,
            min=0.0,
            max=100.0,
            precision=1,
            subtype='PERCENTAGE'
            )
    
    florianfelix's avatar
    florianfelix committed
        interpolation: 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'
            )
    
    florianfelix's avatar
    florianfelix committed
        lock_x: BoolProperty(
    
            name="Lock X",
            description="Lock editing of the x-coordinate",
            default=False
            )
    
    florianfelix's avatar
    florianfelix committed
        lock_y: BoolProperty(
    
            name="Lock Y",
            description="Lock editing of the y-coordinate",
            default=False
            )
    
    florianfelix's avatar
    florianfelix committed
        lock_z: BoolProperty(
    
            name="Lock Z",
            description="Lock editing of the z-coordinate",
            default=False
            )
    
    florianfelix's avatar
    florianfelix committed
        regular: BoolProperty(
    
            name="Regular",
            description="Distribute vertices at constant distances along the curve",
            default=True
            )
    
    florianfelix's avatar
    florianfelix committed
        restriction: 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_move = col.column(align=True)
            row = col_move.row(align=True)
            if self.lock_x:
    
                row.prop(self, "lock_x", text="X", icon='LOCKED')
    
                row.prop(self, "lock_x", text="X", icon='UNLOCKED')
    
            if self.lock_y:
    
                row.prop(self, "lock_y", text="Y", icon='LOCKED')
    
                row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
    
            if self.lock_z:
    
                row.prop(self, "lock_z", text="Z", icon='LOCKED')
    
                row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
    
            col_move.prop(self, "influence")
    
        def invoke(self, context, event):
            # load custom settings
            settings_load(self)
            return self.execute(context)
    
        def execute(self, context):
            # 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, False)
    
                derived, bm_mod, loops = curve_get_input(object, bm, self.boundaries)
    
                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
    
            if self.lock_x or self.lock_y or self.lock_z:
                lock = [self.lock_x, self.lock_y, self.lock_z]
            else:
                lock = False
            move_verts(object, bm, mapping, move, lock, self.influence)
    
    class Flatten(Operator):
    
        bl_idname = "mesh.looptools_flatten"
        bl_label = "Flatten"
        bl_description = "Flatten vertices on a best-fitting plane"
        bl_options = {'REGISTER', 'UNDO'}
    
    florianfelix's avatar
    florianfelix committed
        influence: FloatProperty(
    
            name="Influence",
            description="Force of the tool",
            default=100.0,
            min=0.0,
            max=100.0,
            precision=1,
            subtype='PERCENTAGE'
            )
    
    florianfelix's avatar
    florianfelix committed
        lock_x: BoolProperty(
    
            name="Lock X",
            description="Lock editing of the x-coordinate",
            default=False
            )
    
    florianfelix's avatar
    florianfelix committed
        lock_y: BoolProperty(
    
            name="Lock Y",
            description="Lock editing of the y-coordinate",
            default=False
            )
    
    florianfelix's avatar
    florianfelix committed
        lock_z: BoolProperty(name="Lock Z",
    
            description="Lock editing of the z-coordinate",
            default=False
            )
    
    florianfelix's avatar
    florianfelix committed
        plane: 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'
            )
    
    florianfelix's avatar
    florianfelix committed
        restriction: 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_move = col.column(align=True)
            row = col_move.row(align=True)
            if self.lock_x:
    
                row.prop(self, "lock_x", text="X", icon='LOCKED')
    
                row.prop(self, "lock_x", text="X", icon='UNLOCKED')
    
            if self.lock_y:
    
                row.prop(self, "lock_y", text="Y", icon='LOCKED')
    
                row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
    
            if self.lock_z:
    
                row.prop(self, "lock_z", text="Z", icon='LOCKED')
    
                row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
    
            col_move.prop(self, "influence")
    
        def invoke(self, context, event):
            # load custom settings
            settings_load(self)
            return self.execute(context)
    
        def execute(self, context):
            # 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 vertices to new locations
            if self.lock_x or self.lock_y or self.lock_z:
                lock = [self.lock_x, self.lock_y, self.lock_z]
            else:
                lock = False
            move_verts(object, bm, False, move, lock, self.influence)
    
    # Annotation operator
    class RemoveAnnotation(Operator):
        bl_idname = "remove.annotation"
    
        bl_label = "Remove Annotation"
    
        bl_description = "Remove all Annotation Strokes"
    
    meta-androcto's avatar
    meta-androcto committed
        bl_options = {'REGISTER', 'UNDO'}
    
    meta-androcto's avatar
    meta-androcto committed
        def execute(self, context):
    
            try:
                bpy.data.grease_pencils[0].layers.active.clear()
            except:
    
                self.report({'INFO'}, "No Annotation data to Unlink")
    
                return {'CANCELLED'}
    
    meta-androcto's avatar
    meta-androcto committed
            return{'FINISHED'}
    
    # GPencil operator
    class RemoveGPencil(Operator):
        bl_idname = "remove.gp"
        bl_label = "Remove GPencil"
        bl_description = "Remove all GPencil Strokes"
        bl_options = {'REGISTER', 'UNDO'}
    
        def execute(self, context):
    
            try:
                looptools =  context.window_manager.looptools
                looptools.gstretch_guide.data.layers.data.clear()
                looptools.gstretch_guide.data.update_tag()
            except:
                self.report({'INFO'}, "No GPencil data to Unlink")
                return {'CANCELLED'}
    
            return{'FINISHED'}
    
    
    class GStretch(Operator):
    
    Bart Crouch's avatar
    Bart Crouch committed
        bl_idname = "mesh.looptools_gstretch"
        bl_label = "Gstretch"
    
        bl_description = "Stretch selected vertices to active stroke"
    
    Bart Crouch's avatar
    Bart Crouch committed
        bl_options = {'REGISTER', 'UNDO'}
    
    florianfelix's avatar
    florianfelix committed
        conversion: EnumProperty(
    
            name="Conversion",
            items=(("distance", "Distance", "Set the distance between vertices "
    
                   ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
    
                    "number of vertices that converted strokes will have"),
    
                   ("vertices", "Exact vertices", "Set the exact number of vertices "
    
                    "that converted strokes will have. Short strokes "
    
                    "with few points may contain less vertices than this number."),
    
                   ("none", "No simplification", "Convert each point "
    
                    "to a vertex")),
    
            description="If strokes are converted to geometry, "
    
                        "use this simplification method",
            default='limit_vertices'
            )
    
    florianfelix's avatar
    florianfelix committed
        conversion_distance: FloatProperty(
    
            name="Distance",
            description="Absolute distance between vertices along the converted "
    
            default=0.1,
            min=0.000001,
            soft_min=0.01,
            soft_max=100
            )
    
    florianfelix's avatar
    florianfelix committed
        conversion_max: IntProperty(
    
            name="Max Vertices",
    
            description="Maximum number of vertices strokes will "
    
                        "have, when they are converted to geomtery",
            default=32,
            min=3,
            soft_max=500,
            update=gstretch_update_min
            )
    
    florianfelix's avatar
    florianfelix committed
        conversion_min: IntProperty(
    
            name="Min Vertices",
    
            description="Minimum number of vertices strokes will "
    
                        "have, when they are converted to geomtery",
            default=8,
            min=3,
            soft_max=500,
            update=gstretch_update_max
            )
    
    florianfelix's avatar
    florianfelix committed
        conversion_vertices: IntProperty(
    
            name="Vertices",
    
            description="Number of vertices 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
            )
    
    florianfelix's avatar
    florianfelix committed
        delete_strokes: BoolProperty(
    
            name="Delete strokes",
    
            description="Remove strokes if they have been used."
                        "WARNING: DOES NOT SUPPORT UNDO",
    
    florianfelix's avatar
    florianfelix committed
        influence: FloatProperty(
    
            name="Influence",
            description="Force of the tool",
            default=100.0,
            min=0.0,
            max=100.0,
            precision=1,
            subtype='PERCENTAGE'
            )
    
    florianfelix's avatar
    florianfelix committed
        lock_x: BoolProperty(
    
            name="Lock X",
            description="Lock editing of the x-coordinate",
            default=False
            )
    
    florianfelix's avatar
    florianfelix committed
        lock_y: BoolProperty(
    
            name="Lock Y",
            description="Lock editing of the y-coordinate",
            default=False
            )
    
    florianfelix's avatar
    florianfelix committed
        lock_z: BoolProperty(
    
            name="Lock Z",
            description="Lock editing of the z-coordinate",
            default=False
            )
    
    florianfelix's avatar
    florianfelix committed
        method: 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 "
    
            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):
    
            looptools =  context.window_manager.looptools