Screencast Keys Addon: Improved mouse silhouette, fixed box width to fit to text...
[blender-addons.git] / mesh_looptools.py
blob5bfc832f0f3fdce154694dd410b36b988e8a9492
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 #####
19 bl_info = {
20 "name": "LoopTools",
21 "author": "Bart Crouch",
22 "version": (4, 5, 2),
23 "blender": (2, 69, 3),
24 "location": "View3D > Toolbar and View3D > Specials (W-key)",
25 "warning": "",
26 "description": "Mesh modelling toolkit. Several tools to aid modelling",
27 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
28 "Scripts/Modeling/LoopTools",
29 "tracker_url": "https://developer.blender.org/T26189",
30 "category": "Mesh"}
33 import bmesh
34 import bpy
35 import collections
36 import mathutils
37 import math
38 from bpy_extras import view3d_utils
41 ##########################################
42 ####### General functions ################
43 ##########################################
46 # used by all tools to improve speed on reruns
47 looptools_cache = {}
50 # force a full recalculation next time
51 def cache_delete(tool):
52 if tool in looptools_cache:
53 del looptools_cache[tool]
56 # check cache for stored information
57 def cache_read(tool, object, bm, input_method, boundaries):
58 # current tool not cached yet
59 if tool not in looptools_cache:
60 return(False, False, False, False, False)
61 # check if selected object didn't change
62 if object.name != looptools_cache[tool]["object"]:
63 return(False, False, False, False, False)
64 # check if input didn't change
65 if input_method != looptools_cache[tool]["input_method"]:
66 return(False, False, False, False, False)
67 if boundaries != looptools_cache[tool]["boundaries"]:
68 return(False, False, False, False, False)
69 modifiers = [mod.name for mod in object.modifiers if mod.show_viewport \
70 and mod.type == 'MIRROR']
71 if modifiers != looptools_cache[tool]["modifiers"]:
72 return(False, False, False, False, False)
73 input = [v.index for v in bm.verts if v.select and not v.hide]
74 if input != looptools_cache[tool]["input"]:
75 return(False, False, False, False, False)
76 # reading values
77 single_loops = looptools_cache[tool]["single_loops"]
78 loops = looptools_cache[tool]["loops"]
79 derived = looptools_cache[tool]["derived"]
80 mapping = looptools_cache[tool]["mapping"]
82 return(True, single_loops, loops, derived, mapping)
85 # store information in the cache
86 def cache_write(tool, object, bm, input_method, boundaries, single_loops,
87 loops, derived, mapping):
88 # clear cache of current tool
89 if tool in looptools_cache:
90 del looptools_cache[tool]
91 # prepare values to be saved to cache
92 input = [v.index for v in bm.verts if v.select and not v.hide]
93 modifiers = [mod.name for mod in object.modifiers if mod.show_viewport \
94 and mod.type == 'MIRROR']
95 # update cache
96 looptools_cache[tool] = {"input": input, "object": object.name,
97 "input_method": input_method, "boundaries": boundaries,
98 "single_loops": single_loops, "loops": loops,
99 "derived": derived, "mapping": mapping, "modifiers": modifiers}
102 # calculates natural cubic splines through all given knots
103 def calculate_cubic_splines(bm_mod, tknots, knots):
104 # hack for circular loops
105 if knots[0] == knots[-1] and len(knots) > 1:
106 circular = True
107 k_new1 = []
108 for k in range(-1, -5, -1):
109 if k - 1 < -len(knots):
110 k += len(knots)
111 k_new1.append(knots[k-1])
112 k_new2 = []
113 for k in range(4):
114 if k + 1 > len(knots) - 1:
115 k -= len(knots)
116 k_new2.append(knots[k+1])
117 for k in k_new1:
118 knots.insert(0, k)
119 for k in k_new2:
120 knots.append(k)
121 t_new1 = []
122 total1 = 0
123 for t in range(-1, -5, -1):
124 if t - 1 < -len(tknots):
125 t += len(tknots)
126 total1 += tknots[t] - tknots[t-1]
127 t_new1.append(tknots[0] - total1)
128 t_new2 = []
129 total2 = 0
130 for t in range(4):
131 if t + 1 > len(tknots) - 1:
132 t -= len(tknots)
133 total2 += tknots[t+1] - tknots[t]
134 t_new2.append(tknots[-1] + total2)
135 for t in t_new1:
136 tknots.insert(0, t)
137 for t in t_new2:
138 tknots.append(t)
139 else:
140 circular = False
141 # end of hack
143 n = len(knots)
144 if n < 2:
145 return False
146 x = tknots[:]
147 locs = [bm_mod.verts[k].co[:] for k in knots]
148 result = []
149 for j in range(3):
150 a = []
151 for i in locs:
152 a.append(i[j])
153 h = []
154 for i in range(n-1):
155 if x[i+1] - x[i] == 0:
156 h.append(1e-8)
157 else:
158 h.append(x[i+1] - x[i])
159 q = [False]
160 for i in range(1, n-1):
161 q.append(3/h[i]*(a[i+1]-a[i]) - 3/h[i-1]*(a[i]-a[i-1]))
162 l = [1.0]
163 u = [0.0]
164 z = [0.0]
165 for i in range(1, n-1):
166 l.append(2*(x[i+1]-x[i-1]) - h[i-1]*u[i-1])
167 if l[i] == 0:
168 l[i] = 1e-8
169 u.append(h[i] / l[i])
170 z.append((q[i] - h[i-1] * z[i-1]) / l[i])
171 l.append(1.0)
172 z.append(0.0)
173 b = [False for i in range(n-1)]
174 c = [False for i in range(n)]
175 d = [False for i in range(n-1)]
176 c[n-1] = 0.0
177 for i in range(n-2, -1, -1):
178 c[i] = z[i] - u[i]*c[i+1]
179 b[i] = (a[i+1]-a[i])/h[i] - h[i]*(c[i+1]+2*c[i])/3
180 d[i] = (c[i+1]-c[i]) / (3*h[i])
181 for i in range(n-1):
182 result.append([a[i], b[i], c[i], d[i], x[i]])
183 splines = []
184 for i in range(len(knots)-1):
185 splines.append([result[i], result[i+n-1], result[i+(n-1)*2]])
186 if circular: # cleaning up after hack
187 knots = knots[4:-4]
188 tknots = tknots[4:-4]
190 return(splines)
193 # calculates linear splines through all given knots
194 def calculate_linear_splines(bm_mod, tknots, knots):
195 splines = []
196 for i in range(len(knots)-1):
197 a = bm_mod.verts[knots[i]].co
198 b = bm_mod.verts[knots[i+1]].co
199 d = b-a
200 t = tknots[i]
201 u = tknots[i+1]-t
202 splines.append([a, d, t, u]) # [locStart, locDif, tStart, tDif]
204 return(splines)
207 # calculate a best-fit plane to the given vertices
208 def calculate_plane(bm_mod, loop, method="best_fit", object=False):
209 # getting the vertex locations
210 locs = [bm_mod.verts[v].co.copy() for v in loop[0]]
212 # calculating the center of masss
213 com = mathutils.Vector()
214 for loc in locs:
215 com += loc
216 com /= len(locs)
217 x, y, z = com
219 if method == 'best_fit':
220 # creating the covariance matrix
221 mat = mathutils.Matrix(((0.0, 0.0, 0.0),
222 (0.0, 0.0, 0.0),
223 (0.0, 0.0, 0.0),
225 for loc in locs:
226 mat[0][0] += (loc[0]-x)**2
227 mat[1][0] += (loc[0]-x)*(loc[1]-y)
228 mat[2][0] += (loc[0]-x)*(loc[2]-z)
229 mat[0][1] += (loc[1]-y)*(loc[0]-x)
230 mat[1][1] += (loc[1]-y)**2
231 mat[2][1] += (loc[1]-y)*(loc[2]-z)
232 mat[0][2] += (loc[2]-z)*(loc[0]-x)
233 mat[1][2] += (loc[2]-z)*(loc[1]-y)
234 mat[2][2] += (loc[2]-z)**2
236 # calculating the normal to the plane
237 normal = False
238 try:
239 mat = matrix_invert(mat)
240 except:
241 ax = 2
242 if math.fabs(sum(mat[0])) < math.fabs(sum(mat[1])):
243 if math.fabs(sum(mat[0])) < math.fabs(sum(mat[2])):
244 ax = 0
245 elif math.fabs(sum(mat[1])) < math.fabs(sum(mat[2])):
246 ax = 1
247 if ax == 0:
248 normal = mathutils.Vector((1.0, 0.0, 0.0))
249 elif ax == 1:
250 normal = mathutils.Vector((0.0, 1.0, 0.0))
251 else:
252 normal = mathutils.Vector((0.0, 0.0, 1.0))
253 if not normal:
254 # warning! this is different from .normalize()
255 itermax = 500
256 iter = 0
257 vec = mathutils.Vector((1.0, 1.0, 1.0))
258 vec2 = (mat * vec)/(mat * vec).length
259 while vec != vec2 and iter<itermax:
260 iter+=1
261 vec = vec2
262 vec2 = mat * vec
263 if vec2.length != 0:
264 vec2 /= vec2.length
265 if vec2.length == 0:
266 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
267 normal = vec2
269 elif method == 'normal':
270 # averaging the vertex normals
271 v_normals = [bm_mod.verts[v].normal for v in loop[0]]
272 normal = mathutils.Vector()
273 for v_normal in v_normals:
274 normal += v_normal
275 normal /= len(v_normals)
276 normal.normalize()
278 elif method == 'view':
279 # calculate view normal
280 rotation = bpy.context.space_data.region_3d.view_matrix.to_3x3().\
281 inverted()
282 normal = rotation * mathutils.Vector((0.0, 0.0, 1.0))
283 if object:
284 normal = object.matrix_world.inverted().to_euler().to_matrix() * \
285 normal
287 return(com, normal)
290 # calculate splines based on given interpolation method (controller function)
291 def calculate_splines(interpolation, bm_mod, tknots, knots):
292 if interpolation == 'cubic':
293 splines = calculate_cubic_splines(bm_mod, tknots, knots[:])
294 else: # interpolations == 'linear'
295 splines = calculate_linear_splines(bm_mod, tknots, knots[:])
297 return(splines)
300 # check loops and only return valid ones
301 def check_loops(loops, mapping, bm_mod):
302 valid_loops = []
303 for loop, circular in loops:
304 # loop needs to have at least 3 vertices
305 if len(loop) < 3:
306 continue
307 # loop needs at least 1 vertex in the original, non-mirrored mesh
308 if mapping:
309 all_virtual = True
310 for vert in loop:
311 if mapping[vert] > -1:
312 all_virtual = False
313 break
314 if all_virtual:
315 continue
316 # vertices can not all be at the same location
317 stacked = True
318 for i in range(len(loop) - 1):
319 if (bm_mod.verts[loop[i]].co - \
320 bm_mod.verts[loop[i+1]].co).length > 1e-6:
321 stacked = False
322 break
323 if stacked:
324 continue
325 # passed all tests, loop is valid
326 valid_loops.append([loop, circular])
328 return(valid_loops)
331 # input: bmesh, output: dict with the edge-key as key and face-index as value
332 def dict_edge_faces(bm):
333 edge_faces = dict([[edgekey(edge), []] for edge in bm.edges if \
334 not edge.hide])
335 for face in bm.faces:
336 if face.hide:
337 continue
338 for key in face_edgekeys(face):
339 edge_faces[key].append(face.index)
341 return(edge_faces)
344 # input: bmesh (edge-faces optional), output: dict with face-face connections
345 def dict_face_faces(bm, edge_faces=False):
346 if not edge_faces:
347 edge_faces = dict_edge_faces(bm)
349 connected_faces = dict([[face.index, []] for face in bm.faces if \
350 not face.hide])
351 for face in bm.faces:
352 if face.hide:
353 continue
354 for edge_key in face_edgekeys(face):
355 for connected_face in edge_faces[edge_key]:
356 if connected_face == face.index:
357 continue
358 connected_faces[face.index].append(connected_face)
360 return(connected_faces)
363 # input: bmesh, output: dict with the vert index as key and edge-keys as value
364 def dict_vert_edges(bm):
365 vert_edges = dict([[v.index, []] for v in bm.verts if not v.hide])
366 for edge in bm.edges:
367 if edge.hide:
368 continue
369 ek = edgekey(edge)
370 for vert in ek:
371 vert_edges[vert].append(ek)
373 return(vert_edges)
376 # input: bmesh, output: dict with the vert index as key and face index as value
377 def dict_vert_faces(bm):
378 vert_faces = dict([[v.index, []] for v in bm.verts if not v.hide])
379 for face in bm.faces:
380 if not face.hide:
381 for vert in face.verts:
382 vert_faces[vert.index].append(face.index)
384 return(vert_faces)
387 # input: list of edge-keys, output: dictionary with vertex-vertex connections
388 def dict_vert_verts(edge_keys):
389 # create connection data
390 vert_verts = {}
391 for ek in edge_keys:
392 for i in range(2):
393 if ek[i] in vert_verts:
394 vert_verts[ek[i]].append(ek[1-i])
395 else:
396 vert_verts[ek[i]] = [ek[1-i]]
398 return(vert_verts)
401 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
402 def edgekey(edge):
403 return(tuple(sorted([edge.verts[0].index, edge.verts[1].index])))
406 # returns the edgekeys of a bmesh face
407 def face_edgekeys(face):
408 return([tuple(sorted([edge.verts[0].index, edge.verts[1].index])) for \
409 edge in face.edges])
412 # calculate input loops
413 def get_connected_input(object, bm, scene, input):
414 # get mesh with modifiers applied
415 derived, bm_mod = get_derived_bmesh(object, bm, scene)
417 # calculate selected loops
418 edge_keys = [edgekey(edge) for edge in bm_mod.edges if \
419 edge.select and not edge.hide]
420 loops = get_connected_selections(edge_keys)
422 # if only selected loops are needed, we're done
423 if input == 'selected':
424 return(derived, bm_mod, loops)
425 # elif input == 'all':
426 loops = get_parallel_loops(bm_mod, loops)
428 return(derived, bm_mod, loops)
431 # sorts all edge-keys into a list of loops
432 def get_connected_selections(edge_keys):
433 # create connection data
434 vert_verts = dict_vert_verts(edge_keys)
436 # find loops consisting of connected selected edges
437 loops = []
438 while len(vert_verts) > 0:
439 loop = [iter(vert_verts.keys()).__next__()]
440 growing = True
441 flipped = False
443 # extend loop
444 while growing:
445 # no more connection data for current vertex
446 if loop[-1] not in vert_verts:
447 if not flipped:
448 loop.reverse()
449 flipped = True
450 else:
451 growing = False
452 else:
453 extended = False
454 for i, next_vert in enumerate(vert_verts[loop[-1]]):
455 if next_vert not in loop:
456 vert_verts[loop[-1]].pop(i)
457 if len(vert_verts[loop[-1]]) == 0:
458 del vert_verts[loop[-1]]
459 # remove connection both ways
460 if next_vert in vert_verts:
461 if len(vert_verts[next_vert]) == 1:
462 del vert_verts[next_vert]
463 else:
464 vert_verts[next_vert].remove(loop[-1])
465 loop.append(next_vert)
466 extended = True
467 break
468 if not extended:
469 # found one end of the loop, continue with next
470 if not flipped:
471 loop.reverse()
472 flipped = True
473 # found both ends of the loop, stop growing
474 else:
475 growing = False
477 # check if loop is circular
478 if loop[0] in vert_verts:
479 if loop[-1] in vert_verts[loop[0]]:
480 # is circular
481 if len(vert_verts[loop[0]]) == 1:
482 del vert_verts[loop[0]]
483 else:
484 vert_verts[loop[0]].remove(loop[-1])
485 if len(vert_verts[loop[-1]]) == 1:
486 del vert_verts[loop[-1]]
487 else:
488 vert_verts[loop[-1]].remove(loop[0])
489 loop = [loop, True]
490 else:
491 # not circular
492 loop = [loop, False]
493 else:
494 # not circular
495 loop = [loop, False]
497 loops.append(loop)
499 return(loops)
502 # get the derived mesh data, if there is a mirror modifier
503 def get_derived_bmesh(object, bm, scene):
504 # check for mirror modifiers
505 if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
506 derived = True
507 # disable other modifiers
508 show_viewport = [mod.name for mod in object.modifiers if \
509 mod.show_viewport]
510 for mod in object.modifiers:
511 if mod.type != 'MIRROR':
512 mod.show_viewport = False
513 # get derived mesh
514 bm_mod = bmesh.new()
515 mesh_mod = object.to_mesh(scene, True, 'PREVIEW')
516 bm_mod.from_mesh(mesh_mod)
517 bpy.context.blend_data.meshes.remove(mesh_mod)
518 # re-enable other modifiers
519 for mod_name in show_viewport:
520 object.modifiers[mod_name].show_viewport = True
521 # no mirror modifiers, so no derived mesh necessary
522 else:
523 derived = False
524 bm_mod = bm
526 return(derived, bm_mod)
529 # return a mapping of derived indices to indices
530 def get_mapping(derived, bm, bm_mod, single_vertices, full_search, loops):
531 if not derived:
532 return(False)
534 if full_search:
535 verts = [v for v in bm.verts if not v.hide]
536 else:
537 verts = [v for v in bm.verts if v.select and not v.hide]
539 # non-selected vertices around single vertices also need to be mapped
540 if single_vertices:
541 mapping = dict([[vert, -1] for vert in single_vertices])
542 verts_mod = [bm_mod.verts[vert] for vert in single_vertices]
543 for v in verts:
544 for v_mod in verts_mod:
545 if (v.co - v_mod.co).length < 1e-6:
546 mapping[v_mod.index] = v.index
547 break
548 real_singles = [v_real for v_real in mapping.values() if v_real>-1]
550 verts_indices = [vert.index for vert in verts]
551 for face in [face for face in bm.faces if not face.select \
552 and not face.hide]:
553 for vert in face.verts:
554 if vert.index in real_singles:
555 for v in face.verts:
556 if not v.index in verts_indices:
557 if v not in verts:
558 verts.append(v)
559 break
561 # create mapping of derived indices to indices
562 mapping = dict([[vert, -1] for loop in loops for vert in loop[0]])
563 if single_vertices:
564 for single in single_vertices:
565 mapping[single] = -1
566 verts_mod = [bm_mod.verts[i] for i in mapping.keys()]
567 for v in verts:
568 for v_mod in verts_mod:
569 if (v.co - v_mod.co).length < 1e-6:
570 mapping[v_mod.index] = v.index
571 verts_mod.remove(v_mod)
572 break
574 return(mapping)
577 # calculate the determinant of a matrix
578 def matrix_determinant(m):
579 determinant = m[0][0] * m[1][1] * m[2][2] + m[0][1] * m[1][2] * m[2][0] \
580 + m[0][2] * m[1][0] * m[2][1] - m[0][2] * m[1][1] * m[2][0] \
581 - m[0][1] * m[1][0] * m[2][2] - m[0][0] * m[1][2] * m[2][1]
583 return(determinant)
586 # custom matrix inversion, to provide higher precision than the built-in one
587 def matrix_invert(m):
588 r = mathutils.Matrix((
589 (m[1][1]*m[2][2] - m[1][2]*m[2][1], m[0][2]*m[2][1] - m[0][1]*m[2][2],
590 m[0][1]*m[1][2] - m[0][2]*m[1][1]),
591 (m[1][2]*m[2][0] - m[1][0]*m[2][2], m[0][0]*m[2][2] - m[0][2]*m[2][0],
592 m[0][2]*m[1][0] - m[0][0]*m[1][2]),
593 (m[1][0]*m[2][1] - m[1][1]*m[2][0], m[0][1]*m[2][0] - m[0][0]*m[2][1],
594 m[0][0]*m[1][1] - m[0][1]*m[1][0])))
596 return (r * (1 / matrix_determinant(m)))
599 # returns a list of all loops parallel to the input, input included
600 def get_parallel_loops(bm_mod, loops):
601 # get required dictionaries
602 edge_faces = dict_edge_faces(bm_mod)
603 connected_faces = dict_face_faces(bm_mod, edge_faces)
604 # turn vertex loops into edge loops
605 edgeloops = []
606 for loop in loops:
607 edgeloop = [[sorted([loop[0][i], loop[0][i+1]]) for i in \
608 range(len(loop[0])-1)], loop[1]]
609 if loop[1]: # circular
610 edgeloop[0].append(sorted([loop[0][-1], loop[0][0]]))
611 edgeloops.append(edgeloop[:])
612 # variables to keep track while iterating
613 all_edgeloops = []
614 has_branches = False
616 for loop in edgeloops:
617 # initialise with original loop
618 all_edgeloops.append(loop[0])
619 newloops = [loop[0]]
620 verts_used = []
621 for edge in loop[0]:
622 if edge[0] not in verts_used:
623 verts_used.append(edge[0])
624 if edge[1] not in verts_used:
625 verts_used.append(edge[1])
627 # find parallel loops
628 while len(newloops) > 0:
629 side_a = []
630 side_b = []
631 for i in newloops[-1]:
632 i = tuple(i)
633 forbidden_side = False
634 if not i in edge_faces:
635 # weird input with branches
636 has_branches = True
637 break
638 for face in edge_faces[i]:
639 if len(side_a) == 0 and forbidden_side != "a":
640 side_a.append(face)
641 if forbidden_side:
642 break
643 forbidden_side = "a"
644 continue
645 elif side_a[-1] in connected_faces[face] and \
646 forbidden_side != "a":
647 side_a.append(face)
648 if forbidden_side:
649 break
650 forbidden_side = "a"
651 continue
652 if len(side_b) == 0 and forbidden_side != "b":
653 side_b.append(face)
654 if forbidden_side:
655 break
656 forbidden_side = "b"
657 continue
658 elif side_b[-1] in connected_faces[face] and \
659 forbidden_side != "b":
660 side_b.append(face)
661 if forbidden_side:
662 break
663 forbidden_side = "b"
664 continue
666 if has_branches:
667 # weird input with branches
668 break
670 newloops.pop(-1)
671 sides = []
672 if side_a:
673 sides.append(side_a)
674 if side_b:
675 sides.append(side_b)
677 for side in sides:
678 extraloop = []
679 for fi in side:
680 for key in face_edgekeys(bm_mod.faces[fi]):
681 if key[0] not in verts_used and key[1] not in \
682 verts_used:
683 extraloop.append(key)
684 break
685 if extraloop:
686 for key in extraloop:
687 for new_vert in key:
688 if new_vert not in verts_used:
689 verts_used.append(new_vert)
690 newloops.append(extraloop)
691 all_edgeloops.append(extraloop)
693 # input contains branches, only return selected loop
694 if has_branches:
695 return(loops)
697 # change edgeloops into normal loops
698 loops = []
699 for edgeloop in all_edgeloops:
700 loop = []
701 # grow loop by comparing vertices between consecutive edge-keys
702 for i in range(len(edgeloop)-1):
703 for vert in range(2):
704 if edgeloop[i][vert] in edgeloop[i+1]:
705 loop.append(edgeloop[i][vert])
706 break
707 if loop:
708 # add starting vertex
709 for vert in range(2):
710 if edgeloop[0][vert] != loop[0]:
711 loop = [edgeloop[0][vert]] + loop
712 break
713 # add ending vertex
714 for vert in range(2):
715 if edgeloop[-1][vert] != loop[-1]:
716 loop.append(edgeloop[-1][vert])
717 break
718 # check if loop is circular
719 if loop[0] == loop[-1]:
720 circular = True
721 loop = loop[:-1]
722 else:
723 circular = False
724 loops.append([loop, circular])
726 return(loops)
729 # gather initial data
730 def initialise():
731 global_undo = bpy.context.user_preferences.edit.use_global_undo
732 bpy.context.user_preferences.edit.use_global_undo = False
733 object = bpy.context.active_object
734 if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
735 # ensure that selection is synced for the derived mesh
736 bpy.ops.object.mode_set(mode='OBJECT')
737 bpy.ops.object.mode_set(mode='EDIT')
738 bm = bmesh.from_edit_mesh(object.data)
740 return(global_undo, object, bm)
743 # move the vertices to their new locations
744 def move_verts(object, bm, mapping, move, influence):
745 for loop in move:
746 for index, loc in loop:
747 if mapping:
748 if mapping[index] == -1:
749 continue
750 else:
751 index = mapping[index]
752 if influence >= 0:
753 bm.verts[index].co = loc*(influence/100) + \
754 bm.verts[index].co*((100-influence)/100)
755 else:
756 bm.verts[index].co = loc
757 bm.normal_update()
758 object.data.update()
761 # load custom tool settings
762 def settings_load(self):
763 lt = bpy.context.window_manager.looptools
764 tool = self.name.split()[0].lower()
765 keys = self.as_keywords().keys()
766 for key in keys:
767 setattr(self, key, getattr(lt, tool + "_" + key))
770 # store custom tool settings
771 def settings_write(self):
772 lt = bpy.context.window_manager.looptools
773 tool = self.name.split()[0].lower()
774 keys = self.as_keywords().keys()
775 for key in keys:
776 setattr(lt, tool + "_" + key, getattr(self, key))
779 # clean up and set settings back to original state
780 def terminate(global_undo):
781 # update editmesh cached data
782 obj = bpy.context.active_object
783 if obj.mode == 'EDIT':
784 bmesh.update_edit_mesh(obj.data, tessface=True, destructive=True)
786 bpy.context.user_preferences.edit.use_global_undo = global_undo
789 ##########################################
790 ####### Bridge functions #################
791 ##########################################
793 # calculate a cubic spline through the middle section of 4 given coordinates
794 def bridge_calculate_cubic_spline(bm, coordinates):
795 result = []
796 x = [0, 1, 2, 3]
798 for j in range(3):
799 a = []
800 for i in coordinates:
801 a.append(float(i[j]))
802 h = []
803 for i in range(3):
804 h.append(x[i+1]-x[i])
805 q = [False]
806 for i in range(1,3):
807 q.append(3.0/h[i]*(a[i+1]-a[i])-3.0/h[i-1]*(a[i]-a[i-1]))
808 l = [1.0]
809 u = [0.0]
810 z = [0.0]
811 for i in range(1,3):
812 l.append(2.0*(x[i+1]-x[i-1])-h[i-1]*u[i-1])
813 u.append(h[i]/l[i])
814 z.append((q[i]-h[i-1]*z[i-1])/l[i])
815 l.append(1.0)
816 z.append(0.0)
817 b = [False for i in range(3)]
818 c = [False for i in range(4)]
819 d = [False for i in range(3)]
820 c[3] = 0.0
821 for i in range(2,-1,-1):
822 c[i] = z[i]-u[i]*c[i+1]
823 b[i] = (a[i+1]-a[i])/h[i]-h[i]*(c[i+1]+2.0*c[i])/3.0
824 d[i] = (c[i+1]-c[i])/(3.0*h[i])
825 for i in range(3):
826 result.append([a[i], b[i], c[i], d[i], x[i]])
827 spline = [result[1], result[4], result[7]]
829 return(spline)
832 # return a list with new vertex location vectors, a list with face vertex
833 # integers, and the highest vertex integer in the virtual mesh
834 def bridge_calculate_geometry(bm, lines, vertex_normals, segments,
835 interpolation, cubic_strength, min_width, max_vert_index):
836 new_verts = []
837 faces = []
839 # calculate location based on interpolation method
840 def get_location(line, segment, splines):
841 v1 = bm.verts[lines[line][0]].co
842 v2 = bm.verts[lines[line][1]].co
843 if interpolation == 'linear':
844 return v1 + (segment/segments) * (v2-v1)
845 else: # interpolation == 'cubic'
846 m = (segment/segments)
847 ax,bx,cx,dx,tx = splines[line][0]
848 x = ax+bx*m+cx*m**2+dx*m**3
849 ay,by,cy,dy,ty = splines[line][1]
850 y = ay+by*m+cy*m**2+dy*m**3
851 az,bz,cz,dz,tz = splines[line][2]
852 z = az+bz*m+cz*m**2+dz*m**3
853 return mathutils.Vector((x, y, z))
855 # no interpolation needed
856 if segments == 1:
857 for i, line in enumerate(lines):
858 if i < len(lines)-1:
859 faces.append([line[0], lines[i+1][0], lines[i+1][1], line[1]])
860 # more than 1 segment, interpolate
861 else:
862 # calculate splines (if necessary) once, so no recalculations needed
863 if interpolation == 'cubic':
864 splines = []
865 for line in lines:
866 v1 = bm.verts[line[0]].co
867 v2 = bm.verts[line[1]].co
868 size = (v2-v1).length * cubic_strength
869 splines.append(bridge_calculate_cubic_spline(bm,
870 [v1+size*vertex_normals[line[0]], v1, v2,
871 v2+size*vertex_normals[line[1]]]))
872 else:
873 splines = False
875 # create starting situation
876 virtual_width = [(bm.verts[lines[i][0]].co -
877 bm.verts[lines[i+1][0]].co).length for i
878 in range(len(lines)-1)]
879 new_verts = [get_location(0, seg, splines) for seg in range(1,
880 segments)]
881 first_line_indices = [i for i in range(max_vert_index+1,
882 max_vert_index+segments)]
884 prev_verts = new_verts[:] # vertex locations of verts on previous line
885 prev_vert_indices = first_line_indices[:]
886 max_vert_index += segments - 1 # highest vertex index in virtual mesh
887 next_verts = [] # vertex locations of verts on current line
888 next_vert_indices = []
890 for i, line in enumerate(lines):
891 if i < len(lines)-1:
892 v1 = line[0]
893 v2 = lines[i+1][0]
894 end_face = True
895 for seg in range(1, segments):
896 loc1 = prev_verts[seg-1]
897 loc2 = get_location(i+1, seg, splines)
898 if (loc1-loc2).length < (min_width/100)*virtual_width[i] \
899 and line[1]==lines[i+1][1]:
900 # triangle, no new vertex
901 faces.append([v1, v2, prev_vert_indices[seg-1],
902 prev_vert_indices[seg-1]])
903 next_verts += prev_verts[seg-1:]
904 next_vert_indices += prev_vert_indices[seg-1:]
905 end_face = False
906 break
907 else:
908 if i == len(lines)-2 and lines[0] == lines[-1]:
909 # quad with first line, no new vertex
910 faces.append([v1, v2, first_line_indices[seg-1],
911 prev_vert_indices[seg-1]])
912 v2 = first_line_indices[seg-1]
913 v1 = prev_vert_indices[seg-1]
914 else:
915 # quad, add new vertex
916 max_vert_index += 1
917 faces.append([v1, v2, max_vert_index,
918 prev_vert_indices[seg-1]])
919 v2 = max_vert_index
920 v1 = prev_vert_indices[seg-1]
921 new_verts.append(loc2)
922 next_verts.append(loc2)
923 next_vert_indices.append(max_vert_index)
924 if end_face:
925 faces.append([v1, v2, lines[i+1][1], line[1]])
927 prev_verts = next_verts[:]
928 prev_vert_indices = next_vert_indices[:]
929 next_verts = []
930 next_vert_indices = []
932 return(new_verts, faces, max_vert_index)
935 # calculate lines (list of lists, vertex indices) that are used for bridging
936 def bridge_calculate_lines(bm, loops, mode, twist, reverse):
937 lines = []
938 loop1, loop2 = [i[0] for i in loops]
939 loop1_circular, loop2_circular = [i[1] for i in loops]
940 circular = loop1_circular or loop2_circular
941 circle_full = False
943 # calculate loop centers
944 centers = []
945 for loop in [loop1, loop2]:
946 center = mathutils.Vector()
947 for vertex in loop:
948 center += bm.verts[vertex].co
949 center /= len(loop)
950 centers.append(center)
951 for i, loop in enumerate([loop1, loop2]):
952 for vertex in loop:
953 if bm.verts[vertex].co == centers[i]:
954 # prevent zero-length vectors in angle comparisons
955 centers[i] += mathutils.Vector((0.01, 0, 0))
956 break
957 center1, center2 = centers
959 # calculate the normals of the virtual planes that the loops are on
960 normals = []
961 normal_plurity = False
962 for i, loop in enumerate([loop1, loop2]):
963 # covariance matrix
964 mat = mathutils.Matrix(((0.0, 0.0, 0.0),
965 (0.0, 0.0, 0.0),
966 (0.0, 0.0, 0.0)))
967 x, y, z = centers[i]
968 for loc in [bm.verts[vertex].co for vertex in loop]:
969 mat[0][0] += (loc[0]-x)**2
970 mat[1][0] += (loc[0]-x)*(loc[1]-y)
971 mat[2][0] += (loc[0]-x)*(loc[2]-z)
972 mat[0][1] += (loc[1]-y)*(loc[0]-x)
973 mat[1][1] += (loc[1]-y)**2
974 mat[2][1] += (loc[1]-y)*(loc[2]-z)
975 mat[0][2] += (loc[2]-z)*(loc[0]-x)
976 mat[1][2] += (loc[2]-z)*(loc[1]-y)
977 mat[2][2] += (loc[2]-z)**2
978 # plane normal
979 normal = False
980 if sum(mat[0]) < 1e-6 or sum(mat[1]) < 1e-6 or sum(mat[2]) < 1e-6:
981 normal_plurity = True
982 try:
983 mat.invert()
984 except:
985 if sum(mat[0]) == 0:
986 normal = mathutils.Vector((1.0, 0.0, 0.0))
987 elif sum(mat[1]) == 0:
988 normal = mathutils.Vector((0.0, 1.0, 0.0))
989 elif sum(mat[2]) == 0:
990 normal = mathutils.Vector((0.0, 0.0, 1.0))
991 if not normal:
992 # warning! this is different from .normalize()
993 itermax = 500
994 iter = 0
995 vec = mathutils.Vector((1.0, 1.0, 1.0))
996 vec2 = (mat * vec)/(mat * vec).length
997 while vec != vec2 and iter<itermax:
998 iter+=1
999 vec = vec2
1000 vec2 = mat * vec
1001 if vec2.length != 0:
1002 vec2 /= vec2.length
1003 if vec2.length == 0:
1004 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
1005 normal = vec2
1006 normals.append(normal)
1007 # have plane normals face in the same direction (maximum angle: 90 degrees)
1008 if ((center1 + normals[0]) - center2).length < \
1009 ((center1 - normals[0]) - center2).length:
1010 normals[0].negate()
1011 if ((center2 + normals[1]) - center1).length > \
1012 ((center2 - normals[1]) - center1).length:
1013 normals[1].negate()
1015 # rotation matrix, representing the difference between the plane normals
1016 axis = normals[0].cross(normals[1])
1017 axis = mathutils.Vector([loc if abs(loc) > 1e-8 else 0 for loc in axis])
1018 if axis.angle(mathutils.Vector((0, 0, 1)), 0) > 1.5707964:
1019 axis.negate()
1020 angle = normals[0].dot(normals[1])
1021 rotation_matrix = mathutils.Matrix.Rotation(angle, 4, axis)
1023 # if circular, rotate loops so they are aligned
1024 if circular:
1025 # make sure loop1 is the circular one (or both are circular)
1026 if loop2_circular and not loop1_circular:
1027 loop1_circular, loop2_circular = True, False
1028 loop1, loop2 = loop2, loop1
1030 # match start vertex of loop1 with loop2
1031 target_vector = bm.verts[loop2[0]].co - center2
1032 dif_angles = [[(rotation_matrix * (bm.verts[vertex].co - center1)
1033 ).angle(target_vector, 0), False, i] for
1034 i, vertex in enumerate(loop1)]
1035 dif_angles.sort()
1036 if len(loop1) != len(loop2):
1037 angle_limit = dif_angles[0][0] * 1.2 # 20% margin
1038 dif_angles = [[(bm.verts[loop2[0]].co - \
1039 bm.verts[loop1[index]].co).length, angle, index] for \
1040 angle, distance, index in dif_angles if angle <= angle_limit]
1041 dif_angles.sort()
1042 loop1 = loop1[dif_angles[0][2]:] + loop1[:dif_angles[0][2]]
1044 # have both loops face the same way
1045 if normal_plurity and not circular:
1046 second_to_first, second_to_second, second_to_last = \
1047 [(bm.verts[loop1[1]].co - center1).\
1048 angle(bm.verts[loop2[i]].co - center2) for i in [0, 1, -1]]
1049 last_to_first, last_to_second = [(bm.verts[loop1[-1]].co - \
1050 center1).angle(bm.verts[loop2[i]].co - center2) for \
1051 i in [0, 1]]
1052 if (min(last_to_first, last_to_second)*1.1 < min(second_to_first, \
1053 second_to_second)) or (loop2_circular and second_to_last*1.1 < \
1054 min(second_to_first, second_to_second)):
1055 loop1.reverse()
1056 if circular:
1057 loop1 = [loop1[-1]] + loop1[:-1]
1058 else:
1059 angle = (bm.verts[loop1[0]].co - center1).\
1060 cross(bm.verts[loop1[1]].co - center1).angle(normals[0], 0)
1061 target_angle = (bm.verts[loop2[0]].co - center2).\
1062 cross(bm.verts[loop2[1]].co - center2).angle(normals[1], 0)
1063 limit = 1.5707964 # 0.5*pi, 90 degrees
1064 if not ((angle > limit and target_angle > limit) or \
1065 (angle < limit and target_angle < limit)):
1066 loop1.reverse()
1067 if circular:
1068 loop1 = [loop1[-1]] + loop1[:-1]
1069 elif normals[0].angle(normals[1]) > limit:
1070 loop1.reverse()
1071 if circular:
1072 loop1 = [loop1[-1]] + loop1[:-1]
1074 # both loops have the same length
1075 if len(loop1) == len(loop2):
1076 # manual override
1077 if twist:
1078 if abs(twist) < len(loop1):
1079 loop1 = loop1[twist:]+loop1[:twist]
1080 if reverse:
1081 loop1.reverse()
1083 lines.append([loop1[0], loop2[0]])
1084 for i in range(1, len(loop1)):
1085 lines.append([loop1[i], loop2[i]])
1087 # loops of different lengths
1088 else:
1089 # make loop1 longest loop
1090 if len(loop2) > len(loop1):
1091 loop1, loop2 = loop2, loop1
1092 loop1_circular, loop2_circular = loop2_circular, loop1_circular
1094 # manual override
1095 if twist:
1096 if abs(twist) < len(loop1):
1097 loop1 = loop1[twist:]+loop1[:twist]
1098 if reverse:
1099 loop1.reverse()
1101 # shortest angle difference doesn't always give correct start vertex
1102 if loop1_circular and not loop2_circular:
1103 shifting = 1
1104 while shifting:
1105 if len(loop1) - shifting < len(loop2):
1106 shifting = False
1107 break
1108 to_last, to_first = [(rotation_matrix *
1109 (bm.verts[loop1[-1]].co - center1)).angle((bm.\
1110 verts[loop2[i]].co - center2), 0) for i in [-1, 0]]
1111 if to_first < to_last:
1112 loop1 = [loop1[-1]] + loop1[:-1]
1113 shifting += 1
1114 else:
1115 shifting = False
1116 break
1118 # basic shortest side first
1119 if mode == 'basic':
1120 lines.append([loop1[0], loop2[0]])
1121 for i in range(1, len(loop1)):
1122 if i >= len(loop2) - 1:
1123 # triangles
1124 lines.append([loop1[i], loop2[-1]])
1125 else:
1126 # quads
1127 lines.append([loop1[i], loop2[i]])
1129 # shortest edge algorithm
1130 else: # mode == 'shortest'
1131 lines.append([loop1[0], loop2[0]])
1132 prev_vert2 = 0
1133 for i in range(len(loop1) -1):
1134 if prev_vert2 == len(loop2) - 1 and not loop2_circular:
1135 # force triangles, reached end of loop2
1136 tri, quad = 0, 1
1137 elif prev_vert2 == len(loop2) - 1 and loop2_circular:
1138 # at end of loop2, but circular, so check with first vert
1139 tri, quad = [(bm.verts[loop1[i+1]].co -
1140 bm.verts[loop2[j]].co).length
1141 for j in [prev_vert2, 0]]
1143 circle_full = 2
1144 elif len(loop1) - 1 - i == len(loop2) - 1 - prev_vert2 and \
1145 not circle_full:
1146 # force quads, otherwise won't make it to end of loop2
1147 tri, quad = 1, 0
1148 else:
1149 # calculate if tri or quad gives shortest edge
1150 tri, quad = [(bm.verts[loop1[i+1]].co -
1151 bm.verts[loop2[j]].co).length
1152 for j in range(prev_vert2, prev_vert2+2)]
1154 # triangle
1155 if tri < quad:
1156 lines.append([loop1[i+1], loop2[prev_vert2]])
1157 if circle_full == 2:
1158 circle_full = False
1159 # quad
1160 elif not circle_full:
1161 lines.append([loop1[i+1], loop2[prev_vert2+1]])
1162 prev_vert2 += 1
1163 # quad to first vertex of loop2
1164 else:
1165 lines.append([loop1[i+1], loop2[0]])
1166 prev_vert2 = 0
1167 circle_full = True
1169 # final face for circular loops
1170 if loop1_circular and loop2_circular:
1171 lines.append([loop1[0], loop2[0]])
1173 return(lines)
1176 # calculate number of segments needed
1177 def bridge_calculate_segments(bm, lines, loops, segments):
1178 # return if amount of segments is set by user
1179 if segments != 0:
1180 return segments
1182 # edge lengths
1183 average_edge_length = [(bm.verts[vertex].co - \
1184 bm.verts[loop[0][i+1]].co).length for loop in loops for \
1185 i, vertex in enumerate(loop[0][:-1])]
1186 # closing edges of circular loops
1187 average_edge_length += [(bm.verts[loop[0][-1]].co - \
1188 bm.verts[loop[0][0]].co).length for loop in loops if loop[1]]
1190 # average lengths
1191 average_edge_length = sum(average_edge_length) / len(average_edge_length)
1192 average_bridge_length = sum([(bm.verts[v1].co - \
1193 bm.verts[v2].co).length for v1, v2 in lines]) / len(lines)
1195 segments = max(1, round(average_bridge_length / average_edge_length))
1197 return(segments)
1200 # return dictionary with vertex index as key, and the normal vector as value
1201 def bridge_calculate_virtual_vertex_normals(bm, lines, loops, edge_faces,
1202 edgekey_to_edge):
1203 if not edge_faces: # interpolation isn't set to cubic
1204 return False
1206 # pity reduce() isn't one of the basic functions in python anymore
1207 def average_vector_dictionary(dic):
1208 for key, vectors in dic.items():
1209 #if type(vectors) == type([]) and len(vectors) > 1:
1210 if len(vectors) > 1:
1211 average = mathutils.Vector()
1212 for vector in vectors:
1213 average += vector
1214 average /= len(vectors)
1215 dic[key] = [average]
1216 return dic
1218 # get all edges of the loop
1219 edges = [[edgekey_to_edge[tuple(sorted([loops[j][0][i],
1220 loops[j][0][i+1]]))] for i in range(len(loops[j][0])-1)] for \
1221 j in [0,1]]
1222 edges = edges[0] + edges[1]
1223 for j in [0, 1]:
1224 if loops[j][1]: # circular
1225 edges.append(edgekey_to_edge[tuple(sorted([loops[j][0][0],
1226 loops[j][0][-1]]))])
1229 calculation based on face topology (assign edge-normals to vertices)
1231 edge_normal = face_normal x edge_vector
1232 vertex_normal = average(edge_normals)
1234 vertex_normals = dict([(vertex, []) for vertex in loops[0][0]+loops[1][0]])
1235 for edge in edges:
1236 faces = edge_faces[edgekey(edge)] # valid faces connected to edge
1238 if faces:
1239 # get edge coordinates
1240 v1, v2 = [bm.verts[edgekey(edge)[i]].co for i in [0,1]]
1241 edge_vector = v1 - v2
1242 if edge_vector.length < 1e-4:
1243 # zero-length edge, vertices at same location
1244 continue
1245 edge_center = (v1 + v2) / 2
1247 # average face coordinates, if connected to more than 1 valid face
1248 if len(faces) > 1:
1249 face_normal = mathutils.Vector()
1250 face_center = mathutils.Vector()
1251 for face in faces:
1252 face_normal += face.normal
1253 face_center += face.calc_center_median()
1254 face_normal /= len(faces)
1255 face_center /= len(faces)
1256 else:
1257 face_normal = faces[0].normal
1258 face_center = faces[0].calc_center_median()
1259 if face_normal.length < 1e-4:
1260 # faces with a surface of 0 have no face normal
1261 continue
1263 # calculate virtual edge normal
1264 edge_normal = edge_vector.cross(face_normal)
1265 edge_normal.length = 0.01
1266 if (face_center - (edge_center + edge_normal)).length > \
1267 (face_center - (edge_center - edge_normal)).length:
1268 # make normal face the correct way
1269 edge_normal.negate()
1270 edge_normal.normalize()
1271 # add virtual edge normal as entry for both vertices it connects
1272 for vertex in edgekey(edge):
1273 vertex_normals[vertex].append(edge_normal)
1276 calculation based on connection with other loop (vertex focused method)
1277 - used for vertices that aren't connected to any valid faces
1279 plane_normal = edge_vector x connection_vector
1280 vertex_normal = plane_normal x edge_vector
1282 vertices = [vertex for vertex, normal in vertex_normals.items() if not \
1283 normal]
1285 if vertices:
1286 # edge vectors connected to vertices
1287 edge_vectors = dict([[vertex, []] for vertex in vertices])
1288 for edge in edges:
1289 for v in edgekey(edge):
1290 if v in edge_vectors:
1291 edge_vector = bm.verts[edgekey(edge)[0]].co - \
1292 bm.verts[edgekey(edge)[1]].co
1293 if edge_vector.length < 1e-4:
1294 # zero-length edge, vertices at same location
1295 continue
1296 edge_vectors[v].append(edge_vector)
1298 # connection vectors between vertices of both loops
1299 connection_vectors = dict([[vertex, []] for vertex in vertices])
1300 connections = dict([[vertex, []] for vertex in vertices])
1301 for v1, v2 in lines:
1302 if v1 in connection_vectors or v2 in connection_vectors:
1303 new_vector = bm.verts[v1].co - bm.verts[v2].co
1304 if new_vector.length < 1e-4:
1305 # zero-length connection vector,
1306 # vertices in different loops at same location
1307 continue
1308 if v1 in connection_vectors:
1309 connection_vectors[v1].append(new_vector)
1310 connections[v1].append(v2)
1311 if v2 in connection_vectors:
1312 connection_vectors[v2].append(new_vector)
1313 connections[v2].append(v1)
1314 connection_vectors = average_vector_dictionary(connection_vectors)
1315 connection_vectors = dict([[vertex, vector[0]] if vector else \
1316 [vertex, []] for vertex, vector in connection_vectors.items()])
1318 for vertex, values in edge_vectors.items():
1319 # vertex normal doesn't matter, just assign a random vector to it
1320 if not connection_vectors[vertex]:
1321 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1322 continue
1324 # calculate to what location the vertex is connected,
1325 # used to determine what way to flip the normal
1326 connected_center = mathutils.Vector()
1327 for v in connections[vertex]:
1328 connected_center += bm.verts[v].co
1329 if len(connections[vertex]) > 1:
1330 connected_center /= len(connections[vertex])
1331 if len(connections[vertex]) == 0:
1332 # shouldn't be possible, but better safe than sorry
1333 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1334 continue
1336 # can't do proper calculations, because of zero-length vector
1337 if not values:
1338 if (connected_center - (bm.verts[vertex].co + \
1339 connection_vectors[vertex])).length < (connected_center - \
1340 (bm.verts[vertex].co - connection_vectors[vertex])).\
1341 length:
1342 connection_vectors[vertex].negate()
1343 vertex_normals[vertex] = [connection_vectors[vertex].\
1344 normalized()]
1345 continue
1347 # calculate vertex normals using edge-vectors,
1348 # connection-vectors and the derived plane normal
1349 for edge_vector in values:
1350 plane_normal = edge_vector.cross(connection_vectors[vertex])
1351 vertex_normal = edge_vector.cross(plane_normal)
1352 vertex_normal.length = 0.1
1353 if (connected_center - (bm.verts[vertex].co + \
1354 vertex_normal)).length < (connected_center - \
1355 (bm.verts[vertex].co - vertex_normal)).length:
1356 # make normal face the correct way
1357 vertex_normal.negate()
1358 vertex_normal.normalize()
1359 vertex_normals[vertex].append(vertex_normal)
1361 # average virtual vertex normals, based on all edges it's connected to
1362 vertex_normals = average_vector_dictionary(vertex_normals)
1363 vertex_normals = dict([[vertex, vector[0]] for vertex, vector in \
1364 vertex_normals.items()])
1366 return(vertex_normals)
1369 # add vertices to mesh
1370 def bridge_create_vertices(bm, vertices):
1371 for i in range(len(vertices)):
1372 bm.verts.new(vertices[i])
1375 # add faces to mesh
1376 def bridge_create_faces(object, bm, faces, twist):
1377 # have the normal point the correct way
1378 if twist < 0:
1379 [face.reverse() for face in faces]
1380 faces = [face[2:]+face[:2] if face[0]==face[1] else face for \
1381 face in faces]
1383 # eekadoodle prevention
1384 for i in range(len(faces)):
1385 if not faces[i][-1]:
1386 if faces[i][0] == faces[i][-1]:
1387 faces[i] = [faces[i][1], faces[i][2], faces[i][3], faces[i][1]]
1388 else:
1389 faces[i] = [faces[i][-1]] + faces[i][:-1]
1390 # result of converting from pre-bmesh period
1391 if faces[i][-1] == faces[i][-2]:
1392 faces[i] = faces[i][:-1]
1394 new_faces = []
1395 for i in range(len(faces)):
1396 new_faces.append(bm.faces.new([bm.verts[v] for v in faces[i]]))
1397 bm.normal_update()
1398 object.data.update(calc_edges=True) # calc_edges prevents memory-corruption
1400 return(new_faces)
1403 # calculate input loops
1404 def bridge_get_input(bm):
1405 # create list of internal edges, which should be skipped
1406 eks_of_selected_faces = [item for sublist in [face_edgekeys(face) for \
1407 face in bm.faces if face.select and not face.hide] for item in sublist]
1408 edge_count = {}
1409 for ek in eks_of_selected_faces:
1410 if ek in edge_count:
1411 edge_count[ek] += 1
1412 else:
1413 edge_count[ek] = 1
1414 internal_edges = [ek for ek in edge_count if edge_count[ek] > 1]
1416 # sort correct edges into loops
1417 selected_edges = [edgekey(edge) for edge in bm.edges if edge.select \
1418 and not edge.hide and edgekey(edge) not in internal_edges]
1419 loops = get_connected_selections(selected_edges)
1421 return(loops)
1424 # return values needed by the bridge operator
1425 def bridge_initialise(bm, interpolation):
1426 if interpolation == 'cubic':
1427 # dict with edge-key as key and list of connected valid faces as value
1428 face_blacklist = [face.index for face in bm.faces if face.select or \
1429 face.hide]
1430 edge_faces = dict([[edgekey(edge), []] for edge in bm.edges if not \
1431 edge.hide])
1432 for face in bm.faces:
1433 if face.index in face_blacklist:
1434 continue
1435 for key in face_edgekeys(face):
1436 edge_faces[key].append(face)
1437 # dictionary with the edge-key as key and edge as value
1438 edgekey_to_edge = dict([[edgekey(edge), edge] for edge in \
1439 bm.edges if edge.select and not edge.hide])
1440 else:
1441 edge_faces = False
1442 edgekey_to_edge = False
1444 # selected faces input
1445 old_selected_faces = [face.index for face in bm.faces if face.select \
1446 and not face.hide]
1448 # find out if faces created by bridging should be smoothed
1449 smooth = False
1450 if bm.faces:
1451 if sum([face.smooth for face in bm.faces])/len(bm.faces) \
1452 >= 0.5:
1453 smooth = True
1455 return(edge_faces, edgekey_to_edge, old_selected_faces, smooth)
1458 # return a string with the input method
1459 def bridge_input_method(loft, loft_loop):
1460 method = ""
1461 if loft:
1462 if loft_loop:
1463 method = "Loft loop"
1464 else:
1465 method = "Loft no-loop"
1466 else:
1467 method = "Bridge"
1469 return(method)
1472 # match up loops in pairs, used for multi-input bridging
1473 def bridge_match_loops(bm, loops):
1474 # calculate average loop normals and centers
1475 normals = []
1476 centers = []
1477 for vertices, circular in loops:
1478 normal = mathutils.Vector()
1479 center = mathutils.Vector()
1480 for vertex in vertices:
1481 normal += bm.verts[vertex].normal
1482 center += bm.verts[vertex].co
1483 normals.append(normal / len(vertices) / 10)
1484 centers.append(center / len(vertices))
1486 # possible matches if loop normals are faced towards the center
1487 # of the other loop
1488 matches = dict([[i, []] for i in range(len(loops))])
1489 matches_amount = 0
1490 for i in range(len(loops) + 1):
1491 for j in range(i+1, len(loops)):
1492 if (centers[i] - centers[j]).length > (centers[i] - (centers[j] \
1493 + normals[j])).length and (centers[j] - centers[i]).length > \
1494 (centers[j] - (centers[i] + normals[i])).length:
1495 matches_amount += 1
1496 matches[i].append([(centers[i] - centers[j]).length, i, j])
1497 matches[j].append([(centers[i] - centers[j]).length, j, i])
1498 # if no loops face each other, just make matches between all the loops
1499 if matches_amount == 0:
1500 for i in range(len(loops) + 1):
1501 for j in range(i+1, len(loops)):
1502 matches[i].append([(centers[i] - centers[j]).length, i, j])
1503 matches[j].append([(centers[i] - centers[j]).length, j, i])
1504 for key, value in matches.items():
1505 value.sort()
1507 # matches based on distance between centers and number of vertices in loops
1508 new_order = []
1509 for loop_index in range(len(loops)):
1510 if loop_index in new_order:
1511 continue
1512 loop_matches = matches[loop_index]
1513 if not loop_matches:
1514 continue
1515 shortest_distance = loop_matches[0][0]
1516 shortest_distance *= 1.1
1517 loop_matches = [[abs(len(loops[loop_index][0]) - \
1518 len(loops[loop[2]][0])), loop[0], loop[1], loop[2]] for loop in \
1519 loop_matches if loop[0] < shortest_distance]
1520 loop_matches.sort()
1521 for match in loop_matches:
1522 if match[3] not in new_order:
1523 new_order += [loop_index, match[3]]
1524 break
1526 # reorder loops based on matches
1527 if len(new_order) >= 2:
1528 loops = [loops[i] for i in new_order]
1530 return(loops)
1533 # remove old_selected_faces
1534 def bridge_remove_internal_faces(bm, old_selected_faces):
1535 # collect bmesh faces and internal bmesh edges
1536 remove_faces = [bm.faces[face] for face in old_selected_faces]
1537 edges = collections.Counter([edge.index for face in remove_faces for \
1538 edge in face.edges])
1539 remove_edges = [bm.edges[edge] for edge in edges if edges[edge] > 1]
1541 # remove internal faces and edges
1542 for face in remove_faces:
1543 bm.faces.remove(face)
1544 for edge in remove_edges:
1545 bm.edges.remove(edge)
1548 # update list of internal faces that are flagged for removal
1549 def bridge_save_unused_faces(bm, old_selected_faces, loops):
1550 # key: vertex index, value: lists of selected faces using it
1551 vertex_to_face = dict([[i, []] for i in range(len(bm.verts))])
1552 [[vertex_to_face[vertex.index].append(face) for vertex in \
1553 bm.faces[face].verts] for face in old_selected_faces]
1555 # group selected faces that are connected
1556 groups = []
1557 grouped_faces = []
1558 for face in old_selected_faces:
1559 if face in grouped_faces:
1560 continue
1561 grouped_faces.append(face)
1562 group = [face]
1563 new_faces = [face]
1564 while new_faces:
1565 grow_face = new_faces[0]
1566 for vertex in bm.faces[grow_face].verts:
1567 vertex_face_group = [face for face in vertex_to_face[\
1568 vertex.index] if face not in grouped_faces]
1569 new_faces += vertex_face_group
1570 grouped_faces += vertex_face_group
1571 group += vertex_face_group
1572 new_faces.pop(0)
1573 groups.append(group)
1575 # key: vertex index, value: True/False (is it in a loop that is used)
1576 used_vertices = dict([[i, 0] for i in range(len(bm.verts))])
1577 for loop in loops:
1578 for vertex in loop[0]:
1579 used_vertices[vertex] = True
1581 # check if group is bridged, if not remove faces from internal faces list
1582 for group in groups:
1583 used = False
1584 for face in group:
1585 if used:
1586 break
1587 for vertex in bm.faces[face].verts:
1588 if used_vertices[vertex.index]:
1589 used = True
1590 break
1591 if not used:
1592 for face in group:
1593 old_selected_faces.remove(face)
1596 # add the newly created faces to the selection
1597 def bridge_select_new_faces(new_faces, smooth):
1598 for face in new_faces:
1599 face.select_set(True)
1600 face.smooth = smooth
1603 # sort loops, so they are connected in the correct order when lofting
1604 def bridge_sort_loops(bm, loops, loft_loop):
1605 # simplify loops to single points, and prepare for pathfinding
1606 x, y, z = [[sum([bm.verts[i].co[j] for i in loop[0]]) / \
1607 len(loop[0]) for loop in loops] for j in range(3)]
1608 nodes = [mathutils.Vector((x[i], y[i], z[i])) for i in range(len(loops))]
1610 active_node = 0
1611 open = [i for i in range(1, len(loops))]
1612 path = [[0,0]]
1613 # connect node to path, that is shortest to active_node
1614 while len(open) > 0:
1615 distances = [(nodes[active_node] - nodes[i]).length for i in open]
1616 active_node = open[distances.index(min(distances))]
1617 open.remove(active_node)
1618 path.append([active_node, min(distances)])
1619 # check if we didn't start in the middle of the path
1620 for i in range(2, len(path)):
1621 if (nodes[path[i][0]]-nodes[0]).length < path[i][1]:
1622 temp = path[:i]
1623 path.reverse()
1624 path = path[:-i] + temp
1625 break
1627 # reorder loops
1628 loops = [loops[i[0]] for i in path]
1629 # if requested, duplicate first loop at last position, so loft can loop
1630 if loft_loop:
1631 loops = loops + [loops[0]]
1633 return(loops)
1636 # remapping old indices to new position in list
1637 def bridge_update_old_selection(bm, old_selected_faces):
1638 #old_indices = old_selected_faces[:]
1639 #old_selected_faces = []
1640 #for i, face in enumerate(bm.faces):
1641 # if face.index in old_indices:
1642 # old_selected_faces.append(i)
1644 old_selected_faces = [i for i, face in enumerate(bm.faces) if face.index \
1645 in old_selected_faces]
1647 return(old_selected_faces)
1650 ##########################################
1651 ####### Circle functions #################
1652 ##########################################
1654 # convert 3d coordinates to 2d coordinates on plane
1655 def circle_3d_to_2d(bm_mod, loop, com, normal):
1656 # project vertices onto the plane
1657 verts = [bm_mod.verts[v] for v in loop[0]]
1658 verts_projected = [[v.co - (v.co - com).dot(normal) * normal, v.index]
1659 for v in verts]
1661 # calculate two vectors (p and q) along the plane
1662 m = mathutils.Vector((normal[0] + 1.0, normal[1], normal[2]))
1663 p = m - (m.dot(normal) * normal)
1664 if p.dot(p) == 0.0:
1665 m = mathutils.Vector((normal[0], normal[1] + 1.0, normal[2]))
1666 p = m - (m.dot(normal) * normal)
1667 q = p.cross(normal)
1669 # change to 2d coordinates using perpendicular projection
1670 locs_2d = []
1671 for loc, vert in verts_projected:
1672 vloc = loc - com
1673 x = p.dot(vloc) / p.dot(p)
1674 y = q.dot(vloc) / q.dot(q)
1675 locs_2d.append([x, y, vert])
1677 return(locs_2d, p, q)
1680 # calculate a best-fit circle to the 2d locations on the plane
1681 def circle_calculate_best_fit(locs_2d):
1682 # initial guess
1683 x0 = 0.0
1684 y0 = 0.0
1685 r = 1.0
1687 # calculate center and radius (non-linear least squares solution)
1688 for iter in range(500):
1689 jmat = []
1690 k = []
1691 for v in locs_2d:
1692 d = (v[0]**2-2.0*x0*v[0]+v[1]**2-2.0*y0*v[1]+x0**2+y0**2)**0.5
1693 jmat.append([(x0-v[0])/d, (y0-v[1])/d, -1.0])
1694 k.append(-(((v[0]-x0)**2+(v[1]-y0)**2)**0.5-r))
1695 jmat2 = mathutils.Matrix(((0.0, 0.0, 0.0),
1696 (0.0, 0.0, 0.0),
1697 (0.0, 0.0, 0.0),
1699 k2 = mathutils.Vector((0.0, 0.0, 0.0))
1700 for i in range(len(jmat)):
1701 k2 += mathutils.Vector(jmat[i])*k[i]
1702 jmat2[0][0] += jmat[i][0]**2
1703 jmat2[1][0] += jmat[i][0]*jmat[i][1]
1704 jmat2[2][0] += jmat[i][0]*jmat[i][2]
1705 jmat2[1][1] += jmat[i][1]**2
1706 jmat2[2][1] += jmat[i][1]*jmat[i][2]
1707 jmat2[2][2] += jmat[i][2]**2
1708 jmat2[0][1] = jmat2[1][0]
1709 jmat2[0][2] = jmat2[2][0]
1710 jmat2[1][2] = jmat2[2][1]
1711 try:
1712 jmat2.invert()
1713 except:
1714 pass
1715 dx0, dy0, dr = jmat2 * k2
1716 x0 += dx0
1717 y0 += dy0
1718 r += dr
1719 # stop iterating if we're close enough to optimal solution
1720 if abs(dx0)<1e-6 and abs(dy0)<1e-6 and abs(dr)<1e-6:
1721 break
1723 # return center of circle and radius
1724 return(x0, y0, r)
1727 # calculate circle so no vertices have to be moved away from the center
1728 def circle_calculate_min_fit(locs_2d):
1729 # center of circle
1730 x0 = (min([i[0] for i in locs_2d])+max([i[0] for i in locs_2d]))/2.0
1731 y0 = (min([i[1] for i in locs_2d])+max([i[1] for i in locs_2d]))/2.0
1732 center = mathutils.Vector([x0, y0])
1733 # radius of circle
1734 r = min([(mathutils.Vector([i[0], i[1]])-center).length for i in locs_2d])
1736 # return center of circle and radius
1737 return(x0, y0, r)
1740 # calculate the new locations of the vertices that need to be moved
1741 def circle_calculate_verts(flatten, bm_mod, locs_2d, com, p, q, normal):
1742 # changing 2d coordinates back to 3d coordinates
1743 locs_3d = []
1744 for loc in locs_2d:
1745 locs_3d.append([loc[2], loc[0]*p + loc[1]*q + com])
1747 if flatten: # flat circle
1748 return(locs_3d)
1750 else: # project the locations on the existing mesh
1751 vert_edges = dict_vert_edges(bm_mod)
1752 vert_faces = dict_vert_faces(bm_mod)
1753 faces = [f for f in bm_mod.faces if not f.hide]
1754 rays = [normal, -normal]
1755 new_locs = []
1756 for loc in locs_3d:
1757 projection = False
1758 if bm_mod.verts[loc[0]].co == loc[1]: # vertex hasn't moved
1759 projection = loc[1]
1760 else:
1761 dif = normal.angle(loc[1]-bm_mod.verts[loc[0]].co)
1762 if -1e-6 < dif < 1e-6 or math.pi-1e-6 < dif < math.pi+1e-6:
1763 # original location is already along projection normal
1764 projection = bm_mod.verts[loc[0]].co
1765 else:
1766 # quick search through adjacent faces
1767 for face in vert_faces[loc[0]]:
1768 verts = [v.co for v in bm_mod.faces[face].verts]
1769 if len(verts) == 3: # triangle
1770 v1, v2, v3 = verts
1771 v4 = False
1772 else: # assume quad
1773 v1, v2, v3, v4 = verts[:4]
1774 for ray in rays:
1775 intersect = mathutils.geometry.\
1776 intersect_ray_tri(v1, v2, v3, ray, loc[1])
1777 if intersect:
1778 projection = intersect
1779 break
1780 elif v4:
1781 intersect = mathutils.geometry.\
1782 intersect_ray_tri(v1, v3, v4, ray, loc[1])
1783 if intersect:
1784 projection = intersect
1785 break
1786 if projection:
1787 break
1788 if not projection:
1789 # check if projection is on adjacent edges
1790 for edgekey in vert_edges[loc[0]]:
1791 line1 = bm_mod.verts[edgekey[0]].co
1792 line2 = bm_mod.verts[edgekey[1]].co
1793 intersect, dist = mathutils.geometry.intersect_point_line(\
1794 loc[1], line1, line2)
1795 if 1e-6 < dist < 1 - 1e-6:
1796 projection = intersect
1797 break
1798 if not projection:
1799 # full search through the entire mesh
1800 hits = []
1801 for face in faces:
1802 verts = [v.co for v in face.verts]
1803 if len(verts) == 3: # triangle
1804 v1, v2, v3 = verts
1805 v4 = False
1806 else: # assume quad
1807 v1, v2, v3, v4 = verts[:4]
1808 for ray in rays:
1809 intersect = mathutils.geometry.intersect_ray_tri(\
1810 v1, v2, v3, ray, loc[1])
1811 if intersect:
1812 hits.append([(loc[1] - intersect).length,
1813 intersect])
1814 break
1815 elif v4:
1816 intersect = mathutils.geometry.intersect_ray_tri(\
1817 v1, v3, v4, ray, loc[1])
1818 if intersect:
1819 hits.append([(loc[1] - intersect).length,
1820 intersect])
1821 break
1822 if len(hits) >= 1:
1823 # if more than 1 hit with mesh, closest hit is new loc
1824 hits.sort()
1825 projection = hits[0][1]
1826 if not projection:
1827 # nothing to project on, remain at flat location
1828 projection = loc[1]
1829 new_locs.append([loc[0], projection])
1831 # return new positions of projected circle
1832 return(new_locs)
1835 # check loops and only return valid ones
1836 def circle_check_loops(single_loops, loops, mapping, bm_mod):
1837 valid_single_loops = {}
1838 valid_loops = []
1839 for i, [loop, circular] in enumerate(loops):
1840 # loop needs to have at least 3 vertices
1841 if len(loop) < 3:
1842 continue
1843 # loop needs at least 1 vertex in the original, non-mirrored mesh
1844 if mapping:
1845 all_virtual = True
1846 for vert in loop:
1847 if mapping[vert] > -1:
1848 all_virtual = False
1849 break
1850 if all_virtual:
1851 continue
1852 # loop has to be non-collinear
1853 collinear = True
1854 loc0 = mathutils.Vector(bm_mod.verts[loop[0]].co[:])
1855 loc1 = mathutils.Vector(bm_mod.verts[loop[1]].co[:])
1856 for v in loop[2:]:
1857 locn = mathutils.Vector(bm_mod.verts[v].co[:])
1858 if loc0 == loc1 or loc1 == locn:
1859 loc0 = loc1
1860 loc1 = locn
1861 continue
1862 d1 = loc1-loc0
1863 d2 = locn-loc1
1864 if -1e-6 < d1.angle(d2, 0) < 1e-6:
1865 loc0 = loc1
1866 loc1 = locn
1867 continue
1868 collinear = False
1869 break
1870 if collinear:
1871 continue
1872 # passed all tests, loop is valid
1873 valid_loops.append([loop, circular])
1874 valid_single_loops[len(valid_loops)-1] = single_loops[i]
1876 return(valid_single_loops, valid_loops)
1879 # calculate the location of single input vertices that need to be flattened
1880 def circle_flatten_singles(bm_mod, com, p, q, normal, single_loop):
1881 new_locs = []
1882 for vert in single_loop:
1883 loc = mathutils.Vector(bm_mod.verts[vert].co[:])
1884 new_locs.append([vert, loc - (loc-com).dot(normal)*normal])
1886 return(new_locs)
1889 # calculate input loops
1890 def circle_get_input(object, bm, scene):
1891 # get mesh with modifiers applied
1892 derived, bm_mod = get_derived_bmesh(object, bm, scene)
1894 # create list of edge-keys based on selection state
1895 faces = False
1896 for face in bm.faces:
1897 if face.select and not face.hide:
1898 faces = True
1899 break
1900 if faces:
1901 # get selected, non-hidden , non-internal edge-keys
1902 eks_selected = [key for keys in [face_edgekeys(face) for face in \
1903 bm_mod.faces if face.select and not face.hide] for key in keys]
1904 edge_count = {}
1905 for ek in eks_selected:
1906 if ek in edge_count:
1907 edge_count[ek] += 1
1908 else:
1909 edge_count[ek] = 1
1910 edge_keys = [edgekey(edge) for edge in bm_mod.edges if edge.select \
1911 and not edge.hide and edge_count.get(edgekey(edge), 1)==1]
1912 else:
1913 # no faces, so no internal edges either
1914 edge_keys = [edgekey(edge) for edge in bm_mod.edges if edge.select \
1915 and not edge.hide]
1917 # add edge-keys around single vertices
1918 verts_connected = dict([[vert, 1] for edge in [edge for edge in \
1919 bm_mod.edges if edge.select and not edge.hide] for vert in \
1920 edgekey(edge)])
1921 single_vertices = [vert.index for vert in bm_mod.verts if \
1922 vert.select and not vert.hide and not \
1923 verts_connected.get(vert.index, False)]
1925 if single_vertices and len(bm.faces)>0:
1926 vert_to_single = dict([[v.index, []] for v in bm_mod.verts \
1927 if not v.hide])
1928 for face in [face for face in bm_mod.faces if not face.select \
1929 and not face.hide]:
1930 for vert in face.verts:
1931 vert = vert.index
1932 if vert in single_vertices:
1933 for ek in face_edgekeys(face):
1934 if not vert in ek:
1935 edge_keys.append(ek)
1936 if vert not in vert_to_single[ek[0]]:
1937 vert_to_single[ek[0]].append(vert)
1938 if vert not in vert_to_single[ek[1]]:
1939 vert_to_single[ek[1]].append(vert)
1940 break
1942 # sort edge-keys into loops
1943 loops = get_connected_selections(edge_keys)
1945 # find out to which loops the single vertices belong
1946 single_loops = dict([[i, []] for i in range(len(loops))])
1947 if single_vertices and len(bm.faces)>0:
1948 for i, [loop, circular] in enumerate(loops):
1949 for vert in loop:
1950 if vert_to_single[vert]:
1951 for single in vert_to_single[vert]:
1952 if single not in single_loops[i]:
1953 single_loops[i].append(single)
1955 return(derived, bm_mod, single_vertices, single_loops, loops)
1958 # recalculate positions based on the influence of the circle shape
1959 def circle_influence_locs(locs_2d, new_locs_2d, influence):
1960 for i in range(len(locs_2d)):
1961 oldx, oldy, j = locs_2d[i]
1962 newx, newy, k = new_locs_2d[i]
1963 altx = newx*(influence/100)+ oldx*((100-influence)/100)
1964 alty = newy*(influence/100)+ oldy*((100-influence)/100)
1965 locs_2d[i] = [altx, alty, j]
1967 return(locs_2d)
1970 # project 2d locations on circle, respecting distance relations between verts
1971 def circle_project_non_regular(locs_2d, x0, y0, r):
1972 for i in range(len(locs_2d)):
1973 x, y, j = locs_2d[i]
1974 loc = mathutils.Vector([x-x0, y-y0])
1975 loc.length = r
1976 locs_2d[i] = [loc[0], loc[1], j]
1978 return(locs_2d)
1981 # project 2d locations on circle, with equal distance between all vertices
1982 def circle_project_regular(locs_2d, x0, y0, r):
1983 # find offset angle and circling direction
1984 x, y, i = locs_2d[0]
1985 loc = mathutils.Vector([x-x0, y-y0])
1986 loc.length = r
1987 offset_angle = loc.angle(mathutils.Vector([1.0, 0.0]), 0.0)
1988 loca = mathutils.Vector([x-x0, y-y0, 0.0])
1989 if loc[1] < -1e-6:
1990 offset_angle *= -1
1991 x, y, j = locs_2d[1]
1992 locb = mathutils.Vector([x-x0, y-y0, 0.0])
1993 if loca.cross(locb)[2] >= 0:
1994 ccw = 1
1995 else:
1996 ccw = -1
1997 # distribute vertices along the circle
1998 for i in range(len(locs_2d)):
1999 t = offset_angle + ccw * (i / len(locs_2d) * 2 * math.pi)
2000 x = math.cos(t) * r
2001 y = math.sin(t) * r
2002 locs_2d[i] = [x, y, locs_2d[i][2]]
2004 return(locs_2d)
2007 # shift loop, so the first vertex is closest to the center
2008 def circle_shift_loop(bm_mod, loop, com):
2009 verts, circular = loop
2010 distances = [[(bm_mod.verts[vert].co - com).length, i] \
2011 for i, vert in enumerate(verts)]
2012 distances.sort()
2013 shift = distances[0][1]
2014 loop = [verts[shift:] + verts[:shift], circular]
2016 return(loop)
2019 ##########################################
2020 ####### Curve functions ##################
2021 ##########################################
2023 # create lists with knots and points, all correctly sorted
2024 def curve_calculate_knots(loop, verts_selected):
2025 knots = [v for v in loop[0] if v in verts_selected]
2026 points = loop[0][:]
2027 # circular loop, potential for weird splines
2028 if loop[1]:
2029 offset = int(len(loop[0]) / 4)
2030 kpos = []
2031 for k in knots:
2032 kpos.append(loop[0].index(k))
2033 kdif = []
2034 for i in range(len(kpos) - 1):
2035 kdif.append(kpos[i+1] - kpos[i])
2036 kdif.append(len(loop[0]) - kpos[-1] + kpos[0])
2037 kadd = []
2038 for k in kdif:
2039 if k > 2 * offset:
2040 kadd.append([kdif.index(k), True])
2041 # next 2 lines are optional, they insert
2042 # an extra control point in small gaps
2043 #elif k > offset:
2044 # kadd.append([kdif.index(k), False])
2045 kins = []
2046 krot = False
2047 for k in kadd: # extra knots to be added
2048 if k[1]: # big gap (break circular spline)
2049 kpos = loop[0].index(knots[k[0]]) + offset
2050 if kpos > len(loop[0]) - 1:
2051 kpos -= len(loop[0])
2052 kins.append([knots[k[0]], loop[0][kpos]])
2053 kpos2 = k[0] + 1
2054 if kpos2 > len(knots)-1:
2055 kpos2 -= len(knots)
2056 kpos2 = loop[0].index(knots[kpos2]) - offset
2057 if kpos2 < 0:
2058 kpos2 += len(loop[0])
2059 kins.append([loop[0][kpos], loop[0][kpos2]])
2060 krot = loop[0][kpos2]
2061 else: # small gap (keep circular spline)
2062 k1 = loop[0].index(knots[k[0]])
2063 k2 = k[0] + 1
2064 if k2 > len(knots)-1:
2065 k2 -= len(knots)
2066 k2 = loop[0].index(knots[k2])
2067 if k2 < k1:
2068 dif = len(loop[0]) - 1 - k1 + k2
2069 else:
2070 dif = k2 - k1
2071 kn = k1 + int(dif/2)
2072 if kn > len(loop[0]) - 1:
2073 kn -= len(loop[0])
2074 kins.append([loop[0][k1], loop[0][kn]])
2075 for j in kins: # insert new knots
2076 knots.insert(knots.index(j[0]) + 1, j[1])
2077 if not krot: # circular loop
2078 knots.append(knots[0])
2079 points = loop[0][loop[0].index(knots[0]):]
2080 points += loop[0][0:loop[0].index(knots[0]) + 1]
2081 else: # non-circular loop (broken by script)
2082 krot = knots.index(krot)
2083 knots = knots[krot:] + knots[0:krot]
2084 if loop[0].index(knots[0]) > loop[0].index(knots[-1]):
2085 points = loop[0][loop[0].index(knots[0]):]
2086 points += loop[0][0:loop[0].index(knots[-1])+1]
2087 else:
2088 points = loop[0][loop[0].index(knots[0]):\
2089 loop[0].index(knots[-1]) + 1]
2090 # non-circular loop, add first and last point as knots
2091 else:
2092 if loop[0][0] not in knots:
2093 knots.insert(0, loop[0][0])
2094 if loop[0][-1] not in knots:
2095 knots.append(loop[0][-1])
2097 return(knots, points)
2100 # calculate relative positions compared to first knot
2101 def curve_calculate_t(bm_mod, knots, points, pknots, regular, circular):
2102 tpoints = []
2103 loc_prev = False
2104 len_total = 0
2106 for p in points:
2107 if p in knots:
2108 loc = pknots[knots.index(p)] # use projected knot location
2109 else:
2110 loc = mathutils.Vector(bm_mod.verts[p].co[:])
2111 if not loc_prev:
2112 loc_prev = loc
2113 len_total += (loc-loc_prev).length
2114 tpoints.append(len_total)
2115 loc_prev = loc
2116 tknots = []
2117 for p in points:
2118 if p in knots:
2119 tknots.append(tpoints[points.index(p)])
2120 if circular:
2121 tknots[-1] = tpoints[-1]
2123 # regular option
2124 if regular:
2125 tpoints_average = tpoints[-1] / (len(tpoints) - 1)
2126 for i in range(1, len(tpoints) - 1):
2127 tpoints[i] = i * tpoints_average
2128 for i in range(len(knots)):
2129 tknots[i] = tpoints[points.index(knots[i])]
2130 if circular:
2131 tknots[-1] = tpoints[-1]
2134 return(tknots, tpoints)
2137 # change the location of non-selected points to their place on the spline
2138 def curve_calculate_vertices(bm_mod, knots, tknots, points, tpoints, splines,
2139 interpolation, restriction):
2140 newlocs = {}
2141 move = []
2143 for p in points:
2144 if p in knots:
2145 continue
2146 m = tpoints[points.index(p)]
2147 if m in tknots:
2148 n = tknots.index(m)
2149 else:
2150 t = tknots[:]
2151 t.append(m)
2152 t.sort()
2153 n = t.index(m) - 1
2154 if n > len(splines) - 1:
2155 n = len(splines) - 1
2156 elif n < 0:
2157 n = 0
2159 if interpolation == 'cubic':
2160 ax, bx, cx, dx, tx = splines[n][0]
2161 x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
2162 ay, by, cy, dy, ty = splines[n][1]
2163 y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
2164 az, bz, cz, dz, tz = splines[n][2]
2165 z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
2166 newloc = mathutils.Vector([x,y,z])
2167 else: # interpolation == 'linear'
2168 a, d, t, u = splines[n]
2169 newloc = ((m-t)/u)*d + a
2171 if restriction != 'none': # vertex movement is restricted
2172 newlocs[p] = newloc
2173 else: # set the vertex to its new location
2174 move.append([p, newloc])
2176 if restriction != 'none': # vertex movement is restricted
2177 for p in points:
2178 if p in newlocs:
2179 newloc = newlocs[p]
2180 else:
2181 move.append([p, bm_mod.verts[p].co])
2182 continue
2183 oldloc = bm_mod.verts[p].co
2184 normal = bm_mod.verts[p].normal
2185 dloc = newloc - oldloc
2186 if dloc.length < 1e-6:
2187 move.append([p, newloc])
2188 elif restriction == 'extrude': # only extrusions
2189 if dloc.angle(normal, 0) < 0.5 * math.pi + 1e-6:
2190 move.append([p, newloc])
2191 else: # restriction == 'indent' only indentations
2192 if dloc.angle(normal) > 0.5 * math.pi - 1e-6:
2193 move.append([p, newloc])
2195 return(move)
2198 # trim loops to part between first and last selected vertices (including)
2199 def curve_cut_boundaries(bm_mod, loops):
2200 cut_loops = []
2201 for loop, circular in loops:
2202 if circular:
2203 # don't cut
2204 cut_loops.append([loop, circular])
2205 continue
2206 selected = [bm_mod.verts[v].select for v in loop]
2207 first = selected.index(True)
2208 selected.reverse()
2209 last = -selected.index(True)
2210 if last == 0:
2211 cut_loops.append([loop[first:], circular])
2212 else:
2213 cut_loops.append([loop[first:last], circular])
2215 return(cut_loops)
2218 # calculate input loops
2219 def curve_get_input(object, bm, boundaries, scene):
2220 # get mesh with modifiers applied
2221 derived, bm_mod = get_derived_bmesh(object, bm, scene)
2223 # vertices that still need a loop to run through it
2224 verts_unsorted = [v.index for v in bm_mod.verts if \
2225 v.select and not v.hide]
2226 # necessary dictionaries
2227 vert_edges = dict_vert_edges(bm_mod)
2228 edge_faces = dict_edge_faces(bm_mod)
2229 correct_loops = []
2231 # find loops through each selected vertex
2232 while len(verts_unsorted) > 0:
2233 loops = curve_vertex_loops(bm_mod, verts_unsorted[0], vert_edges,
2234 edge_faces)
2235 verts_unsorted.pop(0)
2237 # check if loop is fully selected
2238 search_perpendicular = False
2239 i = -1
2240 for loop, circular in loops:
2241 i += 1
2242 selected = [v for v in loop if bm_mod.verts[v].select]
2243 if len(selected) < 2:
2244 # only one selected vertex on loop, don't use
2245 loops.pop(i)
2246 continue
2247 elif len(selected) == len(loop):
2248 search_perpendicular = loop
2249 break
2250 # entire loop is selected, find perpendicular loops
2251 if search_perpendicular:
2252 for vert in loop:
2253 if vert in verts_unsorted:
2254 verts_unsorted.remove(vert)
2255 perp_loops = curve_perpendicular_loops(bm_mod, loop,
2256 vert_edges, edge_faces)
2257 for perp_loop in perp_loops:
2258 correct_loops.append(perp_loop)
2259 # normal input
2260 else:
2261 for loop, circular in loops:
2262 correct_loops.append([loop, circular])
2264 # boundaries option
2265 if boundaries:
2266 correct_loops = curve_cut_boundaries(bm_mod, correct_loops)
2268 return(derived, bm_mod, correct_loops)
2271 # return all loops that are perpendicular to the given one
2272 def curve_perpendicular_loops(bm_mod, start_loop, vert_edges, edge_faces):
2273 # find perpendicular loops
2274 perp_loops = []
2275 for start_vert in start_loop:
2276 loops = curve_vertex_loops(bm_mod, start_vert, vert_edges,
2277 edge_faces)
2278 for loop, circular in loops:
2279 selected = [v for v in loop if bm_mod.verts[v].select]
2280 if len(selected) == len(loop):
2281 continue
2282 else:
2283 perp_loops.append([loop, circular, loop.index(start_vert)])
2285 # trim loops to same lengths
2286 shortest = [[len(loop[0]), i] for i, loop in enumerate(perp_loops)\
2287 if not loop[1]]
2288 if not shortest:
2289 # all loops are circular, not trimming
2290 return([[loop[0], loop[1]] for loop in perp_loops])
2291 else:
2292 shortest = min(shortest)
2293 shortest_start = perp_loops[shortest[1]][2]
2294 before_start = shortest_start
2295 after_start = shortest[0] - shortest_start - 1
2296 bigger_before = before_start > after_start
2297 trimmed_loops = []
2298 for loop in perp_loops:
2299 # have the loop face the same direction as the shortest one
2300 if bigger_before:
2301 if loop[2] < len(loop[0]) / 2:
2302 loop[0].reverse()
2303 loop[2] = len(loop[0]) - loop[2] - 1
2304 else:
2305 if loop[2] > len(loop[0]) / 2:
2306 loop[0].reverse()
2307 loop[2] = len(loop[0]) - loop[2] - 1
2308 # circular loops can shift, to prevent wrong trimming
2309 if loop[1]:
2310 shift = shortest_start - loop[2]
2311 if loop[2] + shift > 0 and loop[2] + shift < len(loop[0]):
2312 loop[0] = loop[0][-shift:] + loop[0][:-shift]
2313 loop[2] += shift
2314 if loop[2] < 0:
2315 loop[2] += len(loop[0])
2316 elif loop[2] > len(loop[0]) -1:
2317 loop[2] -= len(loop[0])
2318 # trim
2319 start = max(0, loop[2] - before_start)
2320 end = min(len(loop[0]), loop[2] + after_start + 1)
2321 trimmed_loops.append([loop[0][start:end], False])
2323 return(trimmed_loops)
2326 # project knots on non-selected geometry
2327 def curve_project_knots(bm_mod, verts_selected, knots, points, circular):
2328 # function to project vertex on edge
2329 def project(v1, v2, v3):
2330 # v1 and v2 are part of a line
2331 # v3 is projected onto it
2332 v2 -= v1
2333 v3 -= v1
2334 p = v3.project(v2)
2335 return(p + v1)
2337 if circular: # project all knots
2338 start = 0
2339 end = len(knots)
2340 pknots = []
2341 else: # first and last knot shouldn't be projected
2342 start = 1
2343 end = -1
2344 pknots = [mathutils.Vector(bm_mod.verts[knots[0]].co[:])]
2345 for knot in knots[start:end]:
2346 if knot in verts_selected:
2347 knot_left = knot_right = False
2348 for i in range(points.index(knot)-1, -1*len(points), -1):
2349 if points[i] not in knots:
2350 knot_left = points[i]
2351 break
2352 for i in range(points.index(knot)+1, 2*len(points)):
2353 if i > len(points) - 1:
2354 i -= len(points)
2355 if points[i] not in knots:
2356 knot_right = points[i]
2357 break
2358 if knot_left and knot_right and knot_left != knot_right:
2359 knot_left = mathutils.Vector(\
2360 bm_mod.verts[knot_left].co[:])
2361 knot_right = mathutils.Vector(\
2362 bm_mod.verts[knot_right].co[:])
2363 knot = mathutils.Vector(bm_mod.verts[knot].co[:])
2364 pknots.append(project(knot_left, knot_right, knot))
2365 else:
2366 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2367 else: # knot isn't selected, so shouldn't be changed
2368 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2369 if not circular:
2370 pknots.append(mathutils.Vector(bm_mod.verts[knots[-1]].co[:]))
2372 return(pknots)
2375 # find all loops through a given vertex
2376 def curve_vertex_loops(bm_mod, start_vert, vert_edges, edge_faces):
2377 edges_used = []
2378 loops = []
2380 for edge in vert_edges[start_vert]:
2381 if edge in edges_used:
2382 continue
2383 loop = []
2384 circular = False
2385 for vert in edge:
2386 active_faces = edge_faces[edge]
2387 new_vert = vert
2388 growing = True
2389 while growing:
2390 growing = False
2391 new_edges = vert_edges[new_vert]
2392 loop.append(new_vert)
2393 if len(loop) > 1:
2394 edges_used.append(tuple(sorted([loop[-1], loop[-2]])))
2395 if len(new_edges) < 3 or len(new_edges) > 4:
2396 # pole
2397 break
2398 else:
2399 # find next edge
2400 for new_edge in new_edges:
2401 if new_edge in edges_used:
2402 continue
2403 eliminate = False
2404 for new_face in edge_faces[new_edge]:
2405 if new_face in active_faces:
2406 eliminate = True
2407 break
2408 if eliminate:
2409 continue
2410 # found correct new edge
2411 active_faces = edge_faces[new_edge]
2412 v1, v2 = new_edge
2413 if v1 != new_vert:
2414 new_vert = v1
2415 else:
2416 new_vert = v2
2417 if new_vert == loop[0]:
2418 circular = True
2419 else:
2420 growing = True
2421 break
2422 if circular:
2423 break
2424 loop.reverse()
2425 loops.append([loop, circular])
2427 return(loops)
2430 ##########################################
2431 ####### Flatten functions ################
2432 ##########################################
2434 # sort input into loops
2435 def flatten_get_input(bm):
2436 vert_verts = dict_vert_verts([edgekey(edge) for edge in bm.edges \
2437 if edge.select and not edge.hide])
2438 verts = [v.index for v in bm.verts if v.select and not v.hide]
2440 # no connected verts, consider all selected verts as a single input
2441 if not vert_verts:
2442 return([[verts, False]])
2444 loops = []
2445 while len(verts) > 0:
2446 # start of loop
2447 loop = [verts[0]]
2448 verts.pop(0)
2449 if loop[-1] in vert_verts:
2450 to_grow = vert_verts[loop[-1]]
2451 else:
2452 to_grow = []
2453 # grow loop
2454 while len(to_grow) > 0:
2455 new_vert = to_grow[0]
2456 to_grow.pop(0)
2457 if new_vert in loop:
2458 continue
2459 loop.append(new_vert)
2460 verts.remove(new_vert)
2461 to_grow += vert_verts[new_vert]
2462 # add loop to loops
2463 loops.append([loop, False])
2465 return(loops)
2468 # calculate position of vertex projections on plane
2469 def flatten_project(bm, loop, com, normal):
2470 verts = [bm.verts[v] for v in loop[0]]
2471 verts_projected = [[v.index, mathutils.Vector(v.co[:]) - \
2472 (mathutils.Vector(v.co[:])-com).dot(normal)*normal] for v in verts]
2474 return(verts_projected)
2477 ##########################################
2478 ####### Gstretch functions ###############
2479 ##########################################
2481 # fake stroke class, used to create custom strokes if no GP data is found
2482 class gstretch_fake_stroke():
2483 def __init__(self, points):
2484 self.points = [gstretch_fake_stroke_point(p) for p in points]
2487 # fake stroke point class, used in fake strokes
2488 class gstretch_fake_stroke_point():
2489 def __init__(self, loc):
2490 self.co = loc
2493 # flips loops, if necessary, to obtain maximum alignment to stroke
2494 def gstretch_align_pairs(ls_pairs, object, bm_mod, method):
2495 # returns total distance between all verts in loop and corresponding stroke
2496 def distance_loop_stroke(loop, stroke, object, bm_mod, method):
2497 stroke_lengths_cache = False
2498 loop_length = len(loop[0])
2499 total_distance = 0
2501 if method != 'regular':
2502 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2504 for i, v_index in enumerate(loop[0]):
2505 if method == 'regular':
2506 relative_distance = i / (loop_length - 1)
2507 else:
2508 relative_distance = relative_lengths[i]
2510 loc1 = object.matrix_world * bm_mod.verts[v_index].co
2511 loc2, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2512 relative_distance, stroke_lengths_cache)
2513 total_distance += (loc2 - loc1).length
2515 return(total_distance)
2517 if ls_pairs:
2518 for (loop, stroke) in ls_pairs:
2519 distance_loop_stroke
2520 total_dist = distance_loop_stroke(loop, stroke, object, bm_mod,
2521 method)
2522 loop[0].reverse()
2523 total_dist_rev = distance_loop_stroke(loop, stroke, object, bm_mod,
2524 method)
2525 if total_dist_rev > total_dist:
2526 loop[0].reverse()
2528 return(ls_pairs)
2531 # calculate vertex positions on stroke
2532 def gstretch_calculate_verts(loop, stroke, object, bm_mod, method):
2533 move = []
2534 stroke_lengths_cache = False
2535 loop_length = len(loop[0])
2536 matrix_inverse = object.matrix_world.inverted()
2538 # return intersection of line with stroke, or None
2539 def intersect_line_stroke(vec1, vec2, stroke):
2540 for i, p in enumerate(stroke.points[1:]):
2541 intersections = mathutils.geometry.intersect_line_line(vec1, vec2,
2542 p.co, stroke.points[i].co)
2543 if intersections and \
2544 (intersections[0] - intersections[1]).length < 1e-2:
2545 x, dist = mathutils.geometry.intersect_point_line(
2546 intersections[0], p.co, stroke.points[i].co)
2547 if -1 < dist < 1:
2548 return(intersections[0])
2549 return(None)
2551 if method == 'project':
2552 projection_vectors = []
2553 vert_edges = dict_vert_edges(bm_mod)
2555 for v_index in loop[0]:
2556 for ek in vert_edges[v_index]:
2557 v1, v2 = ek
2558 v1 = bm_mod.verts[v1]
2559 v2 = bm_mod.verts[v2]
2560 if v1.select + v2.select == 1 and not v1.hide and not v2.hide:
2561 vec1 = object.matrix_world * v1.co
2562 vec2 = object.matrix_world * v2.co
2563 intersection = intersect_line_stroke(vec1, vec2, stroke)
2564 if intersection:
2565 break
2566 if not intersection:
2567 v = bm_mod.verts[v_index]
2568 intersection = intersect_line_stroke(v.co, v.co + v.normal,
2569 stroke)
2570 if intersection:
2571 move.append([v_index, matrix_inverse * intersection])
2573 else:
2574 if method == 'irregular':
2575 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2577 for i, v_index in enumerate(loop[0]):
2578 if method == 'regular':
2579 relative_distance = i / (loop_length - 1)
2580 else: # method == 'irregular'
2581 relative_distance = relative_lengths[i]
2582 loc, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2583 relative_distance, stroke_lengths_cache)
2584 loc = matrix_inverse * loc
2585 move.append([v_index, loc])
2587 return(move)
2590 # create new vertices, based on GP strokes
2591 def gstretch_create_verts(object, bm_mod, strokes, method, conversion,
2592 conversion_distance, conversion_max, conversion_min, conversion_vertices):
2593 move = []
2594 stroke_verts = []
2595 mat_world = object.matrix_world.inverted()
2596 singles = gstretch_match_single_verts(bm_mod, strokes, mat_world)
2598 for stroke in strokes:
2599 stroke_verts.append([stroke, []])
2600 min_end_point = 0
2601 if conversion == 'vertices':
2602 min_end_point = conversion_vertices
2603 end_point = conversion_vertices
2604 elif conversion == 'limit_vertices':
2605 min_end_point = conversion_min
2606 end_point = conversion_max
2607 else:
2608 end_point = len(stroke.points)
2609 # creation of new vertices at fixed user-defined distances
2610 if conversion == 'distance':
2611 method = 'project'
2612 prev_point = stroke.points[0]
2613 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * \
2614 prev_point.co))
2615 distance = 0
2616 limit = conversion_distance
2617 for point in stroke.points:
2618 new_distance = distance + (point.co - prev_point.co).length
2619 iteration = 0
2620 while new_distance > limit:
2621 to_cover = limit - distance + (limit * iteration)
2622 new_loc = prev_point.co + to_cover * \
2623 (point.co - prev_point.co).normalized()
2624 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * \
2625 new_loc))
2626 new_distance -= limit
2627 iteration += 1
2628 distance = new_distance
2629 prev_point = point
2630 # creation of new vertices for other methods
2631 else:
2632 # add vertices at stroke points
2633 for point in stroke.points[:end_point]:
2634 stroke_verts[-1][1].append(bm_mod.verts.new(\
2635 mat_world * point.co))
2636 # add more vertices, beyond the points that are available
2637 if min_end_point > min(len(stroke.points), end_point):
2638 for i in range(min_end_point -
2639 (min(len(stroke.points), end_point))):
2640 stroke_verts[-1][1].append(bm_mod.verts.new(\
2641 mat_world * point.co))
2642 # force even spreading of points, so they are placed on stroke
2643 method = 'regular'
2644 bm_mod.verts.index_update()
2645 for stroke, verts_seq in stroke_verts:
2646 if len(verts_seq) < 2:
2647 continue
2648 # spread vertices evenly over the stroke
2649 if method == 'regular':
2650 loop = [[vert.index for vert in verts_seq], False]
2651 move += gstretch_calculate_verts(loop, stroke, object, bm_mod,
2652 method)
2653 # create edges
2654 for i, vert in enumerate(verts_seq):
2655 if i > 0:
2656 bm_mod.edges.new((verts_seq[i-1], verts_seq[i]))
2657 vert.select = True
2658 # connect single vertices to the closest stroke
2659 if singles:
2660 for vert, m_stroke, point in singles:
2661 if m_stroke != stroke:
2662 continue
2663 bm_mod.edges.new((vert, verts_seq[point]))
2665 bmesh.update_edit_mesh(object.data)
2667 return(move)
2670 # erases the grease pencil stroke
2671 def gstretch_erase_stroke(stroke, context):
2672 # change 3d coordinate into a stroke-point
2673 def sp(loc, context):
2674 lib = {'name': "",
2675 'pen_flip': False,
2676 'is_start': False,
2677 'location': (0, 0, 0),
2678 'mouse': (view3d_utils.location_3d_to_region_2d(\
2679 context.region, context.space_data.region_3d, loc)),
2680 'pressure': 1,
2681 'time': 0}
2682 return(lib)
2684 if type(stroke) != bpy.types.GPencilStroke:
2685 # fake stroke, there is nothing to delete
2686 return
2688 erase_stroke = [sp(p.co, context) for p in stroke.points]
2689 if erase_stroke:
2690 erase_stroke[0]['is_start'] = True
2691 bpy.ops.gpencil.draw(mode='ERASER', stroke=erase_stroke)
2694 # get point on stroke, given by relative distance (0.0 - 1.0)
2695 def gstretch_eval_stroke(stroke, distance, stroke_lengths_cache=False):
2696 # use cache if available
2697 if not stroke_lengths_cache:
2698 lengths = [0]
2699 for i, p in enumerate(stroke.points[1:]):
2700 lengths.append((p.co - stroke.points[i].co).length + \
2701 lengths[-1])
2702 total_length = max(lengths[-1], 1e-7)
2703 stroke_lengths_cache = [length / total_length for length in
2704 lengths]
2705 stroke_lengths = stroke_lengths_cache[:]
2707 if distance in stroke_lengths:
2708 loc = stroke.points[stroke_lengths.index(distance)].co
2709 elif distance > stroke_lengths[-1]:
2710 # should be impossible, but better safe than sorry
2711 loc = stroke.points[-1].co
2712 else:
2713 stroke_lengths.append(distance)
2714 stroke_lengths.sort()
2715 stroke_index = stroke_lengths.index(distance)
2716 interval_length = stroke_lengths[stroke_index+1] - \
2717 stroke_lengths[stroke_index-1]
2718 distance_relative = (distance - stroke_lengths[stroke_index-1]) / \
2719 interval_length
2720 interval_vector = stroke.points[stroke_index].co - \
2721 stroke.points[stroke_index-1].co
2722 loc = stroke.points[stroke_index-1].co + \
2723 distance_relative * interval_vector
2725 return(loc, stroke_lengths_cache)
2728 # create fake grease pencil strokes for the active object
2729 def gstretch_get_fake_strokes(object, bm_mod, loops):
2730 strokes = []
2731 for loop in loops:
2732 p1 = object.matrix_world * bm_mod.verts[loop[0][0]].co
2733 p2 = object.matrix_world * bm_mod.verts[loop[0][-1]].co
2734 strokes.append(gstretch_fake_stroke([p1, p2]))
2736 return(strokes)
2739 # get grease pencil strokes for the active object
2740 def gstretch_get_strokes(object):
2741 gp = object.grease_pencil
2742 if not gp:
2743 return(None)
2744 layer = gp.layers.active
2745 if not layer:
2746 return(None)
2747 frame = layer.active_frame
2748 if not frame:
2749 return(None)
2750 strokes = frame.strokes
2751 if len(strokes) < 1:
2752 return(None)
2754 return(strokes)
2757 # returns a list with loop-stroke pairs
2758 def gstretch_match_loops_strokes(loops, strokes, object, bm_mod):
2759 if not loops or not strokes:
2760 return(None)
2762 # calculate loop centers
2763 loop_centers = []
2764 for loop in loops:
2765 center = mathutils.Vector()
2766 for v_index in loop[0]:
2767 center += bm_mod.verts[v_index].co
2768 center /= len(loop[0])
2769 center = object.matrix_world * center
2770 loop_centers.append([center, loop])
2772 # calculate stroke centers
2773 stroke_centers = []
2774 for stroke in strokes:
2775 center = mathutils.Vector()
2776 for p in stroke.points:
2777 center += p.co
2778 center /= len(stroke.points)
2779 stroke_centers.append([center, stroke, 0])
2781 # match, first by stroke use count, then by distance
2782 ls_pairs = []
2783 for lc in loop_centers:
2784 distances = []
2785 for i, sc in enumerate(stroke_centers):
2786 distances.append([sc[2], (lc[0] - sc[0]).length, i])
2787 distances.sort()
2788 best_stroke = distances[0][2]
2789 ls_pairs.append([lc[1], stroke_centers[best_stroke][1]])
2790 stroke_centers[best_stroke][2] += 1 # increase stroke use count
2792 return(ls_pairs)
2795 # match single selected vertices to the closest stroke endpoint
2796 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2797 def gstretch_match_single_verts(bm_mod, strokes, mat_world):
2798 # calculate stroke endpoints in object space
2799 endpoints = []
2800 for stroke in strokes:
2801 endpoints.append((mat_world * stroke.points[0].co, stroke, 0))
2802 endpoints.append((mat_world * stroke.points[-1].co, stroke, -1))
2804 distances = []
2805 # find single vertices (not connected to other selected verts)
2806 for vert in bm_mod.verts:
2807 if not vert.select:
2808 continue
2809 single = True
2810 for edge in vert.link_edges:
2811 if edge.other_vert(vert).select:
2812 single = False
2813 break
2814 if not single:
2815 continue
2816 # calculate distances from vertex to endpoints
2817 distance = [((vert.co - loc).length, vert, stroke, stroke_point,
2818 endpoint_index) for endpoint_index, (loc, stroke, stroke_point) in
2819 enumerate(endpoints)]
2820 distance.sort()
2821 distances.append(distance[0])
2823 # create matches, based on shortest distance first
2824 singles = []
2825 while distances:
2826 distances.sort()
2827 singles.append((distances[0][1], distances[0][2], distances[0][3]))
2828 endpoints.pop(distances[0][4])
2829 distances.pop(0)
2830 distances_new = []
2831 for (i, vert, j, k, l) in distances:
2832 distance_new = [((vert.co - loc).length, vert, stroke, stroke_point,
2833 endpoint_index) for endpoint_index, (loc, stroke,
2834 stroke_point) in enumerate(endpoints)]
2835 distance_new.sort()
2836 distances_new.append(distance_new[0])
2837 distances = distances_new
2839 return(singles)
2842 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2843 def gstretch_relative_lengths(loop, bm_mod):
2844 lengths = [0]
2845 for i, v_index in enumerate(loop[0][1:]):
2846 lengths.append((bm_mod.verts[v_index].co - \
2847 bm_mod.verts[loop[0][i]].co).length + lengths[-1])
2848 total_length = max(lengths[-1], 1e-7)
2849 relative_lengths = [length / total_length for length in
2850 lengths]
2852 return(relative_lengths)
2855 # convert cache-stored strokes into usable (fake) GP strokes
2856 def gstretch_safe_to_true_strokes(safe_strokes):
2857 strokes = []
2858 for safe_stroke in safe_strokes:
2859 strokes.append(gstretch_fake_stroke(safe_stroke))
2861 return(strokes)
2864 # convert a GP stroke into a list of points which can be stored in cache
2865 def gstretch_true_to_safe_strokes(strokes):
2866 safe_strokes = []
2867 for stroke in strokes:
2868 safe_strokes.append([p.co.copy() for p in stroke.points])
2870 return(safe_strokes)
2873 # force consistency in GUI, max value can never be lower than min value
2874 def gstretch_update_max(self, context):
2875 # called from operator settings (after execution)
2876 if 'conversion_min' in self.keys():
2877 if self.conversion_min > self.conversion_max:
2878 self.conversion_max = self.conversion_min
2879 # called from toolbar
2880 else:
2881 lt = context.window_manager.looptools
2882 if lt.gstretch_conversion_min > lt.gstretch_conversion_max:
2883 lt.gstretch_conversion_max = lt.gstretch_conversion_min
2886 # force consistency in GUI, min value can never be higher than max value
2887 def gstretch_update_min(self, context):
2888 # called from operator settings (after execution)
2889 if 'conversion_max' in self.keys():
2890 if self.conversion_max < self.conversion_min:
2891 self.conversion_min = self.conversion_max
2892 # called from toolbar
2893 else:
2894 lt = context.window_manager.looptools
2895 if lt.gstretch_conversion_max < lt.gstretch_conversion_min:
2896 lt.gstretch_conversion_min = lt.gstretch_conversion_max
2899 ##########################################
2900 ####### Relax functions ##################
2901 ##########################################
2903 # create lists with knots and points, all correctly sorted
2904 def relax_calculate_knots(loops):
2905 all_knots = []
2906 all_points = []
2907 for loop, circular in loops:
2908 knots = [[], []]
2909 points = [[], []]
2910 if circular:
2911 if len(loop)%2 == 1: # odd
2912 extend = [False, True, 0, 1, 0, 1]
2913 else: # even
2914 extend = [True, False, 0, 1, 1, 2]
2915 else:
2916 if len(loop)%2 == 1: # odd
2917 extend = [False, False, 0, 1, 1, 2]
2918 else: # even
2919 extend = [False, False, 0, 1, 1, 2]
2920 for j in range(2):
2921 if extend[j]:
2922 loop = [loop[-1]] + loop + [loop[0]]
2923 for i in range(extend[2+2*j], len(loop), 2):
2924 knots[j].append(loop[i])
2925 for i in range(extend[3+2*j], len(loop), 2):
2926 if loop[i] == loop[-1] and not circular:
2927 continue
2928 if len(points[j]) == 0:
2929 points[j].append(loop[i])
2930 elif loop[i] != points[j][0]:
2931 points[j].append(loop[i])
2932 if circular:
2933 if knots[j][0] != knots[j][-1]:
2934 knots[j].append(knots[j][0])
2935 if len(points[1]) == 0:
2936 knots.pop(1)
2937 points.pop(1)
2938 for k in knots:
2939 all_knots.append(k)
2940 for p in points:
2941 all_points.append(p)
2943 return(all_knots, all_points)
2946 # calculate relative positions compared to first knot
2947 def relax_calculate_t(bm_mod, knots, points, regular):
2948 all_tknots = []
2949 all_tpoints = []
2950 for i in range(len(knots)):
2951 amount = len(knots[i]) + len(points[i])
2952 mix = []
2953 for j in range(amount):
2954 if j%2 == 0:
2955 mix.append([True, knots[i][round(j/2)]])
2956 elif j == amount-1:
2957 mix.append([True, knots[i][-1]])
2958 else:
2959 mix.append([False, points[i][int(j/2)]])
2960 len_total = 0
2961 loc_prev = False
2962 tknots = []
2963 tpoints = []
2964 for m in mix:
2965 loc = mathutils.Vector(bm_mod.verts[m[1]].co[:])
2966 if not loc_prev:
2967 loc_prev = loc
2968 len_total += (loc - loc_prev).length
2969 if m[0]:
2970 tknots.append(len_total)
2971 else:
2972 tpoints.append(len_total)
2973 loc_prev = loc
2974 if regular:
2975 tpoints = []
2976 for p in range(len(points[i])):
2977 tpoints.append((tknots[p] + tknots[p+1]) / 2)
2978 all_tknots.append(tknots)
2979 all_tpoints.append(tpoints)
2981 return(all_tknots, all_tpoints)
2984 # change the location of the points to their place on the spline
2985 def relax_calculate_verts(bm_mod, interpolation, tknots, knots, tpoints,
2986 points, splines):
2987 change = []
2988 move = []
2989 for i in range(len(knots)):
2990 for p in points[i]:
2991 m = tpoints[i][points[i].index(p)]
2992 if m in tknots[i]:
2993 n = tknots[i].index(m)
2994 else:
2995 t = tknots[i][:]
2996 t.append(m)
2997 t.sort()
2998 n = t.index(m)-1
2999 if n > len(splines[i]) - 1:
3000 n = len(splines[i]) - 1
3001 elif n < 0:
3002 n = 0
3004 if interpolation == 'cubic':
3005 ax, bx, cx, dx, tx = splines[i][n][0]
3006 x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
3007 ay, by, cy, dy, ty = splines[i][n][1]
3008 y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
3009 az, bz, cz, dz, tz = splines[i][n][2]
3010 z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
3011 change.append([p, mathutils.Vector([x,y,z])])
3012 else: # interpolation == 'linear'
3013 a, d, t, u = splines[i][n]
3014 if u == 0:
3015 u = 1e-8
3016 change.append([p, ((m-t)/u)*d + a])
3017 for c in change:
3018 move.append([c[0], (bm_mod.verts[c[0]].co + c[1]) / 2])
3020 return(move)
3023 ##########################################
3024 ####### Space functions ##################
3025 ##########################################
3027 # calculate relative positions compared to first knot
3028 def space_calculate_t(bm_mod, knots):
3029 tknots = []
3030 loc_prev = False
3031 len_total = 0
3032 for k in knots:
3033 loc = mathutils.Vector(bm_mod.verts[k].co[:])
3034 if not loc_prev:
3035 loc_prev = loc
3036 len_total += (loc - loc_prev).length
3037 tknots.append(len_total)
3038 loc_prev = loc
3039 amount = len(knots)
3040 t_per_segment = len_total / (amount - 1)
3041 tpoints = [i * t_per_segment for i in range(amount)]
3043 return(tknots, tpoints)
3046 # change the location of the points to their place on the spline
3047 def space_calculate_verts(bm_mod, interpolation, tknots, tpoints, points,
3048 splines):
3049 move = []
3050 for p in points:
3051 m = tpoints[points.index(p)]
3052 if m in tknots:
3053 n = tknots.index(m)
3054 else:
3055 t = tknots[:]
3056 t.append(m)
3057 t.sort()
3058 n = t.index(m) - 1
3059 if n > len(splines) - 1:
3060 n = len(splines) - 1
3061 elif n < 0:
3062 n = 0
3064 if interpolation == 'cubic':
3065 ax, bx, cx, dx, tx = splines[n][0]
3066 x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
3067 ay, by, cy, dy, ty = splines[n][1]
3068 y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
3069 az, bz, cz, dz, tz = splines[n][2]
3070 z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
3071 move.append([p, mathutils.Vector([x,y,z])])
3072 else: # interpolation == 'linear'
3073 a, d, t, u = splines[n]
3074 move.append([p, ((m-t)/u)*d + a])
3076 return(move)
3079 ##########################################
3080 ####### Operators ########################
3081 ##########################################
3083 # bridge operator
3084 class Bridge(bpy.types.Operator):
3085 bl_idname = 'mesh.looptools_bridge'
3086 bl_label = "Bridge / Loft"
3087 bl_description = "Bridge two, or loft several, loops of vertices"
3088 bl_options = {'REGISTER', 'UNDO'}
3090 cubic_strength = bpy.props.FloatProperty(name = "Strength",
3091 description = "Higher strength results in more fluid curves",
3092 default = 1.0,
3093 soft_min = -3.0,
3094 soft_max = 3.0)
3095 interpolation = bpy.props.EnumProperty(name = "Interpolation mode",
3096 items = (('cubic', "Cubic", "Gives curved results"),
3097 ('linear', "Linear", "Basic, fast, straight interpolation")),
3098 description = "Interpolation mode: algorithm used when creating "\
3099 "segments",
3100 default = 'cubic')
3101 loft = bpy.props.BoolProperty(name = "Loft",
3102 description = "Loft multiple loops, instead of considering them as "\
3103 "a multi-input for bridging",
3104 default = False)
3105 loft_loop = bpy.props.BoolProperty(name = "Loop",
3106 description = "Connect the first and the last loop with each other",
3107 default = False)
3108 min_width = bpy.props.IntProperty(name = "Minimum width",
3109 description = "Segments with an edge smaller than this are merged "\
3110 "(compared to base edge)",
3111 default = 0,
3112 min = 0,
3113 max = 100,
3114 subtype = 'PERCENTAGE')
3115 mode = bpy.props.EnumProperty(name = "Mode",
3116 items = (('basic', "Basic", "Fast algorithm"), ('shortest',
3117 "Shortest edge", "Slower algorithm with better vertex matching")),
3118 description = "Algorithm used for bridging",
3119 default = 'shortest')
3120 remove_faces = bpy.props.BoolProperty(name = "Remove faces",
3121 description = "Remove faces that are internal after bridging",
3122 default = True)
3123 reverse = bpy.props.BoolProperty(name = "Reverse",
3124 description = "Manually override the direction in which the loops "\
3125 "are bridged. Only use if the tool gives the wrong " \
3126 "result",
3127 default = False)
3128 segments = bpy.props.IntProperty(name = "Segments",
3129 description = "Number of segments used to bridge the gap "\
3130 "(0 = automatic)",
3131 default = 1,
3132 min = 0,
3133 soft_max = 20)
3134 twist = bpy.props.IntProperty(name = "Twist",
3135 description = "Twist what vertices are connected to each other",
3136 default = 0)
3138 @classmethod
3139 def poll(cls, context):
3140 ob = context.active_object
3141 return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3143 def draw(self, context):
3144 layout = self.layout
3145 #layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3147 # top row
3148 col_top = layout.column(align=True)
3149 row = col_top.row(align=True)
3150 col_left = row.column(align=True)
3151 col_right = row.column(align=True)
3152 col_right.active = self.segments != 1
3153 col_left.prop(self, "segments")
3154 col_right.prop(self, "min_width", text="")
3155 # bottom row
3156 bottom_left = col_left.row()
3157 bottom_left.active = self.segments != 1
3158 bottom_left.prop(self, "interpolation", text="")
3159 bottom_right = col_right.row()
3160 bottom_right.active = self.interpolation == 'cubic'
3161 bottom_right.prop(self, "cubic_strength")
3162 # boolean properties
3163 col_top.prop(self, "remove_faces")
3164 if self.loft:
3165 col_top.prop(self, "loft_loop")
3167 # override properties
3168 col_top.separator()
3169 row = layout.row(align = True)
3170 row.prop(self, "twist")
3171 row.prop(self, "reverse")
3173 def invoke(self, context, event):
3174 # load custom settings
3175 context.window_manager.looptools.bridge_loft = self.loft
3176 settings_load(self)
3177 return self.execute(context)
3179 def execute(self, context):
3180 # initialise
3181 global_undo, object, bm = initialise()
3182 edge_faces, edgekey_to_edge, old_selected_faces, smooth = \
3183 bridge_initialise(bm, self.interpolation)
3184 settings_write(self)
3186 # check cache to see if we can save time
3187 input_method = bridge_input_method(self.loft, self.loft_loop)
3188 cached, single_loops, loops, derived, mapping = cache_read("Bridge",
3189 object, bm, input_method, False)
3190 if not cached:
3191 # get loops
3192 loops = bridge_get_input(bm)
3193 if loops:
3194 # reorder loops if there are more than 2
3195 if len(loops) > 2:
3196 if self.loft:
3197 loops = bridge_sort_loops(bm, loops, self.loft_loop)
3198 else:
3199 loops = bridge_match_loops(bm, loops)
3201 # saving cache for faster execution next time
3202 if not cached:
3203 cache_write("Bridge", object, bm, input_method, False, False,
3204 loops, False, False)
3206 if loops:
3207 # calculate new geometry
3208 vertices = []
3209 faces = []
3210 max_vert_index = len(bm.verts)-1
3211 for i in range(1, len(loops)):
3212 if not self.loft and i%2 == 0:
3213 continue
3214 lines = bridge_calculate_lines(bm, loops[i-1:i+1],
3215 self.mode, self.twist, self.reverse)
3216 vertex_normals = bridge_calculate_virtual_vertex_normals(bm,
3217 lines, loops[i-1:i+1], edge_faces, edgekey_to_edge)
3218 segments = bridge_calculate_segments(bm, lines,
3219 loops[i-1:i+1], self.segments)
3220 new_verts, new_faces, max_vert_index = \
3221 bridge_calculate_geometry(bm, lines, vertex_normals,
3222 segments, self.interpolation, self.cubic_strength,
3223 self.min_width, max_vert_index)
3224 if new_verts:
3225 vertices += new_verts
3226 if new_faces:
3227 faces += new_faces
3228 # make sure faces in loops that aren't used, aren't removed
3229 if self.remove_faces and old_selected_faces:
3230 bridge_save_unused_faces(bm, old_selected_faces, loops)
3231 # create vertices
3232 if vertices:
3233 bridge_create_vertices(bm, vertices)
3234 # create faces
3235 if faces:
3236 new_faces = bridge_create_faces(object, bm, faces, self.twist)
3237 old_selected_faces = [i for i, face in enumerate(bm.faces) \
3238 if face.index in old_selected_faces] # updating list
3239 bridge_select_new_faces(new_faces, smooth)
3240 # edge-data could have changed, can't use cache next run
3241 if faces and not vertices:
3242 cache_delete("Bridge")
3243 # delete internal faces
3244 if self.remove_faces and old_selected_faces:
3245 bridge_remove_internal_faces(bm, old_selected_faces)
3246 # make sure normals are facing outside
3247 bmesh.update_edit_mesh(object.data, tessface=False,
3248 destructive=True)
3249 bpy.ops.mesh.normals_make_consistent()
3251 # cleaning up
3252 terminate(global_undo)
3254 return{'FINISHED'}
3257 # circle operator
3258 class Circle(bpy.types.Operator):
3259 bl_idname = "mesh.looptools_circle"
3260 bl_label = "Circle"
3261 bl_description = "Move selected vertices into a circle shape"
3262 bl_options = {'REGISTER', 'UNDO'}
3264 custom_radius = bpy.props.BoolProperty(name = "Radius",
3265 description = "Force a custom radius",
3266 default = False)
3267 fit = bpy.props.EnumProperty(name = "Method",
3268 items = (("best", "Best fit", "Non-linear least squares"),
3269 ("inside", "Fit inside","Only move vertices towards the center")),
3270 description = "Method used for fitting a circle to the vertices",
3271 default = 'best')
3272 flatten = bpy.props.BoolProperty(name = "Flatten",
3273 description = "Flatten the circle, instead of projecting it on the " \
3274 "mesh",
3275 default = True)
3276 influence = bpy.props.FloatProperty(name = "Influence",
3277 description = "Force of the tool",
3278 default = 100.0,
3279 min = 0.0,
3280 max = 100.0,
3281 precision = 1,
3282 subtype = 'PERCENTAGE')
3283 radius = bpy.props.FloatProperty(name = "Radius",
3284 description = "Custom radius for circle",
3285 default = 1.0,
3286 min = 0.0,
3287 soft_max = 1000.0)
3288 regular = bpy.props.BoolProperty(name = "Regular",
3289 description = "Distribute vertices at constant distances along the " \
3290 "circle",
3291 default = True)
3293 @classmethod
3294 def poll(cls, context):
3295 ob = context.active_object
3296 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3298 def draw(self, context):
3299 layout = self.layout
3300 col = layout.column()
3302 col.prop(self, "fit")
3303 col.separator()
3305 col.prop(self, "flatten")
3306 row = col.row(align=True)
3307 row.prop(self, "custom_radius")
3308 row_right = row.row(align=True)
3309 row_right.active = self.custom_radius
3310 row_right.prop(self, "radius", text="")
3311 col.prop(self, "regular")
3312 col.separator()
3314 col.prop(self, "influence")
3316 def invoke(self, context, event):
3317 # load custom settings
3318 settings_load(self)
3319 return self.execute(context)
3321 def execute(self, context):
3322 # initialise
3323 global_undo, object, bm = initialise()
3324 settings_write(self)
3325 # check cache to see if we can save time
3326 cached, single_loops, loops, derived, mapping = cache_read("Circle",
3327 object, bm, False, False)
3328 if cached:
3329 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
3330 else:
3331 # find loops
3332 derived, bm_mod, single_vertices, single_loops, loops = \
3333 circle_get_input(object, bm, context.scene)
3334 mapping = get_mapping(derived, bm, bm_mod, single_vertices,
3335 False, loops)
3336 single_loops, loops = circle_check_loops(single_loops, loops,
3337 mapping, bm_mod)
3339 # saving cache for faster execution next time
3340 if not cached:
3341 cache_write("Circle", object, bm, False, False, single_loops,
3342 loops, derived, mapping)
3344 move = []
3345 for i, loop in enumerate(loops):
3346 # best fitting flat plane
3347 com, normal = calculate_plane(bm_mod, loop)
3348 # if circular, shift loop so we get a good starting vertex
3349 if loop[1]:
3350 loop = circle_shift_loop(bm_mod, loop, com)
3351 # flatten vertices on plane
3352 locs_2d, p, q = circle_3d_to_2d(bm_mod, loop, com, normal)
3353 # calculate circle
3354 if self.fit == 'best':
3355 x0, y0, r = circle_calculate_best_fit(locs_2d)
3356 else: # self.fit == 'inside'
3357 x0, y0, r = circle_calculate_min_fit(locs_2d)
3358 # radius override
3359 if self.custom_radius:
3360 r = self.radius / p.length
3361 # calculate positions on circle
3362 if self.regular:
3363 new_locs_2d = circle_project_regular(locs_2d[:], x0, y0, r)
3364 else:
3365 new_locs_2d = circle_project_non_regular(locs_2d[:], x0, y0, r)
3366 # take influence into account
3367 locs_2d = circle_influence_locs(locs_2d, new_locs_2d,
3368 self.influence)
3369 # calculate 3d positions of the created 2d input
3370 move.append(circle_calculate_verts(self.flatten, bm_mod,
3371 locs_2d, com, p, q, normal))
3372 # flatten single input vertices on plane defined by loop
3373 if self.flatten and single_loops:
3374 move.append(circle_flatten_singles(bm_mod, com, p, q,
3375 normal, single_loops[i]))
3377 # move vertices to new locations
3378 move_verts(object, bm, mapping, move, -1)
3380 # cleaning up
3381 if derived:
3382 bm_mod.free()
3383 terminate(global_undo)
3385 return{'FINISHED'}
3388 # curve operator
3389 class Curve(bpy.types.Operator):
3390 bl_idname = "mesh.looptools_curve"
3391 bl_label = "Curve"
3392 bl_description = "Turn a loop into a smooth curve"
3393 bl_options = {'REGISTER', 'UNDO'}
3395 boundaries = bpy.props.BoolProperty(name = "Boundaries",
3396 description = "Limit the tool to work within the boundaries of the "\
3397 "selected vertices",
3398 default = False)
3399 influence = bpy.props.FloatProperty(name = "Influence",
3400 description = "Force of the tool",
3401 default = 100.0,
3402 min = 0.0,
3403 max = 100.0,
3404 precision = 1,
3405 subtype = 'PERCENTAGE')
3406 interpolation = bpy.props.EnumProperty(name = "Interpolation",
3407 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3408 ("linear", "Linear", "Simple and fast linear algorithm")),
3409 description = "Algorithm used for interpolation",
3410 default = 'cubic')
3411 regular = bpy.props.BoolProperty(name = "Regular",
3412 description = "Distribute vertices at constant distances along the" \
3413 "curve",
3414 default = True)
3415 restriction = bpy.props.EnumProperty(name = "Restriction",
3416 items = (("none", "None", "No restrictions on vertex movement"),
3417 ("extrude", "Extrude only","Only allow extrusions (no "\
3418 "indentations)"),
3419 ("indent", "Indent only", "Only allow indentation (no "\
3420 "extrusions)")),
3421 description = "Restrictions on how the vertices can be moved",
3422 default = 'none')
3424 @classmethod
3425 def poll(cls, context):
3426 ob = context.active_object
3427 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3429 def draw(self, context):
3430 layout = self.layout
3431 col = layout.column()
3433 col.prop(self, "interpolation")
3434 col.prop(self, "restriction")
3435 col.prop(self, "boundaries")
3436 col.prop(self, "regular")
3437 col.separator()
3439 col.prop(self, "influence")
3441 def invoke(self, context, event):
3442 # load custom settings
3443 settings_load(self)
3444 return self.execute(context)
3446 def execute(self, context):
3447 # initialise
3448 global_undo, object, bm = initialise()
3449 settings_write(self)
3450 # check cache to see if we can save time
3451 cached, single_loops, loops, derived, mapping = cache_read("Curve",
3452 object, bm, False, self.boundaries)
3453 if cached:
3454 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
3455 else:
3456 # find loops
3457 derived, bm_mod, loops = curve_get_input(object, bm,
3458 self.boundaries, context.scene)
3459 mapping = get_mapping(derived, bm, bm_mod, False, True, loops)
3460 loops = check_loops(loops, mapping, bm_mod)
3461 verts_selected = [v.index for v in bm_mod.verts if v.select \
3462 and not v.hide]
3464 # saving cache for faster execution next time
3465 if not cached:
3466 cache_write("Curve", object, bm, False, self.boundaries, False,
3467 loops, derived, mapping)
3469 move = []
3470 for loop in loops:
3471 knots, points = curve_calculate_knots(loop, verts_selected)
3472 pknots = curve_project_knots(bm_mod, verts_selected, knots,
3473 points, loop[1])
3474 tknots, tpoints = curve_calculate_t(bm_mod, knots, points,
3475 pknots, self.regular, loop[1])
3476 splines = calculate_splines(self.interpolation, bm_mod,
3477 tknots, knots)
3478 move.append(curve_calculate_vertices(bm_mod, knots, tknots,
3479 points, tpoints, splines, self.interpolation,
3480 self.restriction))
3482 # move vertices to new locations
3483 move_verts(object, bm, mapping, move, self.influence)
3485 # cleaning up
3486 if derived:
3487 bm_mod.free()
3488 terminate(global_undo)
3490 return{'FINISHED'}
3493 # flatten operator
3494 class Flatten(bpy.types.Operator):
3495 bl_idname = "mesh.looptools_flatten"
3496 bl_label = "Flatten"
3497 bl_description = "Flatten vertices on a best-fitting plane"
3498 bl_options = {'REGISTER', 'UNDO'}
3500 influence = bpy.props.FloatProperty(name = "Influence",
3501 description = "Force of the tool",
3502 default = 100.0,
3503 min = 0.0,
3504 max = 100.0,
3505 precision = 1,
3506 subtype = 'PERCENTAGE')
3507 plane = bpy.props.EnumProperty(name = "Plane",
3508 items = (("best_fit", "Best fit", "Calculate a best fitting plane"),
3509 ("normal", "Normal", "Derive plane from averaging vertex "\
3510 "normals"),
3511 ("view", "View", "Flatten on a plane perpendicular to the "\
3512 "viewing angle")),
3513 description = "Plane on which vertices are flattened",
3514 default = 'best_fit')
3515 restriction = bpy.props.EnumProperty(name = "Restriction",
3516 items = (("none", "None", "No restrictions on vertex movement"),
3517 ("bounding_box", "Bounding box", "Vertices are restricted to "\
3518 "movement inside the bounding box of the selection")),
3519 description = "Restrictions on how the vertices can be moved",
3520 default = 'none')
3522 @classmethod
3523 def poll(cls, context):
3524 ob = context.active_object
3525 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3527 def draw(self, context):
3528 layout = self.layout
3529 col = layout.column()
3531 col.prop(self, "plane")
3532 #col.prop(self, "restriction")
3533 col.separator()
3535 col.prop(self, "influence")
3537 def invoke(self, context, event):
3538 # load custom settings
3539 settings_load(self)
3540 return self.execute(context)
3542 def execute(self, context):
3543 # initialise
3544 global_undo, object, bm = initialise()
3545 settings_write(self)
3546 # check cache to see if we can save time
3547 cached, single_loops, loops, derived, mapping = cache_read("Flatten",
3548 object, bm, False, False)
3549 if not cached:
3550 # order input into virtual loops
3551 loops = flatten_get_input(bm)
3552 loops = check_loops(loops, mapping, bm)
3554 # saving cache for faster execution next time
3555 if not cached:
3556 cache_write("Flatten", object, bm, False, False, False, loops,
3557 False, False)
3559 move = []
3560 for loop in loops:
3561 # calculate plane and position of vertices on them
3562 com, normal = calculate_plane(bm, loop, method=self.plane,
3563 object=object)
3564 to_move = flatten_project(bm, loop, com, normal)
3565 if self.restriction == 'none':
3566 move.append(to_move)
3567 else:
3568 move.append(to_move)
3569 move_verts(object, bm, False, move, self.influence)
3571 # cleaning up
3572 terminate(global_undo)
3574 return{'FINISHED'}
3577 # gstretch operator
3578 class GStretch(bpy.types.Operator):
3579 bl_idname = "mesh.looptools_gstretch"
3580 bl_label = "Gstretch"
3581 bl_description = "Stretch selected vertices to Grease Pencil stroke"
3582 bl_options = {'REGISTER', 'UNDO'}
3584 conversion = bpy.props.EnumProperty(name = "Conversion",
3585 items = (("distance", "Distance", "Set the distance between vertices "\
3586 "of the converted grease pencil stroke"),
3587 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "\
3588 "number of vertices that converted GP strokes will have"),
3589 ("vertices", "Exact vertices", "Set the exact number of vertices "\
3590 "that converted grease pencil strokes will have. Short strokes "\
3591 "with few points may contain less vertices than this number."),
3592 ("none", "No simplification", "Convert each grease pencil point "\
3593 "to a vertex")),
3594 description = "If grease pencil strokes are converted to geometry, "\
3595 "use this simplification method",
3596 default = 'limit_vertices')
3597 conversion_distance = bpy.props.FloatProperty(name = "Distance",
3598 description = "Absolute distance between vertices along the converted "\
3599 "grease pencil stroke",
3600 default = 0.1,
3601 min = 0.000001,
3602 soft_min = 0.01,
3603 soft_max = 100)
3604 conversion_max = bpy.props.IntProperty(name = "Max Vertices",
3605 description = "Maximum number of vertices grease pencil strokes will "\
3606 "have, when they are converted to geomtery",
3607 default = 32,
3608 min = 3,
3609 soft_max = 500,
3610 update = gstretch_update_min)
3611 conversion_min = bpy.props.IntProperty(name = "Min Vertices",
3612 description = "Minimum number of vertices grease pencil strokes will "\
3613 "have, when they are converted to geomtery",
3614 default = 8,
3615 min = 3,
3616 soft_max = 500,
3617 update = gstretch_update_max)
3618 conversion_vertices = bpy.props.IntProperty(name = "Vertices",
3619 description = "Number of vertices grease pencil strokes will "\
3620 "have, when they are converted to geometry. If strokes have less "\
3621 "points than required, the 'Spread evenly' method is used.",
3622 default = 32,
3623 min = 3,
3624 soft_max = 500)
3625 delete_strokes = bpy.props.BoolProperty(name="Delete strokes",
3626 description = "Remove Grease Pencil strokes if they have been used "\
3627 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
3628 default = False)
3629 influence = bpy.props.FloatProperty(name = "Influence",
3630 description = "Force of the tool",
3631 default = 100.0,
3632 min = 0.0,
3633 max = 100.0,
3634 precision = 1,
3635 subtype = 'PERCENTAGE')
3636 method = bpy.props.EnumProperty(name = "Method",
3637 items = (("project", "Project", "Project vertices onto the stroke, "\
3638 "using vertex normals and connected edges"),
3639 ("irregular", "Spread", "Distribute vertices along the full "\
3640 "stroke, retaining relative distances between the vertices"),
3641 ("regular", "Spread evenly", "Distribute vertices at regular "\
3642 "distances along the full stroke")),
3643 description = "Method of distributing the vertices over the Grease "\
3644 "Pencil stroke",
3645 default = 'regular')
3647 @classmethod
3648 def poll(cls, context):
3649 ob = context.active_object
3650 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3652 def draw(self, context):
3653 layout = self.layout
3654 col = layout.column()
3656 col.prop(self, "method")
3657 col.prop(self, "delete_strokes")
3658 col.separator()
3660 col_conv = col.column(align=True)
3661 col_conv.prop(self, "conversion", text="")
3662 if self.conversion == 'distance':
3663 col_conv.prop(self, "conversion_distance")
3664 elif self.conversion == 'limit_vertices':
3665 row = col_conv.row(align=True)
3666 row.prop(self, "conversion_min", text="Min")
3667 row.prop(self, "conversion_max", text="Max")
3668 elif self.conversion == 'vertices':
3669 col_conv.prop(self, "conversion_vertices")
3670 col.separator()
3672 col.prop(self, "influence")
3674 def invoke(self, context, event):
3675 # flush cached strokes
3676 if 'Gstretch' in looptools_cache:
3677 looptools_cache['Gstretch']['single_loops'] = []
3678 # load custom settings
3679 settings_load(self)
3680 return self.execute(context)
3682 def execute(self, context):
3683 # initialise
3684 global_undo, object, bm = initialise()
3685 settings_write(self)
3687 # check cache to see if we can save time
3688 cached, safe_strokes, loops, derived, mapping = cache_read("Gstretch",
3689 object, bm, False, False)
3690 if cached:
3691 if safe_strokes:
3692 strokes = gstretch_safe_to_true_strokes(safe_strokes)
3693 # cached strokes were flushed (see operator's invoke function)
3694 elif object.grease_pencil:
3695 strokes = gstretch_get_strokes(object)
3696 else:
3697 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
3698 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
3699 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
3700 else:
3701 # find loops
3702 derived, bm_mod, loops = get_connected_input(object, bm,
3703 context.scene, input='selected')
3704 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
3705 loops = check_loops(loops, mapping, bm_mod)
3706 # get strokes
3707 if object.grease_pencil:
3708 strokes = gstretch_get_strokes(object)
3709 else:
3710 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
3712 # saving cache for faster execution next time
3713 if not cached:
3714 if strokes:
3715 safe_strokes = gstretch_true_to_safe_strokes(strokes)
3716 else:
3717 safe_strokes = []
3718 cache_write("Gstretch", object, bm, False, False,
3719 safe_strokes, loops, derived, mapping)
3721 # pair loops and strokes
3722 ls_pairs = gstretch_match_loops_strokes(loops, strokes, object, bm_mod)
3723 ls_pairs = gstretch_align_pairs(ls_pairs, object, bm_mod, self.method)
3725 move = []
3726 if not loops:
3727 # no selected geometry, convert GP to verts
3728 if strokes:
3729 move.append(gstretch_create_verts(object, bm, strokes,
3730 self.method, self.conversion, self.conversion_distance,
3731 self.conversion_max, self.conversion_min,
3732 self.conversion_vertices))
3733 for stroke in strokes:
3734 gstretch_erase_stroke(stroke, context)
3735 elif ls_pairs:
3736 for (loop, stroke) in ls_pairs:
3737 move.append(gstretch_calculate_verts(loop, stroke, object,
3738 bm_mod, self.method))
3739 if self.delete_strokes:
3740 if type(stroke) != bpy.types.GPencilStroke:
3741 # in case of cached fake stroke, get the real one
3742 if object.grease_pencil:
3743 strokes = gstretch_get_strokes(object)
3744 ls_pairs = gstretch_match_loops_strokes(loops,
3745 strokes, object, bm_mod)
3746 ls_pairs = gstretch_align_pairs(ls_pairs, object,
3747 bm_mod, self.method)
3748 for (l, s) in ls_pairs:
3749 if l == loop:
3750 stroke = s
3751 break
3752 gstretch_erase_stroke(stroke, context)
3754 # move vertices to new locations
3755 bmesh.update_edit_mesh(object.data, tessface=True,
3756 destructive=True)
3757 move_verts(object, bm, mapping, move, self.influence)
3759 # cleaning up
3760 if derived:
3761 bm_mod.free()
3762 terminate(global_undo)
3764 return{'FINISHED'}
3767 # relax operator
3768 class Relax(bpy.types.Operator):
3769 bl_idname = "mesh.looptools_relax"
3770 bl_label = "Relax"
3771 bl_description = "Relax the loop, so it is smoother"
3772 bl_options = {'REGISTER', 'UNDO'}
3774 input = bpy.props.EnumProperty(name = "Input",
3775 items = (("all", "Parallel (all)", "Also use non-selected "\
3776 "parallel loops as input"),
3777 ("selected", "Selection","Only use selected vertices as input")),
3778 description = "Loops that are relaxed",
3779 default = 'selected')
3780 interpolation = bpy.props.EnumProperty(name = "Interpolation",
3781 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3782 ("linear", "Linear", "Simple and fast linear algorithm")),
3783 description = "Algorithm used for interpolation",
3784 default = 'cubic')
3785 iterations = bpy.props.EnumProperty(name = "Iterations",
3786 items = (("1", "1", "One"),
3787 ("3", "3", "Three"),
3788 ("5", "5", "Five"),
3789 ("10", "10", "Ten"),
3790 ("25", "25", "Twenty-five")),
3791 description = "Number of times the loop is relaxed",
3792 default = "1")
3793 regular = bpy.props.BoolProperty(name = "Regular",
3794 description = "Distribute vertices at constant distances along the" \
3795 "loop",
3796 default = True)
3798 @classmethod
3799 def poll(cls, context):
3800 ob = context.active_object
3801 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3803 def draw(self, context):
3804 layout = self.layout
3805 col = layout.column()
3807 col.prop(self, "interpolation")
3808 col.prop(self, "input")
3809 col.prop(self, "iterations")
3810 col.prop(self, "regular")
3812 def invoke(self, context, event):
3813 # load custom settings
3814 settings_load(self)
3815 return self.execute(context)
3817 def execute(self, context):
3818 # initialise
3819 global_undo, object, bm = initialise()
3820 settings_write(self)
3821 # check cache to see if we can save time
3822 cached, single_loops, loops, derived, mapping = cache_read("Relax",
3823 object, bm, self.input, False)
3824 if cached:
3825 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
3826 else:
3827 # find loops
3828 derived, bm_mod, loops = get_connected_input(object, bm,
3829 context.scene, self.input)
3830 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
3831 loops = check_loops(loops, mapping, bm_mod)
3832 knots, points = relax_calculate_knots(loops)
3834 # saving cache for faster execution next time
3835 if not cached:
3836 cache_write("Relax", object, bm, self.input, False, False, loops,
3837 derived, mapping)
3839 for iteration in range(int(self.iterations)):
3840 # calculate splines and new positions
3841 tknots, tpoints = relax_calculate_t(bm_mod, knots, points,
3842 self.regular)
3843 splines = []
3844 for i in range(len(knots)):
3845 splines.append(calculate_splines(self.interpolation, bm_mod,
3846 tknots[i], knots[i]))
3847 move = [relax_calculate_verts(bm_mod, self.interpolation,
3848 tknots, knots, tpoints, points, splines)]
3849 move_verts(object, bm, mapping, move, -1)
3851 # cleaning up
3852 if derived:
3853 bm_mod.free()
3854 terminate(global_undo)
3856 return{'FINISHED'}
3859 # space operator
3860 class Space(bpy.types.Operator):
3861 bl_idname = "mesh.looptools_space"
3862 bl_label = "Space"
3863 bl_description = "Space the vertices in a regular distrubtion on the loop"
3864 bl_options = {'REGISTER', 'UNDO'}
3866 influence = bpy.props.FloatProperty(name = "Influence",
3867 description = "Force of the tool",
3868 default = 100.0,
3869 min = 0.0,
3870 max = 100.0,
3871 precision = 1,
3872 subtype = 'PERCENTAGE')
3873 input = bpy.props.EnumProperty(name = "Input",
3874 items = (("all", "Parallel (all)", "Also use non-selected "\
3875 "parallel loops as input"),
3876 ("selected", "Selection","Only use selected vertices as input")),
3877 description = "Loops that are spaced",
3878 default = 'selected')
3879 interpolation = bpy.props.EnumProperty(name = "Interpolation",
3880 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3881 ("linear", "Linear", "Vertices are projected on existing edges")),
3882 description = "Algorithm used for interpolation",
3883 default = 'cubic')
3885 @classmethod
3886 def poll(cls, context):
3887 ob = context.active_object
3888 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3890 def draw(self, context):
3891 layout = self.layout
3892 col = layout.column()
3894 col.prop(self, "interpolation")
3895 col.prop(self, "input")
3896 col.separator()
3898 col.prop(self, "influence")
3900 def invoke(self, context, event):
3901 # load custom settings
3902 settings_load(self)
3903 return self.execute(context)
3905 def execute(self, context):
3906 # initialise
3907 global_undo, object, bm = initialise()
3908 settings_write(self)
3909 # check cache to see if we can save time
3910 cached, single_loops, loops, derived, mapping = cache_read("Space",
3911 object, bm, self.input, False)
3912 if cached:
3913 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
3914 else:
3915 # find loops
3916 derived, bm_mod, loops = get_connected_input(object, bm,
3917 context.scene, self.input)
3918 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
3919 loops = check_loops(loops, mapping, bm_mod)
3921 # saving cache for faster execution next time
3922 if not cached:
3923 cache_write("Space", object, bm, self.input, False, False, loops,
3924 derived, mapping)
3926 move = []
3927 for loop in loops:
3928 # calculate splines and new positions
3929 if loop[1]: # circular
3930 loop[0].append(loop[0][0])
3931 tknots, tpoints = space_calculate_t(bm_mod, loop[0][:])
3932 splines = calculate_splines(self.interpolation, bm_mod,
3933 tknots, loop[0][:])
3934 move.append(space_calculate_verts(bm_mod, self.interpolation,
3935 tknots, tpoints, loop[0][:-1], splines))
3936 # move vertices to new locations
3937 move_verts(object, bm, mapping, move, self.influence)
3939 # cleaning up
3940 if derived:
3941 bm_mod.free()
3942 terminate(global_undo)
3944 return{'FINISHED'}
3947 ##########################################
3948 ####### GUI and registration #############
3949 ##########################################
3951 # menu containing all tools
3952 class VIEW3D_MT_edit_mesh_looptools(bpy.types.Menu):
3953 bl_label = "LoopTools"
3955 def draw(self, context):
3956 layout = self.layout
3958 layout.operator("mesh.looptools_bridge", text="Bridge").loft = False
3959 layout.operator("mesh.looptools_circle")
3960 layout.operator("mesh.looptools_curve")
3961 layout.operator("mesh.looptools_flatten")
3962 layout.operator("mesh.looptools_gstretch")
3963 layout.operator("mesh.looptools_bridge", text="Loft").loft = True
3964 layout.operator("mesh.looptools_relax")
3965 layout.operator("mesh.looptools_space")
3968 # panel containing all tools
3969 class VIEW3D_PT_tools_looptools(bpy.types.Panel):
3970 bl_space_type = 'VIEW_3D'
3971 bl_region_type = 'TOOLS'
3972 bl_category = 'Tools'
3973 bl_context = "mesh_edit"
3974 bl_label = "LoopTools"
3975 bl_options = {'DEFAULT_CLOSED'}
3977 def draw(self, context):
3978 layout = self.layout
3979 col = layout.column(align=True)
3980 lt = context.window_manager.looptools
3982 # bridge - first line
3983 split = col.split(percentage=0.15, align=True)
3984 if lt.display_bridge:
3985 split.prop(lt, "display_bridge", text="", icon='DOWNARROW_HLT')
3986 else:
3987 split.prop(lt, "display_bridge", text="", icon='RIGHTARROW')
3988 split.operator("mesh.looptools_bridge", text="Bridge").loft = False
3989 # bridge - settings
3990 if lt.display_bridge:
3991 box = col.column(align=True).box().column()
3992 #box.prop(self, "mode")
3994 # top row
3995 col_top = box.column(align=True)
3996 row = col_top.row(align=True)
3997 col_left = row.column(align=True)
3998 col_right = row.column(align=True)
3999 col_right.active = lt.bridge_segments != 1
4000 col_left.prop(lt, "bridge_segments")
4001 col_right.prop(lt, "bridge_min_width", text="")
4002 # bottom row
4003 bottom_left = col_left.row()
4004 bottom_left.active = lt.bridge_segments != 1
4005 bottom_left.prop(lt, "bridge_interpolation", text="")
4006 bottom_right = col_right.row()
4007 bottom_right.active = lt.bridge_interpolation == 'cubic'
4008 bottom_right.prop(lt, "bridge_cubic_strength")
4009 # boolean properties
4010 col_top.prop(lt, "bridge_remove_faces")
4012 # override properties
4013 col_top.separator()
4014 row = box.row(align = True)
4015 row.prop(lt, "bridge_twist")
4016 row.prop(lt, "bridge_reverse")
4018 # circle - first line
4019 split = col.split(percentage=0.15, align=True)
4020 if lt.display_circle:
4021 split.prop(lt, "display_circle", text="", icon='DOWNARROW_HLT')
4022 else:
4023 split.prop(lt, "display_circle", text="", icon='RIGHTARROW')
4024 split.operator("mesh.looptools_circle")
4025 # circle - settings
4026 if lt.display_circle:
4027 box = col.column(align=True).box().column()
4028 box.prop(lt, "circle_fit")
4029 box.separator()
4031 box.prop(lt, "circle_flatten")
4032 row = box.row(align=True)
4033 row.prop(lt, "circle_custom_radius")
4034 row_right = row.row(align=True)
4035 row_right.active = lt.circle_custom_radius
4036 row_right.prop(lt, "circle_radius", text="")
4037 box.prop(lt, "circle_regular")
4038 box.separator()
4040 box.prop(lt, "circle_influence")
4042 # curve - first line
4043 split = col.split(percentage=0.15, align=True)
4044 if lt.display_curve:
4045 split.prop(lt, "display_curve", text="", icon='DOWNARROW_HLT')
4046 else:
4047 split.prop(lt, "display_curve", text="", icon='RIGHTARROW')
4048 split.operator("mesh.looptools_curve")
4049 # curve - settings
4050 if lt.display_curve:
4051 box = col.column(align=True).box().column()
4052 box.prop(lt, "curve_interpolation")
4053 box.prop(lt, "curve_restriction")
4054 box.prop(lt, "curve_boundaries")
4055 box.prop(lt, "curve_regular")
4056 box.separator()
4058 box.prop(lt, "curve_influence")
4060 # flatten - first line
4061 split = col.split(percentage=0.15, align=True)
4062 if lt.display_flatten:
4063 split.prop(lt, "display_flatten", text="", icon='DOWNARROW_HLT')
4064 else:
4065 split.prop(lt, "display_flatten", text="", icon='RIGHTARROW')
4066 split.operator("mesh.looptools_flatten")
4067 # flatten - settings
4068 if lt.display_flatten:
4069 box = col.column(align=True).box().column()
4070 box.prop(lt, "flatten_plane")
4071 #box.prop(lt, "flatten_restriction")
4072 box.separator()
4074 box.prop(lt, "flatten_influence")
4076 # gstretch - first line
4077 split = col.split(percentage=0.15, align=True)
4078 if lt.display_gstretch:
4079 split.prop(lt, "display_gstretch", text="", icon='DOWNARROW_HLT')
4080 else:
4081 split.prop(lt, "display_gstretch", text="", icon='RIGHTARROW')
4082 split.operator("mesh.looptools_gstretch")
4083 # gstretch settings
4084 if lt.display_gstretch:
4085 box = col.column(align=True).box().column()
4086 box.prop(lt, "gstretch_method")
4087 box.prop(lt, "gstretch_delete_strokes")
4088 box.separator()
4090 col_conv = box.column(align=True)
4091 col_conv.prop(lt, "gstretch_conversion", text="")
4092 if lt.gstretch_conversion == 'distance':
4093 col_conv.prop(lt, "gstretch_conversion_distance")
4094 elif lt.gstretch_conversion == 'limit_vertices':
4095 row = col_conv.row(align=True)
4096 row.prop(lt, "gstretch_conversion_min", text="Min")
4097 row.prop(lt, "gstretch_conversion_max", text="Max")
4098 elif lt.gstretch_conversion == 'vertices':
4099 col_conv.prop(lt, "gstretch_conversion_vertices")
4100 box.separator()
4102 box.prop(lt, "gstretch_influence")
4104 # loft - first line
4105 split = col.split(percentage=0.15, align=True)
4106 if lt.display_loft:
4107 split.prop(lt, "display_loft", text="", icon='DOWNARROW_HLT')
4108 else:
4109 split.prop(lt, "display_loft", text="", icon='RIGHTARROW')
4110 split.operator("mesh.looptools_bridge", text="Loft").loft = True
4111 # loft - settings
4112 if lt.display_loft:
4113 box = col.column(align=True).box().column()
4114 #box.prop(self, "mode")
4116 # top row
4117 col_top = box.column(align=True)
4118 row = col_top.row(align=True)
4119 col_left = row.column(align=True)
4120 col_right = row.column(align=True)
4121 col_right.active = lt.bridge_segments != 1
4122 col_left.prop(lt, "bridge_segments")
4123 col_right.prop(lt, "bridge_min_width", text="")
4124 # bottom row
4125 bottom_left = col_left.row()
4126 bottom_left.active = lt.bridge_segments != 1
4127 bottom_left.prop(lt, "bridge_interpolation", text="")
4128 bottom_right = col_right.row()
4129 bottom_right.active = lt.bridge_interpolation == 'cubic'
4130 bottom_right.prop(lt, "bridge_cubic_strength")
4131 # boolean properties
4132 col_top.prop(lt, "bridge_remove_faces")
4133 col_top.prop(lt, "bridge_loft_loop")
4135 # override properties
4136 col_top.separator()
4137 row = box.row(align = True)
4138 row.prop(lt, "bridge_twist")
4139 row.prop(lt, "bridge_reverse")
4141 # relax - first line
4142 split = col.split(percentage=0.15, align=True)
4143 if lt.display_relax:
4144 split.prop(lt, "display_relax", text="", icon='DOWNARROW_HLT')
4145 else:
4146 split.prop(lt, "display_relax", text="", icon='RIGHTARROW')
4147 split.operator("mesh.looptools_relax")
4148 # relax - settings
4149 if lt.display_relax:
4150 box = col.column(align=True).box().column()
4151 box.prop(lt, "relax_interpolation")
4152 box.prop(lt, "relax_input")
4153 box.prop(lt, "relax_iterations")
4154 box.prop(lt, "relax_regular")
4156 # space - first line
4157 split = col.split(percentage=0.15, align=True)
4158 if lt.display_space:
4159 split.prop(lt, "display_space", text="", icon='DOWNARROW_HLT')
4160 else:
4161 split.prop(lt, "display_space", text="", icon='RIGHTARROW')
4162 split.operator("mesh.looptools_space")
4163 # space - settings
4164 if lt.display_space:
4165 box = col.column(align=True).box().column()
4166 box.prop(lt, "space_interpolation")
4167 box.prop(lt, "space_input")
4168 box.separator()
4170 box.prop(lt, "space_influence")
4173 # property group containing all properties for the gui in the panel
4174 class LoopToolsProps(bpy.types.PropertyGroup):
4176 Fake module like class
4177 bpy.context.window_manager.looptools
4180 # general display properties
4181 display_bridge = bpy.props.BoolProperty(name = "Bridge settings",
4182 description = "Display settings of the Bridge tool",
4183 default = False)
4184 display_circle = bpy.props.BoolProperty(name = "Circle settings",
4185 description = "Display settings of the Circle tool",
4186 default = False)
4187 display_curve = bpy.props.BoolProperty(name = "Curve settings",
4188 description = "Display settings of the Curve tool",
4189 default = False)
4190 display_flatten = bpy.props.BoolProperty(name = "Flatten settings",
4191 description = "Display settings of the Flatten tool",
4192 default = False)
4193 display_gstretch = bpy.props.BoolProperty(name = "Gstretch settings",
4194 description = "Display settings of the Gstretch tool",
4195 default = False)
4196 display_loft = bpy.props.BoolProperty(name = "Loft settings",
4197 description = "Display settings of the Loft tool",
4198 default = False)
4199 display_relax = bpy.props.BoolProperty(name = "Relax settings",
4200 description = "Display settings of the Relax tool",
4201 default = False)
4202 display_space = bpy.props.BoolProperty(name = "Space settings",
4203 description = "Display settings of the Space tool",
4204 default = False)
4206 # bridge properties
4207 bridge_cubic_strength = bpy.props.FloatProperty(name = "Strength",
4208 description = "Higher strength results in more fluid curves",
4209 default = 1.0,
4210 soft_min = -3.0,
4211 soft_max = 3.0)
4212 bridge_interpolation = bpy.props.EnumProperty(name = "Interpolation mode",
4213 items = (('cubic', "Cubic", "Gives curved results"),
4214 ('linear', "Linear", "Basic, fast, straight interpolation")),
4215 description = "Interpolation mode: algorithm used when creating "\
4216 "segments",
4217 default = 'cubic')
4218 bridge_loft = bpy.props.BoolProperty(name = "Loft",
4219 description = "Loft multiple loops, instead of considering them as "\
4220 "a multi-input for bridging",
4221 default = False)
4222 bridge_loft_loop = bpy.props.BoolProperty(name = "Loop",
4223 description = "Connect the first and the last loop with each other",
4224 default = False)
4225 bridge_min_width = bpy.props.IntProperty(name = "Minimum width",
4226 description = "Segments with an edge smaller than this are merged "\
4227 "(compared to base edge)",
4228 default = 0,
4229 min = 0,
4230 max = 100,
4231 subtype = 'PERCENTAGE')
4232 bridge_mode = bpy.props.EnumProperty(name = "Mode",
4233 items = (('basic', "Basic", "Fast algorithm"),
4234 ('shortest', "Shortest edge", "Slower algorithm with " \
4235 "better vertex matching")),
4236 description = "Algorithm used for bridging",
4237 default = 'shortest')
4238 bridge_remove_faces = bpy.props.BoolProperty(name = "Remove faces",
4239 description = "Remove faces that are internal after bridging",
4240 default = True)
4241 bridge_reverse = bpy.props.BoolProperty(name = "Reverse",
4242 description = "Manually override the direction in which the loops "\
4243 "are bridged. Only use if the tool gives the wrong " \
4244 "result",
4245 default = False)
4246 bridge_segments = bpy.props.IntProperty(name = "Segments",
4247 description = "Number of segments used to bridge the gap "\
4248 "(0 = automatic)",
4249 default = 1,
4250 min = 0,
4251 soft_max = 20)
4252 bridge_twist = bpy.props.IntProperty(name = "Twist",
4253 description = "Twist what vertices are connected to each other",
4254 default = 0)
4256 # circle properties
4257 circle_custom_radius = bpy.props.BoolProperty(name = "Radius",
4258 description = "Force a custom radius",
4259 default = False)
4260 circle_fit = bpy.props.EnumProperty(name = "Method",
4261 items = (("best", "Best fit", "Non-linear least squares"),
4262 ("inside", "Fit inside","Only move vertices towards the center")),
4263 description = "Method used for fitting a circle to the vertices",
4264 default = 'best')
4265 circle_flatten = bpy.props.BoolProperty(name = "Flatten",
4266 description = "Flatten the circle, instead of projecting it on the " \
4267 "mesh",
4268 default = True)
4269 circle_influence = bpy.props.FloatProperty(name = "Influence",
4270 description = "Force of the tool",
4271 default = 100.0,
4272 min = 0.0,
4273 max = 100.0,
4274 precision = 1,
4275 subtype = 'PERCENTAGE')
4276 circle_radius = bpy.props.FloatProperty(name = "Radius",
4277 description = "Custom radius for circle",
4278 default = 1.0,
4279 min = 0.0,
4280 soft_max = 1000.0)
4281 circle_regular = bpy.props.BoolProperty(name = "Regular",
4282 description = "Distribute vertices at constant distances along the " \
4283 "circle",
4284 default = True)
4286 # curve properties
4287 curve_boundaries = bpy.props.BoolProperty(name = "Boundaries",
4288 description = "Limit the tool to work within the boundaries of the "\
4289 "selected vertices",
4290 default = False)
4291 curve_influence = bpy.props.FloatProperty(name = "Influence",
4292 description = "Force of the tool",
4293 default = 100.0,
4294 min = 0.0,
4295 max = 100.0,
4296 precision = 1,
4297 subtype = 'PERCENTAGE')
4298 curve_interpolation = bpy.props.EnumProperty(name = "Interpolation",
4299 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4300 ("linear", "Linear", "Simple and fast linear algorithm")),
4301 description = "Algorithm used for interpolation",
4302 default = 'cubic')
4303 curve_regular = bpy.props.BoolProperty(name = "Regular",
4304 description = "Distribute vertices at constant distances along the " \
4305 "curve",
4306 default = True)
4307 curve_restriction = bpy.props.EnumProperty(name = "Restriction",
4308 items = (("none", "None", "No restrictions on vertex movement"),
4309 ("extrude", "Extrude only","Only allow extrusions (no "\
4310 "indentations)"),
4311 ("indent", "Indent only", "Only allow indentation (no "\
4312 "extrusions)")),
4313 description = "Restrictions on how the vertices can be moved",
4314 default = 'none')
4316 # flatten properties
4317 flatten_influence = bpy.props.FloatProperty(name = "Influence",
4318 description = "Force of the tool",
4319 default = 100.0,
4320 min = 0.0,
4321 max = 100.0,
4322 precision = 1,
4323 subtype = 'PERCENTAGE')
4324 flatten_plane = bpy.props.EnumProperty(name = "Plane",
4325 items = (("best_fit", "Best fit", "Calculate a best fitting plane"),
4326 ("normal", "Normal", "Derive plane from averaging vertex "\
4327 "normals"),
4328 ("view", "View", "Flatten on a plane perpendicular to the "\
4329 "viewing angle")),
4330 description = "Plane on which vertices are flattened",
4331 default = 'best_fit')
4332 flatten_restriction = bpy.props.EnumProperty(name = "Restriction",
4333 items = (("none", "None", "No restrictions on vertex movement"),
4334 ("bounding_box", "Bounding box", "Vertices are restricted to "\
4335 "movement inside the bounding box of the selection")),
4336 description = "Restrictions on how the vertices can be moved",
4337 default = 'none')
4339 # gstretch properties
4340 gstretch_conversion = bpy.props.EnumProperty(name = "Conversion",
4341 items = (("distance", "Distance", "Set the distance between vertices "\
4342 "of the converted grease pencil stroke"),
4343 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "\
4344 "number of vertices that converted GP strokes will have"),
4345 ("vertices", "Exact vertices", "Set the exact number of vertices "\
4346 "that converted grease pencil strokes will have. Short strokes "\
4347 "with few points may contain less vertices than this number."),
4348 ("none", "No simplification", "Convert each grease pencil point "\
4349 "to a vertex")),
4350 description = "If grease pencil strokes are converted to geometry, "\
4351 "use this simplification method",
4352 default = 'limit_vertices')
4353 gstretch_conversion_distance = bpy.props.FloatProperty(name = "Distance",
4354 description = "Absolute distance between vertices along the converted "\
4355 "grease pencil stroke",
4356 default = 0.1,
4357 min = 0.000001,
4358 soft_min = 0.01,
4359 soft_max = 100)
4360 gstretch_conversion_max = bpy.props.IntProperty(name = "Max Vertices",
4361 description = "Maximum number of vertices grease pencil strokes will "\
4362 "have, when they are converted to geomtery",
4363 default = 32,
4364 min = 3,
4365 soft_max = 500,
4366 update = gstretch_update_min)
4367 gstretch_conversion_min = bpy.props.IntProperty(name = "Min Vertices",
4368 description = "Minimum number of vertices grease pencil strokes will "\
4369 "have, when they are converted to geomtery",
4370 default = 8,
4371 min = 3,
4372 soft_max = 500,
4373 update = gstretch_update_max)
4374 gstretch_conversion_vertices = bpy.props.IntProperty(name = "Vertices",
4375 description = "Number of vertices grease pencil strokes will "\
4376 "have, when they are converted to geometry. If strokes have less "\
4377 "points than required, the 'Spread evenly' method is used.",
4378 default = 32,
4379 min = 3,
4380 soft_max = 500)
4381 gstretch_delete_strokes = bpy.props.BoolProperty(name="Delete strokes",
4382 description = "Remove Grease Pencil strokes if they have been used "\
4383 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
4384 default = False)
4385 gstretch_influence = bpy.props.FloatProperty(name = "Influence",
4386 description = "Force of the tool",
4387 default = 100.0,
4388 min = 0.0,
4389 max = 100.0,
4390 precision = 1,
4391 subtype = 'PERCENTAGE')
4392 gstretch_method = bpy.props.EnumProperty(name = "Method",
4393 items = (("project", "Project", "Project vertices onto the stroke, "\
4394 "using vertex normals and connected edges"),
4395 ("irregular", "Spread", "Distribute vertices along the full "\
4396 "stroke, retaining relative distances between the vertices"),
4397 ("regular", "Spread evenly", "Distribute vertices at regular "\
4398 "distances along the full stroke")),
4399 description = "Method of distributing the vertices over the Grease "\
4400 "Pencil stroke",
4401 default = 'regular')
4403 # relax properties
4404 relax_input = bpy.props.EnumProperty(name = "Input",
4405 items = (("all", "Parallel (all)", "Also use non-selected "\
4406 "parallel loops as input"),
4407 ("selected", "Selection","Only use selected vertices as input")),
4408 description = "Loops that are relaxed",
4409 default = 'selected')
4410 relax_interpolation = bpy.props.EnumProperty(name = "Interpolation",
4411 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4412 ("linear", "Linear", "Simple and fast linear algorithm")),
4413 description = "Algorithm used for interpolation",
4414 default = 'cubic')
4415 relax_iterations = bpy.props.EnumProperty(name = "Iterations",
4416 items = (("1", "1", "One"),
4417 ("3", "3", "Three"),
4418 ("5", "5", "Five"),
4419 ("10", "10", "Ten"),
4420 ("25", "25", "Twenty-five")),
4421 description = "Number of times the loop is relaxed",
4422 default = "1")
4423 relax_regular = bpy.props.BoolProperty(name = "Regular",
4424 description = "Distribute vertices at constant distances along the" \
4425 "loop",
4426 default = True)
4428 # space properties
4429 space_influence = bpy.props.FloatProperty(name = "Influence",
4430 description = "Force of the tool",
4431 default = 100.0,
4432 min = 0.0,
4433 max = 100.0,
4434 precision = 1,
4435 subtype = 'PERCENTAGE')
4436 space_input = bpy.props.EnumProperty(name = "Input",
4437 items = (("all", "Parallel (all)", "Also use non-selected "\
4438 "parallel loops as input"),
4439 ("selected", "Selection","Only use selected vertices as input")),
4440 description = "Loops that are spaced",
4441 default = 'selected')
4442 space_interpolation = bpy.props.EnumProperty(name = "Interpolation",
4443 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4444 ("linear", "Linear", "Vertices are projected on existing edges")),
4445 description = "Algorithm used for interpolation",
4446 default = 'cubic')
4449 # draw function for integration in menus
4450 def menu_func(self, context):
4451 self.layout.menu("VIEW3D_MT_edit_mesh_looptools")
4452 self.layout.separator()
4455 # define classes for registration
4456 classes = [VIEW3D_MT_edit_mesh_looptools,
4457 VIEW3D_PT_tools_looptools,
4458 LoopToolsProps,
4459 Bridge,
4460 Circle,
4461 Curve,
4462 Flatten,
4463 GStretch,
4464 Relax,
4465 Space]
4468 # registering and menu integration
4469 def register():
4470 for c in classes:
4471 bpy.utils.register_class(c)
4472 bpy.types.VIEW3D_MT_edit_mesh_specials.prepend(menu_func)
4473 bpy.types.WindowManager.looptools = bpy.props.PointerProperty(\
4474 type = LoopToolsProps)
4477 # unregistering and removing menus
4478 def unregister():
4479 for c in classes:
4480 bpy.utils.unregister_class(c)
4481 bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func)
4482 try:
4483 del bpy.types.WindowManager.looptools
4484 except:
4485 pass
4488 if __name__ == "__main__":
4489 register()