Mesh Tools: Add files from mesh edit tools
authormeta-androcto <meta.androcto1@gmail.com>
Wed, 5 Jun 2019 23:54:02 +0000 (09:54 +1000)
committermeta-androcto <meta.androcto1@gmail.com>
Wed, 5 Jun 2019 23:54:02 +0000 (09:54 +1000)
mesh_tools/face_inset_fillet.py [new file with mode: 0644]
mesh_tools/mesh_cut_faces.py [new file with mode: 0644]
mesh_tools/mesh_mextrude_plus.py [new file with mode: 0644]
mesh_tools/vertex_align.py [new file with mode: 0644]

diff --git a/mesh_tools/face_inset_fillet.py b/mesh_tools/face_inset_fillet.py
new file mode 100644 (file)
index 0000000..8af709c
--- /dev/null
@@ -0,0 +1,335 @@
+# -*- coding: utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# based completely on addon by zmj100
+# added some distance limits to prevent overlap - max12345
+
+
+import bpy
+import bmesh
+from bpy.types import Operator
+from bpy.props import (
+        FloatProperty,
+        IntProperty,
+        BoolProperty,
+        EnumProperty,
+        )
+from math import (
+        sin, cos, tan,
+        degrees, radians,
+        )
+from mathutils import Matrix
+
+
+def edit_mode_out():
+    bpy.ops.object.mode_set(mode='OBJECT')
+
+
+def edit_mode_in():
+    bpy.ops.object.mode_set(mode='EDIT')
+
+
+def angle_rotation(rp, q, axis, angle):
+    # returns the vector made by the rotation of the vector q
+    # rp by angle around axis and then adds rp
+
+    return (Matrix.Rotation(angle, 3, axis) * (q - rp)) + rp
+
+
+def face_inset_fillet(bme, face_index_list, inset_amount, distance,
+                      number_of_sides, out, radius, type_enum, kp):
+    list_del = []
+
+    for faceindex in face_index_list:
+
+        bme.faces.ensure_lookup_table()
+        # loops through the faces...
+        f = bme.faces[faceindex]
+        f.select_set(False)
+        list_del.append(f)
+        f.normal_update()
+        vertex_index_list = [v.index for v in f.verts]
+        dict_0 = {}
+        orientation_vertex_list = []
+        n = len(vertex_index_list)
+        for i in range(n):
+            # loops through the vertices
+            dict_0[i] = []
+            bme.verts.ensure_lookup_table()
+            p = (bme.verts[vertex_index_list[i]].co).copy()
+            p1 = (bme.verts[vertex_index_list[(i - 1) % n]].co).copy()
+            p2 = (bme.verts[vertex_index_list[(i + 1) % n]].co).copy()
+            # copies some vert coordinates, always the 3 around i
+            dict_0[i].append(bme.verts[vertex_index_list[i]])
+            # appends the bmesh vert of the appropriate index to the dict
+            vec1 = p - p1
+            vec2 = p - p2
+            # vectors for the other corner points to the cornerpoint
+            # corresponding to i / p
+            angle = vec1.angle(vec2)
+
+            adj = inset_amount / tan(angle * 0.5)
+            h = (adj ** 2 + inset_amount ** 2) ** 0.5
+            if round(degrees(angle)) == 180 or round(degrees(angle)) == 0.0:
+                # if the corner is a straight line...
+                # I think this creates some new points...
+                if out is True:
+                    val = ((f.normal).normalized() * inset_amount)
+                else:
+                    val = -((f.normal).normalized() * inset_amount)
+                p6 = angle_rotation(p, p + val, vec1, radians(90))
+            else:
+                # if the corner is an actual corner
+                val = ((f.normal).normalized() * h)
+                if out is True:
+                    # this -(p - (vec2.normalized() * adj))) is just the freaking axis afaik...
+                    p6 = angle_rotation(
+                                p, p + val,
+                                -(p - (vec2.normalized() * adj)),
+                                -radians(90)
+                                )
+                else:
+                    p6 = angle_rotation(
+                                p, p - val,
+                                ((p - (vec1.normalized() * adj)) - (p - (vec2.normalized() * adj))),
+                                -radians(90)
+                                )
+
+                orientation_vertex_list.append(p6)
+
+        new_inner_face = []
+        orientation_vertex_list_length = len(orientation_vertex_list)
+        ovll = orientation_vertex_list_length
+
+        for j in range(ovll):
+            q = orientation_vertex_list[j]
+            q1 = orientation_vertex_list[(j - 1) % ovll]
+            q2 = orientation_vertex_list[(j + 1) % ovll]
+            # again, these are just vectors between somewhat displaced corner vertices
+            vec1_ = q - q1
+            vec2_ = q - q2
+            ang_ = vec1_.angle(vec2_)
+
+            # the angle between them
+            if round(degrees(ang_)) == 180 or round(degrees(ang_)) == 0.0:
+                # again... if it's really a line...
+                v = bme.verts.new(q)
+                new_inner_face.append(v)
+                dict_0[j].append(v)
+            else:
+                # s.a.
+                if radius is False:
+                    h_ = distance * (1 / cos(ang_ * 0.5))
+                    d = distance
+                elif radius is True:
+                    h_ = distance / sin(ang_ * 0.5)
+                    d = distance / tan(ang_ * 0.5)
+                # max(d) is vec1_.magnitude * 0.5
+                # or vec2_.magnitude * 0.5 respectively
+
+                # only functional difference v
+                if d > vec1_.magnitude * 0.5:
+                    d = vec1_.magnitude * 0.5
+
+                if d > vec2_.magnitude * 0.5:
+                    d = vec2_.magnitude * 0.5
+                # only functional difference ^
+
+                q3 = q - (vec1_.normalized() * d)
+                q4 = q - (vec2_.normalized() * d)
+                # these are new verts somewhat offset from the corners
+                rp_ = q - ((q - ((q3 + q4) * 0.5)).normalized() * h_)
+                # reference point inside the curvature
+                axis_ = vec1_.cross(vec2_)
+                # this should really be just the face normal
+                vec3_ = rp_ - q3
+                vec4_ = rp_ - q4
+                rot_ang = vec3_.angle(vec4_)
+                cornerverts = []
+
+                for o in range(number_of_sides + 1):
+                    # this calculates the actual new vertices
+                    q5 = angle_rotation(rp_, q4, axis_, rot_ang * o / number_of_sides)
+                    v = bme.verts.new(q5)
+
+                    # creates new bmesh vertices from it
+                    bme.verts.index_update()
+
+                    dict_0[j].append(v)
+                    cornerverts.append(v)
+
+                cornerverts.reverse()
+                new_inner_face.extend(cornerverts)
+
+        if out is False:
+            f = bme.faces.new(new_inner_face)
+            f.select_set(True)
+        elif out is True and kp is True:
+            f = bme.faces.new(new_inner_face)
+            f.select_set(True)
+
+        n2_ = len(dict_0)
+        # these are the new side faces, those that don't depend on cornertype
+        for o in range(n2_):
+            list_a = dict_0[o]
+            list_b = dict_0[(o + 1) % n2_]
+            bme.faces.new([list_a[0], list_b[0], list_b[-1], list_a[1]])
+            bme.faces.index_update()
+        # cornertype 1 - ngon faces
+        if type_enum == 'opt0':
+            for k in dict_0:
+                if len(dict_0[k]) > 2:
+                    bme.faces.new(dict_0[k])
+                    bme.faces.index_update()
+        # cornertype 2 - triangulated faces
+        if type_enum == 'opt1':
+            for k_ in dict_0:
+                q_ = dict_0[k_][0]
+                dict_0[k_].pop(0)
+                n3_ = len(dict_0[k_])
+                for kk in range(n3_ - 1):
+                    bme.faces.new([dict_0[k_][kk], dict_0[k_][(kk + 1) % n3_], q_])
+                    bme.faces.index_update()
+
+    del_ = [bme.faces.remove(f) for f in list_del]
+
+    if del_:
+        del del_
+
+
+# Operator
+
+class MESH_OT_face_inset_fillet(Operator):
+    bl_idname = "mesh.face_inset_fillet"
+    bl_label = "Face Inset Fillet"
+    bl_description = ("Inset selected and Fillet (make round) the corners \n"
+                     "of the newly created Faces")
+    bl_options = {"REGISTER", "UNDO"}
+
+    # inset amount
+    inset_amount: FloatProperty(
+            name="Inset amount",
+            description="Define the size of the Inset relative to the selection",
+            default=0.04,
+            min=0, max=100.0,
+            step=1,
+            precision=3
+            )
+    # number of sides
+    number_of_sides: IntProperty(
+            name="Number of sides",
+            description="Define the roundness of the corners by specifying\n"
+                        "the subdivision count",
+            default=4,
+            min=1, max=100,
+            step=1
+            )
+    distance: FloatProperty(
+            name="",
+            description="Use distance or radius for corners' size calculation",
+            default=0.04,
+            min=0.00001, max=100.0,
+            step=1,
+            precision=3
+            )
+    out: BoolProperty(
+            name="Outside",
+            description="Inset the Faces outwards in relation to the selection\n"
+                        "Note: depending on the geometry, can give unsatisfactory results",
+            default=False
+            )
+    radius: BoolProperty(
+            name="Radius",
+            description="Use radius for corners' size calculation",
+            default=False
+            )
+    type_enum: EnumProperty(
+            items=(('opt0', "N-gon", "N-gon corners - Keep the corner Faces uncut"),
+                   ('opt1', "Triangle", "Triangulate corners")),
+            name="Corner Type",
+            default="opt0"
+            )
+    kp: BoolProperty(
+            name="Keep faces",
+            description="Do not delete the inside Faces\n"
+                        "Only available if the Out option is checked",
+            default=False
+            )
+
+    def draw(self, context):
+        layout = self.layout
+
+        layout.label(text="Corner Type:")
+
+        row = layout.row()
+        row.prop(self, "type_enum", text="")
+
+        row = layout.row(align=True)
+        row.prop(self, "out")
+
+        if self.out is True:
+            row.prop(self, "kp")
+
+        row = layout.row()
+        row.prop(self, "inset_amount")
+
+        row = layout.row()
+        row.prop(self, "number_of_sides")
+
+        row = layout.row()
+        row.prop(self, "radius")
+
+        row = layout.row()
+        dist_rad = "Radius" if self.radius else "Distance"
+        row.prop(self, "distance", text=dist_rad)
+
+    def execute(self, context):
+        # this really just prepares everything for the main function
+        inset_amount = self.inset_amount
+        number_of_sides = self.number_of_sides
+        distance = self.distance
+        out = self.out
+        radius = self.radius
+        type_enum = self.type_enum
+        kp = self.kp
+
+        edit_mode_out()
+        ob_act = context.active_object
+        bme = bmesh.new()
+        bme.from_mesh(ob_act.data)
+        # this
+        face_index_list = [f.index for f in bme.faces if f.select and f.is_valid]
+
+        if len(face_index_list) == 0:
+            self.report({'WARNING'},
+                        "No suitable Face selection found. Operation cancelled")
+            edit_mode_in()
+
+            return {'CANCELLED'}
+
+        elif len(face_index_list) != 0:
+            face_inset_fillet(bme, face_index_list,
+                              inset_amount, distance, number_of_sides,
+                              out, radius, type_enum, kp)
+
+        bme.to_mesh(ob_act.data)
+        edit_mode_in()
+
+        return {'FINISHED'}
diff --git a/mesh_tools/mesh_cut_faces.py b/mesh_tools/mesh_cut_faces.py
new file mode 100644 (file)
index 0000000..1522b15
--- /dev/null
@@ -0,0 +1,266 @@
+# gpl author: Stanislav Blinov
+
+bl_info = {
+    "name": "Cut Faces",
+    "author": "Stanislav Blinov",
+    "version": (1, 0, 0),
+    "blender": (2, 72, 0),
+    "description": "Cut Faces and Deselect Boundary operators",
+    "category": "Mesh", }
+
+import bpy
+import bmesh
+from bpy.types import Operator
+from bpy.props import (
+        BoolProperty,
+        IntProperty,
+        EnumProperty,
+        )
+
+
+def bmesh_from_object(object):
+    mesh = object.data
+    if object.mode == 'EDIT':
+        bm = bmesh.from_edit_mesh(mesh)
+    else:
+        bm = bmesh.new()
+        bm.from_mesh(mesh)
+    return bm
+
+
+def bmesh_release(bm, object):
+    mesh = object.data
+    bm.select_flush_mode()
+    if object.mode == 'EDIT':
+        bmesh.update_edit_mesh(mesh, True)
+    else:
+        bm.to_mesh(mesh)
+        bm.free()
+
+
+def calc_face(face, keep_caps=True):
+
+    assert face.tag
+
+    def radial_loops(loop):
+        next = loop.link_loop_radial_next
+        while next != loop:
+            result, next = next, next.link_loop_radial_next
+            yield result
+
+    result = []
+
+    face.tag = False
+    selected = []
+    to_select = []
+    for loop in face.loops:
+        self_selected = False
+        # Iterate over selected adjacent faces
+        for radial_loop in filter(lambda l: l.face.select, radial_loops(loop)):
+            # Tag the edge if no other face done so already
+            if not loop.edge.tag:
+                loop.edge.tag = True
+                self_selected = True
+
+            adjacent_face = radial_loop.face
+            # Only walk adjacent face if current face tagged the edge
+            if adjacent_face.tag and self_selected:
+                result += calc_face(adjacent_face, keep_caps)
+
+        if loop.edge.tag:
+            (selected, to_select)[self_selected].append(loop)
+
+    for loop in to_select:
+        result.append(loop.edge)
+        selected.append(loop)
+
+    # Select opposite edge in quads
+    if keep_caps and len(selected) == 1 and len(face.verts) == 4:
+        result.append(selected[0].link_loop_next.link_loop_next.edge)
+
+    return result
+
+
+def get_edge_rings(bm, keep_caps=True):
+
+    def tag_face(face):
+        if face.select:
+            face.tag = True
+            for edge in face.edges:
+                edge.tag = False
+        return face.select
+
+    # fetch selected faces while setting up tags
+    selected_faces = [f for f in bm.faces if tag_face(f)]
+
+    edges = []
+
+    try:
+        # generate a list of edges to select:
+        # traversing only tagged faces, since calc_face can walk and untag islands
+        for face in filter(lambda f: f.tag, selected_faces):
+            edges += calc_face(face, keep_caps)
+    finally:
+        # housekeeping: clear tags
+        for face in selected_faces:
+            face.tag = False
+            for edge in face.edges:
+                edge.tag = False
+
+    return edges
+
+
+class MESH_xOT_deselect_boundary(Operator):
+    bl_idname = "mesh.ext_deselect_boundary"
+    bl_label = "Deselect Boundary"
+    bl_description = ("Deselect boundary edges of selected faces\n"
+                      "Note: if all Faces are selected there is no boundary,\n"
+                      "so the tool will not have results")
+    bl_options = {'REGISTER', 'UNDO'}
+
+    keep_cap_edges: BoolProperty(
+                        name="Keep Cap Edges",
+                        description="Keep quad strip cap edges selected",
+                        default=False
+                        )
+
+    @classmethod
+    def poll(cls, context):
+        active_object = context.active_object
+        return active_object and active_object.type == 'MESH' and active_object.mode == 'EDIT'
+
+    def execute(self, context):
+        object = context.active_object
+        bm = bmesh_from_object(object)
+
+        try:
+            edges = get_edge_rings(bm, keep_caps=self.keep_cap_edges)
+            if not edges:
+                self.report({'WARNING'}, "No suitable Face selection found. Operation cancelled")
+                return {'CANCELLED'}
+
+            bpy.ops.mesh.select_all(action='DESELECT')
+            bm.select_mode = {'EDGE'}
+
+            for edge in edges:
+                edge.select = True
+            context.tool_settings.mesh_select_mode[:] = False, True, False
+
+        finally:
+            bmesh_release(bm, object)
+
+        return {'FINISHED'}
+
+
+class MESH_xOT_cut_faces(Operator):
+    bl_idname = "mesh.ext_cut_faces"
+    bl_label = "Cut Faces"
+    bl_description = "Cut selected faces, connected through their adjacent edges"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    # from bmesh_operators.h
+    SUBD_INNERVERT = 0
+    SUBD_PATH = 1
+    SUBD_FAN = 2
+    SUBD_STRAIGHT_CUT = 3
+
+    num_cuts: IntProperty(
+            name="Number of Cuts",
+            default=1,
+            min=1,
+            max=100,
+            subtype='UNSIGNED'
+            )
+    use_single_edge: BoolProperty(
+            name="Quad/Tri Mode",
+            description="Cut boundary faces",
+            default=False
+            )
+    corner_type: EnumProperty(
+            items=[('SUBD_INNERVERT', "Inner Vert", ""),
+                   ('SUBD_PATH', "Path", ""),
+                   ('SUBD_FAN', "Fan", ""),
+                   ('SUBD_STRAIGHT_CUT', "Straight Cut", ""),
+                   ],
+            name="Quad Corner Type",
+            description="How to subdivide quad corners",
+            default='SUBD_STRAIGHT_CUT'
+            )
+    use_grid_fill: BoolProperty(
+            name="Use Grid Fill",
+            description="Fill fully enclosed faces with a grid",
+            default=True
+            )
+
+    @classmethod
+    def poll(cls, context):
+        active_object = context.active_object
+        return active_object and active_object.type == 'MESH' and active_object.mode == 'EDIT'
+
+    def draw(self, context):
+        layout = self.layout
+
+        layout.label(text="Number of Cuts:")
+        layout.prop(self, "num_cuts", text="")
+
+        layout.prop(self, "use_single_edge")
+        layout.prop(self, "use_grid_fill")
+
+        layout.label(text="Quad Corner Type:")
+        layout.prop(self, "corner_type", text="")
+
+    def cut_edges(self, context):
+        object = context.active_object
+        bm = bmesh_from_object(object)
+
+        try:
+            edges = get_edge_rings(bm, keep_caps=True)
+            if not edges:
+                self.report({'WARNING'},
+                            "No suitable Face selection found. Operation cancelled")
+                return False
+
+            result = bmesh.ops.subdivide_edges(
+                            bm,
+                            edges=edges,
+                            cuts=int(self.num_cuts),
+                            use_grid_fill=bool(self.use_grid_fill),
+                            use_single_edge=bool(self.use_single_edge),
+                            quad_corner_type=eval("self." + self.corner_type)
+                            )
+            bpy.ops.mesh.select_all(action='DESELECT')
+            bm.select_mode = {'EDGE'}
+
+            inner = result['geom_inner']
+            for edge in filter(lambda e: isinstance(e, bmesh.types.BMEdge), inner):
+                edge.select = True
+
+        finally:
+            bmesh_release(bm, object)
+
+        return True
+
+    def execute(self, context):
+
+        if not self.cut_edges(context):
+            return {'CANCELLED'}
+
+        context.tool_settings.mesh_select_mode[:] = False, True, False
+        # Try to select all possible loops
+        bpy.ops.mesh.loop_multi_select(ring=False)
+
+        return {'FINISHED'}
+
+
+def register():
+    bpy.utils.register_class(MESH_xOT_deselect_boundary)
+    bpy.utils.register_class(MESH_xOT_cut_faces)
+
+
+def unregister():
+    bpy.utils.unregister_class(MESH_xOT_deselect_boundary)
+    bpy.utils.unregister_class(MESH_xOT_cut_faces)
+
+
+if __name__ == "__main__":
+    register()
diff --git a/mesh_tools/mesh_mextrude_plus.py b/mesh_tools/mesh_mextrude_plus.py
new file mode 100644 (file)
index 0000000..5fa2aa2
--- /dev/null
@@ -0,0 +1,370 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# Repeats extrusion + rotation + scale for one or more faces
+# Original code by liero
+# Update by Jimmy Hazevoet 03/2017 for Blender 2.79
+# normal rotation, probability, scaled offset, object coords, initial and per step noise
+
+
+bl_info = {
+    "name": "MExtrude Plus1",
+    "author": "liero, Jimmy Hazevoet",
+    "version": (1, 3, 0),
+    "blender": (2, 77, 0),
+    "location": "View3D > Tool Shelf",
+    "description": "Repeat extrusions from faces to create organic shapes",
+    "warning": "",
+    "wiki_url": "",
+    "category": "Mesh"}
+
+
+import bpy
+import bmesh
+import random
+from bpy.types import Operator
+from random import gauss
+from math import radians
+from mathutils import (
+        Euler, Vector,
+        )
+from bpy.props import (
+        FloatProperty,
+        IntProperty,
+        BoolProperty,
+        )
+
+
+def gloc(self, r):
+    return Vector((self.offx, self.offy, self.offz))
+
+
+def vloc(self, r):
+    random.seed(self.ran + r)
+    return self.off * (1 + gauss(0, self.var1 / 3))
+
+
+def nrot(self, n):
+    return Euler((radians(self.nrotx) * n[0],
+                  radians(self.nroty) * n[1],
+                  radians(self.nrotz) * n[2]), 'XYZ')
+
+
+def vrot(self, r):
+    random.seed(self.ran + r)
+    return Euler((radians(self.rotx) + gauss(0, self.var2 / 3),
+                  radians(self.roty) + gauss(0, self.var2 / 3),
+                  radians(self.rotz) + gauss(0, self.var2 / 3)), 'XYZ')
+
+
+def vsca(self, r):
+    random.seed(self.ran + r)
+    return self.sca * (1 + gauss(0, self.var3 / 3))
+
+
+class MExtrude(Operator):
+    bl_idname = "object.mextrude"
+    bl_label = "Multi Extrude"
+    bl_description = ("Extrude selected Faces with Rotation,\n"
+                      "Scaling, Variation, Randomization")
+    bl_options = {"REGISTER", "UNDO", "PRESET"}
+
+    off: FloatProperty(
+            name="Offset",
+            soft_min=0.001, soft_max=10,
+            min=-100, max=100,
+            default=1.0,
+            description="Translation"
+            )
+    offx: FloatProperty(
+            name="Loc X",
+            soft_min=-10.0, soft_max=10.0,
+            min=-100.0, max=100.0,
+            default=0.0,
+            description="Global Translation X"
+            )
+    offy: FloatProperty(
+            name="Loc Y",
+            soft_min=-10.0, soft_max=10.0,
+            min=-100.0, max=100.0,
+            default=0.0,
+            description="Global Translation Y"
+            )
+    offz: FloatProperty(
+            name="Loc Z",
+            soft_min=-10.0, soft_max=10.0,
+            min=-100.0, max=100.0,
+            default=0.0,
+            description="Global Translation Z"
+            )
+    rotx: FloatProperty(
+            name="Rot X",
+            min=-85, max=85,
+            soft_min=-30, soft_max=30,
+            default=0,
+            description="X Rotation"
+            )
+    roty: FloatProperty(
+            name="Rot Y",
+            min=-85, max=85,
+            soft_min=-30,
+            soft_max=30,
+            default=0,
+            description="Y Rotation"
+            )
+    rotz: FloatProperty(
+            name="Rot Z",
+            min=-85, max=85,
+            soft_min=-30, soft_max=30,
+            default=-0,
+            description="Z Rotation"
+            )
+    nrotx: FloatProperty(
+            name="N Rot X",
+            min=-85, max=85,
+            soft_min=-30, soft_max=30,
+            default=0,
+            description="Normal X Rotation"
+            )
+    nroty: FloatProperty(
+            name="N Rot Y",
+            min=-85, max=85,
+            soft_min=-30, soft_max=30,
+            default=0,
+            description="Normal Y Rotation"
+            )
+    nrotz: FloatProperty(
+            name="N Rot Z",
+            min=-85, max=85,
+            soft_min=-30, soft_max=30,
+            default=-0,
+            description="Normal Z Rotation"
+            )
+    sca: FloatProperty(
+            name="Scale",
+            min=0.01, max=10,
+            soft_min=0.5, soft_max=1.5,
+            default=1.0,
+            description="Scaling of the selected faces after extrusion"
+            )
+    var1: FloatProperty(
+            name="Offset Var", min=-10, max=10,
+            soft_min=-1, soft_max=1,
+            default=0,
+            description="Offset variation"
+            )
+    var2: FloatProperty(
+            name="Rotation Var",
+            min=-10, max=10,
+            soft_min=-1, soft_max=1,
+            default=0,
+            description="Rotation variation"
+            )
+    var3: FloatProperty(
+            name="Scale Noise",
+            min=-10, max=10,
+            soft_min=-1, soft_max=1,
+            default=0,
+            description="Scaling noise"
+            )
+    var4: IntProperty(
+            name="Probability",
+            min=0, max=100,
+            default=100,
+            description="Probability, chance of extruding a face"
+            )
+    num: IntProperty(
+            name="Repeat",
+            min=1, max=500,
+            soft_max=100,
+            default=5,
+            description="Repetitions"
+            )
+    ran: IntProperty(
+            name="Seed",
+            min=-9999, max=9999,
+            default=0,
+            description="Seed to feed random values"
+            )
+    opt1: BoolProperty(
+            name="Polygon coordinates",
+            default=True,
+            description="Polygon coordinates, Object coordinates"
+            )
+    opt2: BoolProperty(
+            name="Proportional offset",
+            default=False,
+            description="Scale * Offset"
+            )
+    opt3: BoolProperty(
+            name="Per step rotation noise",
+            default=False,
+            description="Per step rotation noise, Initial rotation noise"
+            )
+    opt4: BoolProperty(
+            name="Per step scale noise",
+            default=False,
+            description="Per step scale noise, Initial scale noise"
+            )
+
+    @classmethod
+    def poll(cls, context):
+        obj = context.object
+        return (obj and obj.type == 'MESH')
+
+    def draw(self, context):
+        layout = self.layout
+        col = layout.column(align=True)
+        col.label(text="Transformations:")
+        col.prop(self, "off", slider=True)
+        col.prop(self, "offx", slider=True)
+        col.prop(self, "offy", slider=True)
+        col.prop(self, "offz", slider=True)
+
+        col = layout.column(align=True)
+        col.prop(self, "rotx", slider=True)
+        col.prop(self, "roty", slider=True)
+        col.prop(self, "rotz", slider=True)
+        col.prop(self, "nrotx", slider=True)
+        col.prop(self, "nroty", slider=True)
+        col.prop(self, "nrotz", slider=True)
+        col = layout.column(align=True)
+        col.prop(self, "sca", slider=True)
+
+        col = layout.column(align=True)
+        col.label(text="Variation settings:")
+        col.prop(self, "var1", slider=True)
+        col.prop(self, "var2", slider=True)
+        col.prop(self, "var3", slider=True)
+        col.prop(self, "var4", slider=True)
+        col.prop(self, "ran")
+        col = layout.column(align=False)
+        col.prop(self, 'num')
+
+        col = layout.column(align=True)
+        col.label(text="Options:")
+        col.prop(self, "opt1")
+        col.prop(self, "opt2")
+        col.prop(self, "opt3")
+        col.prop(self, "opt4")
+
+    def execute(self, context):
+        obj = bpy.context.object
+        om = obj.mode
+        bpy.context.tool_settings.mesh_select_mode = [False, False, True]
+        origin = Vector([0.0, 0.0, 0.0])
+
+        # bmesh operations
+        bpy.ops.object.mode_set()
+        bm = bmesh.new()
+        bm.from_mesh(obj.data)
+        sel = [f for f in bm.faces if f.select]
+
+        after = []
+
+        # faces loop
+        for i, of in enumerate(sel):
+            nro = nrot(self, of.normal)
+            off = vloc(self, i)
+            loc = gloc(self, i)
+            of.normal_update()
+
+            # initial rotation noise
+            if self.opt3 is False:
+                rot = vrot(self, i)
+            # initial scale noise
+            if self.opt4 is False:
+                s = vsca(self, i)
+
+            # extrusion loop
+            for r in range(self.num):
+                # random probability % for extrusions
+                if self.var4 > int(random.random() * 100):
+                    nf = of.copy()
+                    nf.normal_update()
+                    no = nf.normal.copy()
+
+                    # face/obj co√∂rdinates
+                    if self.opt1 is True:
+                        ce = nf.calc_center_bounds()
+                    else:
+                        ce = origin
+
+                    # per step rotation noise
+                    if self.opt3 is True:
+                        rot = vrot(self, i + r)
+                    # per step scale noise
+                    if self.opt4 is True:
+                        s = vsca(self, i + r)
+
+                    # proportional, scale * offset
+                    if self.opt2 is True:
+                        off = s * off
+
+                    for v in nf.verts:
+                        v.co -= ce
+                        v.co.rotate(nro)
+                        v.co.rotate(rot)
+                        v.co += ce + loc + no * off
+                        v.co = v.co.lerp(ce, 1 - s)
+
+                    # extrude code from TrumanBlending
+                    for a, b in zip(of.loops, nf.loops):
+                        sf = bm.faces.new((a.vert, a.link_loop_next.vert,
+                                           b.link_loop_next.vert, b.vert))
+                        sf.normal_update()
+                    bm.faces.remove(of)
+                    of = nf
+
+            after.append(of)
+
+        for v in bm.verts:
+            v.select = False
+        for e in bm.edges:
+            e.select = False
+
+        for f in after:
+            if f not in sel:
+                f.select = True
+            else:
+                f.select = False
+
+        bm.to_mesh(obj.data)
+        obj.data.update()
+
+        # restore user settings
+        bpy.ops.object.mode_set(mode=om)
+
+        if not len(sel):
+            self.report({"WARNING"},
+                        "No suitable Face selection found. Operation cancelled")
+            return {'CANCELLED'}
+
+        return {'FINISHED'}
+
+
+def register():
+    bpy.utils.register_module(__name__)
+
+
+def unregister():
+    bpy.utils.unregister_module(__name__)
+
+
+if __name__ == '__main__':
+    register()
diff --git a/mesh_tools/vertex_align.py b/mesh_tools/vertex_align.py
new file mode 100644 (file)
index 0000000..eb66d74
--- /dev/null
@@ -0,0 +1,301 @@
+# -*- coding: utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# Note: Property group was moved to __init__
+
+bl_info = {
+    "name": "Vertex Align",
+    "author": "",
+    "version": (0, 1, 7),
+    "blender": (2, 61, 0),
+    "location": "View3D > Tool Shelf",
+    "description": "",
+    "warning": "",
+    "wiki_url": "",
+    "category": "Mesh"}
+
+
+import bpy
+from bpy.props import (
+        BoolVectorProperty,
+        FloatVectorProperty,
+        )
+from mathutils import Vector
+from bpy.types import Operator
+
+
+# Edit Mode Toggle
+def edit_mode_out():
+    bpy.ops.object.mode_set(mode='OBJECT')
+
+
+def edit_mode_in():
+    bpy.ops.object.mode_set(mode='EDIT')
+
+
+def get_mesh_data_():
+    edit_mode_out()
+    ob_act = bpy.context.active_object
+    me = ob_act.data
+    edit_mode_in()
+    return me
+
+
+def list_clear_(l):
+    l[:] = []
+    return l
+
+
+class va_buf():
+    list_v = []
+    list_0 = []
+
+
+# Store The Vertex coordinates
+class Vertex_align_store(Operator):
+    bl_idname = "vertex_align.store_id"
+    bl_label = "Active Vertex"
+    bl_description = ("Store Selected Vertex coordinates as an align point\n"
+                      "Single Selected Vertex only")
+
+    @classmethod
+    def poll(cls, context):
+        obj = context.active_object
+        return (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH')
+
+    def execute(self, context):
+        try:
+            me = get_mesh_data_()
+            list_0 = [v.index for v in me.vertices if v.select]
+
+            if len(list_0) == 1:
+                list_clear_(va_buf.list_v)
+                for v in me.vertices:
+                    if v.select:
+                        va_buf.list_v.append(v.index)
+                        bpy.ops.mesh.select_all(action='DESELECT')
+            else:
+                self.report({'WARNING'}, "Please select just One Vertex")
+                return {'CANCELLED'}
+        except:
+            self.report({'WARNING'}, "Storing selection could not be completed")
+            return {'CANCELLED'}
+
+        self.report({'INFO'}, "Selected Vertex coordinates are stored")
+
+        return {'FINISHED'}
+
+
+# Align to original
+class Vertex_align_original(Operator):
+    bl_idname = "vertex_align.align_original"
+    bl_label = "Align to original"
+    bl_description = "Align selection to stored single vertex coordinates"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    @classmethod
+    def poll(cls, context):
+        obj = context.active_object
+        return (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH')
+
+    def draw(self, context):
+        layout = self.layout
+        layout.label(text="Axis:")
+
+        row = layout.row(align=True)
+        row.prop(context.scene.mesh_extra_tools, "vert_align_axis",
+                 text="X", index=0, toggle=True)
+        row.prop(context.scene.mesh_extra_tools, "vert_align_axis",
+                 text="Y", index=1, toggle=True)
+        row.prop(context.scene.mesh_extra_tools, "vert_align_axis",
+                 text="Z", index=2, toggle=True)
+
+    def execute(self, context):
+        edit_mode_out()
+        ob_act = context.active_object
+        me = ob_act.data
+        cen1 = context.scene.mesh_extra_tools.vert_align_axis
+        list_0 = [v.index for v in me.vertices if v.select]
+
+        if len(va_buf.list_v) == 0:
+            self.report({'INFO'},
+                        "Original vertex not stored in memory. Operation Cancelled")
+            edit_mode_in()
+            return {'CANCELLED'}
+
+        elif len(va_buf.list_v) != 0:
+            if len(list_0) == 0:
+                self.report({'INFO'}, "No vertices selected. Operation Cancelled")
+                edit_mode_in()
+                return {'CANCELLED'}
+
+            elif len(list_0) != 0:
+                vo = (me.vertices[va_buf.list_v[0]].co).copy()
+                if cen1[0] is True:
+                    for i in list_0:
+                        v = (me.vertices[i].co).copy()
+                        me.vertices[i].co = Vector((vo[0], v[1], v[2]))
+                if cen1[1] is True:
+                    for i in list_0:
+                        v = (me.vertices[i].co).copy()
+                        me.vertices[i].co = Vector((v[0], vo[1], v[2]))
+                if cen1[2] is True:
+                    for i in list_0:
+                        v = (me.vertices[i].co).copy()
+                        me.vertices[i].co = Vector((v[0], v[1], vo[2]))
+        edit_mode_in()
+
+        return {'FINISHED'}
+
+
+# Align to custom coordinates
+class Vertex_align_coord_list(Operator):
+    bl_idname = "vertex_align.coord_list_id"
+    bl_label = ""
+    bl_description = "Align to custom coordinates"
+
+    @classmethod
+    def poll(cls, context):
+        obj = context.active_object
+        return (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH')
+
+    def execute(self, context):
+        edit_mode_out()
+        ob_act = context.active_object
+        me = ob_act.data
+        list_clear_(va_buf.list_0)
+        va_buf.list_0 = [v.index for v in me.vertices if v.select][:]
+
+        if len(va_buf.list_0) == 0:
+            self.report({'INFO'}, "No vertices selected. Operation Cancelled")
+            edit_mode_in()
+            return {'CANCELLED'}
+
+        elif len(va_buf.list_0) != 0:
+            bpy.ops.vertex_align.coord_menu_id('INVOKE_DEFAULT')
+
+        edit_mode_in()
+
+        return {'FINISHED'}
+
+
+# Align to custom coordinates menu
+class Vertex_align_coord_menu(Operator):
+    bl_idname = "vertex_align.coord_menu_id"
+    bl_label = "Tweak custom coordinates"
+    bl_description = "Change the custom coordinates for aligning"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    def_axis_coord: FloatVectorProperty(
+            name="",
+            description="Enter the values of coordinates",
+            default=(0.0, 0.0, 0.0),
+            min=-100.0, max=100.0,
+            step=1, size=3,
+            subtype='XYZ',
+            precision=3
+            )
+    use_axis_coord = BoolVectorProperty(
+            name="Axis",
+            description="Choose Custom Coordinates axis",
+            default=(False,) * 3,
+            size=3,
+            )
+    is_not_undo = False
+
+    @classmethod
+    def poll(cls, context):
+        obj = context.active_object
+        return (obj and obj.type == 'MESH')
+
+    def using_store(self, context):
+        scene = context.scene
+        return scene.mesh_extra_tools.vert_align_use_stored
+
+    def draw(self, context):
+        layout = self.layout
+
+        if self.using_store(context) and self.is_not_undo:
+            layout.label(text="Using Stored Coordinates", icon="INFO")
+
+        row = layout.split(0.25)
+        row.prop(self, "use_axis_coord", index=0, text="X")
+        row.prop(self, "def_axis_coord", index=0)
+
+        row = layout.split(0.25)
+        row.prop(self, "use_axis_coord", index=1, text="Y")
+        row.prop(self, "def_axis_coord", index=1)
+
+        row = layout.split(0.25)
+        row.prop(self, "use_axis_coord", index=2, text="Z")
+        row.prop(self, "def_axis_coord", index=2)
+
+    def invoke(self, context, event):
+        self.is_not_undo = True
+        scene = context.scene
+        if self.using_store(context):
+            self.def_axis_coord = scene.mesh_extra_tools.vert_align_store_axis
+
+        return context.window_manager.invoke_props_dialog(self, width=200)
+
+    def execute(self, context):
+        self.is_not_undo = False
+        edit_mode_out()
+        ob_act = context.active_object
+        me = ob_act.data
+
+        for i in va_buf.list_0:
+            v = (me.vertices[i].co).copy()
+            tmp = Vector((v[0], v[1], v[2]))
+
+            if self.use_axis_coord[0] is True:
+                tmp[0] = self.def_axis_coord[0]
+            if self.use_axis_coord[1] is True:
+                tmp[1] = self.def_axis_coord[1]
+            if self.use_axis_coord[2] is True:
+                tmp[2] = self.def_axis_coord[2]
+            me.vertices[i].co = tmp
+
+        edit_mode_in()
+
+        return {'FINISHED'}
+
+
+#  Register
+classes = (
+    Vertex_align_store,
+    Vertex_align_original,
+    Vertex_align_coord_list,
+    Vertex_align_coord_menu,
+    )
+
+
+def register():
+    for cls in classes:
+        bpy.utils.register_class(cls)
+
+
+def unregister():
+    for cls in classes:
+        bpy.utils.unregister_class(cls)
+
+
+if __name__ == "__main__":
+    register()