Addon - Align Verts

Scripting in Blender with Python, and working on the API

Moderators: jesterKing, stiv

BlazeCell
Posts: 3
Joined: Tue May 21, 2013 7:33 am

Addon - Align Verts

Postby BlazeCell » Wed May 22, 2013 1:21 am

After looking around online for a way to align verts along an arbitrary axis in Blender, I came up rather empty handed.

The two techniques that I found were:
1) Scaling along an axis and setting the scale to zero to flatten the verts along that axis.
2) Joining two vertices, subdividing the new edge, and then sliding the vertices as desired.

Neither of those techniques fully fulfilled my needs, so I decided to create my own addon.

The script takes a selection of 3 or more verts and aligns all the verts along the line formed by the first and last selected verts. Additionally, you can press X, Y, or Z to preserve the corresponding coordinate values of the selected verts while still having them align.

I've set up the code to work with any type of selection (Verts, Edges, or Faces) but Verts is the most straight forward and useful.


NOTE: There is a bug where if you select the mesh elements in one mode, say Edges, and then switch to another, say Verts, and then run the script it causes Blender to quietly crash. I'm not sure what is causing this issue but the script works fine so long as you only select elements in one mode before running the script.

Code: Select all

bl_info = {
    "name": "Align Verts",
    "author": "BlazeCell",
    "version": (0, 1),
    "blender": (2, 66, 1),
    "location": "View3D > Mesh > Vertices (Ctrl+V)",
    "warning": "",
    "description": "Aligns the selected vertices along the line formed by the first and last selected vertices.",
    "wiki_url": "",
    "tracker_url": "",
    "category": "Mesh"}

import bpy
import bmesh
from mathutils import Vector

def extract_unique_verts(selected_verts, vertex_sequence):
    # Loop thru the vertex sequence and extract
    #   unique verts into the selected verts
    for elem in vertex_sequence:
        add = True
        for sel_elem in selected_verts:
            if elem == sel_elem['elem']:
                add = False
                break

        # If no match was found, add the element to the selected verts
        if add:
            # Store both the vert element and the initial coordinates
            selected_verts.append({'elem': elem, 'init_co': Vector(elem.co)})

def retrieve_selected_verts(obj, mesh):
    mesh.verts.index_update()
    selection_iter = iter(mesh.select_history)

    selected_verts = []
    elem = next(selection_iter, None)

    # Loop thru the selected elements and extract the selected vertices
    while (elem is not None):
        if type(elem).__name__ == 'BMVert':
            extract_unique_verts(selected_verts, [elem])
        elif type(elem).__name__ == 'BMEdge':
            extract_unique_verts(selected_verts, elem.verts)
        elif type(elem).__name__ == 'BMFace':
            extract_unique_verts(selected_verts, elem.verts)
        elif type(elem).__name__ == 'BMLoop':
            extract_unique_verts(selected_verts, [elem.vert])
        elem = next(selection_iter, None)

    return selected_verts

def revert_selected_verts_to_init(obj, mesh, selected_verts):
   
    print('REVERTING VERTS...')

    for vert in selected_verts:
        print("\tinit: " + str(vert['init_co']))
        print("\tprev: " + str(vert['elem'].co))
        vert['elem'].co = Vector(vert['init_co'])
        print("\tnew:  " + str(vert['elem'].co))

    # Show the updates in the viewport and recalculate n-gon tessellation
    bmesh.update_edit_mesh(obj.data, True)
   
    print('VERTS REVERTED')

def align_verts(obj, mesh, selected_verts, line_start, line_end, mode):
   
    print('Aligning verts...')
    print('Mode: ' + mode)

    # Do nothing if the mode is DO_NOTHING
    if (mode != 'DO_NOTHING'):

        print('ALIGNING...')
        print('BLAH')

        # obj = bpy.context.active_object

        # Determine the line direction
        line_direction = line_end - line_start
        line_direction.normalize()

        # By default, set the plane normal to the line direction to find the closest alignment point
        plane_normal = line_direction

        # Change the plane normal if we're need to preserve a coordinate value
        if (mode == 'PRESERVE_X'):
            # Set the plane normal to unit X to preserve the X values
            plane_normal = Vector([1.0, 0.0, 0.0])
        elif (mode == 'PRESERVE_Y'):
            # Set the plane normal to unit Y to preserve the Y values
            plane_normal = Vector([0.0, 1.0, 0.0])
        elif (mode == 'PRESERVE_Z'):
            # Set the plane normal to unit Z to preserve the Z values
            plane_normal = Vector([0.0, 0.0, 1.0])

        # Loop thru the vertices and align them according to the intersection
        #   of the line and the plane based on the plane normal
        for vert in selected_verts:

            print('Vert:')

            # If the denominator is zero, the line is parallel with
            #   the plane, therefore no intersection, so skip it
            denominator = line_direction.dot(plane_normal)
            if (denominator == 0.0):
                continue

            # Set the plane point to the vertex position
            plane_point = Vector(vert['init_co'])

            # Calculate the numerator
            numerator = (plane_point - line_start).dot(plane_normal)

            # Calculate the intersect distance
            intersect_distance = numerator / denominator

            # Calculate the intersection point
            intersection_point = line_start + line_direction * intersect_distance

            # Set the vert element postion to the intersection point
            print("\tinit: " + str(vert['init_co']))
            print("\tprev: " + str(vert['elem'].co))
            vert['elem'].co = intersection_point
            print("\tnew:  " + str(vert['elem'].co))

        # Show the updates in the viewport and recalculate n-gon tessellation
        bmesh.update_edit_mesh(obj.data, True)

    print('Verts aligned')

