-
Notifications
You must be signed in to change notification settings - Fork 40
/
mesh_makecoplanar.py
139 lines (120 loc) · 4.11 KB
/
mesh_makecoplanar.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
bl_info = {
"name":"Make Coplanar",
"author": "Scott Michaud",
"version": (0,6),
"blender": (2,80,0),
"location": "View3D > Specials > Make Coplanar",
"Description": "Forces currently selected vertices into their average plane, and a second selection of vertices into that plane.",
"warning": "",
"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'}
def get_timestamp():
out_time = time.time()
return out_time * 1000
#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))
avg_distance = 0.0
#If less then three vertices selected, cannot get a plane.
if num < 3:
return -1
#Get a sample of normals. Each vertex will contribute to three.
#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 = (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)
list_normals.append(cross_12)
#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 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.
#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
if num == 3:
return 0
else:
return 1
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):
distance = selected_vtxs[i].co.dot(avg_normal)
delta = distance - avg_distance
adjust = avg_normal * delta
selected_vtxs[i].co -= adjust
selected_vtxs[i].select = False
bpy.context.active_object.data.update()
def execute(self, context):
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_context = 'INVOKE_DEFAULT'
self.layout.operator(MakeCoplanar.bl_idname, text="Make Coplanar")
def register():
bpy.utils.register_class(MakeCoplanar)
bpy.types.VIEW3D_MT_edit_mesh_vertices.append(menu_func)
def unregister():
bpy.utils.unregister_class(MakeCoplanar)
bpy.types.VIEW3D_MT_edit_mesh_vertices.remove(menu_func)
if __name__ == "__main__":
register()