Skip to content

Commit

Permalink
Implemented second stage
Browse files Browse the repository at this point in the history
Pretty much a rewrite. Still awkward from a UX standpoint, but it works.
  • Loading branch information
ScottMichaud committed Dec 6, 2014
1 parent 62ef851 commit dec5bd8
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 57 deletions.
24 changes: 7 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#Mesh: Make Coplanar (v0.1)
#Mesh: Make Coplanar (v0.5)

##Install Instructions:

Expand All @@ -19,19 +19,9 @@ C:\Program Files\Blender Foundation\Blender\2.72\scripts\addons
2. Either:
1. Press the space bar and type "Make Coplanar". OR
2. Press Ctrl + V (Vertex Tools Menu) and select "Make Coplanar"

These vertices will be crushed to the nearest point in an approximate plane of best fit for all selected vertices.

##TODO:

In a future release, I intend to make this an interactive tool. The expected user interaction steps are:

1. Select three-or-more vertices that define your intended plane.
2. Run the "Make Coplanar" command. If four-or-more, the plane will be an approximate average of these and the vertices will snap to it.
3. Your selected vertices will deselect.
4. Select more vertices to add to plane, defined in Step 1.
5. Press "Enter" to apply these extra vertices, or "Esc" to cancel.
6. Step 6 will depend on user feedback, it could be either:
1. All vertices will be deselected. OR
2. The original vertices (from Step 1) will be selected. OR
3. All affected vertices (from Step 1 and Step 4) will be selected.
3. The desired plane to move vertices to will be calculated:
1. If 3 vertices were selected, it will be the plane that they form.
2. If 4+ vertices were selected, an average plane will be calculated. These vertices will be moved onto it.
4. Your vertices will unselect.
5. Select more vertices to align with that plane.
6. Press Enter to move this second selection onto the plane, or ESC to just move the first selection (if 4+).
109 changes: 69 additions & 40 deletions mesh_makecoplanar.py
Original file line number Diff line number Diff line change
@@ -1,99 +1,128 @@
bl_info = {
"name": "Make Coplanar",
"name":"Make Coplanar",
"author": "Scott Michaud",
"version": (0, 1),
"blender": (2, 72, 0),
"version": (0,5),
"blender": (2,72,0),
"location": "View3D > Specials > Make Coplanar",
"description": "Forces currently selected vertices to be coplanar. TODO: Subsequent selections will conform to that plane.",
"Description": "Forces currently selected vertices into their average plane, and a second selection of vertices into that plane.",
"warning": "",
"category": "Mesh",
"category": "Mesh"
}

import bpy
import bmesh
import mathutils

import time

class MakeCoplanar(bpy.types.Operator):
bl_idname = 'mesh.makecoplanar'
bl_label = 'Make Coplanar'
bl_options = {'REGISTER', 'UNDO'}

normal = None
distance = None
def get_timestamp():
out_time = time.time()
return out_time * 1000


def make_coplanar(context):

bm = bmesh.from_edit_mesh(bpy.context.active_object.data)
#Returns 1 if moves need to happen in first stage. 0 if nor. -1 if error.
def get_plane(self, context):
self.bm = bmesh.from_edit_mesh(bpy.context.active_object.data)
bm = self.bm
vtxs = bm.verts
selected_vtxs = [i for i in vtxs if i.select]
num = len(selected_vtxs)
list_normals = []
list_distances = []
avg_normal = mathutils.Vector((0.0,0.0,0.0)) #Initialize with 3D Zero vector.
avg_normal = mathutils.Vector((0.0,0.0,0.0))
avg_distance = 0.0

#If less then three vertices selected, cannot get a plane.
if num < 3:
return
return -1

#Get a sample of normals. Each vertex will contribute to three.
#All pairings is better, but factorial complexity -- worse than exponential.
#This is linear and probably good enough.
#All pairings would be better, but this is O(n) and probably good enough.
for i in range(num):
first_next = (i + 1) % num #Allow wrapping around for last two entries.
second_next = (i + 2) % num
second_next = (1 + 2) % num
vector_1 = selected_vtxs[i].co - selected_vtxs[first_next].co
vector_2 = selected_vtxs[i].co - selected_vtxs[second_next].co
cross_12 = vector_1.cross(vector_2)
cross_12.normalize()
list_normals.append(cross_12)

#Make all normals acute (or right) to one another.
#This makes best use of floating point precision. Subtraction sucks.
#Make all normals acute (or right-angle) to one another.
#This makes best use of floating point precision.
for i in range(1, num):
test_scalar = list_normals[i].dot(list_normals[0])
if test_scalar < 0:
list_normals[i] = -1 * list_normals[i]

#Find the average normal, then normalize it.
#Find the average normal by summing all samples and normalizing.
for i in range(num):
avg_normal += list_normals[i]
avg_normal.normalize()
self.normal = avg_normal

#Get distance for each vertex to a plane that crosses origin.
#Also, get the average.
#The average distance will define our plane, that distance away from origin down average normal direction.
for i in range(num):
distance = selected_vtxs[i].co.dot(avg_normal)
list_distances.append(distance)
avg_distance += distance
avg_distance /= num
self.distance = avg_distance

#Store data for interactive tool.

MakeCoplanar.normal = avg_normal
MakeCoplanar.distance = avg_distance

#No sense adjusting 3 vertices, which are already coplanar, by rounding error.
#We have the data for future vertices.
if num == 3:
return
return 0
else:
return 1

#Move each vertex +/- how far it is away from the average distance...
#... along the average normal direction.
def conform_plane(self, context):
bm = self.bm
vtxs = bm.verts
selected_vtxs = [i for i in vtxs if i.select]
num = len(selected_vtxs)
avg_normal = self.normal
avg_distance = self.distance

#Move vertexes to the nearest point on plane calculated in get_plane()
for i in range(num):
delta = list_distances[i] - avg_distance
adjust = avg_normal * delta #It's a vector, pointing in avg_normal direction, magnitude delta.
distance = selected_vtxs[i].co.dot(avg_normal)
delta = distance - avg_distance
adjust = avg_normal * delta
selected_vtxs[i].co -= adjust

bpy.context.active_object.data.update() #Update the base mesh.
selected_vtxs[i].select = False
bpy.context.active_object.data.update()

def execute(self, context):
MakeCoplanar.make_coplanar(context)
bpy.context.scene.update()

#Currently, exit immediately.
return {'FINISHED'}

def modal(self, context, event):
if event.type == 'ESC':
return {'CANCELLED'}
elif event.type == 'RET' and event.value == 'PRESS':
delta_time = (MakeCoplanar.get_timestamp()) - (self.start_time)
if delta_time > 250.0:
self.conform_plane(context)
return {'FINISHED'}
else:
self.execute(context)
return {'PASS_THROUGH'}

def invoke(self, context, event):
returncode = self.get_plane(context)
self.start_time = MakeCoplanar.get_timestamp()
if returncode == -1:
return {'CANCELLED'}
if returncode == 0:
context.window_manager.modal_handler_add(self)
self.execute(context)
return {'RUNNING_MODAL'}
if returncode == 1:
context.window_manager.modal_handler_add(self)
self.conform_plane(context)
self.execute(context)
return {'RUNNING_MODAL'}

def menu_func(self, context):
self.layout.operator(MakeCoplanar.bl_idname, text="Make Coplanar")

Expand Down

0 comments on commit dec5bd8

Please sign in to comment.