#makes AlignVerts an operator
class AlignVerts(bpy.types.Operator):
    """Align selected vertices"""
    bl_idname = "mesh.align_verts"
    bl_label = "Align Verts"
    bl_options = {'REGISTER', 'UNDO'}

    obj = None
    mesh = None
    selected_verts = []
    line_start = Vector((0.0, 0.0, 0.0))
    line_end   = Vector((0.0, 0.0, 0.0))
    mode = 'DO_NOTHING'

    def __init__(self):
        print('START')

    def __del__(self):
        print('END')

    def operation(self, context):
        # Ouput state information to the status bar
        context.area.header_text_set("Mode: %s  |  Change Mode: (X,Y,Z), Confirm: (LeftClick,Space,Enter), Cancel: (RightClick,Esc)" % self.mode)
       
        # Align the selected vertices
        align_verts(self.obj, self.mesh, self.selected_verts, self.line_start, self.line_end, self.mode)

    def modal(self, context, event):
       
        if event.type == 'X' and event.value == 'RELEASE':

            # Toggle preserving of X coordinate values
            if self.mode == 'PRESERVE_X':
                self.mode = 'CLOSEST'
            else:
                self.mode = 'PRESERVE_X'

            # Perform the operation
            self.operation(context)

        elif event.type == 'Y' and event.value == 'RELEASE':

            # Toggle preserving of Y coordinate values
            if self.mode == 'PRESERVE_Y':
                self.mode = 'CLOSEST'
            else:
                self.mode = 'PRESERVE_Y'

            # Perform the operation
            self.operation(context)

        elif event.type == 'Z' and event.value == 'RELEASE':

            # Toggle preserving of Z coordinate values
            if self.mode == 'PRESERVE_Z':
                self.mode = 'CLOSEST'
            else:
                self.mode = 'PRESERVE_Z'

            # Perform the operation
            self.operation(context)

        # Confirm
        elif event.type in ('LEFTMOUSE', 'RET', 'LINE_FEED', 'NUMPAD_ENTER', 'SPACE'):

            # Clear the status bar
            context.area.header_text_set()

            print('FINISHED')
            return {'FINISHED'}
        # Cancel
        elif event.type in ('RIGHTMOUSE', 'ESC'):

            # Revert the selected verts to their initial positions
            revert_selected_verts_to_init(self.obj, self.mesh, self.selected_verts)

            # Clear the status bar
            context.area.header_text_set()
           
            print('CANCELLED')
            return {'CANCELLED'}

        # Continue
        print('CONTINUE')
        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        print('INVOKE')

        # Register this class as the handler of the modal event
        context.window_manager.modal_handler_add(self)

        self.obj = bpy.context.active_object
        self.mesh = bmesh.from_edit_mesh(self.obj.data)

        # Retrieve the selected verts
        self.selected_verts = retrieve_selected_verts(self.obj, self.mesh)

        print('Selected Vert Count: ' + str(len(self.selected_verts)))

        # If we have more than 2 selected verts, then we have something we can align
        if (len(self.selected_verts) > 2):

            # Remove the first and last verts from the selected verts and
            #   use them to define the line start and end, respectively
            self.line_start = self.selected_verts.pop(0)['init_co'];
            self.line_end = self.selected_verts.pop()['init_co'];

            # Default the alignment mode to CLOSEST
            self.mode = 'CLOSEST';

            # Perform the operation
            self.operation(context)

            # Continue
            print('EXIT INVOKE')
            return {'RUNNING_MODAL'}

        # Otherwise cancel, there's nothing we can align
        else:
            print('CANCELLED')
            return {'CANCELLED'}
       
        # Continue
        print('EXIT INVOKE')
        return {'RUNNING_MODAL'}


# Register the operator
def menu_func(self, context):
    # Operators triggered by menus have their operator context by default set to EXEC_REGION_WIN.
    #   We need it set to INVOKE_DEFAULT to call invoke() instead of execute()
    self.layout.operator_context = 'INVOKE_DEFAULT'

    # Create the menu button
    self.layout.operator(AlignVerts.bl_idname, text="Align Verts")


def register():
    bpy.utils.register_class(AlignVerts)

    # Add "Align" menu to the "Mesh->Vertices" menu.
    bpy.types.VIEW3D_MT_edit_mesh_vertices.append(menu_func)


def unregister():
    bpy.utils.unregister_class(AlignVerts)

    # Remove "Align" menu from the "Mesh->Vertices" menu.
    bpy.types.VIEW3D_MT_edit_mesh_vertices.remove(menu_func)

if __name__ == "__main__":
    register()


Any issues or recommendations for this script are welcome and appreciated. =)

Return to “Python”

Who is online

Users browsing this forum: No registered users and 1 guest