1
# ##### BEGIN GPL LICENSE BLOCK #####
3
# This program is free software; you can redistribute it and/or
4
# modify it under the terms of the GNU General Public License
5
# as published by the Free Software Foundation; either version 2
6
# of the License, or (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software Foundation,
15
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
# ##### END GPL LICENSE BLOCK #####
21
'author': "Bart Crouch",
24
'location': "View3D > Toolbar and View3D > Specials (W-key)",
25
'warning': "Bridge & Loft functions removed",
26
'description': "Mesh modelling toolkit. Several tools to aid modelling",
27
'wiki_url': "http://wiki.blender.org/index.php/Extensions:2.5/Py/"\
28
"Scripts/Modeling/LoopTools",
29
'tracker_url': "http://projects.blender.org/tracker/index.php?"\
30
"func=detail&aid=26189",
39
##########################################
40
####### General functions ################
41
##########################################
44
# used by all tools to improve speed on reruns
48
# force a full recalculation next time
49
def cache_delete(tool):
50
if tool in looptools_cache:
51
del looptools_cache[tool]
54
# check cache for stored information
55
def cache_read(tool, object, mesh, input_method, boundaries):
56
# current tool not cached yet
57
if tool not in looptools_cache:
58
return(False, False, False, False, False)
59
# check if selected object didn't change
60
if object.name != looptools_cache[tool]["object"]:
61
return(False, False, False, False, False)
62
# check if input didn't change
63
if input_method != looptools_cache[tool]["input_method"]:
64
return(False, False, False, False, False)
65
if boundaries != looptools_cache[tool]["boundaries"]:
66
return(False, False, False, False, False)
67
modifiers = [mod.name for mod in object.modifiers if mod.show_viewport \
68
and mod.type == 'MIRROR']
69
if modifiers != looptools_cache[tool]["modifiers"]:
70
return(False, False, False, False, False)
71
input = [v.index for v in mesh.vertices if v.select and not v.hide]
72
if input != looptools_cache[tool]["input"]:
73
return(False, False, False, False, False)
75
single_loops = looptools_cache[tool]["single_loops"]
76
loops = looptools_cache[tool]["loops"]
77
derived = looptools_cache[tool]["derived"]
78
mapping = looptools_cache[tool]["mapping"]
80
return(True, single_loops, loops, derived, mapping)
83
# store information in the cache
84
def cache_write(tool, object, mesh, input_method, boundaries, single_loops,
85
loops, derived, mapping):
86
# clear cache of current tool
87
if tool in looptools_cache:
88
del looptools_cache[tool]
89
# prepare values to be saved to cache
90
input = [v.index for v in mesh.vertices if v.select and not v.hide]
91
modifiers = [mod.name for mod in object.modifiers if mod.show_viewport \
92
and mod.type == 'MIRROR']
94
looptools_cache[tool] = {"input": input, "object": object.name,
95
"input_method": input_method, "boundaries": boundaries,
96
"single_loops": single_loops, "loops": loops,
97
"derived": derived, "mapping": mapping, "modifiers": modifiers}
100
# calculates natural cubic splines through all given knots
101
def calculate_cubic_splines(mesh_mod, tknots, knots):
102
# hack for circular loops
103
if knots[0] == knots[-1] and len(knots) > 1:
106
for k in range(-1, -5, -1):
107
if k - 1 < -len(knots):
109
k_new1.append(knots[k-1])
112
if k + 1 > len(knots) - 1:
114
k_new2.append(knots[k+1])
121
for t in range(-1, -5, -1):
122
if t - 1 < -len(tknots):
124
total1 += tknots[t] - tknots[t-1]
125
t_new1.append(tknots[0] - total1)
129
if t + 1 > len(tknots) - 1:
131
total2 += tknots[t+1] - tknots[t]
132
t_new2.append(tknots[-1] + total2)
145
locs = [mesh_mod.vertices[k].co[:] for k in knots]
153
if x[i+1] - x[i] == 0:
156
h.append(x[i+1] - x[i])
158
for i in range(1, n-1):
159
q.append(3/h[i]*(a[i+1]-a[i]) - 3/h[i-1]*(a[i]-a[i-1]))
163
for i in range(1, n-1):
164
l.append(2*(x[i+1]-x[i-1]) - h[i-1]*u[i-1])
167
u.append(h[i] / l[i])
168
z.append((q[i] - h[i-1] * z[i-1]) / l[i])
171
b = [False for i in range(n-1)]
172
c = [False for i in range(n)]
173
d = [False for i in range(n-1)]
175
for i in range(n-2, -1, -1):
176
c[i] = z[i] - u[i]*c[i+1]
177
b[i] = (a[i+1]-a[i])/h[i] - h[i]*(c[i+1]+2*c[i])/3
178
d[i] = (c[i+1]-c[i]) / (3*h[i])
180
result.append([a[i], b[i], c[i], d[i], x[i]])
182
for i in range(len(knots)-1):
183
splines.append([result[i], result[i+n-1], result[i+(n-1)*2]])
184
if circular: # cleaning up after hack
186
tknots = tknots[4:-4]
191
# calculates linear splines through all given knots
192
def calculate_linear_splines(mesh_mod, tknots, knots):
194
for i in range(len(knots)-1):
195
a = mesh_mod.vertices[knots[i]].co
196
b = mesh_mod.vertices[knots[i+1]].co
200
splines.append([a, d, t, u]) # [locStart, locDif, tStart, tDif]
205
# calculate a best-fit plane to the given vertices
206
def calculate_plane(mesh_mod, loop, method="best_fit", object=False):
207
# getting the vertex locations
208
locs = [mesh_mod.vertices[v].co.copy() for v in loop[0]]
210
# calculating the center of masss
211
com = mathutils.Vector()
217
if method == 'best_fit':
218
# creating the covariance matrix
219
mat = mathutils.Matrix(((0.0, 0.0, 0.0),
224
mat[0][0] += (loc[0]-x)**2
225
mat[1][0] += (loc[0]-x)*(loc[1]-y)
226
mat[2][0] += (loc[0]-x)*(loc[2]-z)
227
mat[0][1] += (loc[1]-y)*(loc[0]-x)
228
mat[1][1] += (loc[1]-y)**2
229
mat[2][1] += (loc[1]-y)*(loc[2]-z)
230
mat[0][2] += (loc[2]-z)*(loc[0]-x)
231
mat[1][2] += (loc[2]-z)*(loc[1]-y)
232
mat[2][2] += (loc[2]-z)**2
234
# calculating the normal to the plane
239
if sum(mat[0]) == 0.0:
240
normal = mathutils.Vector((1.0, 0.0, 0.0))
241
elif sum(mat[1]) == 0.0:
242
normal = mathutils.Vector((0.0, 1.0, 0.0))
243
elif sum(mat[2]) == 0.0:
244
normal = mathutils.Vector((0.0, 0.0, 1.0))
246
# warning! this is different from .normalize()
249
vec = mathutils.Vector((1.0, 1.0, 1.0))
250
vec2 = (mat * vec)/(mat * vec).length
251
while vec != vec2 and iter<itermax:
258
vec2 = mathutils.Vector((1.0, 1.0, 1.0))
261
elif method == 'normal':
262
# averaging the vertex normals
263
v_normals = [mesh_mod.vertices[v].normal for v in loop[0]]
264
normal = mathutils.Vector()
265
for v_normal in v_normals:
267
normal /= len(v_normals)
270
elif method == 'view':
271
# calculate view normal
272
rotation = bpy.context.space_data.region_3d.view_matrix.to_3x3().\
274
normal = rotation * mathutils.Vector((0.0, 0.0, 1.0))
276
normal = object.matrix_world.inverted().to_euler().to_matrix() * \
282
# calculate splines based on given interpolation method (controller function)
283
def calculate_splines(interpolation, mesh_mod, tknots, knots):
284
if interpolation == 'cubic':
285
splines = calculate_cubic_splines(mesh_mod, tknots, knots[:])
286
else: # interpolations == 'linear'
287
splines = calculate_linear_splines(mesh_mod, tknots, knots[:])
292
# check loops and only return valid ones
293
def check_loops(loops, mapping, mesh_mod):
295
for loop, circular in loops:
296
# loop needs to have at least 3 vertices
299
# loop needs at least 1 vertex in the original, non-mirrored mesh
303
if mapping[vert] > -1:
308
# vertices can not all be at the same location
310
for i in range(len(loop) - 1):
311
if (mesh_mod.vertices[loop[i]].co - \
312
mesh_mod.vertices[loop[i+1]].co).length > 1e-6:
317
# passed all tests, loop is valid
318
valid_loops.append([loop, circular])
323
# input: mesh, output: dict with the edge-key as key and face-index as value
324
def dict_edge_faces(mesh):
325
edge_faces = dict([[edge.key, []] for edge in mesh.edges if not edge.hide])
326
for face in mesh.tessfaces:
329
for key in face.edge_keys:
330
edge_faces[key].append(face.index)
334
# input: mesh (edge-faces optional), output: dict with face-face connections
335
def dict_face_faces(mesh, edge_faces=False):
337
edge_faces = dict_edge_faces(mesh)
339
connected_faces = dict([[face.index, []] for face in mesh.tessfaces if \
341
for face in mesh.tessfaces:
344
for edge_key in face.edge_keys:
345
for connected_face in edge_faces[edge_key]:
346
if connected_face == face.index:
348
connected_faces[face.index].append(connected_face)
350
return(connected_faces)
353
# input: mesh, output: dict with the vert index as key and edge-keys as value
354
def dict_vert_edges(mesh):
355
vert_edges = dict([[v.index, []] for v in mesh.vertices if not v.hide])
356
for edge in mesh.edges:
359
for vert in edge.key:
360
vert_edges[vert].append(edge.key)
365
# input: mesh, output: dict with the vert index as key and face index as value
366
def dict_vert_faces(mesh):
367
vert_faces = dict([[v.index, []] for v in mesh.vertices if not v.hide])
368
for face in mesh.tessfaces:
370
for vert in face.vertices:
371
vert_faces[vert].append(face.index)
376
# input: list of edge-keys, output: dictionary with vertex-vertex connections
377
def dict_vert_verts(edge_keys):
378
# create connection data
382
if ek[i] in vert_verts:
383
vert_verts[ek[i]].append(ek[1-i])
385
vert_verts[ek[i]] = [ek[1-i]]
390
# calculate input loops
391
def get_connected_input(object, mesh, scene, input):
392
# get mesh with modifiers applied
393
derived, mesh_mod = get_derived_mesh(object, mesh, scene)
395
# calculate selected loops
396
edge_keys = [edge.key for edge in mesh_mod.edges if \
397
edge.select and not edge.hide]
398
loops = get_connected_selections(edge_keys)
400
# if only selected loops are needed, we're done
401
if input == 'selected':
402
return(derived, mesh_mod, loops)
403
# elif input == 'all':
404
loops = get_parallel_loops(mesh_mod, loops)
406
return(derived, mesh_mod, loops)
409
# sorts all edge-keys into a list of loops
410
def get_connected_selections(edge_keys):
411
# create connection data
412
vert_verts = dict_vert_verts(edge_keys)
414
# find loops consisting of connected selected edges
416
while len(vert_verts) > 0:
417
loop = [iter(vert_verts.keys()).__next__()]
423
# no more connection data for current vertex
424
if loop[-1] not in vert_verts:
432
for i, next_vert in enumerate(vert_verts[loop[-1]]):
433
if next_vert not in loop:
434
vert_verts[loop[-1]].pop(i)
435
if len(vert_verts[loop[-1]]) == 0:
436
del vert_verts[loop[-1]]
437
# remove connection both ways
438
if next_vert in vert_verts:
439
if len(vert_verts[next_vert]) == 1:
440
del vert_verts[next_vert]
442
vert_verts[next_vert].remove(loop[-1])
443
loop.append(next_vert)
447
# found one end of the loop, continue with next
451
# found both ends of the loop, stop growing
455
# check if loop is circular
456
if loop[0] in vert_verts:
457
if loop[-1] in vert_verts[loop[0]]:
459
if len(vert_verts[loop[0]]) == 1:
460
del vert_verts[loop[0]]
462
vert_verts[loop[0]].remove(loop[-1])
463
if len(vert_verts[loop[-1]]) == 1:
464
del vert_verts[loop[-1]]
466
vert_verts[loop[-1]].remove(loop[0])
480
# get the derived mesh data, if there is a mirror modifier
481
def get_derived_mesh(object, mesh, scene):
482
# check for mirror modifiers
483
if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
485
# disable other modifiers
486
show_viewport = [mod.name for mod in object.modifiers if \
488
for mod in object.modifiers:
489
if mod.type != 'MIRROR':
490
mod.show_viewport = False
492
mesh_mod = object.to_mesh(scene, True, 'PREVIEW')
493
# re-enable other modifiers
494
for mod_name in show_viewport:
495
object.modifiers[mod_name].show_viewport = True
496
# no mirror modifiers, so no derived mesh necessary
501
return(derived, mesh_mod)
504
# return a mapping of derived indices to indices
505
def get_mapping(derived, mesh, mesh_mod, single_vertices, full_search, loops):
510
verts = [v for v in mesh.vertices if not v.hide]
512
verts = [v for v in mesh.vertices if v.select and not v.hide]
514
# non-selected vertices around single vertices also need to be mapped
516
mapping = dict([[vert, -1] for vert in single_vertices])
517
verts_mod = [mesh_mod.vertices[vert] for vert in single_vertices]
519
for v_mod in verts_mod:
520
if (v.co - v_mod.co).length < 1e-6:
521
mapping[v_mod.index] = v.index
523
real_singles = [v_real for v_real in mapping.values() if v_real>-1]
525
verts_indices = [vert.index for vert in verts]
526
for face in [face for face in mesh.tessfaces if not face.select \
528
for vert in face.vertices:
529
if vert in real_singles:
530
for v in face.vertices:
531
if not v in verts_indices:
532
if mesh.vertices[v] not in verts:
533
verts.append(mesh.vertices[v])
536
# create mapping of derived indices to indices
537
mapping = dict([[vert, -1] for loop in loops for vert in loop[0]])
539
for single in single_vertices:
541
verts_mod = [mesh_mod.vertices[i] for i in mapping.keys()]
543
for v_mod in verts_mod:
544
if (v.co - v_mod.co).length < 1e-6:
545
mapping[v_mod.index] = v.index
546
verts_mod.remove(v_mod)
552
# returns a list of all loops parallel to the input, input included
553
def get_parallel_loops(mesh_mod, loops):
554
# get required dictionaries
555
edge_faces = dict_edge_faces(mesh_mod)
556
connected_faces = dict_face_faces(mesh_mod, edge_faces)
557
# turn vertex loops into edge loops
560
edgeloop = [[sorted([loop[0][i], loop[0][i+1]]) for i in \
561
range(len(loop[0])-1)], loop[1]]
562
if loop[1]: # circular
563
edgeloop[0].append(sorted([loop[0][-1], loop[0][0]]))
564
edgeloops.append(edgeloop[:])
565
# variables to keep track while iterating
569
for loop in edgeloops:
570
# initialise with original loop
571
all_edgeloops.append(loop[0])
575
if edge[0] not in verts_used:
576
verts_used.append(edge[0])
577
if edge[1] not in verts_used:
578
verts_used.append(edge[1])
580
# find parallel loops
581
while len(newloops) > 0:
584
for i in newloops[-1]:
586
forbidden_side = False
587
if not i in edge_faces:
588
# weird input with branches
591
for face in edge_faces[i]:
592
if len(side_a) == 0 and forbidden_side != "a":
598
elif side_a[-1] in connected_faces[face] and \
599
forbidden_side != "a":
605
if len(side_b) == 0 and forbidden_side != "b":
611
elif side_b[-1] in connected_faces[face] and \
612
forbidden_side != "b":
620
# weird input with branches
633
for key in mesh_mod.tessfaces[fi].edge_keys:
634
if key[0] not in verts_used and key[1] not in \
636
extraloop.append(key)
639
for key in extraloop:
641
if new_vert not in verts_used:
642
verts_used.append(new_vert)
643
newloops.append(extraloop)
644
all_edgeloops.append(extraloop)
646
# input contains branches, only return selected loop
650
# change edgeloops into normal loops
652
for edgeloop in all_edgeloops:
654
# grow loop by comparing vertices between consecutive edge-keys
655
for i in range(len(edgeloop)-1):
656
for vert in range(2):
657
if edgeloop[i][vert] in edgeloop[i+1]:
658
loop.append(edgeloop[i][vert])
661
# add starting vertex
662
for vert in range(2):
663
if edgeloop[0][vert] != loop[0]:
664
loop = [edgeloop[0][vert]] + loop
667
for vert in range(2):
668
if edgeloop[-1][vert] != loop[-1]:
669
loop.append(edgeloop[-1][vert])
671
# check if loop is circular
672
if loop[0] == loop[-1]:
677
loops.append([loop, circular])
682
# gather initial data
684
global_undo = bpy.context.user_preferences.edit.use_global_undo
685
bpy.context.user_preferences.edit.use_global_undo = False
686
bpy.ops.object.mode_set(mode='OBJECT')
687
object = bpy.context.active_object
688
mesh = bpy.context.active_object.data
690
return(global_undo, object, mesh)
693
# move the vertices to their new locations
694
def move_verts(mesh, mapping, move, influence):
696
for index, loc in loop:
698
if mapping[index] == -1:
701
index = mapping[index]
703
mesh.vertices[index].co = loc*(influence/100) + \
704
mesh.vertices[index].co*((100-influence)/100)
706
mesh.vertices[index].co = loc
709
# load custom tool settings
710
def settings_load(self):
711
lt = bpy.context.window_manager.looptools
712
tool = self.name.split()[0].lower()
713
keys = self.as_keywords().keys()
715
setattr(self, key, getattr(lt, tool + "_" + key))
718
# store custom tool settings
719
def settings_write(self):
720
lt = bpy.context.window_manager.looptools
721
tool = self.name.split()[0].lower()
722
keys = self.as_keywords().keys()
724
setattr(lt, tool + "_" + key, getattr(self, key))
727
# clean up and set settings back to original state
728
def terminate(global_undo):
729
bpy.ops.object.mode_set(mode='EDIT')
730
bpy.context.user_preferences.edit.use_global_undo = global_undo
733
##########################################
734
####### Bridge functions #################
735
##########################################
737
# calculate a cubic spline through the middle section of 4 given coordinates
738
def bridge_calculate_cubic_spline(mesh, coordinates):
744
for i in coordinates:
745
a.append(float(i[j]))
748
h.append(x[i+1]-x[i])
751
q.append(3.0/h[i]*(a[i+1]-a[i])-3.0/h[i-1]*(a[i]-a[i-1]))
756
l.append(2.0*(x[i+1]-x[i-1])-h[i-1]*u[i-1])
758
z.append((q[i]-h[i-1]*z[i-1])/l[i])
761
b = [False for i in range(3)]
762
c = [False for i in range(4)]
763
d = [False for i in range(3)]
765
for i in range(2,-1,-1):
766
c[i] = z[i]-u[i]*c[i+1]
767
b[i] = (a[i+1]-a[i])/h[i]-h[i]*(c[i+1]+2.0*c[i])/3.0
768
d[i] = (c[i+1]-c[i])/(3.0*h[i])
770
result.append([a[i], b[i], c[i], d[i], x[i]])
771
spline = [result[1], result[4], result[7]]
776
# return a list with new vertex location vectors, a list with face vertex
777
# integers, and the highest vertex integer in the virtual mesh
778
def bridge_calculate_geometry(mesh, lines, vertex_normals, segments,
779
interpolation, cubic_strength, min_width, max_vert_index):
783
# calculate location based on interpolation method
784
def get_location(line, segment, splines):
785
v1 = mesh.vertices[lines[line][0]].co
786
v2 = mesh.vertices[lines[line][1]].co
787
if interpolation == 'linear':
788
return v1 + (segment/segments) * (v2-v1)
789
else: # interpolation == 'cubic'
790
m = (segment/segments)
791
ax,bx,cx,dx,tx = splines[line][0]
792
x = ax+bx*m+cx*m**2+dx*m**3
793
ay,by,cy,dy,ty = splines[line][1]
794
y = ay+by*m+cy*m**2+dy*m**3
795
az,bz,cz,dz,tz = splines[line][2]
796
z = az+bz*m+cz*m**2+dz*m**3
797
return mathutils.Vector((x, y, z))
799
# no interpolation needed
801
for i, line in enumerate(lines):
803
faces.append([line[0], lines[i+1][0], lines[i+1][1], line[1]])
804
# more than 1 segment, interpolate
806
# calculate splines (if necessary) once, so no recalculations needed
807
if interpolation == 'cubic':
810
v1 = mesh.vertices[line[0]].co
811
v2 = mesh.vertices[line[1]].co
812
size = (v2-v1).length * cubic_strength
813
splines.append(bridge_calculate_cubic_spline(mesh,
814
[v1+size*vertex_normals[line[0]], v1, v2,
815
v2+size*vertex_normals[line[1]]]))
819
# create starting situation
820
virtual_width = [(mesh.vertices[lines[i][0]].co -
821
mesh.vertices[lines[i+1][0]].co).length for i
822
in range(len(lines)-1)]
823
new_verts = [get_location(0, seg, splines) for seg in range(1,
825
first_line_indices = [i for i in range(max_vert_index+1,
826
max_vert_index+segments)]
828
prev_verts = new_verts[:] # vertex locations of verts on previous line
829
prev_vert_indices = first_line_indices[:]
830
max_vert_index += segments - 1 # highest vertex index in virtual mesh
831
next_verts = [] # vertex locations of verts on current line
832
next_vert_indices = []
834
for i, line in enumerate(lines):
839
for seg in range(1, segments):
840
loc1 = prev_verts[seg-1]
841
loc2 = get_location(i+1, seg, splines)
842
if (loc1-loc2).length < (min_width/100)*virtual_width[i] \
843
and line[1]==lines[i+1][1]:
844
# triangle, no new vertex
845
faces.append([v1, v2, prev_vert_indices[seg-1],
846
prev_vert_indices[seg-1]])
847
next_verts += prev_verts[seg-1:]
848
next_vert_indices += prev_vert_indices[seg-1:]
852
if i == len(lines)-2 and lines[0] == lines[-1]:
853
# quad with first line, no new vertex
854
faces.append([v1, v2, first_line_indices[seg-1],
855
prev_vert_indices[seg-1]])
856
v2 = first_line_indices[seg-1]
857
v1 = prev_vert_indices[seg-1]
859
# quad, add new vertex
861
faces.append([v1, v2, max_vert_index,
862
prev_vert_indices[seg-1]])
864
v1 = prev_vert_indices[seg-1]
865
new_verts.append(loc2)
866
next_verts.append(loc2)
867
next_vert_indices.append(max_vert_index)
869
faces.append([v1, v2, lines[i+1][1], line[1]])
871
prev_verts = next_verts[:]
872
prev_vert_indices = next_vert_indices[:]
874
next_vert_indices = []
876
return(new_verts, faces, max_vert_index)
879
# calculate lines (list of lists, vertex indices) that are used for bridging
880
def bridge_calculate_lines(mesh, loops, mode, twist, reverse):
882
loop1, loop2 = [i[0] for i in loops]
883
loop1_circular, loop2_circular = [i[1] for i in loops]
884
circular = loop1_circular or loop2_circular
887
# calculate loop centers
889
for loop in [loop1, loop2]:
890
center = mathutils.Vector()
892
center += mesh.vertices[vertex].co
894
centers.append(center)
895
for i, loop in enumerate([loop1, loop2]):
897
if mesh.vertices[vertex].co == centers[i]:
898
# prevent zero-length vectors in angle comparisons
899
centers[i] += mathutils.Vector((0.01, 0, 0))
901
center1, center2 = centers
903
# calculate the normals of the virtual planes that the loops are on
905
normal_plurity = False
906
for i, loop in enumerate([loop1, loop2]):
908
mat = mathutils.Matrix(((0.0, 0.0, 0.0),
912
for loc in [mesh.vertices[vertex].co for vertex in loop]:
913
mat[0][0] += (loc[0]-x)**2
914
mat[1][0] += (loc[0]-x)*(loc[1]-y)
915
mat[2][0] += (loc[0]-x)*(loc[2]-z)
916
mat[0][1] += (loc[1]-y)*(loc[0]-x)
917
mat[1][1] += (loc[1]-y)**2
918
mat[2][1] += (loc[1]-y)*(loc[2]-z)
919
mat[0][2] += (loc[2]-z)*(loc[0]-x)
920
mat[1][2] += (loc[2]-z)*(loc[1]-y)
921
mat[2][2] += (loc[2]-z)**2
924
if sum(mat[0]) < 1e-6 or sum(mat[1]) < 1e-6 or sum(mat[2]) < 1e-6:
925
normal_plurity = True
930
normal = mathutils.Vector((1.0, 0.0, 0.0))
931
elif sum(mat[1]) == 0:
932
normal = mathutils.Vector((0.0, 1.0, 0.0))
933
elif sum(mat[2]) == 0:
934
normal = mathutils.Vector((0.0, 0.0, 1.0))
936
# warning! this is different from .normalize()
939
vec = mathutils.Vector((1.0, 1.0, 1.0))
940
vec2 = (mat * vec)/(mat * vec).length
941
while vec != vec2 and iter<itermax:
948
vec2 = mathutils.Vector((1.0, 1.0, 1.0))
950
normals.append(normal)
951
# have plane normals face in the same direction (maximum angle: 90 degrees)
952
if ((center1 + normals[0]) - center2).length < \
953
((center1 - normals[0]) - center2).length:
955
if ((center2 + normals[1]) - center1).length > \
956
((center2 - normals[1]) - center1).length:
959
# rotation matrix, representing the difference between the plane normals
960
axis = normals[0].cross(normals[1])
961
axis = mathutils.Vector([loc if abs(loc) > 1e-8 else 0 for loc in axis])
962
if axis.angle(mathutils.Vector((0, 0, 1)), 0) > 1.5707964:
964
angle = normals[0].dot(normals[1])
965
rotation_matrix = mathutils.Matrix.Rotation(angle, 4, axis)
967
# if circular, rotate loops so they are aligned
969
# make sure loop1 is the circular one (or both are circular)
970
if loop2_circular and not loop1_circular:
971
loop1_circular, loop2_circular = True, False
972
loop1, loop2 = loop2, loop1
974
# match start vertex of loop1 with loop2
975
target_vector = mesh.vertices[loop2[0]].co - center2
976
dif_angles = [[(rotation_matrix * (mesh.vertices[vertex].co - center1)
977
).angle(target_vector, 0), False, i] for
978
i, vertex in enumerate(loop1)]
980
if len(loop1) != len(loop2):
981
angle_limit = dif_angles[0][0] * 1.2 # 20% margin
982
dif_angles = [[(mesh.vertices[loop2[0]].co - \
983
mesh.vertices[loop1[index]].co).length, angle, index] for \
984
angle, distance, index in dif_angles if angle <= angle_limit]
986
loop1 = loop1[dif_angles[0][2]:] + loop1[:dif_angles[0][2]]
988
# have both loops face the same way
989
if normal_plurity and not circular:
990
second_to_first, second_to_second, second_to_last = \
991
[(mesh.vertices[loop1[1]].co - center1).\
992
angle(mesh.vertices[loop2[i]].co - center2) for i in [0, 1, -1]]
993
last_to_first, last_to_second = [(mesh.vertices[loop1[-1]].co - \
994
center1).angle(mesh.vertices[loop2[i]].co - center2) for \
996
if (min(last_to_first, last_to_second)*1.1 < min(second_to_first, \
997
second_to_second)) or (loop2_circular and second_to_last*1.1 < \
998
min(second_to_first, second_to_second)):
1001
loop1 = [loop1[-1]] + loop1[:-1]
1003
angle = (mesh.vertices[loop1[0]].co - center1).\
1004
cross(mesh.vertices[loop1[1]].co - center1).angle(normals[0], 0)
1005
target_angle = (mesh.vertices[loop2[0]].co - center2).\
1006
cross(mesh.vertices[loop2[1]].co - center2).angle(normals[1], 0)
1007
limit = 1.5707964 # 0.5*pi, 90 degrees
1008
if not ((angle > limit and target_angle > limit) or \
1009
(angle < limit and target_angle < limit)):
1012
loop1 = [loop1[-1]] + loop1[:-1]
1013
elif normals[0].angle(normals[1]) > limit:
1016
loop1 = [loop1[-1]] + loop1[:-1]
1018
# both loops have the same length
1019
if len(loop1) == len(loop2):
1022
if abs(twist) < len(loop1):
1023
loop1 = loop1[twist:]+loop1[:twist]
1027
lines.append([loop1[0], loop2[0]])
1028
for i in range(1, len(loop1)):
1029
lines.append([loop1[i], loop2[i]])
1031
# loops of different lengths
1033
# make loop1 longest loop
1034
if len(loop2) > len(loop1):
1035
loop1, loop2 = loop2, loop1
1036
loop1_circular, loop2_circular = loop2_circular, loop1_circular
1040
if abs(twist) < len(loop1):
1041
loop1 = loop1[twist:]+loop1[:twist]
1045
# shortest angle difference doesn't always give correct start vertex
1046
if loop1_circular and not loop2_circular:
1049
if len(loop1) - shifting < len(loop2):
1052
to_last, to_first = [(rotation_matrix *
1053
(mesh.vertices[loop1[-1]].co - center1)).angle((mesh.\
1054
vertices[loop2[i]].co - center2), 0) for i in [-1, 0]]
1055
if to_first < to_last:
1056
loop1 = [loop1[-1]] + loop1[:-1]
1062
# basic shortest side first
1064
lines.append([loop1[0], loop2[0]])
1065
for i in range(1, len(loop1)):
1066
if i >= len(loop2) - 1:
1068
lines.append([loop1[i], loop2[-1]])
1071
lines.append([loop1[i], loop2[i]])
1073
# shortest edge algorithm
1074
else: # mode == 'shortest'
1075
lines.append([loop1[0], loop2[0]])
1077
for i in range(len(loop1) -1):
1078
if prev_vert2 == len(loop2) - 1 and not loop2_circular:
1079
# force triangles, reached end of loop2
1081
elif prev_vert2 == len(loop2) - 1 and loop2_circular:
1082
# at end of loop2, but circular, so check with first vert
1083
tri, quad = [(mesh.vertices[loop1[i+1]].co -
1084
mesh.vertices[loop2[j]].co).length
1085
for j in [prev_vert2, 0]]
1088
elif len(loop1) - 1 - i == len(loop2) - 1 - prev_vert2 and \
1090
# force quads, otherwise won't make it to end of loop2
1093
# calculate if tri or quad gives shortest edge
1094
tri, quad = [(mesh.vertices[loop1[i+1]].co -
1095
mesh.vertices[loop2[j]].co).length
1096
for j in range(prev_vert2, prev_vert2+2)]
1100
lines.append([loop1[i+1], loop2[prev_vert2]])
1101
if circle_full == 2:
1104
elif not circle_full:
1105
lines.append([loop1[i+1], loop2[prev_vert2+1]])
1107
# quad to first vertex of loop2
1109
lines.append([loop1[i+1], loop2[0]])
1113
# final face for circular loops
1114
if loop1_circular and loop2_circular:
1115
lines.append([loop1[0], loop2[0]])
1120
# calculate number of segments needed
1121
def bridge_calculate_segments(mesh, lines, loops, segments):
1122
# return if amount of segments is set by user
1127
average_edge_length = [(mesh.vertices[vertex].co - \
1128
mesh.vertices[loop[0][i+1]].co).length for loop in loops for \
1129
i, vertex in enumerate(loop[0][:-1])]
1130
# closing edges of circular loops
1131
average_edge_length += [(mesh.vertices[loop[0][-1]].co - \
1132
mesh.vertices[loop[0][0]].co).length for loop in loops if loop[1]]
1135
average_edge_length = sum(average_edge_length) / len(average_edge_length)
1136
average_bridge_length = sum([(mesh.vertices[v1].co - \
1137
mesh.vertices[v2].co).length for v1, v2 in lines]) / len(lines)
1139
segments = max(1, round(average_bridge_length / average_edge_length))
1144
# return dictionary with vertex index as key, and the normal vector as value
1145
def bridge_calculate_virtual_vertex_normals(mesh, lines, loops, edge_faces,
1147
if not edge_faces: # interpolation isn't set to cubic
1150
# pity reduce() isn't one of the basic functions in python anymore
1151
def average_vector_dictionary(dic):
1152
for key, vectors in dic.items():
1153
#if type(vectors) == type([]) and len(vectors) > 1:
1154
if len(vectors) > 1:
1155
average = mathutils.Vector()
1156
for vector in vectors:
1158
average /= len(vectors)
1159
dic[key] = [average]
1162
# get all edges of the loop
1163
edges = [[edgekey_to_edge[tuple(sorted([loops[j][0][i],
1164
loops[j][0][i+1]]))] for i in range(len(loops[j][0])-1)] for \
1166
edges = edges[0] + edges[1]
1168
if loops[j][1]: # circular
1169
edges.append(edgekey_to_edge[tuple(sorted([loops[j][0][0],
1170
loops[j][0][-1]]))])
1173
calculation based on face topology (assign edge-normals to vertices)
1175
edge_normal = face_normal x edge_vector
1176
vertex_normal = average(edge_normals)
1178
vertex_normals = dict([(vertex, []) for vertex in loops[0][0]+loops[1][0]])
1180
faces = edge_faces[edge.key] # valid faces connected to edge
1183
# get edge coordinates
1184
v1, v2 = [mesh.vertices[edge.key[i]].co for i in [0,1]]
1185
edge_vector = v1 - v2
1186
if edge_vector.length < 1e-4:
1187
# zero-length edge, vertices at same location
1189
edge_center = (v1 + v2) / 2
1191
# average face coordinates, if connected to more than 1 valid face
1193
face_normal = mathutils.Vector()
1194
face_center = mathutils.Vector()
1196
face_normal += face.normal
1197
face_center += face.center
1198
face_normal /= len(faces)
1199
face_center /= len(faces)
1201
face_normal = faces[0].normal
1202
face_center = faces[0].center
1203
if face_normal.length < 1e-4:
1204
# faces with a surface of 0 have no face normal
1207
# calculate virtual edge normal
1208
edge_normal = edge_vector.cross(face_normal)
1209
edge_normal.length = 0.01
1210
if (face_center - (edge_center + edge_normal)).length > \
1211
(face_center - (edge_center - edge_normal)).length:
1212
# make normal face the correct way
1213
edge_normal.negate()
1214
edge_normal.normalize()
1215
# add virtual edge normal as entry for both vertices it connects
1216
for vertex in edge.key:
1217
vertex_normals[vertex].append(edge_normal)
1220
calculation based on connection with other loop (vertex focused method)
1221
- used for vertices that aren't connected to any valid faces
1223
plane_normal = edge_vector x connection_vector
1224
vertex_normal = plane_normal x edge_vector
1226
vertices = [vertex for vertex, normal in vertex_normals.items() if not \
1230
# edge vectors connected to vertices
1231
edge_vectors = dict([[vertex, []] for vertex in vertices])
1234
if v in edge_vectors:
1235
edge_vector = mesh.vertices[edge.key[0]].co - \
1236
mesh.vertices[edge.key[1]].co
1237
if edge_vector.length < 1e-4:
1238
# zero-length edge, vertices at same location
1240
edge_vectors[v].append(edge_vector)
1242
# connection vectors between vertices of both loops
1243
connection_vectors = dict([[vertex, []] for vertex in vertices])
1244
connections = dict([[vertex, []] for vertex in vertices])
1245
for v1, v2 in lines:
1246
if v1 in connection_vectors or v2 in connection_vectors:
1247
new_vector = mesh.vertices[v1].co - mesh.vertices[v2].co
1248
if new_vector.length < 1e-4:
1249
# zero-length connection vector,
1250
# vertices in different loops at same location
1252
if v1 in connection_vectors:
1253
connection_vectors[v1].append(new_vector)
1254
connections[v1].append(v2)
1255
if v2 in connection_vectors:
1256
connection_vectors[v2].append(new_vector)
1257
connections[v2].append(v1)
1258
connection_vectors = average_vector_dictionary(connection_vectors)
1259
connection_vectors = dict([[vertex, vector[0]] if vector else \
1260
[vertex, []] for vertex, vector in connection_vectors.items()])
1262
for vertex, values in edge_vectors.items():
1263
# vertex normal doesn't matter, just assign a random vector to it
1264
if not connection_vectors[vertex]:
1265
vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1268
# calculate to what location the vertex is connected,
1269
# used to determine what way to flip the normal
1270
connected_center = mathutils.Vector()
1271
for v in connections[vertex]:
1272
connected_center += mesh.vertices[v].co
1273
if len(connections[vertex]) > 1:
1274
connected_center /= len(connections[vertex])
1275
if len(connections[vertex]) == 0:
1276
# shouldn't be possible, but better safe than sorry
1277
vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1280
# can't do proper calculations, because of zero-length vector
1282
if (connected_center - (mesh.vertices[vertex].co + \
1283
connection_vectors[vertex])).length < (connected_center - \
1284
(mesh.vertices[vertex].co - connection_vectors[vertex])).\
1286
connection_vectors[vertex].negate()
1287
vertex_normals[vertex] = [connection_vectors[vertex].\
1291
# calculate vertex normals using edge-vectors,
1292
# connection-vectors and the derived plane normal
1293
for edge_vector in values:
1294
plane_normal = edge_vector.cross(connection_vectors[vertex])
1295
vertex_normal = edge_vector.cross(plane_normal)
1296
vertex_normal.length = 0.1
1297
if (connected_center - (mesh.vertices[vertex].co + \
1298
vertex_normal)).length < (connected_center - \
1299
(mesh.vertices[vertex].co - vertex_normal)).length:
1300
# make normal face the correct way
1301
vertex_normal.negate()
1302
vertex_normal.normalize()
1303
vertex_normals[vertex].append(vertex_normal)
1305
# average virtual vertex normals, based on all edges it's connected to
1306
vertex_normals = average_vector_dictionary(vertex_normals)
1307
vertex_normals = dict([[vertex, vector[0]] for vertex, vector in \
1308
vertex_normals.items()])
1310
return(vertex_normals)
1313
# add vertices to mesh
1314
def bridge_create_vertices(mesh, vertices):
1315
start_index = len(mesh.vertices)
1316
mesh.vertices.add(len(vertices))
1317
for i in range(len(vertices)):
1318
mesh.vertices[start_index + i].co = vertices[i]
1322
def bridge_create_faces(mesh, faces, twist):
1323
# have the normal point the correct way
1325
[face.reverse() for face in faces]
1326
faces = [face[2:]+face[:2] if face[0]==face[1] else face for \
1329
# eekadoodle prevention
1330
for i in range(len(faces)):
1331
if not faces[i][-1]:
1332
if faces[i][0] == faces[i][-1]:
1333
faces[i] = [faces[i][1], faces[i][2], faces[i][3], faces[i][1]]
1335
faces[i] = [faces[i][-1]] + faces[i][:-1]
1337
start_faces = len(mesh.tessfaces)
1338
mesh.tessfaces.add(len(faces))
1339
for i in range(len(faces)):
1340
mesh.tessfaces[start_faces + i].vertices_raw = faces[i]
1341
mesh.update(calc_edges = True) # calc_edges prevents memory-corruption
1344
# calculate input loops
1345
def bridge_get_input(mesh):
1346
# create list of internal edges, which should be skipped
1347
eks_of_selected_faces = [item for sublist in [face.edge_keys for face \
1348
in mesh.tessfaces if face.select and not face.hide] for item in sublist]
1350
for ek in eks_of_selected_faces:
1351
if ek in edge_count:
1355
internal_edges = [ek for ek in edge_count if edge_count[ek] > 1]
1357
# sort correct edges into loops
1358
selected_edges = [edge.key for edge in mesh.edges if edge.select \
1359
and not edge.hide and edge.key not in internal_edges]
1360
loops = get_connected_selections(selected_edges)
1365
# return values needed by the bridge operator
1366
def bridge_initialise(mesh, interpolation):
1367
if interpolation == 'cubic':
1368
# dict with edge-key as key and list of connected valid faces as value
1369
face_blacklist = [face.index for face in mesh.tessfaces if face.select or \
1371
edge_faces = dict([[edge.key, []] for edge in mesh.edges if not \
1373
for face in mesh.tessfaces:
1374
if face.index in face_blacklist:
1376
for key in face.edge_keys:
1377
edge_faces[key].append(face)
1378
# dictionary with the edge-key as key and edge as value
1379
edgekey_to_edge = dict([[edge.key, edge] for edge in mesh.edges if \
1380
edge.select and not edge.hide])
1383
edgekey_to_edge = False
1385
# selected faces input
1386
old_selected_faces = [face.index for face in mesh.tessfaces if face.select \
1389
# find out if faces created by bridging should be smoothed
1392
if sum([face.use_smooth for face in mesh.tessfaces])/len(mesh.tessfaces) \
1396
return(edge_faces, edgekey_to_edge, old_selected_faces, smooth)
1399
# return a string with the input method
1400
def bridge_input_method(loft, loft_loop):
1404
method = "Loft loop"
1406
method = "Loft no-loop"
1413
# match up loops in pairs, used for multi-input bridging
1414
def bridge_match_loops(mesh, loops):
1415
# calculate average loop normals and centers
1418
for vertices, circular in loops:
1419
normal = mathutils.Vector()
1420
center = mathutils.Vector()
1421
for vertex in vertices:
1422
normal += mesh.vertices[vertex].normal
1423
center += mesh.vertices[vertex].co
1424
normals.append(normal / len(vertices) / 10)
1425
centers.append(center / len(vertices))
1427
# possible matches if loop normals are faced towards the center
1429
matches = dict([[i, []] for i in range(len(loops))])
1431
for i in range(len(loops) + 1):
1432
for j in range(i+1, len(loops)):
1433
if (centers[i] - centers[j]).length > (centers[i] - (centers[j] \
1434
+ normals[j])).length and (centers[j] - centers[i]).length > \
1435
(centers[j] - (centers[i] + normals[i])).length:
1437
matches[i].append([(centers[i] - centers[j]).length, i, j])
1438
matches[j].append([(centers[i] - centers[j]).length, j, i])
1439
# if no loops face each other, just make matches between all the loops
1440
if matches_amount == 0:
1441
for i in range(len(loops) + 1):
1442
for j in range(i+1, len(loops)):
1443
matches[i].append([(centers[i] - centers[j]).length, i, j])
1444
matches[j].append([(centers[i] - centers[j]).length, j, i])
1445
for key, value in matches.items():
1448
# matches based on distance between centers and number of vertices in loops
1450
for loop_index in range(len(loops)):
1451
if loop_index in new_order:
1453
loop_matches = matches[loop_index]
1454
if not loop_matches:
1456
shortest_distance = loop_matches[0][0]
1457
shortest_distance *= 1.1
1458
loop_matches = [[abs(len(loops[loop_index][0]) - \
1459
len(loops[loop[2]][0])), loop[0], loop[1], loop[2]] for loop in \
1460
loop_matches if loop[0] < shortest_distance]
1462
for match in loop_matches:
1463
if match[3] not in new_order:
1464
new_order += [loop_index, match[3]]
1467
# reorder loops based on matches
1468
if len(new_order) >= 2:
1469
loops = [loops[i] for i in new_order]
1474
# have normals of selection face outside
1475
def bridge_recalculate_normals():
1476
bpy.ops.object.mode_set(mode = 'EDIT')
1477
bpy.ops.mesh.normals_make_consistent()
1480
# remove old_selected_faces
1481
def bridge_remove_internal_faces(mesh, old_selected_faces):
1482
select_mode = [i for i in bpy.context.tool_settings.mesh_select_mode]
1483
bpy.context.tool_settings.mesh_select_mode = [False, False, True]
1485
# hack to keep track of the current selection
1486
for edge in mesh.edges:
1487
if edge.select and not edge.hide:
1488
edge.bevel_weight = (edge.bevel_weight/3) + 0.2
1490
edge.bevel_weight = (edge.bevel_weight/3) + 0.6
1493
bpy.ops.object.mode_set(mode = 'EDIT')
1494
bpy.ops.mesh.select_all(action = 'DESELECT')
1495
bpy.ops.object.mode_set(mode = 'OBJECT')
1496
for face in old_selected_faces:
1497
mesh.tessfaces[face].select = True
1498
bpy.ops.object.mode_set(mode = 'EDIT')
1499
bpy.ops.mesh.delete(type = 'FACE')
1501
# restore old selection, using hack
1502
bpy.ops.object.mode_set(mode = 'OBJECT')
1503
bpy.context.tool_settings.mesh_select_mode = [False, True, False]
1504
for edge in mesh.edges:
1505
if edge.bevel_weight < 0.6:
1506
edge.bevel_weight = (edge.bevel_weight-0.2) * 3
1509
edge.bevel_weight = (edge.bevel_weight-0.6) * 3
1510
bpy.ops.object.mode_set(mode = 'EDIT')
1511
bpy.ops.object.mode_set(mode = 'OBJECT')
1512
bpy.context.tool_settings.mesh_select_mode = select_mode
1515
# update list of internal faces that are flagged for removal
1516
def bridge_save_unused_faces(mesh, old_selected_faces, loops):
1517
# key: vertex index, value: lists of selected faces using it
1518
vertex_to_face = dict([[i, []] for i in range(len(mesh.vertices))])
1519
[[vertex_to_face[vertex_index].append(face) for vertex_index in \
1520
mesh.tessfaces[face].vertices] for face in old_selected_faces]
1522
# group selected faces that are connected
1525
for face in old_selected_faces:
1526
if face in grouped_faces:
1528
grouped_faces.append(face)
1532
grow_face = new_faces[0]
1533
for vertex in mesh.tessfaces[grow_face].vertices:
1534
vertex_face_group = [face for face in vertex_to_face[vertex] \
1535
if face not in grouped_faces]
1536
new_faces += vertex_face_group
1537
grouped_faces += vertex_face_group
1538
group += vertex_face_group
1540
groups.append(group)
1542
# key: vertex index, value: True/False (is it in a loop that is used)
1543
used_vertices = dict([[i, 0] for i in range(len(mesh.vertices))])
1545
for vertex in loop[0]:
1546
used_vertices[vertex] = True
1548
# check if group is bridged, if not remove faces from internal faces list
1549
for group in groups:
1554
for vertex in mesh.tessfaces[face].vertices:
1555
if used_vertices[vertex]:
1560
old_selected_faces.remove(face)
1563
# add the newly created faces to the selection
1564
def bridge_select_new_faces(mesh, amount, smooth):
1565
select_mode = [i for i in bpy.context.tool_settings.mesh_select_mode]
1566
bpy.context.tool_settings.mesh_select_mode = [False, False, True]
1567
for i in range(amount):
1568
mesh.tessfaces[-(i+1)].select = True
1569
mesh.tessfaces[-(i+1)].use_smooth = smooth
1570
bpy.ops.object.mode_set(mode = 'EDIT')
1571
bpy.ops.object.mode_set(mode = 'OBJECT')
1572
bpy.context.tool_settings.mesh_select_mode = select_mode
1575
# sort loops, so they are connected in the correct order when lofting
1576
def bridge_sort_loops(mesh, loops, loft_loop):
1577
# simplify loops to single points, and prepare for pathfinding
1578
x, y, z = [[sum([mesh.vertices[i].co[j] for i in loop[0]]) / \
1579
len(loop[0]) for loop in loops] for j in range(3)]
1580
nodes = [mathutils.Vector((x[i], y[i], z[i])) for i in range(len(loops))]
1583
open = [i for i in range(1, len(loops))]
1585
# connect node to path, that is shortest to active_node
1586
while len(open) > 0:
1587
distances = [(nodes[active_node] - nodes[i]).length for i in open]
1588
active_node = open[distances.index(min(distances))]
1589
open.remove(active_node)
1590
path.append([active_node, min(distances)])
1591
# check if we didn't start in the middle of the path
1592
for i in range(2, len(path)):
1593
if (nodes[path[i][0]]-nodes[0]).length < path[i][1]:
1596
path = path[:-i] + temp
1600
loops = [loops[i[0]] for i in path]
1601
# if requested, duplicate first loop at last position, so loft can loop
1603
loops = loops + [loops[0]]
1608
##########################################
1609
####### Circle functions #################
1610
##########################################
1612
# convert 3d coordinates to 2d coordinates on plane
1613
def circle_3d_to_2d(mesh_mod, loop, com, normal):
1614
# project vertices onto the plane
1615
verts = [mesh_mod.vertices[v] for v in loop[0]]
1616
verts_projected = [[v.co - (v.co - com).dot(normal) * normal, v.index]
1619
# calculate two vectors (p and q) along the plane
1620
m = mathutils.Vector((normal[0] + 1.0, normal[1], normal[2]))
1621
p = m - (m.dot(normal) * normal)
1623
m = mathutils.Vector((normal[0], normal[1] + 1.0, normal[2]))
1624
p = m - (m.dot(normal) * normal)
1627
# change to 2d coordinates using perpendicular projection
1629
for loc, vert in verts_projected:
1631
x = p.dot(vloc) / p.dot(p)
1632
y = q.dot(vloc) / q.dot(q)
1633
locs_2d.append([x, y, vert])
1635
return(locs_2d, p, q)
1638
# calculate a best-fit circle to the 2d locations on the plane
1639
def circle_calculate_best_fit(locs_2d):
1645
# calculate center and radius (non-linear least squares solution)
1646
for iter in range(500):
1650
d = (v[0]**2-2.0*x0*v[0]+v[1]**2-2.0*y0*v[1]+x0**2+y0**2)**0.5
1651
jmat.append([(x0-v[0])/d, (y0-v[1])/d, -1.0])
1652
k.append(-(((v[0]-x0)**2+(v[1]-y0)**2)**0.5-r))
1653
jmat2 = mathutils.Matrix(((0.0, 0.0, 0.0),
1657
k2 = mathutils.Vector((0.0, 0.0, 0.0))
1658
for i in range(len(jmat)):
1659
k2 += mathutils.Vector(jmat[i])*k[i]
1660
jmat2[0][0] += jmat[i][0]**2
1661
jmat2[1][0] += jmat[i][0]*jmat[i][1]
1662
jmat2[2][0] += jmat[i][0]*jmat[i][2]
1663
jmat2[1][1] += jmat[i][1]**2
1664
jmat2[2][1] += jmat[i][1]*jmat[i][2]
1665
jmat2[2][2] += jmat[i][2]**2
1666
jmat2[0][1] = jmat2[1][0]
1667
jmat2[0][2] = jmat2[2][0]
1668
jmat2[1][2] = jmat2[2][1]
1673
dx0, dy0, dr = jmat2 * k2
1677
# stop iterating if we're close enough to optimal solution
1678
if abs(dx0)<1e-6 and abs(dy0)<1e-6 and abs(dr)<1e-6:
1681
# return center of circle and radius
1685
# calculate circle so no vertices have to be moved away from the center
1686
def circle_calculate_min_fit(locs_2d):
1688
x0 = (min([i[0] for i in locs_2d])+max([i[0] for i in locs_2d]))/2.0
1689
y0 = (min([i[1] for i in locs_2d])+max([i[1] for i in locs_2d]))/2.0
1690
center = mathutils.Vector([x0, y0])
1692
r = min([(mathutils.Vector([i[0], i[1]])-center).length for i in locs_2d])
1694
# return center of circle and radius
1698
# calculate the new locations of the vertices that need to be moved
1699
def circle_calculate_verts(flatten, mesh_mod, locs_2d, com, p, q, normal):
1700
# changing 2d coordinates back to 3d coordinates
1703
locs_3d.append([loc[2], loc[0]*p + loc[1]*q + com])
1705
if flatten: # flat circle
1708
else: # project the locations on the existing mesh
1709
vert_edges = dict_vert_edges(mesh_mod)
1710
vert_faces = dict_vert_faces(mesh_mod)
1711
faces = [f for f in mesh_mod.tessfaces if not f.hide]
1712
rays = [normal, -normal]
1716
if mesh_mod.vertices[loc[0]].co == loc[1]: # vertex hasn't moved
1719
dif = normal.angle(loc[1]-mesh_mod.vertices[loc[0]].co)
1720
if -1e-6 < dif < 1e-6 or math.pi-1e-6 < dif < math.pi+1e-6:
1721
# original location is already along projection normal
1722
projection = mesh_mod.vertices[loc[0]].co
1724
# quick search through adjacent faces
1725
for face in vert_faces[loc[0]]:
1726
verts = [mesh_mod.vertices[v].co for v in \
1727
mesh_mod.tessfaces[face].vertices]
1728
if len(verts) == 3: # triangle
1732
v1, v2, v3, v4 = verts
1734
intersect = mathutils.geometry.\
1735
intersect_ray_tri(v1, v2, v3, ray, loc[1])
1737
projection = intersect
1740
intersect = mathutils.geometry.\
1741
intersect_ray_tri(v1, v3, v4, ray, loc[1])
1743
projection = intersect
1748
# check if projection is on adjacent edges
1749
for edgekey in vert_edges[loc[0]]:
1750
line1 = mesh_mod.vertices[edgekey[0]].co
1751
line2 = mesh_mod.vertices[edgekey[1]].co
1752
intersect, dist = mathutils.geometry.intersect_point_line(\
1753
loc[1], line1, line2)
1754
if 1e-6 < dist < 1 - 1e-6:
1755
projection = intersect
1758
# full search through the entire mesh
1761
verts = [mesh_mod.vertices[v].co for v in face.vertices]
1762
if len(verts) == 3: # triangle
1766
v1, v2, v3, v4 = verts
1768
intersect = mathutils.geometry.intersect_ray_tri(\
1769
v1, v2, v3, ray, loc[1])
1771
hits.append([(loc[1] - intersect).length,
1775
intersect = mathutils.geometry.intersect_ray_tri(\
1776
v1, v3, v4, ray, loc[1])
1778
hits.append([(loc[1] - intersect).length,
1782
# if more than 1 hit with mesh, closest hit is new loc
1784
projection = hits[0][1]
1786
# nothing to project on, remain at flat location
1788
new_locs.append([loc[0], projection])
1790
# return new positions of projected circle
1794
# check loops and only return valid ones
1795
def circle_check_loops(single_loops, loops, mapping, mesh_mod):
1796
valid_single_loops = {}
1798
for i, [loop, circular] in enumerate(loops):
1799
# loop needs to have at least 3 vertices
1802
# loop needs at least 1 vertex in the original, non-mirrored mesh
1806
if mapping[vert] > -1:
1811
# loop has to be non-collinear
1813
loc0 = mathutils.Vector(mesh_mod.vertices[loop[0]].co[:])
1814
loc1 = mathutils.Vector(mesh_mod.vertices[loop[1]].co[:])
1816
locn = mathutils.Vector(mesh_mod.vertices[v].co[:])
1817
if loc0 == loc1 or loc1 == locn:
1823
if -1e-6 < d1.angle(d2, 0) < 1e-6:
1831
# passed all tests, loop is valid
1832
valid_loops.append([loop, circular])
1833
valid_single_loops[len(valid_loops)-1] = single_loops[i]
1835
return(valid_single_loops, valid_loops)
1838
# calculate the location of single input vertices that need to be flattened
1839
def circle_flatten_singles(mesh_mod, com, p, q, normal, single_loop):
1841
for vert in single_loop:
1842
loc = mathutils.Vector(mesh_mod.vertices[vert].co[:])
1843
new_locs.append([vert, loc - (loc-com).dot(normal)*normal])
1848
# calculate input loops
1849
def circle_get_input(object, mesh, scene):
1850
# get mesh with modifiers applied
1851
derived, mesh_mod = get_derived_mesh(object, mesh, scene)
1853
# create list of edge-keys based on selection state
1855
for face in mesh.tessfaces:
1856
if face.select and not face.hide:
1860
# get selected, non-hidden , non-internal edge-keys
1861
eks_selected = [key for keys in [face.edge_keys for face in \
1862
mesh_mod.tessfaces if face.select and not face.hide] for key in keys]
1864
for ek in eks_selected:
1865
if ek in edge_count:
1869
edge_keys = [edge.key for edge in mesh_mod.edges if edge.select \
1870
and not edge.hide and edge_count.get(edge.key, 1)==1]
1872
# no faces, so no internal edges either
1873
edge_keys = [edge.key for edge in mesh_mod.edges if edge.select \
1876
# add edge-keys around single vertices
1877
verts_connected = dict([[vert, 1] for edge in [edge for edge in \
1878
mesh_mod.edges if edge.select and not edge.hide] for vert in edge.key])
1879
single_vertices = [vert.index for vert in mesh_mod.vertices if \
1880
vert.select and not vert.hide and not \
1881
verts_connected.get(vert.index, False)]
1883
if single_vertices and len(mesh.tessfaces)>0:
1884
vert_to_single = dict([[v.index, []] for v in mesh_mod.vertices \
1886
for face in [face for face in mesh_mod.tessfaces if not face.select \
1888
for vert in face.vertices:
1889
if vert in single_vertices:
1890
for ek in face.edge_keys:
1892
edge_keys.append(ek)
1893
if vert not in vert_to_single[ek[0]]:
1894
vert_to_single[ek[0]].append(vert)
1895
if vert not in vert_to_single[ek[1]]:
1896
vert_to_single[ek[1]].append(vert)
1899
# sort edge-keys into loops
1900
loops = get_connected_selections(edge_keys)
1902
# find out to which loops the single vertices belong
1903
single_loops = dict([[i, []] for i in range(len(loops))])
1904
if single_vertices and len(mesh.tessfaces)>0:
1905
for i, [loop, circular] in enumerate(loops):
1907
if vert_to_single[vert]:
1908
for single in vert_to_single[vert]:
1909
if single not in single_loops[i]:
1910
single_loops[i].append(single)
1912
return(derived, mesh_mod, single_vertices, single_loops, loops)
1915
# recalculate positions based on the influence of the circle shape
1916
def circle_influence_locs(locs_2d, new_locs_2d, influence):
1917
for i in range(len(locs_2d)):
1918
oldx, oldy, j = locs_2d[i]
1919
newx, newy, k = new_locs_2d[i]
1920
altx = newx*(influence/100)+ oldx*((100-influence)/100)
1921
alty = newy*(influence/100)+ oldy*((100-influence)/100)
1922
locs_2d[i] = [altx, alty, j]
1927
# project 2d locations on circle, respecting distance relations between verts
1928
def circle_project_non_regular(locs_2d, x0, y0, r):
1929
for i in range(len(locs_2d)):
1930
x, y, j = locs_2d[i]
1931
loc = mathutils.Vector([x-x0, y-y0])
1933
locs_2d[i] = [loc[0], loc[1], j]
1938
# project 2d locations on circle, with equal distance between all vertices
1939
def circle_project_regular(locs_2d, x0, y0, r):
1940
# find offset angle and circling direction
1941
x, y, i = locs_2d[0]
1942
loc = mathutils.Vector([x-x0, y-y0])
1944
offset_angle = loc.angle(mathutils.Vector([1.0, 0.0]), 0.0)
1945
loca = mathutils.Vector([x-x0, y-y0, 0.0])
1948
x, y, j = locs_2d[1]
1949
locb = mathutils.Vector([x-x0, y-y0, 0.0])
1950
if loca.cross(locb)[2] >= 0:
1954
# distribute vertices along the circle
1955
for i in range(len(locs_2d)):
1956
t = offset_angle + ccw * (i / len(locs_2d) * 2 * math.pi)
1959
locs_2d[i] = [x, y, locs_2d[i][2]]
1964
# shift loop, so the first vertex is closest to the center
1965
def circle_shift_loop(mesh_mod, loop, com):
1966
verts, circular = loop
1967
distances = [[(mesh_mod.vertices[vert].co - com).length, i] \
1968
for i, vert in enumerate(verts)]
1970
shift = distances[0][1]
1971
loop = [verts[shift:] + verts[:shift], circular]
1976
##########################################
1977
####### Curve functions ##################
1978
##########################################
1980
# create lists with knots and points, all correctly sorted
1981
def curve_calculate_knots(loop, verts_selected):
1982
knots = [v for v in loop[0] if v in verts_selected]
1984
# circular loop, potential for weird splines
1986
offset = int(len(loop[0]) / 4)
1989
kpos.append(loop[0].index(k))
1991
for i in range(len(kpos) - 1):
1992
kdif.append(kpos[i+1] - kpos[i])
1993
kdif.append(len(loop[0]) - kpos[-1] + kpos[0])
1997
kadd.append([kdif.index(k), True])
1998
# next 2 lines are optional, they insert
1999
# an extra control point in small gaps
2001
# kadd.append([kdif.index(k), False])
2004
for k in kadd: # extra knots to be added
2005
if k[1]: # big gap (break circular spline)
2006
kpos = loop[0].index(knots[k[0]]) + offset
2007
if kpos > len(loop[0]) - 1:
2008
kpos -= len(loop[0])
2009
kins.append([knots[k[0]], loop[0][kpos]])
2011
if kpos2 > len(knots)-1:
2013
kpos2 = loop[0].index(knots[kpos2]) - offset
2015
kpos2 += len(loop[0])
2016
kins.append([loop[0][kpos], loop[0][kpos2]])
2017
krot = loop[0][kpos2]
2018
else: # small gap (keep circular spline)
2019
k1 = loop[0].index(knots[k[0]])
2021
if k2 > len(knots)-1:
2023
k2 = loop[0].index(knots[k2])
2025
dif = len(loop[0]) - 1 - k1 + k2
2028
kn = k1 + int(dif/2)
2029
if kn > len(loop[0]) - 1:
2031
kins.append([loop[0][k1], loop[0][kn]])
2032
for j in kins: # insert new knots
2033
knots.insert(knots.index(j[0]) + 1, j[1])
2034
if not krot: # circular loop
2035
knots.append(knots[0])
2036
points = loop[0][loop[0].index(knots[0]):]
2037
points += loop[0][0:loop[0].index(knots[0]) + 1]
2038
else: # non-circular loop (broken by script)
2039
krot = knots.index(krot)
2040
knots = knots[krot:] + knots[0:krot]
2041
if loop[0].index(knots[0]) > loop[0].index(knots[-1]):
2042
points = loop[0][loop[0].index(knots[0]):]
2043
points += loop[0][0:loop[0].index(knots[-1])+1]
2045
points = loop[0][loop[0].index(knots[0]):\
2046
loop[0].index(knots[-1]) + 1]
2047
# non-circular loop, add first and last point as knots
2049
if loop[0][0] not in knots:
2050
knots.insert(0, loop[0][0])
2051
if loop[0][-1] not in knots:
2052
knots.append(loop[0][-1])
2054
return(knots, points)
2057
# calculate relative positions compared to first knot
2058
def curve_calculate_t(mesh_mod, knots, points, pknots, regular, circular):
2065
loc = pknots[knots.index(p)] # use projected knot location
2067
loc = mathutils.Vector(mesh_mod.vertices[p].co[:])
2070
len_total += (loc-loc_prev).length
2071
tpoints.append(len_total)
2076
tknots.append(tpoints[points.index(p)])
2078
tknots[-1] = tpoints[-1]
2082
tpoints_average = tpoints[-1] / (len(tpoints) - 1)
2083
for i in range(1, len(tpoints) - 1):
2084
tpoints[i] = i * tpoints_average
2085
for i in range(len(knots)):
2086
tknots[i] = tpoints[points.index(knots[i])]
2088
tknots[-1] = tpoints[-1]
2091
return(tknots, tpoints)
2094
# change the location of non-selected points to their place on the spline
2095
def curve_calculate_vertices(mesh_mod, knots, tknots, points, tpoints, splines,
2096
interpolation, restriction):
2103
m = tpoints[points.index(p)]
2111
if n > len(splines) - 1:
2112
n = len(splines) - 1
2116
if interpolation == 'cubic':
2117
ax, bx, cx, dx, tx = splines[n][0]
2118
x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
2119
ay, by, cy, dy, ty = splines[n][1]
2120
y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
2121
az, bz, cz, dz, tz = splines[n][2]
2122
z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
2123
newloc = mathutils.Vector([x,y,z])
2124
else: # interpolation == 'linear'
2125
a, d, t, u = splines[n]
2126
newloc = ((m-t)/u)*d + a
2128
if restriction != 'none': # vertex movement is restricted
2130
else: # set the vertex to its new location
2131
move.append([p, newloc])
2133
if restriction != 'none': # vertex movement is restricted
2138
move.append([p, mesh_mod.vertices[p].co])
2140
oldloc = mesh_mod.vertices[p].co
2141
normal = mesh_mod.vertices[p].normal
2142
dloc = newloc - oldloc
2143
if dloc.length < 1e-6:
2144
move.append([p, newloc])
2145
elif restriction == 'extrude': # only extrusions
2146
if dloc.angle(normal, 0) < 0.5 * math.pi + 1e-6:
2147
move.append([p, newloc])
2148
else: # restriction == 'indent' only indentations
2149
if dloc.angle(normal) > 0.5 * math.pi - 1e-6:
2150
move.append([p, newloc])
2155
# trim loops to part between first and last selected vertices (including)
2156
def curve_cut_boundaries(mesh_mod, loops):
2158
for loop, circular in loops:
2161
cut_loops.append([loop, circular])
2163
selected = [mesh_mod.vertices[v].select for v in loop]
2164
first = selected.index(True)
2166
last = -selected.index(True)
2168
cut_loops.append([loop[first:], circular])
2170
cut_loops.append([loop[first:last], circular])
2175
# calculate input loops
2176
def curve_get_input(object, mesh, boundaries, scene):
2177
# get mesh with modifiers applied
2178
derived, mesh_mod = get_derived_mesh(object, mesh, scene)
2180
# vertices that still need a loop to run through it
2181
verts_unsorted = [v.index for v in mesh_mod.vertices if \
2182
v.select and not v.hide]
2183
# necessary dictionaries
2184
vert_edges = dict_vert_edges(mesh_mod)
2185
edge_faces = dict_edge_faces(mesh_mod)
2188
# find loops through each selected vertex
2189
while len(verts_unsorted) > 0:
2190
loops = curve_vertex_loops(mesh_mod, verts_unsorted[0], vert_edges,
2192
verts_unsorted.pop(0)
2194
# check if loop is fully selected
2195
search_perpendicular = False
2197
for loop, circular in loops:
2199
selected = [v for v in loop if mesh_mod.vertices[v].select]
2200
if len(selected) < 2:
2201
# only one selected vertex on loop, don't use
2204
elif len(selected) == len(loop):
2205
search_perpendicular = loop
2207
# entire loop is selected, find perpendicular loops
2208
if search_perpendicular:
2210
if vert in verts_unsorted:
2211
verts_unsorted.remove(vert)
2212
perp_loops = curve_perpendicular_loops(mesh_mod, loop,
2213
vert_edges, edge_faces)
2214
for perp_loop in perp_loops:
2215
correct_loops.append(perp_loop)
2218
for loop, circular in loops:
2219
correct_loops.append([loop, circular])
2223
correct_loops = curve_cut_boundaries(mesh_mod, correct_loops)
2225
return(derived, mesh_mod, correct_loops)
2228
# return all loops that are perpendicular to the given one
2229
def curve_perpendicular_loops(mesh_mod, start_loop, vert_edges, edge_faces):
2230
# find perpendicular loops
2232
for start_vert in start_loop:
2233
loops = curve_vertex_loops(mesh_mod, start_vert, vert_edges,
2235
for loop, circular in loops:
2236
selected = [v for v in loop if mesh_mod.vertices[v].select]
2237
if len(selected) == len(loop):
2240
perp_loops.append([loop, circular, loop.index(start_vert)])
2242
# trim loops to same lengths
2243
shortest = [[len(loop[0]), i] for i, loop in enumerate(perp_loops)\
2246
# all loops are circular, not trimming
2247
return([[loop[0], loop[1]] for loop in perp_loops])
2249
shortest = min(shortest)
2250
shortest_start = perp_loops[shortest[1]][2]
2251
before_start = shortest_start
2252
after_start = shortest[0] - shortest_start - 1
2253
bigger_before = before_start > after_start
2255
for loop in perp_loops:
2256
# have the loop face the same direction as the shortest one
2258
if loop[2] < len(loop[0]) / 2:
2260
loop[2] = len(loop[0]) - loop[2] - 1
2262
if loop[2] > len(loop[0]) / 2:
2264
loop[2] = len(loop[0]) - loop[2] - 1
2265
# circular loops can shift, to prevent wrong trimming
2267
shift = shortest_start - loop[2]
2268
if loop[2] + shift > 0 and loop[2] + shift < len(loop[0]):
2269
loop[0] = loop[0][-shift:] + loop[0][:-shift]
2272
loop[2] += len(loop[0])
2273
elif loop[2] > len(loop[0]) -1:
2274
loop[2] -= len(loop[0])
2276
start = max(0, loop[2] - before_start)
2277
end = min(len(loop[0]), loop[2] + after_start + 1)
2278
trimmed_loops.append([loop[0][start:end], False])
2280
return(trimmed_loops)
2283
# project knots on non-selected geometry
2284
def curve_project_knots(mesh_mod, verts_selected, knots, points, circular):
2285
# function to project vertex on edge
2286
def project(v1, v2, v3):
2287
# v1 and v2 are part of a line
2288
# v3 is projected onto it
2294
if circular: # project all knots
2298
else: # first and last knot shouldn't be projected
2301
pknots = [mathutils.Vector(mesh_mod.vertices[knots[0]].co[:])]
2302
for knot in knots[start:end]:
2303
if knot in verts_selected:
2304
knot_left = knot_right = False
2305
for i in range(points.index(knot)-1, -1*len(points), -1):
2306
if points[i] not in knots:
2307
knot_left = points[i]
2309
for i in range(points.index(knot)+1, 2*len(points)):
2310
if i > len(points) - 1:
2312
if points[i] not in knots:
2313
knot_right = points[i]
2315
if knot_left and knot_right and knot_left != knot_right:
2316
knot_left = mathutils.Vector(\
2317
mesh_mod.vertices[knot_left].co[:])
2318
knot_right = mathutils.Vector(\
2319
mesh_mod.vertices[knot_right].co[:])
2320
knot = mathutils.Vector(mesh_mod.vertices[knot].co[:])
2321
pknots.append(project(knot_left, knot_right, knot))
2323
pknots.append(mathutils.Vector(mesh_mod.vertices[knot].co[:]))
2324
else: # knot isn't selected, so shouldn't be changed
2325
pknots.append(mathutils.Vector(mesh_mod.vertices[knot].co[:]))
2327
pknots.append(mathutils.Vector(mesh_mod.vertices[knots[-1]].co[:]))
2332
# find all loops through a given vertex
2333
def curve_vertex_loops(mesh_mod, start_vert, vert_edges, edge_faces):
2337
for edge in vert_edges[start_vert]:
2338
if edge in edges_used:
2343
active_faces = edge_faces[edge]
2348
new_edges = vert_edges[new_vert]
2349
loop.append(new_vert)
2351
edges_used.append(tuple(sorted([loop[-1], loop[-2]])))
2352
if len(new_edges) < 3 or len(new_edges) > 4:
2357
for new_edge in new_edges:
2358
if new_edge in edges_used:
2361
for new_face in edge_faces[new_edge]:
2362
if new_face in active_faces:
2367
# found correct new edge
2368
active_faces = edge_faces[new_edge]
2374
if new_vert == loop[0]:
2382
loops.append([loop, circular])
2387
##########################################
2388
####### Flatten functions ################
2389
##########################################
2391
# sort input into loops
2392
def flatten_get_input(mesh):
2393
vert_verts = dict_vert_verts([edge.key for edge in mesh.edges \
2394
if edge.select and not edge.hide])
2395
verts = [v.index for v in mesh.vertices if v.select and not v.hide]
2397
# no connected verts, consider all selected verts as a single input
2399
return([[verts, False]])
2402
while len(verts) > 0:
2406
if loop[-1] in vert_verts:
2407
to_grow = vert_verts[loop[-1]]
2411
while len(to_grow) > 0:
2412
new_vert = to_grow[0]
2414
if new_vert in loop:
2416
loop.append(new_vert)
2417
verts.remove(new_vert)
2418
to_grow += vert_verts[new_vert]
2420
loops.append([loop, False])
2425
# calculate position of vertex projections on plane
2426
def flatten_project(mesh, loop, com, normal):
2427
verts = [mesh.vertices[v] for v in loop[0]]
2428
verts_projected = [[v.index, mathutils.Vector(v.co[:]) - \
2429
(mathutils.Vector(v.co[:])-com).dot(normal)*normal] for v in verts]
2431
return(verts_projected)
2434
##########################################
2435
####### Relax functions ##################
2436
##########################################
2438
# create lists with knots and points, all correctly sorted
2439
def relax_calculate_knots(loops):
2442
for loop, circular in loops:
2446
if len(loop)%2 == 1: # odd
2447
extend = [False, True, 0, 1, 0, 1]
2449
extend = [True, False, 0, 1, 1, 2]
2451
if len(loop)%2 == 1: # odd
2452
extend = [False, False, 0, 1, 1, 2]
2454
extend = [False, False, 0, 1, 1, 2]
2457
loop = [loop[-1]] + loop + [loop[0]]
2458
for i in range(extend[2+2*j], len(loop), 2):
2459
knots[j].append(loop[i])
2460
for i in range(extend[3+2*j], len(loop), 2):
2461
if loop[i] == loop[-1] and not circular:
2463
if len(points[j]) == 0:
2464
points[j].append(loop[i])
2465
elif loop[i] != points[j][0]:
2466
points[j].append(loop[i])
2468
if knots[j][0] != knots[j][-1]:
2469
knots[j].append(knots[j][0])
2470
if len(points[1]) == 0:
2476
all_points.append(p)
2478
return(all_knots, all_points)
2481
# calculate relative positions compared to first knot
2482
def relax_calculate_t(mesh_mod, knots, points, regular):
2485
for i in range(len(knots)):
2486
amount = len(knots[i]) + len(points[i])
2488
for j in range(amount):
2490
mix.append([True, knots[i][round(j/2)]])
2492
mix.append([True, knots[i][-1]])
2494
mix.append([False, points[i][int(j/2)]])
2500
loc = mathutils.Vector(mesh_mod.vertices[m[1]].co[:])
2503
len_total += (loc - loc_prev).length
2505
tknots.append(len_total)
2507
tpoints.append(len_total)
2511
for p in range(len(points[i])):
2512
tpoints.append((tknots[p] + tknots[p+1]) / 2)
2513
all_tknots.append(tknots)
2514
all_tpoints.append(tpoints)
2516
return(all_tknots, all_tpoints)
2519
# change the location of the points to their place on the spline
2520
def relax_calculate_verts(mesh_mod, interpolation, tknots, knots, tpoints,
2524
for i in range(len(knots)):
2526
m = tpoints[i][points[i].index(p)]
2528
n = tknots[i].index(m)
2534
if n > len(splines[i]) - 1:
2535
n = len(splines[i]) - 1
2539
if interpolation == 'cubic':
2540
ax, bx, cx, dx, tx = splines[i][n][0]
2541
x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
2542
ay, by, cy, dy, ty = splines[i][n][1]
2543
y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
2544
az, bz, cz, dz, tz = splines[i][n][2]
2545
z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
2546
change.append([p, mathutils.Vector([x,y,z])])
2547
else: # interpolation == 'linear'
2548
a, d, t, u = splines[i][n]
2551
change.append([p, ((m-t)/u)*d + a])
2553
move.append([c[0], (mesh_mod.vertices[c[0]].co + c[1]) / 2])
2558
##########################################
2559
####### Space functions ##################
2560
##########################################
2562
# calculate relative positions compared to first knot
2563
def space_calculate_t(mesh_mod, knots):
2568
loc = mathutils.Vector(mesh_mod.vertices[k].co[:])
2571
len_total += (loc - loc_prev).length
2572
tknots.append(len_total)
2575
t_per_segment = len_total / (amount - 1)
2576
tpoints = [i * t_per_segment for i in range(amount)]
2578
return(tknots, tpoints)
2581
# change the location of the points to their place on the spline
2582
def space_calculate_verts(mesh_mod, interpolation, tknots, tpoints, points,
2586
m = tpoints[points.index(p)]
2594
if n > len(splines) - 1:
2595
n = len(splines) - 1
2599
if interpolation == 'cubic':
2600
ax, bx, cx, dx, tx = splines[n][0]
2601
x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
2602
ay, by, cy, dy, ty = splines[n][1]
2603
y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
2604
az, bz, cz, dz, tz = splines[n][2]
2605
z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
2606
move.append([p, mathutils.Vector([x,y,z])])
2607
else: # interpolation == 'linear'
2608
a, d, t, u = splines[n]
2609
move.append([p, ((m-t)/u)*d + a])
2614
##########################################
2615
####### Operators ########################
2616
##########################################
2619
class Bridge(bpy.types.Operator):
2620
bl_idname = 'mesh.looptools_bridge'
2621
bl_label = "Bridge / Loft"
2622
bl_description = "Bridge two, or loft several, loops of vertices"
2623
bl_options = {'REGISTER', 'UNDO'}
2625
cubic_strength = bpy.props.FloatProperty(name = "Strength",
2626
description = "Higher strength results in more fluid curves",
2630
interpolation = bpy.props.EnumProperty(name = "Interpolation mode",
2631
items = (('cubic', "Cubic", "Gives curved results"),
2632
('linear', "Linear", "Basic, fast, straight interpolation")),
2633
description = "Interpolation mode: algorithm used when creating "\
2636
loft = bpy.props.BoolProperty(name = "Loft",
2637
description = "Loft multiple loops, instead of considering them as "\
2638
"a multi-input for bridging",
2640
loft_loop = bpy.props.BoolProperty(name = "Loop",
2641
description = "Connect the first and the last loop with each other",
2643
min_width = bpy.props.IntProperty(name = "Minimum width",
2644
description = "Segments with an edge smaller than this are merged "\
2645
"(compared to base edge)",
2649
subtype = 'PERCENTAGE')
2650
mode = bpy.props.EnumProperty(name = "Mode",
2651
items = (('basic', "Basic", "Fast algorithm"), ('shortest',
2652
"Shortest edge", "Slower algorithm with better vertex matching")),
2653
description = "Algorithm used for bridging",
2654
default = 'shortest')
2655
remove_faces = bpy.props.BoolProperty(name = "Remove faces",
2656
description = "Remove faces that are internal after bridging",
2658
reverse = bpy.props.BoolProperty(name = "Reverse",
2659
description = "Manually override the direction in which the loops "\
2660
"are bridged. Only use if the tool gives the wrong " \
2663
segments = bpy.props.IntProperty(name = "Segments",
2664
description = "Number of segments used to bridge the gap "\
2669
twist = bpy.props.IntProperty(name = "Twist",
2670
description = "Twist what vertices are connected to each other",
2674
def poll(cls, context):
2675
ob = context.active_object
2676
return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
2678
def draw(self, context):
2679
layout = self.layout
2680
#layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
2683
col_top = layout.column(align=True)
2684
row = col_top.row(align=True)
2685
col_left = row.column(align=True)
2686
col_right = row.column(align=True)
2687
col_right.active = self.segments != 1
2688
col_left.prop(self, "segments")
2689
col_right.prop(self, "min_width", text="")
2691
bottom_left = col_left.row()
2692
bottom_left.active = self.segments != 1
2693
bottom_left.prop(self, "interpolation", text="")
2694
bottom_right = col_right.row()
2695
bottom_right.active = self.interpolation == 'cubic'
2696
bottom_right.prop(self, "cubic_strength")
2697
# boolean properties
2698
col_top.prop(self, "remove_faces")
2700
col_top.prop(self, "loft_loop")
2702
# override properties
2704
row = layout.row(align = True)
2705
row.prop(self, "twist")
2706
row.prop(self, "reverse")
2708
def invoke(self, context, event):
2709
# load custom settings
2710
context.window_manager.looptools.bridge_loft = self.loft
2712
return self.execute(context)
2714
def execute(self, context):
2716
global_undo, object, mesh = initialise()
2717
edge_faces, edgekey_to_edge, old_selected_faces, smooth = \
2718
bridge_initialise(mesh, self.interpolation)
2719
settings_write(self)
2721
# check cache to see if we can save time
2722
input_method = bridge_input_method(self.loft, self.loft_loop)
2723
cached, single_loops, loops, derived, mapping = cache_read("Bridge",
2724
object, mesh, input_method, False)
2727
loops = bridge_get_input(mesh)
2729
# reorder loops if there are more than 2
2732
loops = bridge_sort_loops(mesh, loops, self.loft_loop)
2734
loops = bridge_match_loops(mesh, loops)
2736
# saving cache for faster execution next time
2738
cache_write("Bridge", object, mesh, input_method, False, False,
2739
loops, False, False)
2742
# calculate new geometry
2745
max_vert_index = len(mesh.vertices)-1
2746
for i in range(1, len(loops)):
2747
if not self.loft and i%2 == 0:
2749
lines = bridge_calculate_lines(mesh, loops[i-1:i+1],
2750
self.mode, self.twist, self.reverse)
2751
vertex_normals = bridge_calculate_virtual_vertex_normals(mesh,
2752
lines, loops[i-1:i+1], edge_faces, edgekey_to_edge)
2753
segments = bridge_calculate_segments(mesh, lines,
2754
loops[i-1:i+1], self.segments)
2755
new_verts, new_faces, max_vert_index = \
2756
bridge_calculate_geometry(mesh, lines, vertex_normals,
2757
segments, self.interpolation, self.cubic_strength,
2758
self.min_width, max_vert_index)
2760
vertices += new_verts
2763
# make sure faces in loops that aren't used, aren't removed
2764
if self.remove_faces and old_selected_faces:
2765
bridge_save_unused_faces(mesh, old_selected_faces, loops)
2768
bridge_create_vertices(mesh, vertices)
2771
bridge_create_faces(mesh, faces, self.twist)
2772
bridge_select_new_faces(mesh, len(faces), smooth)
2773
# edge-data could have changed, can't use cache next run
2774
if faces and not vertices:
2775
cache_delete("Bridge")
2776
# delete internal faces
2777
if self.remove_faces and old_selected_faces:
2778
bridge_remove_internal_faces(mesh, old_selected_faces)
2779
# make sure normals are facing outside
2780
bridge_recalculate_normals()
2782
terminate(global_undo)
2787
class Circle(bpy.types.Operator):
2788
bl_idname = "mesh.looptools_circle"
2790
bl_description = "Move selected vertices into a circle shape"
2791
bl_options = {'REGISTER', 'UNDO'}
2793
custom_radius = bpy.props.BoolProperty(name = "Radius",
2794
description = "Force a custom radius",
2796
fit = bpy.props.EnumProperty(name = "Method",
2797
items = (("best", "Best fit", "Non-linear least squares"),
2798
("inside", "Fit inside","Only move vertices towards the center")),
2799
description = "Method used for fitting a circle to the vertices",
2801
flatten = bpy.props.BoolProperty(name = "Flatten",
2802
description = "Flatten the circle, instead of projecting it on the " \
2805
influence = bpy.props.FloatProperty(name = "Influence",
2806
description = "Force of the tool",
2811
subtype = 'PERCENTAGE')
2812
radius = bpy.props.FloatProperty(name = "Radius",
2813
description = "Custom radius for circle",
2817
regular = bpy.props.BoolProperty(name = "Regular",
2818
description = "Distribute vertices at constant distances along the " \
2823
def poll(cls, context):
2824
ob = context.active_object
2825
return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
2827
def draw(self, context):
2828
layout = self.layout
2829
col = layout.column()
2831
col.prop(self, "fit")
2834
col.prop(self, "flatten")
2835
row = col.row(align=True)
2836
row.prop(self, "custom_radius")
2837
row_right = row.row(align=True)
2838
row_right.active = self.custom_radius
2839
row_right.prop(self, "radius", text="")
2840
col.prop(self, "regular")
2843
col.prop(self, "influence")
2845
def invoke(self, context, event):
2846
# load custom settings
2848
return self.execute(context)
2850
def execute(self, context):
2852
global_undo, object, mesh = initialise()
2853
settings_write(self)
2854
# check cache to see if we can save time
2855
cached, single_loops, loops, derived, mapping = cache_read("Circle",
2856
object, mesh, False, False)
2858
derived, mesh_mod = get_derived_mesh(object, mesh, context.scene)
2861
derived, mesh_mod, single_vertices, single_loops, loops = \
2862
circle_get_input(object, mesh, context.scene)
2863
mapping = get_mapping(derived, mesh, mesh_mod, single_vertices,
2865
single_loops, loops = circle_check_loops(single_loops, loops,
2868
# saving cache for faster execution next time
2870
cache_write("Circle", object, mesh, False, False, single_loops,
2871
loops, derived, mapping)
2874
for i, loop in enumerate(loops):
2875
# best fitting flat plane
2876
com, normal = calculate_plane(mesh_mod, loop)
2877
# if circular, shift loop so we get a good starting vertex
2879
loop = circle_shift_loop(mesh_mod, loop, com)
2880
# flatten vertices on plane
2881
locs_2d, p, q = circle_3d_to_2d(mesh_mod, loop, com, normal)
2883
if self.fit == 'best':
2884
x0, y0, r = circle_calculate_best_fit(locs_2d)
2885
else: # self.fit == 'inside'
2886
x0, y0, r = circle_calculate_min_fit(locs_2d)
2888
if self.custom_radius:
2889
r = self.radius / p.length
2890
# calculate positions on circle
2892
new_locs_2d = circle_project_regular(locs_2d[:], x0, y0, r)
2894
new_locs_2d = circle_project_non_regular(locs_2d[:], x0, y0, r)
2895
# take influence into account
2896
locs_2d = circle_influence_locs(locs_2d, new_locs_2d,
2898
# calculate 3d positions of the created 2d input
2899
move.append(circle_calculate_verts(self.flatten, mesh_mod,
2900
locs_2d, com, p, q, normal))
2901
# flatten single input vertices on plane defined by loop
2902
if self.flatten and single_loops:
2903
move.append(circle_flatten_singles(mesh_mod, com, p, q,
2904
normal, single_loops[i]))
2906
# move vertices to new locations
2907
move_verts(mesh, mapping, move, -1)
2911
bpy.context.blend_data.meshes.remove(mesh_mod)
2912
terminate(global_undo)
2918
class Curve(bpy.types.Operator):
2919
bl_idname = "mesh.looptools_curve"
2921
bl_description = "Turn a loop into a smooth curve"
2922
bl_options = {'REGISTER', 'UNDO'}
2924
boundaries = bpy.props.BoolProperty(name = "Boundaries",
2925
description = "Limit the tool to work within the boundaries of the "\
2926
"selected vertices",
2928
influence = bpy.props.FloatProperty(name = "Influence",
2929
description = "Force of the tool",
2934
subtype = 'PERCENTAGE')
2935
interpolation = bpy.props.EnumProperty(name = "Interpolation",
2936
items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
2937
("linear", "Linear", "Simple and fast linear algorithm")),
2938
description = "Algorithm used for interpolation",
2940
regular = bpy.props.BoolProperty(name = "Regular",
2941
description = "Distribute vertices at constant distances along the" \
2944
restriction = bpy.props.EnumProperty(name = "Restriction",
2945
items = (("none", "None", "No restrictions on vertex movement"),
2946
("extrude", "Extrude only","Only allow extrusions (no "\
2948
("indent", "Indent only", "Only allow indentation (no "\
2950
description = "Restrictions on how the vertices can be moved",
2954
def poll(cls, context):
2955
ob = context.active_object
2956
return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
2958
def draw(self, context):
2959
layout = self.layout
2960
col = layout.column()
2962
col.prop(self, "interpolation")
2963
col.prop(self, "restriction")
2964
col.prop(self, "boundaries")
2965
col.prop(self, "regular")
2968
col.prop(self, "influence")
2970
def invoke(self, context, event):
2971
# load custom settings
2973
return self.execute(context)
2975
def execute(self, context):
2977
global_undo, object, mesh = initialise()
2978
settings_write(self)
2979
# check cache to see if we can save time
2980
cached, single_loops, loops, derived, mapping = cache_read("Curve",
2981
object, mesh, False, self.boundaries)
2983
derived, mesh_mod = get_derived_mesh(object, mesh, context.scene)
2986
derived, mesh_mod, loops = curve_get_input(object, mesh,
2987
self.boundaries, context.scene)
2988
mapping = get_mapping(derived, mesh, mesh_mod, False, True, loops)
2989
loops = check_loops(loops, mapping, mesh_mod)
2990
verts_selected = [v.index for v in mesh_mod.vertices if v.select \
2993
# saving cache for faster execution next time
2995
cache_write("Curve", object, mesh, False, self.boundaries, False,
2996
loops, derived, mapping)
3000
knots, points = curve_calculate_knots(loop, verts_selected)
3001
pknots = curve_project_knots(mesh_mod, verts_selected, knots,
3003
tknots, tpoints = curve_calculate_t(mesh_mod, knots, points,
3004
pknots, self.regular, loop[1])
3005
splines = calculate_splines(self.interpolation, mesh_mod,
3007
move.append(curve_calculate_vertices(mesh_mod, knots, tknots,
3008
points, tpoints, splines, self.interpolation,
3011
# move vertices to new locations
3012
move_verts(mesh, mapping, move, self.influence)
3016
bpy.context.blend_data.meshes.remove(mesh_mod)
3018
terminate(global_undo)
3023
class Flatten(bpy.types.Operator):
3024
bl_idname = "mesh.looptools_flatten"
3025
bl_label = "Flatten"
3026
bl_description = "Flatten vertices on a best-fitting plane"
3027
bl_options = {'REGISTER', 'UNDO'}
3029
influence = bpy.props.FloatProperty(name = "Influence",
3030
description = "Force of the tool",
3035
subtype = 'PERCENTAGE')
3036
plane = bpy.props.EnumProperty(name = "Plane",
3037
items = (("best_fit", "Best fit", "Calculate a best fitting plane"),
3038
("normal", "Normal", "Derive plane from averaging vertex "\
3040
("view", "View", "Flatten on a plane perpendicular to the "\
3042
description = "Plane on which vertices are flattened",
3043
default = 'best_fit')
3044
restriction = bpy.props.EnumProperty(name = "Restriction",
3045
items = (("none", "None", "No restrictions on vertex movement"),
3046
("bounding_box", "Bounding box", "Vertices are restricted to "\
3047
"movement inside the bounding box of the selection")),
3048
description = "Restrictions on how the vertices can be moved",
3052
def poll(cls, context):
3053
ob = context.active_object
3054
return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3056
def draw(self, context):
3057
layout = self.layout
3058
col = layout.column()
3060
col.prop(self, "plane")
3061
#col.prop(self, "restriction")
3064
col.prop(self, "influence")
3066
def invoke(self, context, event):
3067
# load custom settings
3069
return self.execute(context)
3071
def execute(self, context):
3073
global_undo, object, mesh = initialise()
3074
settings_write(self)
3075
# check cache to see if we can save time
3076
cached, single_loops, loops, derived, mapping = cache_read("Flatten",
3077
object, mesh, False, False)
3079
# order input into virtual loops
3080
loops = flatten_get_input(mesh)
3081
loops = check_loops(loops, mapping, mesh)
3083
# saving cache for faster execution next time
3085
cache_write("Flatten", object, mesh, False, False, False, loops,
3090
# calculate plane and position of vertices on them
3091
com, normal = calculate_plane(mesh, loop, method=self.plane,
3093
to_move = flatten_project(mesh, loop, com, normal)
3094
if self.restriction == 'none':
3095
move.append(to_move)
3097
move.append(to_move)
3098
move_verts(mesh, False, move, self.influence)
3100
terminate(global_undo)
3105
class Relax(bpy.types.Operator):
3106
bl_idname = "mesh.looptools_relax"
3108
bl_description = "Relax the loop, so it is smoother"
3109
bl_options = {'REGISTER', 'UNDO'}
3111
input = bpy.props.EnumProperty(name = "Input",
3112
items = (("all", "Parallel (all)", "Also use non-selected "\
3113
"parallel loops as input"),
3114
("selected", "Selection","Only use selected vertices as input")),
3115
description = "Loops that are relaxed",
3116
default = 'selected')
3117
interpolation = bpy.props.EnumProperty(name = "Interpolation",
3118
items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3119
("linear", "Linear", "Simple and fast linear algorithm")),
3120
description = "Algorithm used for interpolation",
3122
iterations = bpy.props.EnumProperty(name = "Iterations",
3123
items = (("1", "1", "One"),
3124
("3", "3", "Three"),
3126
("10", "10", "Ten"),
3127
("25", "25", "Twenty-five")),
3128
description = "Number of times the loop is relaxed",
3130
regular = bpy.props.BoolProperty(name = "Regular",
3131
description = "Distribute vertices at constant distances along the" \
3136
def poll(cls, context):
3137
ob = context.active_object
3138
return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3140
def draw(self, context):
3141
layout = self.layout
3142
col = layout.column()
3144
col.prop(self, "interpolation")
3145
col.prop(self, "input")
3146
col.prop(self, "iterations")
3147
col.prop(self, "regular")
3149
def invoke(self, context, event):
3150
# load custom settings
3152
return self.execute(context)
3154
def execute(self, context):
3156
global_undo, object, mesh = initialise()
3157
settings_write(self)
3158
# check cache to see if we can save time
3159
cached, single_loops, loops, derived, mapping = cache_read("Relax",
3160
object, mesh, self.input, False)
3162
derived, mesh_mod = get_derived_mesh(object, mesh, context.scene)
3165
derived, mesh_mod, loops = get_connected_input(object, mesh,
3166
context.scene, self.input)
3167
mapping = get_mapping(derived, mesh, mesh_mod, False, False, loops)
3168
loops = check_loops(loops, mapping, mesh_mod)
3169
knots, points = relax_calculate_knots(loops)
3171
# saving cache for faster execution next time
3173
cache_write("Relax", object, mesh, self.input, False, False, loops,
3176
for iteration in range(int(self.iterations)):
3177
# calculate splines and new positions
3178
tknots, tpoints = relax_calculate_t(mesh_mod, knots, points,
3181
for i in range(len(knots)):
3182
splines.append(calculate_splines(self.interpolation, mesh_mod,
3183
tknots[i], knots[i]))
3184
move = [relax_calculate_verts(mesh_mod, self.interpolation,
3185
tknots, knots, tpoints, points, splines)]
3186
move_verts(mesh, mapping, move, -1)
3190
bpy.context.blend_data.meshes.remove(mesh_mod)
3191
terminate(global_undo)
3197
class Space(bpy.types.Operator):
3198
bl_idname = "mesh.looptools_space"
3200
bl_description = "Space the vertices in a regular distrubtion on the loop"
3201
bl_options = {'REGISTER', 'UNDO'}
3203
influence = bpy.props.FloatProperty(name = "Influence",
3204
description = "Force of the tool",
3209
subtype = 'PERCENTAGE')
3210
input = bpy.props.EnumProperty(name = "Input",
3211
items = (("all", "Parallel (all)", "Also use non-selected "\
3212
"parallel loops as input"),
3213
("selected", "Selection","Only use selected vertices as input")),
3214
description = "Loops that are spaced",
3215
default = 'selected')
3216
interpolation = bpy.props.EnumProperty(name = "Interpolation",
3217
items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3218
("linear", "Linear", "Vertices are projected on existing edges")),
3219
description = "Algorithm used for interpolation",
3223
def poll(cls, context):
3224
ob = context.active_object
3225
return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3227
def draw(self, context):
3228
layout = self.layout
3229
col = layout.column()
3231
col.prop(self, "interpolation")
3232
col.prop(self, "input")
3235
col.prop(self, "influence")
3237
def invoke(self, context, event):
3238
# load custom settings
3240
return self.execute(context)
3242
def execute(self, context):
3244
global_undo, object, mesh = initialise()
3245
settings_write(self)
3246
# check cache to see if we can save time
3247
cached, single_loops, loops, derived, mapping = cache_read("Space",
3248
object, mesh, self.input, False)
3250
derived, mesh_mod = get_derived_mesh(object, mesh, context.scene)
3253
derived, mesh_mod, loops = get_connected_input(object, mesh,
3254
context.scene, self.input)
3255
mapping = get_mapping(derived, mesh, mesh_mod, False, False, loops)
3256
loops = check_loops(loops, mapping, mesh_mod)
3258
# saving cache for faster execution next time
3260
cache_write("Space", object, mesh, self.input, False, False, loops,
3265
# calculate splines and new positions
3266
if loop[1]: # circular
3267
loop[0].append(loop[0][0])
3268
tknots, tpoints = space_calculate_t(mesh_mod, loop[0][:])
3269
splines = calculate_splines(self.interpolation, mesh_mod,
3271
move.append(space_calculate_verts(mesh_mod, self.interpolation,
3272
tknots, tpoints, loop[0][:-1], splines))
3274
# move vertices to new locations
3275
move_verts(mesh, mapping, move, self.influence)
3279
bpy.context.blend_data.meshes.remove(mesh_mod)
3280
terminate(global_undo)
3285
##########################################
3286
####### GUI and registration #############
3287
##########################################
3289
# menu containing all tools
3290
class VIEW3D_MT_edit_mesh_looptools(bpy.types.Menu):
3291
bl_label = "LoopTools"
3293
def draw(self, context):
3294
layout = self.layout
3296
# layout.operator("mesh.looptools_bridge", text="Bridge").loft = False
3297
layout.operator("mesh.looptools_circle")
3298
layout.operator("mesh.looptools_curve")
3299
layout.operator("mesh.looptools_flatten")
3300
# layout.operator("mesh.looptools_bridge", text="Loft").loft = True
3301
layout.operator("mesh.looptools_relax")
3302
layout.operator("mesh.looptools_space")
3305
# panel containing all tools
3306
class VIEW3D_PT_tools_looptools(bpy.types.Panel):
3307
bl_space_type = 'VIEW_3D'
3308
bl_region_type = 'TOOLS'
3309
bl_context = "mesh_edit"
3310
bl_label = "LoopTools"
3312
def draw(self, context):
3313
layout = self.layout
3314
col = layout.column(align=True)
3315
lt = context.window_manager.looptools
3317
# bridge - first line
3318
# split = col.split(percentage=0.15)
3319
# if lt.display_bridge:
3320
# split.prop(lt, "display_bridge", text="", icon='DOWNARROW_HLT')
3322
# split.prop(lt, "display_bridge", text="", icon='RIGHTARROW')
3323
# split.operator("mesh.looptools_bridge", text="Bridge").loft = False
3325
# if lt.display_bridge:
3326
# box = col.column(align=True).box().column()
3327
#box.prop(self, "mode")
3330
# col_top = box.column(align=True)
3331
# row = col_top.row(align=True)
3332
# col_left = row.column(align=True)
3333
# col_right = row.column(align=True)
3334
# col_right.active = lt.bridge_segments != 1
3335
# col_left.prop(lt, "bridge_segments")
3336
# col_right.prop(lt, "bridge_min_width", text="")
3338
# bottom_left = col_left.row()
3339
# bottom_left.active = lt.bridge_segments != 1
3340
# bottom_left.prop(lt, "bridge_interpolation", text="")
3341
# bottom_right = col_right.row()
3342
# bottom_right.active = lt.bridge_interpolation == 'cubic'
3343
# bottom_right.prop(lt, "bridge_cubic_strength")
3344
# boolean properties
3345
# col_top.prop(lt, "bridge_remove_faces")
3347
# override properties
3348
# col_top.separator()
3349
# row = box.row(align = True)
3350
# row.prop(lt, "bridge_twist")
3351
# row.prop(lt, "bridge_reverse")
3353
# circle - first line
3354
split = col.split(percentage=0.15)
3355
if lt.display_circle:
3356
split.prop(lt, "display_circle", text="", icon='DOWNARROW_HLT')
3358
split.prop(lt, "display_circle", text="", icon='RIGHTARROW')
3359
split.operator("mesh.looptools_circle")
3361
if lt.display_circle:
3362
box = col.column(align=True).box().column()
3363
box.prop(lt, "circle_fit")
3366
box.prop(lt, "circle_flatten")
3367
row = box.row(align=True)
3368
row.prop(lt, "circle_custom_radius")
3369
row_right = row.row(align=True)
3370
row_right.active = lt.circle_custom_radius
3371
row_right.prop(lt, "circle_radius", text="")
3372
box.prop(lt, "circle_regular")
3375
box.prop(lt, "circle_influence")
3377
# curve - first line
3378
split = col.split(percentage=0.15)
3379
if lt.display_curve:
3380
split.prop(lt, "display_curve", text="", icon='DOWNARROW_HLT')
3382
split.prop(lt, "display_curve", text="", icon='RIGHTARROW')
3383
split.operator("mesh.looptools_curve")
3385
if lt.display_curve:
3386
box = col.column(align=True).box().column()
3387
box.prop(lt, "curve_interpolation")
3388
box.prop(lt, "curve_restriction")
3389
box.prop(lt, "curve_boundaries")
3390
box.prop(lt, "curve_regular")
3393
box.prop(lt, "curve_influence")
3395
# flatten - first line
3396
split = col.split(percentage=0.15)
3397
if lt.display_flatten:
3398
split.prop(lt, "display_flatten", text="", icon='DOWNARROW_HLT')
3400
split.prop(lt, "display_flatten", text="", icon='RIGHTARROW')
3401
split.operator("mesh.looptools_flatten")
3402
# flatten - settings
3403
if lt.display_flatten:
3404
box = col.column(align=True).box().column()
3405
box.prop(lt, "flatten_plane")
3406
#box.prop(lt, "flatten_restriction")
3409
box.prop(lt, "flatten_influence")
3412
# split = col.split(percentage=0.15)
3413
# if lt.display_loft:
3414
# split.prop(lt, "display_loft", text="", icon='DOWNARROW_HLT')
3416
# split.prop(lt, "display_loft", text="", icon='RIGHTARROW')
3417
# split.operator("mesh.looptools_bridge", text="Loft").loft = True
3419
# if lt.display_loft:
3420
# box = col.column(align=True).box().column()
3421
# #box.prop(self, "mode")
3424
# col_top = box.column(align=True)
3425
# row = col_top.row(align=True)
3426
# col_left = row.column(align=True)
3427
# col_right = row.column(align=True)
3428
# col_right.active = lt.bridge_segments != 1
3429
# col_left.prop(lt, "bridge_segments")
3430
# col_right.prop(lt, "bridge_min_width", text="")
3432
# bottom_left = col_left.row()
3433
# bottom_left.active = lt.bridge_segments != 1
3434
# bottom_left.prop(lt, "bridge_interpolation", text="")
3435
# bottom_right = col_right.row()
3436
# bottom_right.active = lt.bridge_interpolation == 'cubic'
3437
# bottom_right.prop(lt, "bridge_cubic_strength")
3438
# # boolean properties
3439
# col_top.prop(lt, "bridge_remove_faces")
3440
# col_top.prop(lt, "bridge_loft_loop")
3442
# # override properties
3443
# col_top.separator()
3444
# row = box.row(align = True)
3445
# row.prop(lt, "bridge_twist")
3446
# row.prop(lt, "bridge_reverse")
3448
# relax - first line
3449
split = col.split(percentage=0.15)
3450
if lt.display_relax:
3451
split.prop(lt, "display_relax", text="", icon='DOWNARROW_HLT')
3453
split.prop(lt, "display_relax", text="", icon='RIGHTARROW')
3454
split.operator("mesh.looptools_relax")
3456
if lt.display_relax:
3457
box = col.column(align=True).box().column()
3458
box.prop(lt, "relax_interpolation")
3459
box.prop(lt, "relax_input")
3460
box.prop(lt, "relax_iterations")
3461
box.prop(lt, "relax_regular")
3463
# space - first line
3464
split = col.split(percentage=0.15)
3465
if lt.display_space:
3466
split.prop(lt, "display_space", text="", icon='DOWNARROW_HLT')
3468
split.prop(lt, "display_space", text="", icon='RIGHTARROW')
3469
split.operator("mesh.looptools_space")
3471
if lt.display_space:
3472
box = col.column(align=True).box().column()
3473
box.prop(lt, "space_interpolation")
3474
box.prop(lt, "space_input")
3477
box.prop(lt, "space_influence")
3480
# property group containing all properties for the gui in the panel
3481
class LoopToolsProps(bpy.types.PropertyGroup):
3483
Fake module like class
3484
bpy.context.window_manager.looptools
3487
# general display properties
3488
# display_bridge = bpy.props.BoolProperty(name = "Bridge settings",
3489
# description = "Display settings of the Bridge tool",
3491
display_circle = bpy.props.BoolProperty(name = "Circle settings",
3492
description = "Display settings of the Circle tool",
3494
display_curve = bpy.props.BoolProperty(name = "Curve settings",
3495
description = "Display settings of the Curve tool",
3497
display_flatten = bpy.props.BoolProperty(name = "Flatten settings",
3498
description = "Display settings of the Flatten tool",
3500
# display_loft = bpy.props.BoolProperty(name = "Loft settings",
3501
# description = "Display settings of the Loft tool",
3503
display_relax = bpy.props.BoolProperty(name = "Relax settings",
3504
description = "Display settings of the Relax tool",
3506
display_space = bpy.props.BoolProperty(name = "Space settings",
3507
description = "Display settings of the Space tool",
3511
bridge_cubic_strength = bpy.props.FloatProperty(name = "Strength",
3512
description = "Higher strength results in more fluid curves",
3516
bridge_interpolation = bpy.props.EnumProperty(name = "Interpolation mode",
3517
items = (('cubic', "Cubic", "Gives curved results"),
3518
('linear', "Linear", "Basic, fast, straight interpolation")),
3519
description = "Interpolation mode: algorithm used when creating "\
3522
bridge_loft = bpy.props.BoolProperty(name = "Loft",
3523
description = "Loft multiple loops, instead of considering them as "\
3524
"a multi-input for bridging",
3526
bridge_loft_loop = bpy.props.BoolProperty(name = "Loop",
3527
description = "Connect the first and the last loop with each other",
3529
bridge_min_width = bpy.props.IntProperty(name = "Minimum width",
3530
description = "Segments with an edge smaller than this are merged "\
3531
"(compared to base edge)",
3535
subtype = 'PERCENTAGE')
3536
bridge_mode = bpy.props.EnumProperty(name = "Mode",
3537
items = (('basic', "Basic", "Fast algorithm"),
3538
('shortest', "Shortest edge", "Slower algorithm with " \
3539
"better vertex matching")),
3540
description = "Algorithm used for bridging",
3541
default = 'shortest')
3542
bridge_remove_faces = bpy.props.BoolProperty(name = "Remove faces",
3543
description = "Remove faces that are internal after bridging",
3545
bridge_reverse = bpy.props.BoolProperty(name = "Reverse",
3546
description = "Manually override the direction in which the loops "\
3547
"are bridged. Only use if the tool gives the wrong " \
3550
bridge_segments = bpy.props.IntProperty(name = "Segments",
3551
description = "Number of segments used to bridge the gap "\
3556
bridge_twist = bpy.props.IntProperty(name = "Twist",
3557
description = "Twist what vertices are connected to each other",
3561
circle_custom_radius = bpy.props.BoolProperty(name = "Radius",
3562
description = "Force a custom radius",
3564
circle_fit = bpy.props.EnumProperty(name = "Method",
3565
items = (("best", "Best fit", "Non-linear least squares"),
3566
("inside", "Fit inside","Only move vertices towards the center")),
3567
description = "Method used for fitting a circle to the vertices",
3569
circle_flatten = bpy.props.BoolProperty(name = "Flatten",
3570
description = "Flatten the circle, instead of projecting it on the " \
3573
circle_influence = bpy.props.FloatProperty(name = "Influence",
3574
description = "Force of the tool",
3579
subtype = 'PERCENTAGE')
3580
circle_radius = bpy.props.FloatProperty(name = "Radius",
3581
description = "Custom radius for circle",
3585
circle_regular = bpy.props.BoolProperty(name = "Regular",
3586
description = "Distribute vertices at constant distances along the " \
3591
curve_boundaries = bpy.props.BoolProperty(name = "Boundaries",
3592
description = "Limit the tool to work within the boundaries of the "\
3593
"selected vertices",
3595
curve_influence = bpy.props.FloatProperty(name = "Influence",
3596
description = "Force of the tool",
3601
subtype = 'PERCENTAGE')
3602
curve_interpolation = bpy.props.EnumProperty(name = "Interpolation",
3603
items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3604
("linear", "Linear", "Simple and fast linear algorithm")),
3605
description = "Algorithm used for interpolation",
3607
curve_regular = bpy.props.BoolProperty(name = "Regular",
3608
description = "Distribute vertices at constant distances along the" \
3611
curve_restriction = bpy.props.EnumProperty(name = "Restriction",
3612
items = (("none", "None", "No restrictions on vertex movement"),
3613
("extrude", "Extrude only","Only allow extrusions (no "\
3615
("indent", "Indent only", "Only allow indentation (no "\
3617
description = "Restrictions on how the vertices can be moved",
3620
# flatten properties
3621
flatten_influence = bpy.props.FloatProperty(name = "Influence",
3622
description = "Force of the tool",
3627
subtype = 'PERCENTAGE')
3628
flatten_plane = bpy.props.EnumProperty(name = "Plane",
3629
items = (("best_fit", "Best fit", "Calculate a best fitting plane"),
3630
("normal", "Normal", "Derive plane from averaging vertex "\
3632
("view", "View", "Flatten on a plane perpendicular to the "\
3634
description = "Plane on which vertices are flattened",
3635
default = 'best_fit')
3636
flatten_restriction = bpy.props.EnumProperty(name = "Restriction",
3637
items = (("none", "None", "No restrictions on vertex movement"),
3638
("bounding_box", "Bounding box", "Vertices are restricted to "\
3639
"movement inside the bounding box of the selection")),
3640
description = "Restrictions on how the vertices can be moved",
3644
relax_input = bpy.props.EnumProperty(name = "Input",
3645
items = (("all", "Parallel (all)", "Also use non-selected "\
3646
"parallel loops as input"),
3647
("selected", "Selection","Only use selected vertices as input")),
3648
description = "Loops that are relaxed",
3649
default = 'selected')
3650
relax_interpolation = bpy.props.EnumProperty(name = "Interpolation",
3651
items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3652
("linear", "Linear", "Simple and fast linear algorithm")),
3653
description = "Algorithm used for interpolation",
3655
relax_iterations = bpy.props.EnumProperty(name = "Iterations",
3656
items = (("1", "1", "One"),
3657
("3", "3", "Three"),
3659
("10", "10", "Ten"),
3660
("25", "25", "Twenty-five")),
3661
description = "Number of times the loop is relaxed",
3663
relax_regular = bpy.props.BoolProperty(name = "Regular",
3664
description = "Distribute vertices at constant distances along the" \
3669
space_influence = bpy.props.FloatProperty(name = "Influence",
3670
description = "Force of the tool",
3675
subtype = 'PERCENTAGE')
3676
space_input = bpy.props.EnumProperty(name = "Input",
3677
items = (("all", "Parallel (all)", "Also use non-selected "\
3678
"parallel loops as input"),
3679
("selected", "Selection","Only use selected vertices as input")),
3680
description = "Loops that are spaced",
3681
default = 'selected')
3682
space_interpolation = bpy.props.EnumProperty(name = "Interpolation",
3683
items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3684
("linear", "Linear", "Vertices are projected on existing edges")),
3685
description = "Algorithm used for interpolation",
3689
# draw function for integration in menus
3690
def menu_func(self, context):
3691
self.layout.menu("VIEW3D_MT_edit_mesh_looptools")
3692
self.layout.separator()
3695
# define classes for registration
3696
classes = [VIEW3D_MT_edit_mesh_looptools,
3697
VIEW3D_PT_tools_looptools,
3707
# registering and menu integration
3710
bpy.utils.register_class(c)
3711
bpy.types.VIEW3D_MT_edit_mesh_specials.prepend(menu_func)
3712
bpy.types.WindowManager.looptools = bpy.props.PointerProperty(\
3713
type = LoopToolsProps)
3716
# unregistering and removing menus
3719
bpy.utils.unregister_class(c)
3720
bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func)
3722
del bpy.types.WindowManager.looptools
3727
if __name__ == "__main__":