Rigify: store advanced options in armature instead of window manager.
[blender-addons.git] / mesh_looptools.py
blob41c1be0a40fb9b2b1af2e3493dc7c30f2bf8bf17
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, 6, 7),
23 "blender": (2, 80, 0),
24 "location": "View3D > Sidebar > Edit Tab / Edit Mode Context Menu",
25 "warning": "",
26 "description": "Mesh modelling toolkit. Several tools to aid modelling",
27 "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
28 "Scripts/Modeling/LoopTools",
29 "category": "Mesh",
33 import bmesh
34 import bpy
35 import collections
36 import mathutils
37 import math
38 from bpy_extras import view3d_utils
39 from bpy.types import (
40 Operator,
41 Menu,
42 Panel,
43 PropertyGroup,
44 AddonPreferences,
46 from bpy.props import (
47 BoolProperty,
48 EnumProperty,
49 FloatProperty,
50 IntProperty,
51 PointerProperty,
52 StringProperty,
55 # ########################################
56 # ##### General functions ################
57 # ########################################
59 # used by all tools to improve speed on reruns Unlink
60 looptools_cache = {}
63 def get_strokes(self, context):
64 looptools = context.window_manager.looptools
65 if looptools.gstretch_use_guide == "Annotation":
66 try:
67 strokes = bpy.data.grease_pencils[0].layers.active.active_frame.strokes
68 return True
69 except:
70 self.report({'WARNING'}, "active Annotation strokes not found")
71 return False
72 if looptools.gstretch_use_guide == "GPencil" and not looptools.gstretch_guide == None:
73 try:
74 strokes = looptools.gstretch_guide.data.layers.active.active_frame.strokes
75 return True
76 except:
77 self.report({'WARNING'}, "active GPencil strokes not found")
78 return False
79 else:
80 return False
82 # force a full recalculation next time
83 def cache_delete(tool):
84 if tool in looptools_cache:
85 del looptools_cache[tool]
88 # check cache for stored information
89 def cache_read(tool, object, bm, input_method, boundaries):
90 # current tool not cached yet
91 if tool not in looptools_cache:
92 return(False, False, False, False, False)
93 # check if selected object didn't change
94 if object.name != looptools_cache[tool]["object"]:
95 return(False, False, False, False, False)
96 # check if input didn't change
97 if input_method != looptools_cache[tool]["input_method"]:
98 return(False, False, False, False, False)
99 if boundaries != looptools_cache[tool]["boundaries"]:
100 return(False, False, False, False, False)
101 modifiers = [mod.name for mod in object.modifiers if mod.show_viewport and
102 mod.type == 'MIRROR']
103 if modifiers != looptools_cache[tool]["modifiers"]:
104 return(False, False, False, False, False)
105 input = [v.index for v in bm.verts if v.select and not v.hide]
106 if input != looptools_cache[tool]["input"]:
107 return(False, False, False, False, False)
108 # reading values
109 single_loops = looptools_cache[tool]["single_loops"]
110 loops = looptools_cache[tool]["loops"]
111 derived = looptools_cache[tool]["derived"]
112 mapping = looptools_cache[tool]["mapping"]
114 return(True, single_loops, loops, derived, mapping)
117 # store information in the cache
118 def cache_write(tool, object, bm, input_method, boundaries, single_loops,
119 loops, derived, mapping):
120 # clear cache of current tool
121 if tool in looptools_cache:
122 del looptools_cache[tool]
123 # prepare values to be saved to cache
124 input = [v.index for v in bm.verts if v.select and not v.hide]
125 modifiers = [mod.name for mod in object.modifiers if mod.show_viewport
126 and mod.type == 'MIRROR']
127 # update cache
128 looptools_cache[tool] = {
129 "input": input, "object": object.name,
130 "input_method": input_method, "boundaries": boundaries,
131 "single_loops": single_loops, "loops": loops,
132 "derived": derived, "mapping": mapping, "modifiers": modifiers}
135 # calculates natural cubic splines through all given knots
136 def calculate_cubic_splines(bm_mod, tknots, knots):
137 # hack for circular loops
138 if knots[0] == knots[-1] and len(knots) > 1:
139 circular = True
140 k_new1 = []
141 for k in range(-1, -5, -1):
142 if k - 1 < -len(knots):
143 k += len(knots)
144 k_new1.append(knots[k - 1])
145 k_new2 = []
146 for k in range(4):
147 if k + 1 > len(knots) - 1:
148 k -= len(knots)
149 k_new2.append(knots[k + 1])
150 for k in k_new1:
151 knots.insert(0, k)
152 for k in k_new2:
153 knots.append(k)
154 t_new1 = []
155 total1 = 0
156 for t in range(-1, -5, -1):
157 if t - 1 < -len(tknots):
158 t += len(tknots)
159 total1 += tknots[t] - tknots[t - 1]
160 t_new1.append(tknots[0] - total1)
161 t_new2 = []
162 total2 = 0
163 for t in range(4):
164 if t + 1 > len(tknots) - 1:
165 t -= len(tknots)
166 total2 += tknots[t + 1] - tknots[t]
167 t_new2.append(tknots[-1] + total2)
168 for t in t_new1:
169 tknots.insert(0, t)
170 for t in t_new2:
171 tknots.append(t)
172 else:
173 circular = False
174 # end of hack
176 n = len(knots)
177 if n < 2:
178 return False
179 x = tknots[:]
180 locs = [bm_mod.verts[k].co[:] for k in knots]
181 result = []
182 for j in range(3):
183 a = []
184 for i in locs:
185 a.append(i[j])
186 h = []
187 for i in range(n - 1):
188 if x[i + 1] - x[i] == 0:
189 h.append(1e-8)
190 else:
191 h.append(x[i + 1] - x[i])
192 q = [False]
193 for i in range(1, n - 1):
194 q.append(3 / h[i] * (a[i + 1] - a[i]) - 3 / h[i - 1] * (a[i] - a[i - 1]))
195 l = [1.0]
196 u = [0.0]
197 z = [0.0]
198 for i in range(1, n - 1):
199 l.append(2 * (x[i + 1] - x[i - 1]) - h[i - 1] * u[i - 1])
200 if l[i] == 0:
201 l[i] = 1e-8
202 u.append(h[i] / l[i])
203 z.append((q[i] - h[i - 1] * z[i - 1]) / l[i])
204 l.append(1.0)
205 z.append(0.0)
206 b = [False for i in range(n - 1)]
207 c = [False for i in range(n)]
208 d = [False for i in range(n - 1)]
209 c[n - 1] = 0.0
210 for i in range(n - 2, -1, -1):
211 c[i] = z[i] - u[i] * c[i + 1]
212 b[i] = (a[i + 1] - a[i]) / h[i] - h[i] * (c[i + 1] + 2 * c[i]) / 3
213 d[i] = (c[i + 1] - c[i]) / (3 * h[i])
214 for i in range(n - 1):
215 result.append([a[i], b[i], c[i], d[i], x[i]])
216 splines = []
217 for i in range(len(knots) - 1):
218 splines.append([result[i], result[i + n - 1], result[i + (n - 1) * 2]])
219 if circular: # cleaning up after hack
220 knots = knots[4:-4]
221 tknots = tknots[4:-4]
223 return(splines)
226 # calculates linear splines through all given knots
227 def calculate_linear_splines(bm_mod, tknots, knots):
228 splines = []
229 for i in range(len(knots) - 1):
230 a = bm_mod.verts[knots[i]].co
231 b = bm_mod.verts[knots[i + 1]].co
232 d = b - a
233 t = tknots[i]
234 u = tknots[i + 1] - t
235 splines.append([a, d, t, u]) # [locStart, locDif, tStart, tDif]
237 return(splines)
240 # calculate a best-fit plane to the given vertices
241 def calculate_plane(bm_mod, loop, method="best_fit", object=False):
242 # getting the vertex locations
243 locs = [bm_mod.verts[v].co.copy() for v in loop[0]]
245 # calculating the center of masss
246 com = mathutils.Vector()
247 for loc in locs:
248 com += loc
249 com /= len(locs)
250 x, y, z = com
252 if method == 'best_fit':
253 # creating the covariance matrix
254 mat = mathutils.Matrix(((0.0, 0.0, 0.0),
255 (0.0, 0.0, 0.0),
256 (0.0, 0.0, 0.0),
258 for loc in locs:
259 mat[0][0] += (loc[0] - x) ** 2
260 mat[1][0] += (loc[0] - x) * (loc[1] - y)
261 mat[2][0] += (loc[0] - x) * (loc[2] - z)
262 mat[0][1] += (loc[1] - y) * (loc[0] - x)
263 mat[1][1] += (loc[1] - y) ** 2
264 mat[2][1] += (loc[1] - y) * (loc[2] - z)
265 mat[0][2] += (loc[2] - z) * (loc[0] - x)
266 mat[1][2] += (loc[2] - z) * (loc[1] - y)
267 mat[2][2] += (loc[2] - z) ** 2
269 # calculating the normal to the plane
270 normal = False
271 try:
272 mat = matrix_invert(mat)
273 except:
274 ax = 2
275 if math.fabs(sum(mat[0])) < math.fabs(sum(mat[1])):
276 if math.fabs(sum(mat[0])) < math.fabs(sum(mat[2])):
277 ax = 0
278 elif math.fabs(sum(mat[1])) < math.fabs(sum(mat[2])):
279 ax = 1
280 if ax == 0:
281 normal = mathutils.Vector((1.0, 0.0, 0.0))
282 elif ax == 1:
283 normal = mathutils.Vector((0.0, 1.0, 0.0))
284 else:
285 normal = mathutils.Vector((0.0, 0.0, 1.0))
286 if not normal:
287 # warning! this is different from .normalize()
288 itermax = 500
289 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
290 for i in range(itermax):
291 vec = vec2
292 vec2 = mat @ vec
293 if vec2.length != 0:
294 vec2 /= vec2.length
295 if vec2 == vec:
296 break
297 if vec2.length == 0:
298 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
299 normal = vec2
301 elif method == 'normal':
302 # averaging the vertex normals
303 v_normals = [bm_mod.verts[v].normal for v in loop[0]]
304 normal = mathutils.Vector()
305 for v_normal in v_normals:
306 normal += v_normal
307 normal /= len(v_normals)
308 normal.normalize()
310 elif method == 'view':
311 # calculate view normal
312 rotation = bpy.context.space_data.region_3d.view_matrix.to_3x3().\
313 inverted()
314 normal = rotation @ mathutils.Vector((0.0, 0.0, 1.0))
315 if object:
316 normal = object.matrix_world.inverted().to_euler().to_matrix() @ \
317 normal
319 return(com, normal)
322 # calculate splines based on given interpolation method (controller function)
323 def calculate_splines(interpolation, bm_mod, tknots, knots):
324 if interpolation == 'cubic':
325 splines = calculate_cubic_splines(bm_mod, tknots, knots[:])
326 else: # interpolations == 'linear'
327 splines = calculate_linear_splines(bm_mod, tknots, knots[:])
329 return(splines)
332 # check loops and only return valid ones
333 def check_loops(loops, mapping, bm_mod):
334 valid_loops = []
335 for loop, circular in loops:
336 # loop needs to have at least 3 vertices
337 if len(loop) < 3:
338 continue
339 # loop needs at least 1 vertex in the original, non-mirrored mesh
340 if mapping:
341 all_virtual = True
342 for vert in loop:
343 if mapping[vert] > -1:
344 all_virtual = False
345 break
346 if all_virtual:
347 continue
348 # vertices can not all be at the same location
349 stacked = True
350 for i in range(len(loop) - 1):
351 if (bm_mod.verts[loop[i]].co - bm_mod.verts[loop[i + 1]].co).length > 1e-6:
352 stacked = False
353 break
354 if stacked:
355 continue
356 # passed all tests, loop is valid
357 valid_loops.append([loop, circular])
359 return(valid_loops)
362 # input: bmesh, output: dict with the edge-key as key and face-index as value
363 def dict_edge_faces(bm):
364 edge_faces = dict([[edgekey(edge), []] for edge in bm.edges if not edge.hide])
365 for face in bm.faces:
366 if face.hide:
367 continue
368 for key in face_edgekeys(face):
369 edge_faces[key].append(face.index)
371 return(edge_faces)
374 # input: bmesh (edge-faces optional), output: dict with face-face connections
375 def dict_face_faces(bm, edge_faces=False):
376 if not edge_faces:
377 edge_faces = dict_edge_faces(bm)
379 connected_faces = dict([[face.index, []] for face in bm.faces if not face.hide])
380 for face in bm.faces:
381 if face.hide:
382 continue
383 for edge_key in face_edgekeys(face):
384 for connected_face in edge_faces[edge_key]:
385 if connected_face == face.index:
386 continue
387 connected_faces[face.index].append(connected_face)
389 return(connected_faces)
392 # input: bmesh, output: dict with the vert index as key and edge-keys as value
393 def dict_vert_edges(bm):
394 vert_edges = dict([[v.index, []] for v in bm.verts if not v.hide])
395 for edge in bm.edges:
396 if edge.hide:
397 continue
398 ek = edgekey(edge)
399 for vert in ek:
400 vert_edges[vert].append(ek)
402 return(vert_edges)
405 # input: bmesh, output: dict with the vert index as key and face index as value
406 def dict_vert_faces(bm):
407 vert_faces = dict([[v.index, []] for v in bm.verts if not v.hide])
408 for face in bm.faces:
409 if not face.hide:
410 for vert in face.verts:
411 vert_faces[vert.index].append(face.index)
413 return(vert_faces)
416 # input: list of edge-keys, output: dictionary with vertex-vertex connections
417 def dict_vert_verts(edge_keys):
418 # create connection data
419 vert_verts = {}
420 for ek in edge_keys:
421 for i in range(2):
422 if ek[i] in vert_verts:
423 vert_verts[ek[i]].append(ek[1 - i])
424 else:
425 vert_verts[ek[i]] = [ek[1 - i]]
427 return(vert_verts)
430 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
431 def edgekey(edge):
432 return(tuple(sorted([edge.verts[0].index, edge.verts[1].index])))
435 # returns the edgekeys of a bmesh face
436 def face_edgekeys(face):
437 return([tuple(sorted([edge.verts[0].index, edge.verts[1].index])) for edge in face.edges])
440 # calculate input loops
441 def get_connected_input(object, bm, input):
442 # get mesh with modifiers applied
443 derived, bm_mod = get_derived_bmesh(object, bm)
445 # calculate selected loops
446 edge_keys = [edgekey(edge) for edge in bm_mod.edges if edge.select and not edge.hide]
447 loops = get_connected_selections(edge_keys)
449 # if only selected loops are needed, we're done
450 if input == 'selected':
451 return(derived, bm_mod, loops)
452 # elif input == 'all':
453 loops = get_parallel_loops(bm_mod, loops)
455 return(derived, bm_mod, loops)
458 # sorts all edge-keys into a list of loops
459 def get_connected_selections(edge_keys):
460 # create connection data
461 vert_verts = dict_vert_verts(edge_keys)
463 # find loops consisting of connected selected edges
464 loops = []
465 while len(vert_verts) > 0:
466 loop = [iter(vert_verts.keys()).__next__()]
467 growing = True
468 flipped = False
470 # extend loop
471 while growing:
472 # no more connection data for current vertex
473 if loop[-1] not in vert_verts:
474 if not flipped:
475 loop.reverse()
476 flipped = True
477 else:
478 growing = False
479 else:
480 extended = False
481 for i, next_vert in enumerate(vert_verts[loop[-1]]):
482 if next_vert not in loop:
483 vert_verts[loop[-1]].pop(i)
484 if len(vert_verts[loop[-1]]) == 0:
485 del vert_verts[loop[-1]]
486 # remove connection both ways
487 if next_vert in vert_verts:
488 if len(vert_verts[next_vert]) == 1:
489 del vert_verts[next_vert]
490 else:
491 vert_verts[next_vert].remove(loop[-1])
492 loop.append(next_vert)
493 extended = True
494 break
495 if not extended:
496 # found one end of the loop, continue with next
497 if not flipped:
498 loop.reverse()
499 flipped = True
500 # found both ends of the loop, stop growing
501 else:
502 growing = False
504 # check if loop is circular
505 if loop[0] in vert_verts:
506 if loop[-1] in vert_verts[loop[0]]:
507 # is circular
508 if len(vert_verts[loop[0]]) == 1:
509 del vert_verts[loop[0]]
510 else:
511 vert_verts[loop[0]].remove(loop[-1])
512 if len(vert_verts[loop[-1]]) == 1:
513 del vert_verts[loop[-1]]
514 else:
515 vert_verts[loop[-1]].remove(loop[0])
516 loop = [loop, True]
517 else:
518 # not circular
519 loop = [loop, False]
520 else:
521 # not circular
522 loop = [loop, False]
524 loops.append(loop)
526 return(loops)
529 # get the derived mesh data, if there is a mirror modifier
530 def get_derived_bmesh(object, bm):
531 # check for mirror modifiers
532 if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
533 derived = True
534 # disable other modifiers
535 show_viewport = [mod.name for mod in object.modifiers if mod.show_viewport]
536 for mod in object.modifiers:
537 if mod.type != 'MIRROR':
538 mod.show_viewport = False
539 # get derived mesh
540 bm_mod = bmesh.new()
541 depsgraph = bpy.context.evaluated_depsgraph_get()
542 object_eval = object.evaluated_get(depsgraph)
543 mesh_mod = object_eval.to_mesh()
544 bm_mod.from_mesh(mesh_mod)
545 object_eval.to_mesh_clear()
546 # re-enable other modifiers
547 for mod_name in show_viewport:
548 object.modifiers[mod_name].show_viewport = True
549 # no mirror modifiers, so no derived mesh necessary
550 else:
551 derived = False
552 bm_mod = bm
554 bm_mod.verts.ensure_lookup_table()
555 bm_mod.edges.ensure_lookup_table()
556 bm_mod.faces.ensure_lookup_table()
558 return(derived, bm_mod)
561 # return a mapping of derived indices to indices
562 def get_mapping(derived, bm, bm_mod, single_vertices, full_search, loops):
563 if not derived:
564 return(False)
566 if full_search:
567 verts = [v for v in bm.verts if not v.hide]
568 else:
569 verts = [v for v in bm.verts if v.select and not v.hide]
571 # non-selected vertices around single vertices also need to be mapped
572 if single_vertices:
573 mapping = dict([[vert, -1] for vert in single_vertices])
574 verts_mod = [bm_mod.verts[vert] for vert in single_vertices]
575 for v in verts:
576 for v_mod in verts_mod:
577 if (v.co - v_mod.co).length < 1e-6:
578 mapping[v_mod.index] = v.index
579 break
580 real_singles = [v_real for v_real in mapping.values() if v_real > -1]
582 verts_indices = [vert.index for vert in verts]
583 for face in [face for face in bm.faces if not face.select and not face.hide]:
584 for vert in face.verts:
585 if vert.index in real_singles:
586 for v in face.verts:
587 if v.index not in verts_indices:
588 if v not in verts:
589 verts.append(v)
590 break
592 # create mapping of derived indices to indices
593 mapping = dict([[vert, -1] for loop in loops for vert in loop[0]])
594 if single_vertices:
595 for single in single_vertices:
596 mapping[single] = -1
597 verts_mod = [bm_mod.verts[i] for i in mapping.keys()]
598 for v in verts:
599 for v_mod in verts_mod:
600 if (v.co - v_mod.co).length < 1e-6:
601 mapping[v_mod.index] = v.index
602 verts_mod.remove(v_mod)
603 break
605 return(mapping)
608 # calculate the determinant of a matrix
609 def matrix_determinant(m):
610 determinant = m[0][0] * m[1][1] * m[2][2] + m[0][1] * m[1][2] * m[2][0] \
611 + m[0][2] * m[1][0] * m[2][1] - m[0][2] * m[1][1] * m[2][0] \
612 - m[0][1] * m[1][0] * m[2][2] - m[0][0] * m[1][2] * m[2][1]
614 return(determinant)
617 # custom matrix inversion, to provide higher precision than the built-in one
618 def matrix_invert(m):
619 r = mathutils.Matrix((
620 (m[1][1] * m[2][2] - m[1][2] * m[2][1], m[0][2] * m[2][1] - m[0][1] * m[2][2],
621 m[0][1] * m[1][2] - m[0][2] * m[1][1]),
622 (m[1][2] * m[2][0] - m[1][0] * m[2][2], m[0][0] * m[2][2] - m[0][2] * m[2][0],
623 m[0][2] * m[1][0] - m[0][0] * m[1][2]),
624 (m[1][0] * m[2][1] - m[1][1] * m[2][0], m[0][1] * m[2][0] - m[0][0] * m[2][1],
625 m[0][0] * m[1][1] - m[0][1] * m[1][0])))
627 return (r * (1 / matrix_determinant(m)))
630 # returns a list of all loops parallel to the input, input included
631 def get_parallel_loops(bm_mod, loops):
632 # get required dictionaries
633 edge_faces = dict_edge_faces(bm_mod)
634 connected_faces = dict_face_faces(bm_mod, edge_faces)
635 # turn vertex loops into edge loops
636 edgeloops = []
637 for loop in loops:
638 edgeloop = [[sorted([loop[0][i], loop[0][i + 1]]) for i in
639 range(len(loop[0]) - 1)], loop[1]]
640 if loop[1]: # circular
641 edgeloop[0].append(sorted([loop[0][-1], loop[0][0]]))
642 edgeloops.append(edgeloop[:])
643 # variables to keep track while iterating
644 all_edgeloops = []
645 has_branches = False
647 for loop in edgeloops:
648 # initialise with original loop
649 all_edgeloops.append(loop[0])
650 newloops = [loop[0]]
651 verts_used = []
652 for edge in loop[0]:
653 if edge[0] not in verts_used:
654 verts_used.append(edge[0])
655 if edge[1] not in verts_used:
656 verts_used.append(edge[1])
658 # find parallel loops
659 while len(newloops) > 0:
660 side_a = []
661 side_b = []
662 for i in newloops[-1]:
663 i = tuple(i)
664 forbidden_side = False
665 if i not in edge_faces:
666 # weird input with branches
667 has_branches = True
668 break
669 for face in edge_faces[i]:
670 if len(side_a) == 0 and forbidden_side != "a":
671 side_a.append(face)
672 if forbidden_side:
673 break
674 forbidden_side = "a"
675 continue
676 elif side_a[-1] in connected_faces[face] and \
677 forbidden_side != "a":
678 side_a.append(face)
679 if forbidden_side:
680 break
681 forbidden_side = "a"
682 continue
683 if len(side_b) == 0 and forbidden_side != "b":
684 side_b.append(face)
685 if forbidden_side:
686 break
687 forbidden_side = "b"
688 continue
689 elif side_b[-1] in connected_faces[face] and \
690 forbidden_side != "b":
691 side_b.append(face)
692 if forbidden_side:
693 break
694 forbidden_side = "b"
695 continue
697 if has_branches:
698 # weird input with branches
699 break
701 newloops.pop(-1)
702 sides = []
703 if side_a:
704 sides.append(side_a)
705 if side_b:
706 sides.append(side_b)
708 for side in sides:
709 extraloop = []
710 for fi in side:
711 for key in face_edgekeys(bm_mod.faces[fi]):
712 if key[0] not in verts_used and key[1] not in \
713 verts_used:
714 extraloop.append(key)
715 break
716 if extraloop:
717 for key in extraloop:
718 for new_vert in key:
719 if new_vert not in verts_used:
720 verts_used.append(new_vert)
721 newloops.append(extraloop)
722 all_edgeloops.append(extraloop)
724 # input contains branches, only return selected loop
725 if has_branches:
726 return(loops)
728 # change edgeloops into normal loops
729 loops = []
730 for edgeloop in all_edgeloops:
731 loop = []
732 # grow loop by comparing vertices between consecutive edge-keys
733 for i in range(len(edgeloop) - 1):
734 for vert in range(2):
735 if edgeloop[i][vert] in edgeloop[i + 1]:
736 loop.append(edgeloop[i][vert])
737 break
738 if loop:
739 # add starting vertex
740 for vert in range(2):
741 if edgeloop[0][vert] != loop[0]:
742 loop = [edgeloop[0][vert]] + loop
743 break
744 # add ending vertex
745 for vert in range(2):
746 if edgeloop[-1][vert] != loop[-1]:
747 loop.append(edgeloop[-1][vert])
748 break
749 # check if loop is circular
750 if loop[0] == loop[-1]:
751 circular = True
752 loop = loop[:-1]
753 else:
754 circular = False
755 loops.append([loop, circular])
757 return(loops)
760 # gather initial data
761 def initialise():
762 object = bpy.context.active_object
763 if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
764 # ensure that selection is synced for the derived mesh
765 bpy.ops.object.mode_set(mode='OBJECT')
766 bpy.ops.object.mode_set(mode='EDIT')
767 bm = bmesh.from_edit_mesh(object.data)
769 bm.verts.ensure_lookup_table()
770 bm.edges.ensure_lookup_table()
771 bm.faces.ensure_lookup_table()
773 return(object, bm)
776 # move the vertices to their new locations
777 def move_verts(object, bm, mapping, move, lock, influence):
778 if lock:
779 lock_x, lock_y, lock_z = lock
780 orient_slot = bpy.context.scene.transform_orientation_slots[0]
781 custom = orient_slot.custom_orientation
782 if custom:
783 mat = custom.matrix.to_4x4().inverted() @ object.matrix_world.copy()
784 elif orient_slot.type == 'LOCAL':
785 mat = mathutils.Matrix.Identity(4)
786 elif orient_slot.type == 'VIEW':
787 mat = bpy.context.region_data.view_matrix.copy() @ \
788 object.matrix_world.copy()
789 else: # orientation == 'GLOBAL'
790 mat = object.matrix_world.copy()
791 mat_inv = mat.inverted()
793 for loop in move:
794 for index, loc in loop:
795 if mapping:
796 if mapping[index] == -1:
797 continue
798 else:
799 index = mapping[index]
800 if lock:
801 delta = (loc - bm.verts[index].co) @ mat_inv
802 if lock_x:
803 delta[0] = 0
804 if lock_y:
805 delta[1] = 0
806 if lock_z:
807 delta[2] = 0
808 delta = delta @ mat
809 loc = bm.verts[index].co + delta
810 if influence < 0:
811 new_loc = loc
812 else:
813 new_loc = loc * (influence / 100) + \
814 bm.verts[index].co * ((100 - influence) / 100)
815 bm.verts[index].co = new_loc
816 bm.normal_update()
817 object.data.update()
819 bm.verts.ensure_lookup_table()
820 bm.edges.ensure_lookup_table()
821 bm.faces.ensure_lookup_table()
824 # load custom tool settings
825 def settings_load(self):
826 lt = bpy.context.window_manager.looptools
827 tool = self.name.split()[0].lower()
828 keys = self.as_keywords().keys()
829 for key in keys:
830 setattr(self, key, getattr(lt, tool + "_" + key))
833 # store custom tool settings
834 def settings_write(self):
835 lt = bpy.context.window_manager.looptools
836 tool = self.name.split()[0].lower()
837 keys = self.as_keywords().keys()
838 for key in keys:
839 setattr(lt, tool + "_" + key, getattr(self, key))
842 # clean up and set settings back to original state
843 def terminate():
844 # update editmesh cached data
845 obj = bpy.context.active_object
846 if obj.mode == 'EDIT':
847 bmesh.update_edit_mesh(obj.data, loop_triangles=True, destructive=True)
850 # ########################################
851 # ##### Bridge functions #################
852 # ########################################
854 # calculate a cubic spline through the middle section of 4 given coordinates
855 def bridge_calculate_cubic_spline(bm, coordinates):
856 result = []
857 x = [0, 1, 2, 3]
859 for j in range(3):
860 a = []
861 for i in coordinates:
862 a.append(float(i[j]))
863 h = []
864 for i in range(3):
865 h.append(x[i + 1] - x[i])
866 q = [False]
867 for i in range(1, 3):
868 q.append(3.0 / h[i] * (a[i + 1] - a[i]) - 3.0 / h[i - 1] * (a[i] - a[i - 1]))
869 l = [1.0]
870 u = [0.0]
871 z = [0.0]
872 for i in range(1, 3):
873 l.append(2.0 * (x[i + 1] - x[i - 1]) - h[i - 1] * u[i - 1])
874 u.append(h[i] / l[i])
875 z.append((q[i] - h[i - 1] * z[i - 1]) / l[i])
876 l.append(1.0)
877 z.append(0.0)
878 b = [False for i in range(3)]
879 c = [False for i in range(4)]
880 d = [False for i in range(3)]
881 c[3] = 0.0
882 for i in range(2, -1, -1):
883 c[i] = z[i] - u[i] * c[i + 1]
884 b[i] = (a[i + 1] - a[i]) / h[i] - h[i] * (c[i + 1] + 2.0 * c[i]) / 3.0
885 d[i] = (c[i + 1] - c[i]) / (3.0 * h[i])
886 for i in range(3):
887 result.append([a[i], b[i], c[i], d[i], x[i]])
888 spline = [result[1], result[4], result[7]]
890 return(spline)
893 # return a list with new vertex location vectors, a list with face vertex
894 # integers, and the highest vertex integer in the virtual mesh
895 def bridge_calculate_geometry(bm, lines, vertex_normals, segments,
896 interpolation, cubic_strength, min_width, max_vert_index):
897 new_verts = []
898 faces = []
900 # calculate location based on interpolation method
901 def get_location(line, segment, splines):
902 v1 = bm.verts[lines[line][0]].co
903 v2 = bm.verts[lines[line][1]].co
904 if interpolation == 'linear':
905 return v1 + (segment / segments) * (v2 - v1)
906 else: # interpolation == 'cubic'
907 m = (segment / segments)
908 ax, bx, cx, dx, tx = splines[line][0]
909 x = ax + bx * m + cx * m ** 2 + dx * m ** 3
910 ay, by, cy, dy, ty = splines[line][1]
911 y = ay + by * m + cy * m ** 2 + dy * m ** 3
912 az, bz, cz, dz, tz = splines[line][2]
913 z = az + bz * m + cz * m ** 2 + dz * m ** 3
914 return mathutils.Vector((x, y, z))
916 # no interpolation needed
917 if segments == 1:
918 for i, line in enumerate(lines):
919 if i < len(lines) - 1:
920 faces.append([line[0], lines[i + 1][0], lines[i + 1][1], line[1]])
921 # more than 1 segment, interpolate
922 else:
923 # calculate splines (if necessary) once, so no recalculations needed
924 if interpolation == 'cubic':
925 splines = []
926 for line in lines:
927 v1 = bm.verts[line[0]].co
928 v2 = bm.verts[line[1]].co
929 size = (v2 - v1).length * cubic_strength
930 splines.append(bridge_calculate_cubic_spline(bm,
931 [v1 + size * vertex_normals[line[0]], v1, v2,
932 v2 + size * vertex_normals[line[1]]]))
933 else:
934 splines = False
936 # create starting situation
937 virtual_width = [(bm.verts[lines[i][0]].co -
938 bm.verts[lines[i + 1][0]].co).length for i
939 in range(len(lines) - 1)]
940 new_verts = [get_location(0, seg, splines) for seg in range(1,
941 segments)]
942 first_line_indices = [i for i in range(max_vert_index + 1,
943 max_vert_index + segments)]
945 prev_verts = new_verts[:] # vertex locations of verts on previous line
946 prev_vert_indices = first_line_indices[:]
947 max_vert_index += segments - 1 # highest vertex index in virtual mesh
948 next_verts = [] # vertex locations of verts on current line
949 next_vert_indices = []
951 for i, line in enumerate(lines):
952 if i < len(lines) - 1:
953 v1 = line[0]
954 v2 = lines[i + 1][0]
955 end_face = True
956 for seg in range(1, segments):
957 loc1 = prev_verts[seg - 1]
958 loc2 = get_location(i + 1, seg, splines)
959 if (loc1 - loc2).length < (min_width / 100) * virtual_width[i] \
960 and line[1] == lines[i + 1][1]:
961 # triangle, no new vertex
962 faces.append([v1, v2, prev_vert_indices[seg - 1],
963 prev_vert_indices[seg - 1]])
964 next_verts += prev_verts[seg - 1:]
965 next_vert_indices += prev_vert_indices[seg - 1:]
966 end_face = False
967 break
968 else:
969 if i == len(lines) - 2 and lines[0] == lines[-1]:
970 # quad with first line, no new vertex
971 faces.append([v1, v2, first_line_indices[seg - 1],
972 prev_vert_indices[seg - 1]])
973 v2 = first_line_indices[seg - 1]
974 v1 = prev_vert_indices[seg - 1]
975 else:
976 # quad, add new vertex
977 max_vert_index += 1
978 faces.append([v1, v2, max_vert_index,
979 prev_vert_indices[seg - 1]])
980 v2 = max_vert_index
981 v1 = prev_vert_indices[seg - 1]
982 new_verts.append(loc2)
983 next_verts.append(loc2)
984 next_vert_indices.append(max_vert_index)
985 if end_face:
986 faces.append([v1, v2, lines[i + 1][1], line[1]])
988 prev_verts = next_verts[:]
989 prev_vert_indices = next_vert_indices[:]
990 next_verts = []
991 next_vert_indices = []
993 return(new_verts, faces, max_vert_index)
996 # calculate lines (list of lists, vertex indices) that are used for bridging
997 def bridge_calculate_lines(bm, loops, mode, twist, reverse):
998 lines = []
999 loop1, loop2 = [i[0] for i in loops]
1000 loop1_circular, loop2_circular = [i[1] for i in loops]
1001 circular = loop1_circular or loop2_circular
1002 circle_full = False
1004 # calculate loop centers
1005 centers = []
1006 for loop in [loop1, loop2]:
1007 center = mathutils.Vector()
1008 for vertex in loop:
1009 center += bm.verts[vertex].co
1010 center /= len(loop)
1011 centers.append(center)
1012 for i, loop in enumerate([loop1, loop2]):
1013 for vertex in loop:
1014 if bm.verts[vertex].co == centers[i]:
1015 # prevent zero-length vectors in angle comparisons
1016 centers[i] += mathutils.Vector((0.01, 0, 0))
1017 break
1018 center1, center2 = centers
1020 # calculate the normals of the virtual planes that the loops are on
1021 normals = []
1022 normal_plurity = False
1023 for i, loop in enumerate([loop1, loop2]):
1024 # covariance matrix
1025 mat = mathutils.Matrix(((0.0, 0.0, 0.0),
1026 (0.0, 0.0, 0.0),
1027 (0.0, 0.0, 0.0)))
1028 x, y, z = centers[i]
1029 for loc in [bm.verts[vertex].co for vertex in loop]:
1030 mat[0][0] += (loc[0] - x) ** 2
1031 mat[1][0] += (loc[0] - x) * (loc[1] - y)
1032 mat[2][0] += (loc[0] - x) * (loc[2] - z)
1033 mat[0][1] += (loc[1] - y) * (loc[0] - x)
1034 mat[1][1] += (loc[1] - y) ** 2
1035 mat[2][1] += (loc[1] - y) * (loc[2] - z)
1036 mat[0][2] += (loc[2] - z) * (loc[0] - x)
1037 mat[1][2] += (loc[2] - z) * (loc[1] - y)
1038 mat[2][2] += (loc[2] - z) ** 2
1039 # plane normal
1040 normal = False
1041 if sum(mat[0]) < 1e-6 or sum(mat[1]) < 1e-6 or sum(mat[2]) < 1e-6:
1042 normal_plurity = True
1043 try:
1044 mat.invert()
1045 except:
1046 if sum(mat[0]) == 0:
1047 normal = mathutils.Vector((1.0, 0.0, 0.0))
1048 elif sum(mat[1]) == 0:
1049 normal = mathutils.Vector((0.0, 1.0, 0.0))
1050 elif sum(mat[2]) == 0:
1051 normal = mathutils.Vector((0.0, 0.0, 1.0))
1052 if not normal:
1053 # warning! this is different from .normalize()
1054 itermax = 500
1055 iter = 0
1056 vec = mathutils.Vector((1.0, 1.0, 1.0))
1057 vec2 = (mat @ vec) / (mat @ vec).length
1058 while vec != vec2 and iter < itermax:
1059 iter += 1
1060 vec = vec2
1061 vec2 = mat @ vec
1062 if vec2.length != 0:
1063 vec2 /= vec2.length
1064 if vec2.length == 0:
1065 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
1066 normal = vec2
1067 normals.append(normal)
1068 # have plane normals face in the same direction (maximum angle: 90 degrees)
1069 if ((center1 + normals[0]) - center2).length < \
1070 ((center1 - normals[0]) - center2).length:
1071 normals[0].negate()
1072 if ((center2 + normals[1]) - center1).length > \
1073 ((center2 - normals[1]) - center1).length:
1074 normals[1].negate()
1076 # rotation matrix, representing the difference between the plane normals
1077 axis = normals[0].cross(normals[1])
1078 axis = mathutils.Vector([loc if abs(loc) > 1e-8 else 0 for loc in axis])
1079 if axis.angle(mathutils.Vector((0, 0, 1)), 0) > 1.5707964:
1080 axis.negate()
1081 angle = normals[0].dot(normals[1])
1082 rotation_matrix = mathutils.Matrix.Rotation(angle, 4, axis)
1084 # if circular, rotate loops so they are aligned
1085 if circular:
1086 # make sure loop1 is the circular one (or both are circular)
1087 if loop2_circular and not loop1_circular:
1088 loop1_circular, loop2_circular = True, False
1089 loop1, loop2 = loop2, loop1
1091 # match start vertex of loop1 with loop2
1092 target_vector = bm.verts[loop2[0]].co - center2
1093 dif_angles = [[(rotation_matrix @ (bm.verts[vertex].co - center1)
1094 ).angle(target_vector, 0), False, i] for
1095 i, vertex in enumerate(loop1)]
1096 dif_angles.sort()
1097 if len(loop1) != len(loop2):
1098 angle_limit = dif_angles[0][0] * 1.2 # 20% margin
1099 dif_angles = [
1100 [(bm.verts[loop2[0]].co -
1101 bm.verts[loop1[index]].co).length, angle, index] for
1102 angle, distance, index in dif_angles if angle <= angle_limit
1104 dif_angles.sort()
1105 loop1 = loop1[dif_angles[0][2]:] + loop1[:dif_angles[0][2]]
1107 # have both loops face the same way
1108 if normal_plurity and not circular:
1109 second_to_first, second_to_second, second_to_last = [
1110 (bm.verts[loop1[1]].co - center1).angle(
1111 bm.verts[loop2[i]].co - center2) for i in [0, 1, -1]
1113 last_to_first, last_to_second = [
1114 (bm.verts[loop1[-1]].co -
1115 center1).angle(bm.verts[loop2[i]].co - center2) for
1116 i in [0, 1]
1118 if (min(last_to_first, last_to_second) * 1.1 < min(second_to_first,
1119 second_to_second)) or (loop2_circular and second_to_last * 1.1 <
1120 min(second_to_first, second_to_second)):
1121 loop1.reverse()
1122 if circular:
1123 loop1 = [loop1[-1]] + loop1[:-1]
1124 else:
1125 angle = (bm.verts[loop1[0]].co - center1).\
1126 cross(bm.verts[loop1[1]].co - center1).angle(normals[0], 0)
1127 target_angle = (bm.verts[loop2[0]].co - center2).\
1128 cross(bm.verts[loop2[1]].co - center2).angle(normals[1], 0)
1129 limit = 1.5707964 # 0.5*pi, 90 degrees
1130 if not ((angle > limit and target_angle > limit) or
1131 (angle < limit and target_angle < limit)):
1132 loop1.reverse()
1133 if circular:
1134 loop1 = [loop1[-1]] + loop1[:-1]
1135 elif normals[0].angle(normals[1]) > limit:
1136 loop1.reverse()
1137 if circular:
1138 loop1 = [loop1[-1]] + loop1[:-1]
1140 # both loops have the same length
1141 if len(loop1) == len(loop2):
1142 # manual override
1143 if twist:
1144 if abs(twist) < len(loop1):
1145 loop1 = loop1[twist:] + loop1[:twist]
1146 if reverse:
1147 loop1.reverse()
1149 lines.append([loop1[0], loop2[0]])
1150 for i in range(1, len(loop1)):
1151 lines.append([loop1[i], loop2[i]])
1153 # loops of different lengths
1154 else:
1155 # make loop1 longest loop
1156 if len(loop2) > len(loop1):
1157 loop1, loop2 = loop2, loop1
1158 loop1_circular, loop2_circular = loop2_circular, loop1_circular
1160 # manual override
1161 if twist:
1162 if abs(twist) < len(loop1):
1163 loop1 = loop1[twist:] + loop1[:twist]
1164 if reverse:
1165 loop1.reverse()
1167 # shortest angle difference doesn't always give correct start vertex
1168 if loop1_circular and not loop2_circular:
1169 shifting = 1
1170 while shifting:
1171 if len(loop1) - shifting < len(loop2):
1172 shifting = False
1173 break
1174 to_last, to_first = [
1175 (rotation_matrix @ (bm.verts[loop1[-1]].co - center1)).angle(
1176 (bm.verts[loop2[i]].co - center2), 0) for i in [-1, 0]
1178 if to_first < to_last:
1179 loop1 = [loop1[-1]] + loop1[:-1]
1180 shifting += 1
1181 else:
1182 shifting = False
1183 break
1185 # basic shortest side first
1186 if mode == 'basic':
1187 lines.append([loop1[0], loop2[0]])
1188 for i in range(1, len(loop1)):
1189 if i >= len(loop2) - 1:
1190 # triangles
1191 lines.append([loop1[i], loop2[-1]])
1192 else:
1193 # quads
1194 lines.append([loop1[i], loop2[i]])
1196 # shortest edge algorithm
1197 else: # mode == 'shortest'
1198 lines.append([loop1[0], loop2[0]])
1199 prev_vert2 = 0
1200 for i in range(len(loop1) - 1):
1201 if prev_vert2 == len(loop2) - 1 and not loop2_circular:
1202 # force triangles, reached end of loop2
1203 tri, quad = 0, 1
1204 elif prev_vert2 == len(loop2) - 1 and loop2_circular:
1205 # at end of loop2, but circular, so check with first vert
1206 tri, quad = [(bm.verts[loop1[i + 1]].co -
1207 bm.verts[loop2[j]].co).length
1208 for j in [prev_vert2, 0]]
1209 circle_full = 2
1210 elif len(loop1) - 1 - i == len(loop2) - 1 - prev_vert2 and \
1211 not circle_full:
1212 # force quads, otherwise won't make it to end of loop2
1213 tri, quad = 1, 0
1214 else:
1215 # calculate if tri or quad gives shortest edge
1216 tri, quad = [(bm.verts[loop1[i + 1]].co -
1217 bm.verts[loop2[j]].co).length
1218 for j in range(prev_vert2, prev_vert2 + 2)]
1220 # triangle
1221 if tri < quad:
1222 lines.append([loop1[i + 1], loop2[prev_vert2]])
1223 if circle_full == 2:
1224 circle_full = False
1225 # quad
1226 elif not circle_full:
1227 lines.append([loop1[i + 1], loop2[prev_vert2 + 1]])
1228 prev_vert2 += 1
1229 # quad to first vertex of loop2
1230 else:
1231 lines.append([loop1[i + 1], loop2[0]])
1232 prev_vert2 = 0
1233 circle_full = True
1235 # final face for circular loops
1236 if loop1_circular and loop2_circular:
1237 lines.append([loop1[0], loop2[0]])
1239 return(lines)
1242 # calculate number of segments needed
1243 def bridge_calculate_segments(bm, lines, loops, segments):
1244 # return if amount of segments is set by user
1245 if segments != 0:
1246 return segments
1248 # edge lengths
1249 average_edge_length = [
1250 (bm.verts[vertex].co -
1251 bm.verts[loop[0][i + 1]].co).length for loop in loops for
1252 i, vertex in enumerate(loop[0][:-1])
1254 # closing edges of circular loops
1255 average_edge_length += [
1256 (bm.verts[loop[0][-1]].co -
1257 bm.verts[loop[0][0]].co).length for loop in loops if loop[1]
1260 # average lengths
1261 average_edge_length = sum(average_edge_length) / len(average_edge_length)
1262 average_bridge_length = sum(
1263 [(bm.verts[v1].co -
1264 bm.verts[v2].co).length for v1, v2 in lines]
1265 ) / len(lines)
1267 segments = max(1, round(average_bridge_length / average_edge_length))
1269 return(segments)
1272 # return dictionary with vertex index as key, and the normal vector as value
1273 def bridge_calculate_virtual_vertex_normals(bm, lines, loops, edge_faces,
1274 edgekey_to_edge):
1275 if not edge_faces: # interpolation isn't set to cubic
1276 return False
1278 # pity reduce() isn't one of the basic functions in python anymore
1279 def average_vector_dictionary(dic):
1280 for key, vectors in dic.items():
1281 # if type(vectors) == type([]) and len(vectors) > 1:
1282 if len(vectors) > 1:
1283 average = mathutils.Vector()
1284 for vector in vectors:
1285 average += vector
1286 average /= len(vectors)
1287 dic[key] = [average]
1288 return dic
1290 # get all edges of the loop
1291 edges = [
1292 [edgekey_to_edge[tuple(sorted([loops[j][0][i],
1293 loops[j][0][i + 1]]))] for i in range(len(loops[j][0]) - 1)] for
1294 j in [0, 1]
1296 edges = edges[0] + edges[1]
1297 for j in [0, 1]:
1298 if loops[j][1]: # circular
1299 edges.append(edgekey_to_edge[tuple(sorted([loops[j][0][0],
1300 loops[j][0][-1]]))])
1303 calculation based on face topology (assign edge-normals to vertices)
1305 edge_normal = face_normal x edge_vector
1306 vertex_normal = average(edge_normals)
1308 vertex_normals = dict([(vertex, []) for vertex in loops[0][0] + loops[1][0]])
1309 for edge in edges:
1310 faces = edge_faces[edgekey(edge)] # valid faces connected to edge
1312 if faces:
1313 # get edge coordinates
1314 v1, v2 = [bm.verts[edgekey(edge)[i]].co for i in [0, 1]]
1315 edge_vector = v1 - v2
1316 if edge_vector.length < 1e-4:
1317 # zero-length edge, vertices at same location
1318 continue
1319 edge_center = (v1 + v2) / 2
1321 # average face coordinates, if connected to more than 1 valid face
1322 if len(faces) > 1:
1323 face_normal = mathutils.Vector()
1324 face_center = mathutils.Vector()
1325 for face in faces:
1326 face_normal += face.normal
1327 face_center += face.calc_center_median()
1328 face_normal /= len(faces)
1329 face_center /= len(faces)
1330 else:
1331 face_normal = faces[0].normal
1332 face_center = faces[0].calc_center_median()
1333 if face_normal.length < 1e-4:
1334 # faces with a surface of 0 have no face normal
1335 continue
1337 # calculate virtual edge normal
1338 edge_normal = edge_vector.cross(face_normal)
1339 edge_normal.length = 0.01
1340 if (face_center - (edge_center + edge_normal)).length > \
1341 (face_center - (edge_center - edge_normal)).length:
1342 # make normal face the correct way
1343 edge_normal.negate()
1344 edge_normal.normalize()
1345 # add virtual edge normal as entry for both vertices it connects
1346 for vertex in edgekey(edge):
1347 vertex_normals[vertex].append(edge_normal)
1350 calculation based on connection with other loop (vertex focused method)
1351 - used for vertices that aren't connected to any valid faces
1353 plane_normal = edge_vector x connection_vector
1354 vertex_normal = plane_normal x edge_vector
1356 vertices = [
1357 vertex for vertex, normal in vertex_normals.items() if not normal
1360 if vertices:
1361 # edge vectors connected to vertices
1362 edge_vectors = dict([[vertex, []] for vertex in vertices])
1363 for edge in edges:
1364 for v in edgekey(edge):
1365 if v in edge_vectors:
1366 edge_vector = bm.verts[edgekey(edge)[0]].co - \
1367 bm.verts[edgekey(edge)[1]].co
1368 if edge_vector.length < 1e-4:
1369 # zero-length edge, vertices at same location
1370 continue
1371 edge_vectors[v].append(edge_vector)
1373 # connection vectors between vertices of both loops
1374 connection_vectors = dict([[vertex, []] for vertex in vertices])
1375 connections = dict([[vertex, []] for vertex in vertices])
1376 for v1, v2 in lines:
1377 if v1 in connection_vectors or v2 in connection_vectors:
1378 new_vector = bm.verts[v1].co - bm.verts[v2].co
1379 if new_vector.length < 1e-4:
1380 # zero-length connection vector,
1381 # vertices in different loops at same location
1382 continue
1383 if v1 in connection_vectors:
1384 connection_vectors[v1].append(new_vector)
1385 connections[v1].append(v2)
1386 if v2 in connection_vectors:
1387 connection_vectors[v2].append(new_vector)
1388 connections[v2].append(v1)
1389 connection_vectors = average_vector_dictionary(connection_vectors)
1390 connection_vectors = dict(
1391 [[vertex, vector[0]] if vector else
1392 [vertex, []] for vertex, vector in connection_vectors.items()]
1395 for vertex, values in edge_vectors.items():
1396 # vertex normal doesn't matter, just assign a random vector to it
1397 if not connection_vectors[vertex]:
1398 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1399 continue
1401 # calculate to what location the vertex is connected,
1402 # used to determine what way to flip the normal
1403 connected_center = mathutils.Vector()
1404 for v in connections[vertex]:
1405 connected_center += bm.verts[v].co
1406 if len(connections[vertex]) > 1:
1407 connected_center /= len(connections[vertex])
1408 if len(connections[vertex]) == 0:
1409 # shouldn't be possible, but better safe than sorry
1410 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1411 continue
1413 # can't do proper calculations, because of zero-length vector
1414 if not values:
1415 if (connected_center - (bm.verts[vertex].co +
1416 connection_vectors[vertex])).length < (connected_center -
1417 (bm.verts[vertex].co - connection_vectors[vertex])).length:
1418 connection_vectors[vertex].negate()
1419 vertex_normals[vertex] = [connection_vectors[vertex].normalized()]
1420 continue
1422 # calculate vertex normals using edge-vectors,
1423 # connection-vectors and the derived plane normal
1424 for edge_vector in values:
1425 plane_normal = edge_vector.cross(connection_vectors[vertex])
1426 vertex_normal = edge_vector.cross(plane_normal)
1427 vertex_normal.length = 0.1
1428 if (connected_center - (bm.verts[vertex].co +
1429 vertex_normal)).length < (connected_center -
1430 (bm.verts[vertex].co - vertex_normal)).length:
1431 # make normal face the correct way
1432 vertex_normal.negate()
1433 vertex_normal.normalize()
1434 vertex_normals[vertex].append(vertex_normal)
1436 # average virtual vertex normals, based on all edges it's connected to
1437 vertex_normals = average_vector_dictionary(vertex_normals)
1438 vertex_normals = dict([[vertex, vector[0]] for vertex, vector in vertex_normals.items()])
1440 return(vertex_normals)
1443 # add vertices to mesh
1444 def bridge_create_vertices(bm, vertices):
1445 for i in range(len(vertices)):
1446 bm.verts.new(vertices[i])
1447 bm.verts.ensure_lookup_table()
1450 # add faces to mesh
1451 def bridge_create_faces(object, bm, faces, twist):
1452 # have the normal point the correct way
1453 if twist < 0:
1454 [face.reverse() for face in faces]
1455 faces = [face[2:] + face[:2] if face[0] == face[1] else face for face in faces]
1457 # eekadoodle prevention
1458 for i in range(len(faces)):
1459 if not faces[i][-1]:
1460 if faces[i][0] == faces[i][-1]:
1461 faces[i] = [faces[i][1], faces[i][2], faces[i][3], faces[i][1]]
1462 else:
1463 faces[i] = [faces[i][-1]] + faces[i][:-1]
1464 # result of converting from pre-bmesh period
1465 if faces[i][-1] == faces[i][-2]:
1466 faces[i] = faces[i][:-1]
1468 new_faces = []
1469 for i in range(len(faces)):
1470 new_faces.append(bm.faces.new([bm.verts[v] for v in faces[i]]))
1471 bm.normal_update()
1472 object.data.update(calc_edges=True) # calc_edges prevents memory-corruption
1474 bm.verts.ensure_lookup_table()
1475 bm.edges.ensure_lookup_table()
1476 bm.faces.ensure_lookup_table()
1478 return(new_faces)
1481 # calculate input loops
1482 def bridge_get_input(bm):
1483 # create list of internal edges, which should be skipped
1484 eks_of_selected_faces = [
1485 item for sublist in [face_edgekeys(face) for
1486 face in bm.faces if face.select and not face.hide] for item in sublist
1488 edge_count = {}
1489 for ek in eks_of_selected_faces:
1490 if ek in edge_count:
1491 edge_count[ek] += 1
1492 else:
1493 edge_count[ek] = 1
1494 internal_edges = [ek for ek in edge_count if edge_count[ek] > 1]
1496 # sort correct edges into loops
1497 selected_edges = [
1498 edgekey(edge) for edge in bm.edges if edge.select and
1499 not edge.hide and edgekey(edge) not in internal_edges
1501 loops = get_connected_selections(selected_edges)
1503 return(loops)
1506 # return values needed by the bridge operator
1507 def bridge_initialise(bm, interpolation):
1508 if interpolation == 'cubic':
1509 # dict with edge-key as key and list of connected valid faces as value
1510 face_blacklist = [
1511 face.index for face in bm.faces if face.select or
1512 face.hide
1514 edge_faces = dict(
1515 [[edgekey(edge), []] for edge in bm.edges if not edge.hide]
1517 for face in bm.faces:
1518 if face.index in face_blacklist:
1519 continue
1520 for key in face_edgekeys(face):
1521 edge_faces[key].append(face)
1522 # dictionary with the edge-key as key and edge as value
1523 edgekey_to_edge = dict(
1524 [[edgekey(edge), edge] for edge in bm.edges if edge.select and not edge.hide]
1526 else:
1527 edge_faces = False
1528 edgekey_to_edge = False
1530 # selected faces input
1531 old_selected_faces = [
1532 face.index for face in bm.faces if face.select and not face.hide
1535 # find out if faces created by bridging should be smoothed
1536 smooth = False
1537 if bm.faces:
1538 if sum([face.smooth for face in bm.faces]) / len(bm.faces) >= 0.5:
1539 smooth = True
1541 return(edge_faces, edgekey_to_edge, old_selected_faces, smooth)
1544 # return a string with the input method
1545 def bridge_input_method(loft, loft_loop):
1546 method = ""
1547 if loft:
1548 if loft_loop:
1549 method = "Loft loop"
1550 else:
1551 method = "Loft no-loop"
1552 else:
1553 method = "Bridge"
1555 return(method)
1558 # match up loops in pairs, used for multi-input bridging
1559 def bridge_match_loops(bm, loops):
1560 # calculate average loop normals and centers
1561 normals = []
1562 centers = []
1563 for vertices, circular in loops:
1564 normal = mathutils.Vector()
1565 center = mathutils.Vector()
1566 for vertex in vertices:
1567 normal += bm.verts[vertex].normal
1568 center += bm.verts[vertex].co
1569 normals.append(normal / len(vertices) / 10)
1570 centers.append(center / len(vertices))
1572 # possible matches if loop normals are faced towards the center
1573 # of the other loop
1574 matches = dict([[i, []] for i in range(len(loops))])
1575 matches_amount = 0
1576 for i in range(len(loops) + 1):
1577 for j in range(i + 1, len(loops)):
1578 if (centers[i] - centers[j]).length > \
1579 (centers[i] - (centers[j] + normals[j])).length and \
1580 (centers[j] - centers[i]).length > \
1581 (centers[j] - (centers[i] + normals[i])).length:
1582 matches_amount += 1
1583 matches[i].append([(centers[i] - centers[j]).length, i, j])
1584 matches[j].append([(centers[i] - centers[j]).length, j, i])
1585 # if no loops face each other, just make matches between all the loops
1586 if matches_amount == 0:
1587 for i in range(len(loops) + 1):
1588 for j in range(i + 1, len(loops)):
1589 matches[i].append([(centers[i] - centers[j]).length, i, j])
1590 matches[j].append([(centers[i] - centers[j]).length, j, i])
1591 for key, value in matches.items():
1592 value.sort()
1594 # matches based on distance between centers and number of vertices in loops
1595 new_order = []
1596 for loop_index in range(len(loops)):
1597 if loop_index in new_order:
1598 continue
1599 loop_matches = matches[loop_index]
1600 if not loop_matches:
1601 continue
1602 shortest_distance = loop_matches[0][0]
1603 shortest_distance *= 1.1
1604 loop_matches = [
1605 [abs(len(loops[loop_index][0]) -
1606 len(loops[loop[2]][0])), loop[0], loop[1], loop[2]] for loop in
1607 loop_matches if loop[0] < shortest_distance
1609 loop_matches.sort()
1610 for match in loop_matches:
1611 if match[3] not in new_order:
1612 new_order += [loop_index, match[3]]
1613 break
1615 # reorder loops based on matches
1616 if len(new_order) >= 2:
1617 loops = [loops[i] for i in new_order]
1619 return(loops)
1622 # remove old_selected_faces
1623 def bridge_remove_internal_faces(bm, old_selected_faces):
1624 # collect bmesh faces and internal bmesh edges
1625 remove_faces = [bm.faces[face] for face in old_selected_faces]
1626 edges = collections.Counter(
1627 [edge.index for face in remove_faces for edge in face.edges]
1629 remove_edges = [bm.edges[edge] for edge in edges if edges[edge] > 1]
1631 # remove internal faces and edges
1632 for face in remove_faces:
1633 bm.faces.remove(face)
1634 for edge in remove_edges:
1635 bm.edges.remove(edge)
1637 bm.faces.ensure_lookup_table()
1638 bm.edges.ensure_lookup_table()
1639 bm.verts.ensure_lookup_table()
1642 # update list of internal faces that are flagged for removal
1643 def bridge_save_unused_faces(bm, old_selected_faces, loops):
1644 # key: vertex index, value: lists of selected faces using it
1646 vertex_to_face = dict([[i, []] for i in range(len(bm.verts))])
1647 [[vertex_to_face[vertex.index].append(face) for vertex in
1648 bm.faces[face].verts] for face in old_selected_faces]
1650 # group selected faces that are connected
1651 groups = []
1652 grouped_faces = []
1653 for face in old_selected_faces:
1654 if face in grouped_faces:
1655 continue
1656 grouped_faces.append(face)
1657 group = [face]
1658 new_faces = [face]
1659 while new_faces:
1660 grow_face = new_faces[0]
1661 for vertex in bm.faces[grow_face].verts:
1662 vertex_face_group = [
1663 face for face in vertex_to_face[vertex.index] if
1664 face not in grouped_faces
1666 new_faces += vertex_face_group
1667 grouped_faces += vertex_face_group
1668 group += vertex_face_group
1669 new_faces.pop(0)
1670 groups.append(group)
1672 # key: vertex index, value: True/False (is it in a loop that is used)
1673 used_vertices = dict([[i, 0] for i in range(len(bm.verts))])
1674 for loop in loops:
1675 for vertex in loop[0]:
1676 used_vertices[vertex] = True
1678 # check if group is bridged, if not remove faces from internal faces list
1679 for group in groups:
1680 used = False
1681 for face in group:
1682 if used:
1683 break
1684 for vertex in bm.faces[face].verts:
1685 if used_vertices[vertex.index]:
1686 used = True
1687 break
1688 if not used:
1689 for face in group:
1690 old_selected_faces.remove(face)
1693 # add the newly created faces to the selection
1694 def bridge_select_new_faces(new_faces, smooth):
1695 for face in new_faces:
1696 face.select_set(True)
1697 face.smooth = smooth
1700 # sort loops, so they are connected in the correct order when lofting
1701 def bridge_sort_loops(bm, loops, loft_loop):
1702 # simplify loops to single points, and prepare for pathfinding
1703 x, y, z = [
1704 [sum([bm.verts[i].co[j] for i in loop[0]]) /
1705 len(loop[0]) for loop in loops] for j in range(3)
1707 nodes = [mathutils.Vector((x[i], y[i], z[i])) for i in range(len(loops))]
1709 active_node = 0
1710 open = [i for i in range(1, len(loops))]
1711 path = [[0, 0]]
1712 # connect node to path, that is shortest to active_node
1713 while len(open) > 0:
1714 distances = [(nodes[active_node] - nodes[i]).length for i in open]
1715 active_node = open[distances.index(min(distances))]
1716 open.remove(active_node)
1717 path.append([active_node, min(distances)])
1718 # check if we didn't start in the middle of the path
1719 for i in range(2, len(path)):
1720 if (nodes[path[i][0]] - nodes[0]).length < path[i][1]:
1721 temp = path[:i]
1722 path.reverse()
1723 path = path[:-i] + temp
1724 break
1726 # reorder loops
1727 loops = [loops[i[0]] for i in path]
1728 # if requested, duplicate first loop at last position, so loft can loop
1729 if loft_loop:
1730 loops = loops + [loops[0]]
1732 return(loops)
1735 # remapping old indices to new position in list
1736 def bridge_update_old_selection(bm, old_selected_faces):
1738 old_indices = old_selected_faces[:]
1739 old_selected_faces = []
1740 for i, face in enumerate(bm.faces):
1741 if face.index in old_indices:
1742 old_selected_faces.append(i)
1744 old_selected_faces = [
1745 i for i, face in enumerate(bm.faces) if face.index in old_selected_faces
1748 return(old_selected_faces)
1751 # ########################################
1752 # ##### Circle functions #################
1753 # ########################################
1755 # convert 3d coordinates to 2d coordinates on plane
1756 def circle_3d_to_2d(bm_mod, loop, com, normal):
1757 # project vertices onto the plane
1758 verts = [bm_mod.verts[v] for v in loop[0]]
1759 verts_projected = [[v.co - (v.co - com).dot(normal) * normal, v.index]
1760 for v in verts]
1762 # calculate two vectors (p and q) along the plane
1763 m = mathutils.Vector((normal[0] + 1.0, normal[1], normal[2]))
1764 p = m - (m.dot(normal) * normal)
1765 if p.dot(p) < 1e-6:
1766 m = mathutils.Vector((normal[0], normal[1] + 1.0, normal[2]))
1767 p = m - (m.dot(normal) * normal)
1768 q = p.cross(normal)
1770 # change to 2d coordinates using perpendicular projection
1771 locs_2d = []
1772 for loc, vert in verts_projected:
1773 vloc = loc - com
1774 x = p.dot(vloc) / p.dot(p)
1775 y = q.dot(vloc) / q.dot(q)
1776 locs_2d.append([x, y, vert])
1778 return(locs_2d, p, q)
1781 # calculate a best-fit circle to the 2d locations on the plane
1782 def circle_calculate_best_fit(locs_2d):
1783 # initial guess
1784 x0 = 0.0
1785 y0 = 0.0
1786 r = 1.0
1788 # calculate center and radius (non-linear least squares solution)
1789 for iter in range(500):
1790 jmat = []
1791 k = []
1792 for v in locs_2d:
1793 d = (v[0] ** 2 - 2.0 * x0 * v[0] + v[1] ** 2 - 2.0 * y0 * v[1] + x0 ** 2 + y0 ** 2) ** 0.5
1794 jmat.append([(x0 - v[0]) / d, (y0 - v[1]) / d, -1.0])
1795 k.append(-(((v[0] - x0) ** 2 + (v[1] - y0) ** 2) ** 0.5 - r))
1796 jmat2 = mathutils.Matrix(((0.0, 0.0, 0.0),
1797 (0.0, 0.0, 0.0),
1798 (0.0, 0.0, 0.0),
1800 k2 = mathutils.Vector((0.0, 0.0, 0.0))
1801 for i in range(len(jmat)):
1802 k2 += mathutils.Vector(jmat[i]) * k[i]
1803 jmat2[0][0] += jmat[i][0] ** 2
1804 jmat2[1][0] += jmat[i][0] * jmat[i][1]
1805 jmat2[2][0] += jmat[i][0] * jmat[i][2]
1806 jmat2[1][1] += jmat[i][1] ** 2
1807 jmat2[2][1] += jmat[i][1] * jmat[i][2]
1808 jmat2[2][2] += jmat[i][2] ** 2
1809 jmat2[0][1] = jmat2[1][0]
1810 jmat2[0][2] = jmat2[2][0]
1811 jmat2[1][2] = jmat2[2][1]
1812 try:
1813 jmat2.invert()
1814 except:
1815 pass
1816 dx0, dy0, dr = jmat2 @ k2
1817 x0 += dx0
1818 y0 += dy0
1819 r += dr
1820 # stop iterating if we're close enough to optimal solution
1821 if abs(dx0) < 1e-6 and abs(dy0) < 1e-6 and abs(dr) < 1e-6:
1822 break
1824 # return center of circle and radius
1825 return(x0, y0, r)
1828 # calculate circle so no vertices have to be moved away from the center
1829 def circle_calculate_min_fit(locs_2d):
1830 # center of circle
1831 x0 = (min([i[0] for i in locs_2d]) + max([i[0] for i in locs_2d])) / 2.0
1832 y0 = (min([i[1] for i in locs_2d]) + max([i[1] for i in locs_2d])) / 2.0
1833 center = mathutils.Vector([x0, y0])
1834 # radius of circle
1835 r = min([(mathutils.Vector([i[0], i[1]]) - center).length for i in locs_2d])
1837 # return center of circle and radius
1838 return(x0, y0, r)
1841 # calculate the new locations of the vertices that need to be moved
1842 def circle_calculate_verts(flatten, bm_mod, locs_2d, com, p, q, normal):
1843 # changing 2d coordinates back to 3d coordinates
1844 locs_3d = []
1845 for loc in locs_2d:
1846 locs_3d.append([loc[2], loc[0] * p + loc[1] * q + com])
1848 if flatten: # flat circle
1849 return(locs_3d)
1851 else: # project the locations on the existing mesh
1852 vert_edges = dict_vert_edges(bm_mod)
1853 vert_faces = dict_vert_faces(bm_mod)
1854 faces = [f for f in bm_mod.faces if not f.hide]
1855 rays = [normal, -normal]
1856 new_locs = []
1857 for loc in locs_3d:
1858 projection = False
1859 if bm_mod.verts[loc[0]].co == loc[1]: # vertex hasn't moved
1860 projection = loc[1]
1861 else:
1862 dif = normal.angle(loc[1] - bm_mod.verts[loc[0]].co)
1863 if -1e-6 < dif < 1e-6 or math.pi - 1e-6 < dif < math.pi + 1e-6:
1864 # original location is already along projection normal
1865 projection = bm_mod.verts[loc[0]].co
1866 else:
1867 # quick search through adjacent faces
1868 for face in vert_faces[loc[0]]:
1869 verts = [v.co for v in bm_mod.faces[face].verts]
1870 if len(verts) == 3: # triangle
1871 v1, v2, v3 = verts
1872 v4 = False
1873 else: # assume quad
1874 v1, v2, v3, v4 = verts[:4]
1875 for ray in rays:
1876 intersect = mathutils.geometry.\
1877 intersect_ray_tri(v1, v2, v3, ray, loc[1])
1878 if intersect:
1879 projection = intersect
1880 break
1881 elif v4:
1882 intersect = mathutils.geometry.\
1883 intersect_ray_tri(v1, v3, v4, ray, loc[1])
1884 if intersect:
1885 projection = intersect
1886 break
1887 if projection:
1888 break
1889 if not projection:
1890 # check if projection is on adjacent edges
1891 for edgekey in vert_edges[loc[0]]:
1892 line1 = bm_mod.verts[edgekey[0]].co
1893 line2 = bm_mod.verts[edgekey[1]].co
1894 intersect, dist = mathutils.geometry.intersect_point_line(
1895 loc[1], line1, line2
1897 if 1e-6 < dist < 1 - 1e-6:
1898 projection = intersect
1899 break
1900 if not projection:
1901 # full search through the entire mesh
1902 hits = []
1903 for face in faces:
1904 verts = [v.co for v in face.verts]
1905 if len(verts) == 3: # triangle
1906 v1, v2, v3 = verts
1907 v4 = False
1908 else: # assume quad
1909 v1, v2, v3, v4 = verts[:4]
1910 for ray in rays:
1911 intersect = mathutils.geometry.intersect_ray_tri(
1912 v1, v2, v3, ray, loc[1]
1914 if intersect:
1915 hits.append([(loc[1] - intersect).length,
1916 intersect])
1917 break
1918 elif v4:
1919 intersect = mathutils.geometry.intersect_ray_tri(
1920 v1, v3, v4, ray, loc[1]
1922 if intersect:
1923 hits.append([(loc[1] - intersect).length,
1924 intersect])
1925 break
1926 if len(hits) >= 1:
1927 # if more than 1 hit with mesh, closest hit is new loc
1928 hits.sort()
1929 projection = hits[0][1]
1930 if not projection:
1931 # nothing to project on, remain at flat location
1932 projection = loc[1]
1933 new_locs.append([loc[0], projection])
1935 # return new positions of projected circle
1936 return(new_locs)
1939 # check loops and only return valid ones
1940 def circle_check_loops(single_loops, loops, mapping, bm_mod):
1941 valid_single_loops = {}
1942 valid_loops = []
1943 for i, [loop, circular] in enumerate(loops):
1944 # loop needs to have at least 3 vertices
1945 if len(loop) < 3:
1946 continue
1947 # loop needs at least 1 vertex in the original, non-mirrored mesh
1948 if mapping:
1949 all_virtual = True
1950 for vert in loop:
1951 if mapping[vert] > -1:
1952 all_virtual = False
1953 break
1954 if all_virtual:
1955 continue
1956 # loop has to be non-collinear
1957 collinear = True
1958 loc0 = mathutils.Vector(bm_mod.verts[loop[0]].co[:])
1959 loc1 = mathutils.Vector(bm_mod.verts[loop[1]].co[:])
1960 for v in loop[2:]:
1961 locn = mathutils.Vector(bm_mod.verts[v].co[:])
1962 if loc0 == loc1 or loc1 == locn:
1963 loc0 = loc1
1964 loc1 = locn
1965 continue
1966 d1 = loc1 - loc0
1967 d2 = locn - loc1
1968 if -1e-6 < d1.angle(d2, 0) < 1e-6:
1969 loc0 = loc1
1970 loc1 = locn
1971 continue
1972 collinear = False
1973 break
1974 if collinear:
1975 continue
1976 # passed all tests, loop is valid
1977 valid_loops.append([loop, circular])
1978 valid_single_loops[len(valid_loops) - 1] = single_loops[i]
1980 return(valid_single_loops, valid_loops)
1983 # calculate the location of single input vertices that need to be flattened
1984 def circle_flatten_singles(bm_mod, com, p, q, normal, single_loop):
1985 new_locs = []
1986 for vert in single_loop:
1987 loc = mathutils.Vector(bm_mod.verts[vert].co[:])
1988 new_locs.append([vert, loc - (loc - com).dot(normal) * normal])
1990 return(new_locs)
1993 # calculate input loops
1994 def circle_get_input(object, bm):
1995 # get mesh with modifiers applied
1996 derived, bm_mod = get_derived_bmesh(object, bm)
1998 # create list of edge-keys based on selection state
1999 faces = False
2000 for face in bm.faces:
2001 if face.select and not face.hide:
2002 faces = True
2003 break
2004 if faces:
2005 # get selected, non-hidden , non-internal edge-keys
2006 eks_selected = [
2007 key for keys in [face_edgekeys(face) for face in
2008 bm_mod.faces if face.select and not face.hide] for key in keys
2010 edge_count = {}
2011 for ek in eks_selected:
2012 if ek in edge_count:
2013 edge_count[ek] += 1
2014 else:
2015 edge_count[ek] = 1
2016 edge_keys = [
2017 edgekey(edge) for edge in bm_mod.edges if edge.select and
2018 not edge.hide and edge_count.get(edgekey(edge), 1) == 1
2020 else:
2021 # no faces, so no internal edges either
2022 edge_keys = [
2023 edgekey(edge) for edge in bm_mod.edges if edge.select and not edge.hide
2026 # add edge-keys around single vertices
2027 verts_connected = dict(
2028 [[vert, 1] for edge in [edge for edge in
2029 bm_mod.edges if edge.select and not edge.hide] for vert in
2030 edgekey(edge)]
2032 single_vertices = [
2033 vert.index for vert in bm_mod.verts if
2034 vert.select and not vert.hide and
2035 not verts_connected.get(vert.index, False)
2038 if single_vertices and len(bm.faces) > 0:
2039 vert_to_single = dict(
2040 [[v.index, []] for v in bm_mod.verts if not v.hide]
2042 for face in [face for face in bm_mod.faces if not face.select and not face.hide]:
2043 for vert in face.verts:
2044 vert = vert.index
2045 if vert in single_vertices:
2046 for ek in face_edgekeys(face):
2047 if vert not in ek:
2048 edge_keys.append(ek)
2049 if vert not in vert_to_single[ek[0]]:
2050 vert_to_single[ek[0]].append(vert)
2051 if vert not in vert_to_single[ek[1]]:
2052 vert_to_single[ek[1]].append(vert)
2053 break
2055 # sort edge-keys into loops
2056 loops = get_connected_selections(edge_keys)
2058 # find out to which loops the single vertices belong
2059 single_loops = dict([[i, []] for i in range(len(loops))])
2060 if single_vertices and len(bm.faces) > 0:
2061 for i, [loop, circular] in enumerate(loops):
2062 for vert in loop:
2063 if vert_to_single[vert]:
2064 for single in vert_to_single[vert]:
2065 if single not in single_loops[i]:
2066 single_loops[i].append(single)
2068 return(derived, bm_mod, single_vertices, single_loops, loops)
2071 # recalculate positions based on the influence of the circle shape
2072 def circle_influence_locs(locs_2d, new_locs_2d, influence):
2073 for i in range(len(locs_2d)):
2074 oldx, oldy, j = locs_2d[i]
2075 newx, newy, k = new_locs_2d[i]
2076 altx = newx * (influence / 100) + oldx * ((100 - influence) / 100)
2077 alty = newy * (influence / 100) + oldy * ((100 - influence) / 100)
2078 locs_2d[i] = [altx, alty, j]
2080 return(locs_2d)
2083 # project 2d locations on circle, respecting distance relations between verts
2084 def circle_project_non_regular(locs_2d, x0, y0, r):
2085 for i in range(len(locs_2d)):
2086 x, y, j = locs_2d[i]
2087 loc = mathutils.Vector([x - x0, y - y0])
2088 loc.length = r
2089 locs_2d[i] = [loc[0], loc[1], j]
2091 return(locs_2d)
2094 # project 2d locations on circle, with equal distance between all vertices
2095 def circle_project_regular(locs_2d, x0, y0, r):
2096 # find offset angle and circling direction
2097 x, y, i = locs_2d[0]
2098 loc = mathutils.Vector([x - x0, y - y0])
2099 loc.length = r
2100 offset_angle = loc.angle(mathutils.Vector([1.0, 0.0]), 0.0)
2101 loca = mathutils.Vector([x - x0, y - y0, 0.0])
2102 if loc[1] < -1e-6:
2103 offset_angle *= -1
2104 x, y, j = locs_2d[1]
2105 locb = mathutils.Vector([x - x0, y - y0, 0.0])
2106 if loca.cross(locb)[2] >= 0:
2107 ccw = 1
2108 else:
2109 ccw = -1
2110 # distribute vertices along the circle
2111 for i in range(len(locs_2d)):
2112 t = offset_angle + ccw * (i / len(locs_2d) * 2 * math.pi)
2113 x = math.cos(t) * r
2114 y = math.sin(t) * r
2115 locs_2d[i] = [x, y, locs_2d[i][2]]
2117 return(locs_2d)
2120 # shift loop, so the first vertex is closest to the center
2121 def circle_shift_loop(bm_mod, loop, com):
2122 verts, circular = loop
2123 distances = [
2124 [(bm_mod.verts[vert].co - com).length, i] for i, vert in enumerate(verts)
2126 distances.sort()
2127 shift = distances[0][1]
2128 loop = [verts[shift:] + verts[:shift], circular]
2130 return(loop)
2133 # ########################################
2134 # ##### Curve functions ##################
2135 # ########################################
2137 # create lists with knots and points, all correctly sorted
2138 def curve_calculate_knots(loop, verts_selected):
2139 knots = [v for v in loop[0] if v in verts_selected]
2140 points = loop[0][:]
2141 # circular loop, potential for weird splines
2142 if loop[1]:
2143 offset = int(len(loop[0]) / 4)
2144 kpos = []
2145 for k in knots:
2146 kpos.append(loop[0].index(k))
2147 kdif = []
2148 for i in range(len(kpos) - 1):
2149 kdif.append(kpos[i + 1] - kpos[i])
2150 kdif.append(len(loop[0]) - kpos[-1] + kpos[0])
2151 kadd = []
2152 for k in kdif:
2153 if k > 2 * offset:
2154 kadd.append([kdif.index(k), True])
2155 # next 2 lines are optional, they insert
2156 # an extra control point in small gaps
2157 # elif k > offset:
2158 # kadd.append([kdif.index(k), False])
2159 kins = []
2160 krot = False
2161 for k in kadd: # extra knots to be added
2162 if k[1]: # big gap (break circular spline)
2163 kpos = loop[0].index(knots[k[0]]) + offset
2164 if kpos > len(loop[0]) - 1:
2165 kpos -= len(loop[0])
2166 kins.append([knots[k[0]], loop[0][kpos]])
2167 kpos2 = k[0] + 1
2168 if kpos2 > len(knots) - 1:
2169 kpos2 -= len(knots)
2170 kpos2 = loop[0].index(knots[kpos2]) - offset
2171 if kpos2 < 0:
2172 kpos2 += len(loop[0])
2173 kins.append([loop[0][kpos], loop[0][kpos2]])
2174 krot = loop[0][kpos2]
2175 else: # small gap (keep circular spline)
2176 k1 = loop[0].index(knots[k[0]])
2177 k2 = k[0] + 1
2178 if k2 > len(knots) - 1:
2179 k2 -= len(knots)
2180 k2 = loop[0].index(knots[k2])
2181 if k2 < k1:
2182 dif = len(loop[0]) - 1 - k1 + k2
2183 else:
2184 dif = k2 - k1
2185 kn = k1 + int(dif / 2)
2186 if kn > len(loop[0]) - 1:
2187 kn -= len(loop[0])
2188 kins.append([loop[0][k1], loop[0][kn]])
2189 for j in kins: # insert new knots
2190 knots.insert(knots.index(j[0]) + 1, j[1])
2191 if not krot: # circular loop
2192 knots.append(knots[0])
2193 points = loop[0][loop[0].index(knots[0]):]
2194 points += loop[0][0:loop[0].index(knots[0]) + 1]
2195 else: # non-circular loop (broken by script)
2196 krot = knots.index(krot)
2197 knots = knots[krot:] + knots[0:krot]
2198 if loop[0].index(knots[0]) > loop[0].index(knots[-1]):
2199 points = loop[0][loop[0].index(knots[0]):]
2200 points += loop[0][0:loop[0].index(knots[-1]) + 1]
2201 else:
2202 points = loop[0][loop[0].index(knots[0]):loop[0].index(knots[-1]) + 1]
2203 # non-circular loop, add first and last point as knots
2204 else:
2205 if loop[0][0] not in knots:
2206 knots.insert(0, loop[0][0])
2207 if loop[0][-1] not in knots:
2208 knots.append(loop[0][-1])
2210 return(knots, points)
2213 # calculate relative positions compared to first knot
2214 def curve_calculate_t(bm_mod, knots, points, pknots, regular, circular):
2215 tpoints = []
2216 loc_prev = False
2217 len_total = 0
2219 for p in points:
2220 if p in knots:
2221 loc = pknots[knots.index(p)] # use projected knot location
2222 else:
2223 loc = mathutils.Vector(bm_mod.verts[p].co[:])
2224 if not loc_prev:
2225 loc_prev = loc
2226 len_total += (loc - loc_prev).length
2227 tpoints.append(len_total)
2228 loc_prev = loc
2229 tknots = []
2230 for p in points:
2231 if p in knots:
2232 tknots.append(tpoints[points.index(p)])
2233 if circular:
2234 tknots[-1] = tpoints[-1]
2236 # regular option
2237 if regular:
2238 tpoints_average = tpoints[-1] / (len(tpoints) - 1)
2239 for i in range(1, len(tpoints) - 1):
2240 tpoints[i] = i * tpoints_average
2241 for i in range(len(knots)):
2242 tknots[i] = tpoints[points.index(knots[i])]
2243 if circular:
2244 tknots[-1] = tpoints[-1]
2246 return(tknots, tpoints)
2249 # change the location of non-selected points to their place on the spline
2250 def curve_calculate_vertices(bm_mod, knots, tknots, points, tpoints, splines,
2251 interpolation, restriction):
2252 newlocs = {}
2253 move = []
2255 for p in points:
2256 if p in knots:
2257 continue
2258 m = tpoints[points.index(p)]
2259 if m in tknots:
2260 n = tknots.index(m)
2261 else:
2262 t = tknots[:]
2263 t.append(m)
2264 t.sort()
2265 n = t.index(m) - 1
2266 if n > len(splines) - 1:
2267 n = len(splines) - 1
2268 elif n < 0:
2269 n = 0
2271 if interpolation == 'cubic':
2272 ax, bx, cx, dx, tx = splines[n][0]
2273 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
2274 ay, by, cy, dy, ty = splines[n][1]
2275 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
2276 az, bz, cz, dz, tz = splines[n][2]
2277 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
2278 newloc = mathutils.Vector([x, y, z])
2279 else: # interpolation == 'linear'
2280 a, d, t, u = splines[n]
2281 newloc = ((m - t) / u) * d + a
2283 if restriction != 'none': # vertex movement is restricted
2284 newlocs[p] = newloc
2285 else: # set the vertex to its new location
2286 move.append([p, newloc])
2288 if restriction != 'none': # vertex movement is restricted
2289 for p in points:
2290 if p in newlocs:
2291 newloc = newlocs[p]
2292 else:
2293 move.append([p, bm_mod.verts[p].co])
2294 continue
2295 oldloc = bm_mod.verts[p].co
2296 normal = bm_mod.verts[p].normal
2297 dloc = newloc - oldloc
2298 if dloc.length < 1e-6:
2299 move.append([p, newloc])
2300 elif restriction == 'extrude': # only extrusions
2301 if dloc.angle(normal, 0) < 0.5 * math.pi + 1e-6:
2302 move.append([p, newloc])
2303 else: # restriction == 'indent' only indentations
2304 if dloc.angle(normal) > 0.5 * math.pi - 1e-6:
2305 move.append([p, newloc])
2307 return(move)
2310 # trim loops to part between first and last selected vertices (including)
2311 def curve_cut_boundaries(bm_mod, loops):
2312 cut_loops = []
2313 for loop, circular in loops:
2314 if circular:
2315 # don't cut
2316 cut_loops.append([loop, circular])
2317 continue
2318 selected = [bm_mod.verts[v].select for v in loop]
2319 first = selected.index(True)
2320 selected.reverse()
2321 last = -selected.index(True)
2322 if last == 0:
2323 cut_loops.append([loop[first:], circular])
2324 else:
2325 cut_loops.append([loop[first:last], circular])
2327 return(cut_loops)
2330 # calculate input loops
2331 def curve_get_input(object, bm, boundaries):
2332 # get mesh with modifiers applied
2333 derived, bm_mod = get_derived_bmesh(object, bm)
2335 # vertices that still need a loop to run through it
2336 verts_unsorted = [
2337 v.index for v in bm_mod.verts if v.select and not v.hide
2339 # necessary dictionaries
2340 vert_edges = dict_vert_edges(bm_mod)
2341 edge_faces = dict_edge_faces(bm_mod)
2342 correct_loops = []
2343 # find loops through each selected vertex
2344 while len(verts_unsorted) > 0:
2345 loops = curve_vertex_loops(bm_mod, verts_unsorted[0], vert_edges,
2346 edge_faces)
2347 verts_unsorted.pop(0)
2349 # check if loop is fully selected
2350 search_perpendicular = False
2351 i = -1
2352 for loop, circular in loops:
2353 i += 1
2354 selected = [v for v in loop if bm_mod.verts[v].select]
2355 if len(selected) < 2:
2356 # only one selected vertex on loop, don't use
2357 loops.pop(i)
2358 continue
2359 elif len(selected) == len(loop):
2360 search_perpendicular = loop
2361 break
2362 # entire loop is selected, find perpendicular loops
2363 if search_perpendicular:
2364 for vert in loop:
2365 if vert in verts_unsorted:
2366 verts_unsorted.remove(vert)
2367 perp_loops = curve_perpendicular_loops(bm_mod, loop,
2368 vert_edges, edge_faces)
2369 for perp_loop in perp_loops:
2370 correct_loops.append(perp_loop)
2371 # normal input
2372 else:
2373 for loop, circular in loops:
2374 correct_loops.append([loop, circular])
2376 # boundaries option
2377 if boundaries:
2378 correct_loops = curve_cut_boundaries(bm_mod, correct_loops)
2380 return(derived, bm_mod, correct_loops)
2383 # return all loops that are perpendicular to the given one
2384 def curve_perpendicular_loops(bm_mod, start_loop, vert_edges, edge_faces):
2385 # find perpendicular loops
2386 perp_loops = []
2387 for start_vert in start_loop:
2388 loops = curve_vertex_loops(bm_mod, start_vert, vert_edges,
2389 edge_faces)
2390 for loop, circular in loops:
2391 selected = [v for v in loop if bm_mod.verts[v].select]
2392 if len(selected) == len(loop):
2393 continue
2394 else:
2395 perp_loops.append([loop, circular, loop.index(start_vert)])
2397 # trim loops to same lengths
2398 shortest = [
2399 [len(loop[0]), i] for i, loop in enumerate(perp_loops) if not loop[1]
2401 if not shortest:
2402 # all loops are circular, not trimming
2403 return([[loop[0], loop[1]] for loop in perp_loops])
2404 else:
2405 shortest = min(shortest)
2406 shortest_start = perp_loops[shortest[1]][2]
2407 before_start = shortest_start
2408 after_start = shortest[0] - shortest_start - 1
2409 bigger_before = before_start > after_start
2410 trimmed_loops = []
2411 for loop in perp_loops:
2412 # have the loop face the same direction as the shortest one
2413 if bigger_before:
2414 if loop[2] < len(loop[0]) / 2:
2415 loop[0].reverse()
2416 loop[2] = len(loop[0]) - loop[2] - 1
2417 else:
2418 if loop[2] > len(loop[0]) / 2:
2419 loop[0].reverse()
2420 loop[2] = len(loop[0]) - loop[2] - 1
2421 # circular loops can shift, to prevent wrong trimming
2422 if loop[1]:
2423 shift = shortest_start - loop[2]
2424 if loop[2] + shift > 0 and loop[2] + shift < len(loop[0]):
2425 loop[0] = loop[0][-shift:] + loop[0][:-shift]
2426 loop[2] += shift
2427 if loop[2] < 0:
2428 loop[2] += len(loop[0])
2429 elif loop[2] > len(loop[0]) - 1:
2430 loop[2] -= len(loop[0])
2431 # trim
2432 start = max(0, loop[2] - before_start)
2433 end = min(len(loop[0]), loop[2] + after_start + 1)
2434 trimmed_loops.append([loop[0][start:end], False])
2436 return(trimmed_loops)
2439 # project knots on non-selected geometry
2440 def curve_project_knots(bm_mod, verts_selected, knots, points, circular):
2441 # function to project vertex on edge
2442 def project(v1, v2, v3):
2443 # v1 and v2 are part of a line
2444 # v3 is projected onto it
2445 v2 -= v1
2446 v3 -= v1
2447 p = v3.project(v2)
2448 return(p + v1)
2450 if circular: # project all knots
2451 start = 0
2452 end = len(knots)
2453 pknots = []
2454 else: # first and last knot shouldn't be projected
2455 start = 1
2456 end = -1
2457 pknots = [mathutils.Vector(bm_mod.verts[knots[0]].co[:])]
2458 for knot in knots[start:end]:
2459 if knot in verts_selected:
2460 knot_left = knot_right = False
2461 for i in range(points.index(knot) - 1, -1 * len(points), -1):
2462 if points[i] not in knots:
2463 knot_left = points[i]
2464 break
2465 for i in range(points.index(knot) + 1, 2 * len(points)):
2466 if i > len(points) - 1:
2467 i -= len(points)
2468 if points[i] not in knots:
2469 knot_right = points[i]
2470 break
2471 if knot_left and knot_right and knot_left != knot_right:
2472 knot_left = mathutils.Vector(bm_mod.verts[knot_left].co[:])
2473 knot_right = mathutils.Vector(bm_mod.verts[knot_right].co[:])
2474 knot = mathutils.Vector(bm_mod.verts[knot].co[:])
2475 pknots.append(project(knot_left, knot_right, knot))
2476 else:
2477 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2478 else: # knot isn't selected, so shouldn't be changed
2479 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2480 if not circular:
2481 pknots.append(mathutils.Vector(bm_mod.verts[knots[-1]].co[:]))
2483 return(pknots)
2486 # find all loops through a given vertex
2487 def curve_vertex_loops(bm_mod, start_vert, vert_edges, edge_faces):
2488 edges_used = []
2489 loops = []
2491 for edge in vert_edges[start_vert]:
2492 if edge in edges_used:
2493 continue
2494 loop = []
2495 circular = False
2496 for vert in edge:
2497 active_faces = edge_faces[edge]
2498 new_vert = vert
2499 growing = True
2500 while growing:
2501 growing = False
2502 new_edges = vert_edges[new_vert]
2503 loop.append(new_vert)
2504 if len(loop) > 1:
2505 edges_used.append(tuple(sorted([loop[-1], loop[-2]])))
2506 if len(new_edges) < 3 or len(new_edges) > 4:
2507 # pole
2508 break
2509 else:
2510 # find next edge
2511 for new_edge in new_edges:
2512 if new_edge in edges_used:
2513 continue
2514 eliminate = False
2515 for new_face in edge_faces[new_edge]:
2516 if new_face in active_faces:
2517 eliminate = True
2518 break
2519 if eliminate:
2520 continue
2521 # found correct new edge
2522 active_faces = edge_faces[new_edge]
2523 v1, v2 = new_edge
2524 if v1 != new_vert:
2525 new_vert = v1
2526 else:
2527 new_vert = v2
2528 if new_vert == loop[0]:
2529 circular = True
2530 else:
2531 growing = True
2532 break
2533 if circular:
2534 break
2535 loop.reverse()
2536 loops.append([loop, circular])
2538 return(loops)
2541 # ########################################
2542 # ##### Flatten functions ################
2543 # ########################################
2545 # sort input into loops
2546 def flatten_get_input(bm):
2547 vert_verts = dict_vert_verts(
2548 [edgekey(edge) for edge in bm.edges if edge.select and not edge.hide]
2550 verts = [v.index for v in bm.verts if v.select and not v.hide]
2552 # no connected verts, consider all selected verts as a single input
2553 if not vert_verts:
2554 return([[verts, False]])
2556 loops = []
2557 while len(verts) > 0:
2558 # start of loop
2559 loop = [verts[0]]
2560 verts.pop(0)
2561 if loop[-1] in vert_verts:
2562 to_grow = vert_verts[loop[-1]]
2563 else:
2564 to_grow = []
2565 # grow loop
2566 while len(to_grow) > 0:
2567 new_vert = to_grow[0]
2568 to_grow.pop(0)
2569 if new_vert in loop:
2570 continue
2571 loop.append(new_vert)
2572 verts.remove(new_vert)
2573 to_grow += vert_verts[new_vert]
2574 # add loop to loops
2575 loops.append([loop, False])
2577 return(loops)
2580 # calculate position of vertex projections on plane
2581 def flatten_project(bm, loop, com, normal):
2582 verts = [bm.verts[v] for v in loop[0]]
2583 verts_projected = [
2584 [v.index, mathutils.Vector(v.co[:]) -
2585 (mathutils.Vector(v.co[:]) - com).dot(normal) * normal] for v in verts
2588 return(verts_projected)
2591 # ########################################
2592 # ##### Gstretch functions ###############
2593 # ########################################
2595 # fake stroke class, used to create custom strokes if no GP data is found
2596 class gstretch_fake_stroke():
2597 def __init__(self, points):
2598 self.points = [gstretch_fake_stroke_point(p) for p in points]
2601 # fake stroke point class, used in fake strokes
2602 class gstretch_fake_stroke_point():
2603 def __init__(self, loc):
2604 self.co = loc
2607 # flips loops, if necessary, to obtain maximum alignment to stroke
2608 def gstretch_align_pairs(ls_pairs, object, bm_mod, method):
2609 # returns total distance between all verts in loop and corresponding stroke
2610 def distance_loop_stroke(loop, stroke, object, bm_mod, method):
2611 stroke_lengths_cache = False
2612 loop_length = len(loop[0])
2613 total_distance = 0
2615 if method != 'regular':
2616 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2618 for i, v_index in enumerate(loop[0]):
2619 if method == 'regular':
2620 relative_distance = i / (loop_length - 1)
2621 else:
2622 relative_distance = relative_lengths[i]
2624 loc1 = object.matrix_world @ bm_mod.verts[v_index].co
2625 loc2, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2626 relative_distance, stroke_lengths_cache)
2627 total_distance += (loc2 - loc1).length
2629 return(total_distance)
2631 if ls_pairs:
2632 for (loop, stroke) in ls_pairs:
2633 total_dist = distance_loop_stroke(loop, stroke, object, bm_mod,
2634 method)
2635 loop[0].reverse()
2636 total_dist_rev = distance_loop_stroke(loop, stroke, object, bm_mod,
2637 method)
2638 if total_dist_rev > total_dist:
2639 loop[0].reverse()
2641 return(ls_pairs)
2644 # calculate vertex positions on stroke
2645 def gstretch_calculate_verts(loop, stroke, object, bm_mod, method):
2646 move = []
2647 stroke_lengths_cache = False
2648 loop_length = len(loop[0])
2649 matrix_inverse = object.matrix_world.inverted()
2651 # return intersection of line with stroke, or None
2652 def intersect_line_stroke(vec1, vec2, stroke):
2653 for i, p in enumerate(stroke.points[1:]):
2654 intersections = mathutils.geometry.intersect_line_line(vec1, vec2,
2655 p.co, stroke.points[i].co)
2656 if intersections and \
2657 (intersections[0] - intersections[1]).length < 1e-2:
2658 x, dist = mathutils.geometry.intersect_point_line(
2659 intersections[0], p.co, stroke.points[i].co)
2660 if -1 < dist < 1:
2661 return(intersections[0])
2662 return(None)
2664 if method == 'project':
2665 vert_edges = dict_vert_edges(bm_mod)
2667 for v_index in loop[0]:
2668 intersection = None
2669 for ek in vert_edges[v_index]:
2670 v1, v2 = ek
2671 v1 = bm_mod.verts[v1]
2672 v2 = bm_mod.verts[v2]
2673 if v1.select + v2.select == 1 and not v1.hide and not v2.hide:
2674 vec1 = object.matrix_world @ v1.co
2675 vec2 = object.matrix_world @ v2.co
2676 intersection = intersect_line_stroke(vec1, vec2, stroke)
2677 if intersection:
2678 break
2679 if not intersection:
2680 v = bm_mod.verts[v_index]
2681 intersection = intersect_line_stroke(v.co, v.co + v.normal,
2682 stroke)
2683 if intersection:
2684 move.append([v_index, matrix_inverse @ intersection])
2686 else:
2687 if method == 'irregular':
2688 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2690 for i, v_index in enumerate(loop[0]):
2691 if method == 'regular':
2692 relative_distance = i / (loop_length - 1)
2693 else: # method == 'irregular'
2694 relative_distance = relative_lengths[i]
2695 loc, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2696 relative_distance, stroke_lengths_cache)
2697 loc = matrix_inverse @ loc
2698 move.append([v_index, loc])
2700 return(move)
2703 # create new vertices, based on GP strokes
2704 def gstretch_create_verts(object, bm_mod, strokes, method, conversion,
2705 conversion_distance, conversion_max, conversion_min, conversion_vertices):
2706 move = []
2707 stroke_verts = []
2708 mat_world = object.matrix_world.inverted()
2709 singles = gstretch_match_single_verts(bm_mod, strokes, mat_world)
2711 for stroke in strokes:
2712 stroke_verts.append([stroke, []])
2713 min_end_point = 0
2714 if conversion == 'vertices':
2715 min_end_point = conversion_vertices
2716 end_point = conversion_vertices
2717 elif conversion == 'limit_vertices':
2718 min_end_point = conversion_min
2719 end_point = conversion_max
2720 else:
2721 end_point = len(stroke.points)
2722 # creation of new vertices at fixed user-defined distances
2723 if conversion == 'distance':
2724 method = 'project'
2725 prev_point = stroke.points[0]
2726 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world @ prev_point.co))
2727 distance = 0
2728 limit = conversion_distance
2729 for point in stroke.points:
2730 new_distance = distance + (point.co - prev_point.co).length
2731 iteration = 0
2732 while new_distance > limit:
2733 to_cover = limit - distance + (limit * iteration)
2734 new_loc = prev_point.co + to_cover * \
2735 (point.co - prev_point.co).normalized()
2736 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * new_loc))
2737 new_distance -= limit
2738 iteration += 1
2739 distance = new_distance
2740 prev_point = point
2741 # creation of new vertices for other methods
2742 else:
2743 # add vertices at stroke points
2744 for point in stroke.points[:end_point]:
2745 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world @ point.co))
2746 # add more vertices, beyond the points that are available
2747 if min_end_point > min(len(stroke.points), end_point):
2748 for i in range(min_end_point -
2749 (min(len(stroke.points), end_point))):
2750 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world @ point.co))
2751 # force even spreading of points, so they are placed on stroke
2752 method = 'regular'
2753 bm_mod.verts.ensure_lookup_table()
2754 bm_mod.verts.index_update()
2755 for stroke, verts_seq in stroke_verts:
2756 if len(verts_seq) < 2:
2757 continue
2758 # spread vertices evenly over the stroke
2759 if method == 'regular':
2760 loop = [[vert.index for vert in verts_seq], False]
2761 move += gstretch_calculate_verts(loop, stroke, object, bm_mod,
2762 method)
2763 # create edges
2764 for i, vert in enumerate(verts_seq):
2765 if i > 0:
2766 bm_mod.edges.new((verts_seq[i - 1], verts_seq[i]))
2767 vert.select = True
2768 # connect single vertices to the closest stroke
2769 if singles:
2770 for vert, m_stroke, point in singles:
2771 if m_stroke != stroke:
2772 continue
2773 bm_mod.edges.new((vert, verts_seq[point]))
2774 bm_mod.edges.ensure_lookup_table()
2775 bmesh.update_edit_mesh(object.data)
2777 return(move)
2780 # erases the grease pencil stroke
2781 def gstretch_erase_stroke(stroke, context):
2782 # change 3d coordinate into a stroke-point
2783 def sp(loc, context):
2784 lib = {'name': "",
2785 'pen_flip': False,
2786 'is_start': False,
2787 'location': (0, 0, 0),
2788 'mouse': (
2789 view3d_utils.location_3d_to_region_2d(
2790 context.region, context.space_data.region_3d, loc)
2792 'pressure': 1,
2793 'size': 0,
2794 'time': 0}
2795 return(lib)
2797 if type(stroke) != bpy.types.GPencilStroke:
2798 # fake stroke, there is nothing to delete
2799 return
2801 erase_stroke = [sp(p.co, context) for p in stroke.points]
2802 if erase_stroke:
2803 erase_stroke[0]['is_start'] = True
2804 #bpy.ops.gpencil.draw(mode='ERASER', stroke=erase_stroke)
2805 bpy.ops.gpencil.data_unlink()
2809 # get point on stroke, given by relative distance (0.0 - 1.0)
2810 def gstretch_eval_stroke(stroke, distance, stroke_lengths_cache=False):
2811 # use cache if available
2812 if not stroke_lengths_cache:
2813 lengths = [0]
2814 for i, p in enumerate(stroke.points[1:]):
2815 lengths.append((p.co - stroke.points[i].co).length + lengths[-1])
2816 total_length = max(lengths[-1], 1e-7)
2817 stroke_lengths_cache = [length / total_length for length in
2818 lengths]
2819 stroke_lengths = stroke_lengths_cache[:]
2821 if distance in stroke_lengths:
2822 loc = stroke.points[stroke_lengths.index(distance)].co
2823 elif distance > stroke_lengths[-1]:
2824 # should be impossible, but better safe than sorry
2825 loc = stroke.points[-1].co
2826 else:
2827 stroke_lengths.append(distance)
2828 stroke_lengths.sort()
2829 stroke_index = stroke_lengths.index(distance)
2830 interval_length = stroke_lengths[
2831 stroke_index + 1] - stroke_lengths[stroke_index - 1
2833 distance_relative = (distance - stroke_lengths[stroke_index - 1]) / interval_length
2834 interval_vector = stroke.points[stroke_index].co - stroke.points[stroke_index - 1].co
2835 loc = stroke.points[stroke_index - 1].co + distance_relative * interval_vector
2837 return(loc, stroke_lengths_cache)
2840 # create fake grease pencil strokes for the active object
2841 def gstretch_get_fake_strokes(object, bm_mod, loops):
2842 strokes = []
2843 for loop in loops:
2844 p1 = object.matrix_world @ bm_mod.verts[loop[0][0]].co
2845 p2 = object.matrix_world @ bm_mod.verts[loop[0][-1]].co
2846 strokes.append(gstretch_fake_stroke([p1, p2]))
2848 return(strokes)
2850 # get strokes
2851 def gstretch_get_strokes(self, context):
2852 looptools = context.window_manager.looptools
2853 gp = get_strokes(self, context)
2854 if not gp:
2855 return(None)
2856 if looptools.gstretch_use_guide == "Annotation":
2857 layer = bpy.data.grease_pencils[0].layers.active
2858 if looptools.gstretch_use_guide == "GPencil" and not looptools.gstretch_guide == None:
2859 layer = looptools.gstretch_guide.data.layers.active
2860 if not layer:
2861 return(None)
2862 frame = layer.active_frame
2863 if not frame:
2864 return(None)
2865 strokes = frame.strokes
2866 if len(strokes) < 1:
2867 return(None)
2869 return(strokes)
2871 # returns a list with loop-stroke pairs
2872 def gstretch_match_loops_strokes(loops, strokes, object, bm_mod):
2873 if not loops or not strokes:
2874 return(None)
2876 # calculate loop centers
2877 loop_centers = []
2878 bm_mod.verts.ensure_lookup_table()
2879 for loop in loops:
2880 center = mathutils.Vector()
2881 for v_index in loop[0]:
2882 center += bm_mod.verts[v_index].co
2883 center /= len(loop[0])
2884 center = object.matrix_world @ center
2885 loop_centers.append([center, loop])
2887 # calculate stroke centers
2888 stroke_centers = []
2889 for stroke in strokes:
2890 center = mathutils.Vector()
2891 for p in stroke.points:
2892 center += p.co
2893 center /= len(stroke.points)
2894 stroke_centers.append([center, stroke, 0])
2896 # match, first by stroke use count, then by distance
2897 ls_pairs = []
2898 for lc in loop_centers:
2899 distances = []
2900 for i, sc in enumerate(stroke_centers):
2901 distances.append([sc[2], (lc[0] - sc[0]).length, i])
2902 distances.sort()
2903 best_stroke = distances[0][2]
2904 ls_pairs.append([lc[1], stroke_centers[best_stroke][1]])
2905 stroke_centers[best_stroke][2] += 1 # increase stroke use count
2907 return(ls_pairs)
2910 # match single selected vertices to the closest stroke endpoint
2911 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2912 def gstretch_match_single_verts(bm_mod, strokes, mat_world):
2913 # calculate stroke endpoints in object space
2914 endpoints = []
2915 for stroke in strokes:
2916 endpoints.append((mat_world @ stroke.points[0].co, stroke, 0))
2917 endpoints.append((mat_world @ stroke.points[-1].co, stroke, -1))
2919 distances = []
2920 # find single vertices (not connected to other selected verts)
2921 for vert in bm_mod.verts:
2922 if not vert.select:
2923 continue
2924 single = True
2925 for edge in vert.link_edges:
2926 if edge.other_vert(vert).select:
2927 single = False
2928 break
2929 if not single:
2930 continue
2931 # calculate distances from vertex to endpoints
2932 distance = [((vert.co - loc).length, vert, stroke, stroke_point,
2933 endpoint_index) for endpoint_index, (loc, stroke, stroke_point) in
2934 enumerate(endpoints)]
2935 distance.sort()
2936 distances.append(distance[0])
2938 # create matches, based on shortest distance first
2939 singles = []
2940 while distances:
2941 distances.sort()
2942 singles.append((distances[0][1], distances[0][2], distances[0][3]))
2943 endpoints.pop(distances[0][4])
2944 distances.pop(0)
2945 distances_new = []
2946 for (i, vert, j, k, l) in distances:
2947 distance_new = [((vert.co - loc).length, vert, stroke, stroke_point,
2948 endpoint_index) for endpoint_index, (loc, stroke,
2949 stroke_point) in enumerate(endpoints)]
2950 distance_new.sort()
2951 distances_new.append(distance_new[0])
2952 distances = distances_new
2954 return(singles)
2957 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2958 def gstretch_relative_lengths(loop, bm_mod):
2959 lengths = [0]
2960 for i, v_index in enumerate(loop[0][1:]):
2961 lengths.append(
2962 (bm_mod.verts[v_index].co -
2963 bm_mod.verts[loop[0][i]].co).length + lengths[-1]
2965 total_length = max(lengths[-1], 1e-7)
2966 relative_lengths = [length / total_length for length in
2967 lengths]
2969 return(relative_lengths)
2972 # convert cache-stored strokes into usable (fake) GP strokes
2973 def gstretch_safe_to_true_strokes(safe_strokes):
2974 strokes = []
2975 for safe_stroke in safe_strokes:
2976 strokes.append(gstretch_fake_stroke(safe_stroke))
2978 return(strokes)
2981 # convert a GP stroke into a list of points which can be stored in cache
2982 def gstretch_true_to_safe_strokes(strokes):
2983 safe_strokes = []
2984 for stroke in strokes:
2985 safe_strokes.append([p.co.copy() for p in stroke.points])
2987 return(safe_strokes)
2990 # force consistency in GUI, max value can never be lower than min value
2991 def gstretch_update_max(self, context):
2992 # called from operator settings (after execution)
2993 if 'conversion_min' in self.keys():
2994 if self.conversion_min > self.conversion_max:
2995 self.conversion_max = self.conversion_min
2996 # called from toolbar
2997 else:
2998 lt = context.window_manager.looptools
2999 if lt.gstretch_conversion_min > lt.gstretch_conversion_max:
3000 lt.gstretch_conversion_max = lt.gstretch_conversion_min
3003 # force consistency in GUI, min value can never be higher than max value
3004 def gstretch_update_min(self, context):
3005 # called from operator settings (after execution)
3006 if 'conversion_max' in self.keys():
3007 if self.conversion_max < self.conversion_min:
3008 self.conversion_min = self.conversion_max
3009 # called from toolbar
3010 else:
3011 lt = context.window_manager.looptools
3012 if lt.gstretch_conversion_max < lt.gstretch_conversion_min:
3013 lt.gstretch_conversion_min = lt.gstretch_conversion_max
3016 # ########################################
3017 # ##### Relax functions ##################
3018 # ########################################
3020 # create lists with knots and points, all correctly sorted
3021 def relax_calculate_knots(loops):
3022 all_knots = []
3023 all_points = []
3024 for loop, circular in loops:
3025 knots = [[], []]
3026 points = [[], []]
3027 if circular:
3028 if len(loop) % 2 == 1: # odd
3029 extend = [False, True, 0, 1, 0, 1]
3030 else: # even
3031 extend = [True, False, 0, 1, 1, 2]
3032 else:
3033 if len(loop) % 2 == 1: # odd
3034 extend = [False, False, 0, 1, 1, 2]
3035 else: # even
3036 extend = [False, False, 0, 1, 1, 2]
3037 for j in range(2):
3038 if extend[j]:
3039 loop = [loop[-1]] + loop + [loop[0]]
3040 for i in range(extend[2 + 2 * j], len(loop), 2):
3041 knots[j].append(loop[i])
3042 for i in range(extend[3 + 2 * j], len(loop), 2):
3043 if loop[i] == loop[-1] and not circular:
3044 continue
3045 if len(points[j]) == 0:
3046 points[j].append(loop[i])
3047 elif loop[i] != points[j][0]:
3048 points[j].append(loop[i])
3049 if circular:
3050 if knots[j][0] != knots[j][-1]:
3051 knots[j].append(knots[j][0])
3052 if len(points[1]) == 0:
3053 knots.pop(1)
3054 points.pop(1)
3055 for k in knots:
3056 all_knots.append(k)
3057 for p in points:
3058 all_points.append(p)
3060 return(all_knots, all_points)
3063 # calculate relative positions compared to first knot
3064 def relax_calculate_t(bm_mod, knots, points, regular):
3065 all_tknots = []
3066 all_tpoints = []
3067 for i in range(len(knots)):
3068 amount = len(knots[i]) + len(points[i])
3069 mix = []
3070 for j in range(amount):
3071 if j % 2 == 0:
3072 mix.append([True, knots[i][round(j / 2)]])
3073 elif j == amount - 1:
3074 mix.append([True, knots[i][-1]])
3075 else:
3076 mix.append([False, points[i][int(j / 2)]])
3077 len_total = 0
3078 loc_prev = False
3079 tknots = []
3080 tpoints = []
3081 for m in mix:
3082 loc = mathutils.Vector(bm_mod.verts[m[1]].co[:])
3083 if not loc_prev:
3084 loc_prev = loc
3085 len_total += (loc - loc_prev).length
3086 if m[0]:
3087 tknots.append(len_total)
3088 else:
3089 tpoints.append(len_total)
3090 loc_prev = loc
3091 if regular:
3092 tpoints = []
3093 for p in range(len(points[i])):
3094 tpoints.append((tknots[p] + tknots[p + 1]) / 2)
3095 all_tknots.append(tknots)
3096 all_tpoints.append(tpoints)
3098 return(all_tknots, all_tpoints)
3101 # change the location of the points to their place on the spline
3102 def relax_calculate_verts(bm_mod, interpolation, tknots, knots, tpoints,
3103 points, splines):
3104 change = []
3105 move = []
3106 for i in range(len(knots)):
3107 for p in points[i]:
3108 m = tpoints[i][points[i].index(p)]
3109 if m in tknots[i]:
3110 n = tknots[i].index(m)
3111 else:
3112 t = tknots[i][:]
3113 t.append(m)
3114 t.sort()
3115 n = t.index(m) - 1
3116 if n > len(splines[i]) - 1:
3117 n = len(splines[i]) - 1
3118 elif n < 0:
3119 n = 0
3121 if interpolation == 'cubic':
3122 ax, bx, cx, dx, tx = splines[i][n][0]
3123 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
3124 ay, by, cy, dy, ty = splines[i][n][1]
3125 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
3126 az, bz, cz, dz, tz = splines[i][n][2]
3127 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
3128 change.append([p, mathutils.Vector([x, y, z])])
3129 else: # interpolation == 'linear'
3130 a, d, t, u = splines[i][n]
3131 if u == 0:
3132 u = 1e-8
3133 change.append([p, ((m - t) / u) * d + a])
3134 for c in change:
3135 move.append([c[0], (bm_mod.verts[c[0]].co + c[1]) / 2])
3137 return(move)
3140 # ########################################
3141 # ##### Space functions ##################
3142 # ########################################
3144 # calculate relative positions compared to first knot
3145 def space_calculate_t(bm_mod, knots):
3146 tknots = []
3147 loc_prev = False
3148 len_total = 0
3149 for k in knots:
3150 loc = mathutils.Vector(bm_mod.verts[k].co[:])
3151 if not loc_prev:
3152 loc_prev = loc
3153 len_total += (loc - loc_prev).length
3154 tknots.append(len_total)
3155 loc_prev = loc
3156 amount = len(knots)
3157 t_per_segment = len_total / (amount - 1)
3158 tpoints = [i * t_per_segment for i in range(amount)]
3160 return(tknots, tpoints)
3163 # change the location of the points to their place on the spline
3164 def space_calculate_verts(bm_mod, interpolation, tknots, tpoints, points,
3165 splines):
3166 move = []
3167 for p in points:
3168 m = tpoints[points.index(p)]
3169 if m in tknots:
3170 n = tknots.index(m)
3171 else:
3172 t = tknots[:]
3173 t.append(m)
3174 t.sort()
3175 n = t.index(m) - 1
3176 if n > len(splines) - 1:
3177 n = len(splines) - 1
3178 elif n < 0:
3179 n = 0
3181 if interpolation == 'cubic':
3182 ax, bx, cx, dx, tx = splines[n][0]
3183 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
3184 ay, by, cy, dy, ty = splines[n][1]
3185 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
3186 az, bz, cz, dz, tz = splines[n][2]
3187 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
3188 move.append([p, mathutils.Vector([x, y, z])])
3189 else: # interpolation == 'linear'
3190 a, d, t, u = splines[n]
3191 move.append([p, ((m - t) / u) * d + a])
3193 return(move)
3196 # ########################################
3197 # ##### Operators ########################
3198 # ########################################
3200 # bridge operator
3201 class Bridge(Operator):
3202 bl_idname = 'mesh.looptools_bridge'
3203 bl_label = "Bridge / Loft"
3204 bl_description = "Bridge two, or loft several, loops of vertices"
3205 bl_options = {'REGISTER', 'UNDO'}
3207 cubic_strength: FloatProperty(
3208 name="Strength",
3209 description="Higher strength results in more fluid curves",
3210 default=1.0,
3211 soft_min=-3.0,
3212 soft_max=3.0
3214 interpolation: EnumProperty(
3215 name="Interpolation mode",
3216 items=(('cubic', "Cubic", "Gives curved results"),
3217 ('linear', "Linear", "Basic, fast, straight interpolation")),
3218 description="Interpolation mode: algorithm used when creating "
3219 "segments",
3220 default='cubic'
3222 loft: BoolProperty(
3223 name="Loft",
3224 description="Loft multiple loops, instead of considering them as "
3225 "a multi-input for bridging",
3226 default=False
3228 loft_loop: BoolProperty(
3229 name="Loop",
3230 description="Connect the first and the last loop with each other",
3231 default=False
3233 min_width: IntProperty(
3234 name="Minimum width",
3235 description="Segments with an edge smaller than this are merged "
3236 "(compared to base edge)",
3237 default=0,
3238 min=0,
3239 max=100,
3240 subtype='PERCENTAGE'
3242 mode: EnumProperty(
3243 name="Mode",
3244 items=(('basic', "Basic", "Fast algorithm"),
3245 ('shortest', "Shortest edge", "Slower algorithm with better vertex matching")),
3246 description="Algorithm used for bridging",
3247 default='shortest'
3249 remove_faces: BoolProperty(
3250 name="Remove faces",
3251 description="Remove faces that are internal after bridging",
3252 default=True
3254 reverse: BoolProperty(
3255 name="Reverse",
3256 description="Manually override the direction in which the loops "
3257 "are bridged. Only use if the tool gives the wrong result",
3258 default=False
3260 segments: IntProperty(
3261 name="Segments",
3262 description="Number of segments used to bridge the gap (0=automatic)",
3263 default=1,
3264 min=0,
3265 soft_max=20
3267 twist: IntProperty(
3268 name="Twist",
3269 description="Twist what vertices are connected to each other",
3270 default=0
3273 @classmethod
3274 def poll(cls, context):
3275 ob = context.active_object
3276 return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3278 def draw(self, context):
3279 layout = self.layout
3280 # layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3282 # top row
3283 col_top = layout.column(align=True)
3284 row = col_top.row(align=True)
3285 col_left = row.column(align=True)
3286 col_right = row.column(align=True)
3287 col_right.active = self.segments != 1
3288 col_left.prop(self, "segments")
3289 col_right.prop(self, "min_width", text="")
3290 # bottom row
3291 bottom_left = col_left.row()
3292 bottom_left.active = self.segments != 1
3293 bottom_left.prop(self, "interpolation", text="")
3294 bottom_right = col_right.row()
3295 bottom_right.active = self.interpolation == 'cubic'
3296 bottom_right.prop(self, "cubic_strength")
3297 # boolean properties
3298 col_top.prop(self, "remove_faces")
3299 if self.loft:
3300 col_top.prop(self, "loft_loop")
3302 # override properties
3303 col_top.separator()
3304 row = layout.row(align=True)
3305 row.prop(self, "twist")
3306 row.prop(self, "reverse")
3308 def invoke(self, context, event):
3309 # load custom settings
3310 context.window_manager.looptools.bridge_loft = self.loft
3311 settings_load(self)
3312 return self.execute(context)
3314 def execute(self, context):
3315 # initialise
3316 object, bm = initialise()
3317 edge_faces, edgekey_to_edge, old_selected_faces, smooth = \
3318 bridge_initialise(bm, self.interpolation)
3319 settings_write(self)
3321 # check cache to see if we can save time
3322 input_method = bridge_input_method(self.loft, self.loft_loop)
3323 cached, single_loops, loops, derived, mapping = cache_read("Bridge",
3324 object, bm, input_method, False)
3325 if not cached:
3326 # get loops
3327 loops = bridge_get_input(bm)
3328 if loops:
3329 # reorder loops if there are more than 2
3330 if len(loops) > 2:
3331 if self.loft:
3332 loops = bridge_sort_loops(bm, loops, self.loft_loop)
3333 else:
3334 loops = bridge_match_loops(bm, loops)
3336 # saving cache for faster execution next time
3337 if not cached:
3338 cache_write("Bridge", object, bm, input_method, False, False,
3339 loops, False, False)
3341 if loops:
3342 # calculate new geometry
3343 vertices = []
3344 faces = []
3345 max_vert_index = len(bm.verts) - 1
3346 for i in range(1, len(loops)):
3347 if not self.loft and i % 2 == 0:
3348 continue
3349 lines = bridge_calculate_lines(bm, loops[i - 1:i + 1],
3350 self.mode, self.twist, self.reverse)
3351 vertex_normals = bridge_calculate_virtual_vertex_normals(bm,
3352 lines, loops[i - 1:i + 1], edge_faces, edgekey_to_edge)
3353 segments = bridge_calculate_segments(bm, lines,
3354 loops[i - 1:i + 1], self.segments)
3355 new_verts, new_faces, max_vert_index = \
3356 bridge_calculate_geometry(
3357 bm, lines, vertex_normals,
3358 segments, self.interpolation, self.cubic_strength,
3359 self.min_width, max_vert_index
3361 if new_verts:
3362 vertices += new_verts
3363 if new_faces:
3364 faces += new_faces
3365 # make sure faces in loops that aren't used, aren't removed
3366 if self.remove_faces and old_selected_faces:
3367 bridge_save_unused_faces(bm, old_selected_faces, loops)
3368 # create vertices
3369 if vertices:
3370 bridge_create_vertices(bm, vertices)
3371 # create faces
3372 if faces:
3373 new_faces = bridge_create_faces(object, bm, faces, self.twist)
3374 old_selected_faces = [
3375 i for i, face in enumerate(bm.faces) if face.index in old_selected_faces
3376 ] # updating list
3377 bridge_select_new_faces(new_faces, smooth)
3378 # edge-data could have changed, can't use cache next run
3379 if faces and not vertices:
3380 cache_delete("Bridge")
3381 # delete internal faces
3382 if self.remove_faces and old_selected_faces:
3383 bridge_remove_internal_faces(bm, old_selected_faces)
3384 # make sure normals are facing outside
3385 bmesh.update_edit_mesh(object.data, loop_triangles=False,
3386 destructive=True)
3387 bpy.ops.mesh.normals_make_consistent()
3389 # cleaning up
3390 terminate()
3392 return{'FINISHED'}
3395 # circle operator
3396 class Circle(Operator):
3397 bl_idname = "mesh.looptools_circle"
3398 bl_label = "Circle"
3399 bl_description = "Move selected vertices into a circle shape"
3400 bl_options = {'REGISTER', 'UNDO'}
3402 custom_radius: BoolProperty(
3403 name="Radius",
3404 description="Force a custom radius",
3405 default=False
3407 fit: EnumProperty(
3408 name="Method",
3409 items=(("best", "Best fit", "Non-linear least squares"),
3410 ("inside", "Fit inside", "Only move vertices towards the center")),
3411 description="Method used for fitting a circle to the vertices",
3412 default='best'
3414 flatten: BoolProperty(
3415 name="Flatten",
3416 description="Flatten the circle, instead of projecting it on the mesh",
3417 default=True
3419 influence: FloatProperty(
3420 name="Influence",
3421 description="Force of the tool",
3422 default=100.0,
3423 min=0.0,
3424 max=100.0,
3425 precision=1,
3426 subtype='PERCENTAGE'
3428 lock_x: BoolProperty(
3429 name="Lock X",
3430 description="Lock editing of the x-coordinate",
3431 default=False
3433 lock_y: BoolProperty(
3434 name="Lock Y",
3435 description="Lock editing of the y-coordinate",
3436 default=False
3438 lock_z: BoolProperty(name="Lock Z",
3439 description="Lock editing of the z-coordinate",
3440 default=False
3442 radius: FloatProperty(
3443 name="Radius",
3444 description="Custom radius for circle",
3445 default=1.0,
3446 min=0.0,
3447 soft_max=1000.0
3449 regular: BoolProperty(
3450 name="Regular",
3451 description="Distribute vertices at constant distances along the circle",
3452 default=True
3455 @classmethod
3456 def poll(cls, context):
3457 ob = context.active_object
3458 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3460 def draw(self, context):
3461 layout = self.layout
3462 col = layout.column()
3464 col.prop(self, "fit")
3465 col.separator()
3467 col.prop(self, "flatten")
3468 row = col.row(align=True)
3469 row.prop(self, "custom_radius")
3470 row_right = row.row(align=True)
3471 row_right.active = self.custom_radius
3472 row_right.prop(self, "radius", text="")
3473 col.prop(self, "regular")
3474 col.separator()
3476 col_move = col.column(align=True)
3477 row = col_move.row(align=True)
3478 if self.lock_x:
3479 row.prop(self, "lock_x", text="X", icon='LOCKED')
3480 else:
3481 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3482 if self.lock_y:
3483 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3484 else:
3485 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3486 if self.lock_z:
3487 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3488 else:
3489 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3490 col_move.prop(self, "influence")
3492 def invoke(self, context, event):
3493 # load custom settings
3494 settings_load(self)
3495 return self.execute(context)
3497 def execute(self, context):
3498 # initialise
3499 object, bm = initialise()
3500 settings_write(self)
3501 # check cache to see if we can save time
3502 cached, single_loops, loops, derived, mapping = cache_read("Circle",
3503 object, bm, False, False)
3504 if cached:
3505 derived, bm_mod = get_derived_bmesh(object, bm)
3506 else:
3507 # find loops
3508 derived, bm_mod, single_vertices, single_loops, loops = \
3509 circle_get_input(object, bm)
3510 mapping = get_mapping(derived, bm, bm_mod, single_vertices,
3511 False, loops)
3512 single_loops, loops = circle_check_loops(single_loops, loops,
3513 mapping, bm_mod)
3515 # saving cache for faster execution next time
3516 if not cached:
3517 cache_write("Circle", object, bm, False, False, single_loops,
3518 loops, derived, mapping)
3520 move = []
3521 for i, loop in enumerate(loops):
3522 # best fitting flat plane
3523 com, normal = calculate_plane(bm_mod, loop)
3524 # if circular, shift loop so we get a good starting vertex
3525 if loop[1]:
3526 loop = circle_shift_loop(bm_mod, loop, com)
3527 # flatten vertices on plane
3528 locs_2d, p, q = circle_3d_to_2d(bm_mod, loop, com, normal)
3529 # calculate circle
3530 if self.fit == 'best':
3531 x0, y0, r = circle_calculate_best_fit(locs_2d)
3532 else: # self.fit == 'inside'
3533 x0, y0, r = circle_calculate_min_fit(locs_2d)
3534 # radius override
3535 if self.custom_radius:
3536 r = self.radius / p.length
3537 # calculate positions on circle
3538 if self.regular:
3539 new_locs_2d = circle_project_regular(locs_2d[:], x0, y0, r)
3540 else:
3541 new_locs_2d = circle_project_non_regular(locs_2d[:], x0, y0, r)
3542 # take influence into account
3543 locs_2d = circle_influence_locs(locs_2d, new_locs_2d,
3544 self.influence)
3545 # calculate 3d positions of the created 2d input
3546 move.append(circle_calculate_verts(self.flatten, bm_mod,
3547 locs_2d, com, p, q, normal))
3548 # flatten single input vertices on plane defined by loop
3549 if self.flatten and single_loops:
3550 move.append(circle_flatten_singles(bm_mod, com, p, q,
3551 normal, single_loops[i]))
3553 # move vertices to new locations
3554 if self.lock_x or self.lock_y or self.lock_z:
3555 lock = [self.lock_x, self.lock_y, self.lock_z]
3556 else:
3557 lock = False
3558 move_verts(object, bm, mapping, move, lock, -1)
3560 # cleaning up
3561 if derived:
3562 bm_mod.free()
3563 terminate()
3565 return{'FINISHED'}
3568 # curve operator
3569 class Curve(Operator):
3570 bl_idname = "mesh.looptools_curve"
3571 bl_label = "Curve"
3572 bl_description = "Turn a loop into a smooth curve"
3573 bl_options = {'REGISTER', 'UNDO'}
3575 boundaries: BoolProperty(
3576 name="Boundaries",
3577 description="Limit the tool to work within the boundaries of the selected vertices",
3578 default=False
3580 influence: FloatProperty(
3581 name="Influence",
3582 description="Force of the tool",
3583 default=100.0,
3584 min=0.0,
3585 max=100.0,
3586 precision=1,
3587 subtype='PERCENTAGE'
3589 interpolation: EnumProperty(
3590 name="Interpolation",
3591 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
3592 ("linear", "Linear", "Simple and fast linear algorithm")),
3593 description="Algorithm used for interpolation",
3594 default='cubic'
3596 lock_x: BoolProperty(
3597 name="Lock X",
3598 description="Lock editing of the x-coordinate",
3599 default=False
3601 lock_y: BoolProperty(
3602 name="Lock Y",
3603 description="Lock editing of the y-coordinate",
3604 default=False
3606 lock_z: BoolProperty(
3607 name="Lock Z",
3608 description="Lock editing of the z-coordinate",
3609 default=False
3611 regular: BoolProperty(
3612 name="Regular",
3613 description="Distribute vertices at constant distances along the curve",
3614 default=True
3616 restriction: EnumProperty(
3617 name="Restriction",
3618 items=(("none", "None", "No restrictions on vertex movement"),
3619 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
3620 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
3621 description="Restrictions on how the vertices can be moved",
3622 default='none'
3625 @classmethod
3626 def poll(cls, context):
3627 ob = context.active_object
3628 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3630 def draw(self, context):
3631 layout = self.layout
3632 col = layout.column()
3634 col.prop(self, "interpolation")
3635 col.prop(self, "restriction")
3636 col.prop(self, "boundaries")
3637 col.prop(self, "regular")
3638 col.separator()
3640 col_move = col.column(align=True)
3641 row = col_move.row(align=True)
3642 if self.lock_x:
3643 row.prop(self, "lock_x", text="X", icon='LOCKED')
3644 else:
3645 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3646 if self.lock_y:
3647 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3648 else:
3649 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3650 if self.lock_z:
3651 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3652 else:
3653 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3654 col_move.prop(self, "influence")
3656 def invoke(self, context, event):
3657 # load custom settings
3658 settings_load(self)
3659 return self.execute(context)
3661 def execute(self, context):
3662 # initialise
3663 object, bm = initialise()
3664 settings_write(self)
3665 # check cache to see if we can save time
3666 cached, single_loops, loops, derived, mapping = cache_read("Curve",
3667 object, bm, False, self.boundaries)
3668 if cached:
3669 derived, bm_mod = get_derived_bmesh(object, bm)
3670 else:
3671 # find loops
3672 derived, bm_mod, loops = curve_get_input(object, bm, self.boundaries)
3673 mapping = get_mapping(derived, bm, bm_mod, False, True, loops)
3674 loops = check_loops(loops, mapping, bm_mod)
3675 verts_selected = [
3676 v.index for v in bm_mod.verts if v.select and not v.hide
3679 # saving cache for faster execution next time
3680 if not cached:
3681 cache_write("Curve", object, bm, False, self.boundaries, False,
3682 loops, derived, mapping)
3684 move = []
3685 for loop in loops:
3686 knots, points = curve_calculate_knots(loop, verts_selected)
3687 pknots = curve_project_knots(bm_mod, verts_selected, knots,
3688 points, loop[1])
3689 tknots, tpoints = curve_calculate_t(bm_mod, knots, points,
3690 pknots, self.regular, loop[1])
3691 splines = calculate_splines(self.interpolation, bm_mod,
3692 tknots, knots)
3693 move.append(curve_calculate_vertices(bm_mod, knots, tknots,
3694 points, tpoints, splines, self.interpolation,
3695 self.restriction))
3697 # move vertices to new locations
3698 if self.lock_x or self.lock_y or self.lock_z:
3699 lock = [self.lock_x, self.lock_y, self.lock_z]
3700 else:
3701 lock = False
3702 move_verts(object, bm, mapping, move, lock, self.influence)
3704 # cleaning up
3705 if derived:
3706 bm_mod.free()
3707 terminate()
3709 return{'FINISHED'}
3712 # flatten operator
3713 class Flatten(Operator):
3714 bl_idname = "mesh.looptools_flatten"
3715 bl_label = "Flatten"
3716 bl_description = "Flatten vertices on a best-fitting plane"
3717 bl_options = {'REGISTER', 'UNDO'}
3719 influence: FloatProperty(
3720 name="Influence",
3721 description="Force of the tool",
3722 default=100.0,
3723 min=0.0,
3724 max=100.0,
3725 precision=1,
3726 subtype='PERCENTAGE'
3728 lock_x: BoolProperty(
3729 name="Lock X",
3730 description="Lock editing of the x-coordinate",
3731 default=False
3733 lock_y: BoolProperty(
3734 name="Lock Y",
3735 description="Lock editing of the y-coordinate",
3736 default=False
3738 lock_z: BoolProperty(name="Lock Z",
3739 description="Lock editing of the z-coordinate",
3740 default=False
3742 plane: EnumProperty(
3743 name="Plane",
3744 items=(("best_fit", "Best fit", "Calculate a best fitting plane"),
3745 ("normal", "Normal", "Derive plane from averaging vertex normals"),
3746 ("view", "View", "Flatten on a plane perpendicular to the viewing angle")),
3747 description="Plane on which vertices are flattened",
3748 default='best_fit'
3750 restriction: EnumProperty(
3751 name="Restriction",
3752 items=(("none", "None", "No restrictions on vertex movement"),
3753 ("bounding_box", "Bounding box", "Vertices are restricted to "
3754 "movement inside the bounding box of the selection")),
3755 description="Restrictions on how the vertices can be moved",
3756 default='none'
3759 @classmethod
3760 def poll(cls, context):
3761 ob = context.active_object
3762 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3764 def draw(self, context):
3765 layout = self.layout
3766 col = layout.column()
3768 col.prop(self, "plane")
3769 # col.prop(self, "restriction")
3770 col.separator()
3772 col_move = col.column(align=True)
3773 row = col_move.row(align=True)
3774 if self.lock_x:
3775 row.prop(self, "lock_x", text="X", icon='LOCKED')
3776 else:
3777 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3778 if self.lock_y:
3779 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3780 else:
3781 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3782 if self.lock_z:
3783 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3784 else:
3785 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3786 col_move.prop(self, "influence")
3788 def invoke(self, context, event):
3789 # load custom settings
3790 settings_load(self)
3791 return self.execute(context)
3793 def execute(self, context):
3794 # initialise
3795 object, bm = initialise()
3796 settings_write(self)
3797 # check cache to see if we can save time
3798 cached, single_loops, loops, derived, mapping = cache_read("Flatten",
3799 object, bm, False, False)
3800 if not cached:
3801 # order input into virtual loops
3802 loops = flatten_get_input(bm)
3803 loops = check_loops(loops, mapping, bm)
3805 # saving cache for faster execution next time
3806 if not cached:
3807 cache_write("Flatten", object, bm, False, False, False, loops,
3808 False, False)
3810 move = []
3811 for loop in loops:
3812 # calculate plane and position of vertices on them
3813 com, normal = calculate_plane(bm, loop, method=self.plane,
3814 object=object)
3815 to_move = flatten_project(bm, loop, com, normal)
3816 if self.restriction == 'none':
3817 move.append(to_move)
3818 else:
3819 move.append(to_move)
3821 # move vertices to new locations
3822 if self.lock_x or self.lock_y or self.lock_z:
3823 lock = [self.lock_x, self.lock_y, self.lock_z]
3824 else:
3825 lock = False
3826 move_verts(object, bm, False, move, lock, self.influence)
3828 # cleaning up
3829 terminate()
3831 return{'FINISHED'}
3834 # Annotation operator
3835 class RemoveAnnotation(Operator):
3836 bl_idname = "remove.annotation"
3837 bl_label = "Remove Annotation"
3838 bl_description = "Remove all Annotation Strokes"
3839 bl_options = {'REGISTER', 'UNDO'}
3841 def execute(self, context):
3843 try:
3844 bpy.data.grease_pencils[0].layers.active.clear()
3845 except:
3846 self.report({'INFO'}, "No Annotation data to Unlink")
3847 return {'CANCELLED'}
3849 return{'FINISHED'}
3851 # GPencil operator
3852 class RemoveGPencil(Operator):
3853 bl_idname = "remove.gp"
3854 bl_label = "Remove GPencil"
3855 bl_description = "Remove all GPencil Strokes"
3856 bl_options = {'REGISTER', 'UNDO'}
3858 def execute(self, context):
3860 try:
3861 looptools = context.window_manager.looptools
3862 looptools.gstretch_guide.data.layers.data.clear()
3863 looptools.gstretch_guide.data.update_tag()
3864 except:
3865 self.report({'INFO'}, "No GPencil data to Unlink")
3866 return {'CANCELLED'}
3868 return{'FINISHED'}
3871 class GStretch(Operator):
3872 bl_idname = "mesh.looptools_gstretch"
3873 bl_label = "Gstretch"
3874 bl_description = "Stretch selected vertices to active stroke"
3875 bl_options = {'REGISTER', 'UNDO'}
3877 conversion: EnumProperty(
3878 name="Conversion",
3879 items=(("distance", "Distance", "Set the distance between vertices "
3880 "of the converted stroke"),
3881 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
3882 "number of vertices that converted strokes will have"),
3883 ("vertices", "Exact vertices", "Set the exact number of vertices "
3884 "that converted strokes will have. Short strokes "
3885 "with few points may contain less vertices than this number."),
3886 ("none", "No simplification", "Convert each point "
3887 "to a vertex")),
3888 description="If strokes are converted to geometry, "
3889 "use this simplification method",
3890 default='limit_vertices'
3892 conversion_distance: FloatProperty(
3893 name="Distance",
3894 description="Absolute distance between vertices along the converted "
3895 " stroke",
3896 default=0.1,
3897 min=0.000001,
3898 soft_min=0.01,
3899 soft_max=100
3901 conversion_max: IntProperty(
3902 name="Max Vertices",
3903 description="Maximum number of vertices strokes will "
3904 "have, when they are converted to geomtery",
3905 default=32,
3906 min=3,
3907 soft_max=500,
3908 update=gstretch_update_min
3910 conversion_min: IntProperty(
3911 name="Min Vertices",
3912 description="Minimum number of vertices strokes will "
3913 "have, when they are converted to geomtery",
3914 default=8,
3915 min=3,
3916 soft_max=500,
3917 update=gstretch_update_max
3919 conversion_vertices: IntProperty(
3920 name="Vertices",
3921 description="Number of vertices strokes will "
3922 "have, when they are converted to geometry. If strokes have less "
3923 "points than required, the 'Spread evenly' method is used",
3924 default=32,
3925 min=3,
3926 soft_max=500
3928 delete_strokes: BoolProperty(
3929 name="Delete strokes",
3930 description="Remove strokes if they have been used."
3931 "WARNING: DOES NOT SUPPORT UNDO",
3932 default=False
3934 influence: FloatProperty(
3935 name="Influence",
3936 description="Force of the tool",
3937 default=100.0,
3938 min=0.0,
3939 max=100.0,
3940 precision=1,
3941 subtype='PERCENTAGE'
3943 lock_x: BoolProperty(
3944 name="Lock X",
3945 description="Lock editing of the x-coordinate",
3946 default=False
3948 lock_y: BoolProperty(
3949 name="Lock Y",
3950 description="Lock editing of the y-coordinate",
3951 default=False
3953 lock_z: BoolProperty(
3954 name="Lock Z",
3955 description="Lock editing of the z-coordinate",
3956 default=False
3958 method: EnumProperty(
3959 name="Method",
3960 items=(("project", "Project", "Project vertices onto the stroke, "
3961 "using vertex normals and connected edges"),
3962 ("irregular", "Spread", "Distribute vertices along the full "
3963 "stroke, retaining relative distances between the vertices"),
3964 ("regular", "Spread evenly", "Distribute vertices at regular "
3965 "distances along the full stroke")),
3966 description="Method of distributing the vertices over the "
3967 "stroke",
3968 default='regular'
3971 @classmethod
3972 def poll(cls, context):
3973 ob = context.active_object
3974 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3976 def draw(self, context):
3977 looptools = context.window_manager.looptools
3978 layout = self.layout
3979 col = layout.column()
3981 col.prop(self, "method")
3982 col.separator()
3984 col_conv = col.column(align=True)
3985 col_conv.prop(self, "conversion", text="")
3986 if self.conversion == 'distance':
3987 col_conv.prop(self, "conversion_distance")
3988 elif self.conversion == 'limit_vertices':
3989 row = col_conv.row(align=True)
3990 row.prop(self, "conversion_min", text="Min")
3991 row.prop(self, "conversion_max", text="Max")
3992 elif self.conversion == 'vertices':
3993 col_conv.prop(self, "conversion_vertices")
3994 col.separator()
3996 col_move = col.column(align=True)
3997 row = col_move.row(align=True)
3998 if self.lock_x:
3999 row.prop(self, "lock_x", text="X", icon='LOCKED')
4000 else:
4001 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
4002 if self.lock_y:
4003 row.prop(self, "lock_y", text="Y", icon='LOCKED')
4004 else:
4005 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
4006 if self.lock_z:
4007 row.prop(self, "lock_z", text="Z", icon='LOCKED')
4008 else:
4009 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
4010 col_move.prop(self, "influence")
4011 col.separator()
4012 if looptools.gstretch_use_guide == "Annotation":
4013 col.operator("remove.annotation", text="Delete annotation strokes")
4014 if looptools.gstretch_use_guide == "GPencil":
4015 col.operator("remove.gp", text="Delete GPencil strokes")
4017 def invoke(self, context, event):
4018 # flush cached strokes
4019 if 'Gstretch' in looptools_cache:
4020 looptools_cache['Gstretch']['single_loops'] = []
4021 # load custom settings
4022 settings_load(self)
4023 return self.execute(context)
4025 def execute(self, context):
4026 # initialise
4027 object, bm = initialise()
4028 settings_write(self)
4030 # check cache to see if we can save time
4031 cached, safe_strokes, loops, derived, mapping = cache_read("Gstretch",
4032 object, bm, False, False)
4033 if cached:
4034 straightening = False
4035 if safe_strokes:
4036 strokes = gstretch_safe_to_true_strokes(safe_strokes)
4037 # cached strokes were flushed (see operator's invoke function)
4038 elif get_strokes(self, context):
4039 strokes = gstretch_get_strokes(self, context)
4040 else:
4041 # straightening function (no GP) -> loops ignore modifiers
4042 straightening = True
4043 derived = False
4044 bm_mod = bm.copy()
4045 bm_mod.verts.ensure_lookup_table()
4046 bm_mod.edges.ensure_lookup_table()
4047 bm_mod.faces.ensure_lookup_table()
4048 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
4049 if not straightening:
4050 derived, bm_mod = get_derived_bmesh(object, bm)
4051 else:
4052 # get loops and strokes
4053 if get_strokes(self, context):
4054 # find loops
4055 derived, bm_mod, loops = get_connected_input(object, bm, input='selected')
4056 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4057 loops = check_loops(loops, mapping, bm_mod)
4058 # get strokes
4059 strokes = gstretch_get_strokes(self, context)
4060 else:
4061 # straightening function (no GP) -> loops ignore modifiers
4062 derived = False
4063 mapping = False
4064 bm_mod = bm.copy()
4065 bm_mod.verts.ensure_lookup_table()
4066 bm_mod.edges.ensure_lookup_table()
4067 bm_mod.faces.ensure_lookup_table()
4068 edge_keys = [
4069 edgekey(edge) for edge in bm_mod.edges if
4070 edge.select and not edge.hide
4072 loops = get_connected_selections(edge_keys)
4073 loops = check_loops(loops, mapping, bm_mod)
4074 # create fake strokes
4075 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
4077 # saving cache for faster execution next time
4078 if not cached:
4079 if strokes:
4080 safe_strokes = gstretch_true_to_safe_strokes(strokes)
4081 else:
4082 safe_strokes = []
4083 cache_write("Gstretch", object, bm, False, False,
4084 safe_strokes, loops, derived, mapping)
4086 # pair loops and strokes
4087 ls_pairs = gstretch_match_loops_strokes(loops, strokes, object, bm_mod)
4088 ls_pairs = gstretch_align_pairs(ls_pairs, object, bm_mod, self.method)
4090 move = []
4091 if not loops:
4092 # no selected geometry, convert GP to verts
4093 if strokes:
4094 move.append(gstretch_create_verts(object, bm, strokes,
4095 self.method, self.conversion, self.conversion_distance,
4096 self.conversion_max, self.conversion_min,
4097 self.conversion_vertices))
4098 for stroke in strokes:
4099 gstretch_erase_stroke(stroke, context)
4100 elif ls_pairs:
4101 for (loop, stroke) in ls_pairs:
4102 move.append(gstretch_calculate_verts(loop, stroke, object,
4103 bm_mod, self.method))
4104 if self.delete_strokes:
4105 if type(stroke) != bpy.types.GPencilStroke:
4106 # in case of cached fake stroke, get the real one
4107 if get_strokes(self, context):
4108 strokes = gstretch_get_strokes(self, context)
4109 if loops and strokes:
4110 ls_pairs = gstretch_match_loops_strokes(loops,
4111 strokes, object, bm_mod)
4112 ls_pairs = gstretch_align_pairs(ls_pairs,
4113 object, bm_mod, self.method)
4114 for (l, s) in ls_pairs:
4115 if l == loop:
4116 stroke = s
4117 break
4118 gstretch_erase_stroke(stroke, context)
4120 # move vertices to new locations
4121 if self.lock_x or self.lock_y or self.lock_z:
4122 lock = [self.lock_x, self.lock_y, self.lock_z]
4123 else:
4124 lock = False
4125 bmesh.update_edit_mesh(object.data, loop_triangles=True, destructive=True)
4126 move_verts(object, bm, mapping, move, lock, self.influence)
4128 # cleaning up
4129 if derived:
4130 bm_mod.free()
4131 terminate()
4133 return{'FINISHED'}
4136 # relax operator
4137 class Relax(Operator):
4138 bl_idname = "mesh.looptools_relax"
4139 bl_label = "Relax"
4140 bl_description = "Relax the loop, so it is smoother"
4141 bl_options = {'REGISTER', 'UNDO'}
4143 input: EnumProperty(
4144 name="Input",
4145 items=(("all", "Parallel (all)", "Also use non-selected "
4146 "parallel loops as input"),
4147 ("selected", "Selection", "Only use selected vertices as input")),
4148 description="Loops that are relaxed",
4149 default='selected'
4151 interpolation: EnumProperty(
4152 name="Interpolation",
4153 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4154 ("linear", "Linear", "Simple and fast linear algorithm")),
4155 description="Algorithm used for interpolation",
4156 default='cubic'
4158 iterations: EnumProperty(
4159 name="Iterations",
4160 items=(("1", "1", "One"),
4161 ("3", "3", "Three"),
4162 ("5", "5", "Five"),
4163 ("10", "10", "Ten"),
4164 ("25", "25", "Twenty-five")),
4165 description="Number of times the loop is relaxed",
4166 default="1"
4168 regular: BoolProperty(
4169 name="Regular",
4170 description="Distribute vertices at constant distances along the loop",
4171 default=True
4174 @classmethod
4175 def poll(cls, context):
4176 ob = context.active_object
4177 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4179 def draw(self, context):
4180 layout = self.layout
4181 col = layout.column()
4183 col.prop(self, "interpolation")
4184 col.prop(self, "input")
4185 col.prop(self, "iterations")
4186 col.prop(self, "regular")
4188 def invoke(self, context, event):
4189 # load custom settings
4190 settings_load(self)
4191 return self.execute(context)
4193 def execute(self, context):
4194 # initialise
4195 object, bm = initialise()
4196 settings_write(self)
4197 # check cache to see if we can save time
4198 cached, single_loops, loops, derived, mapping = cache_read("Relax",
4199 object, bm, self.input, False)
4200 if cached:
4201 derived, bm_mod = get_derived_bmesh(object, bm)
4202 else:
4203 # find loops
4204 derived, bm_mod, loops = get_connected_input(object, bm, self.input)
4205 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4206 loops = check_loops(loops, mapping, bm_mod)
4207 knots, points = relax_calculate_knots(loops)
4209 # saving cache for faster execution next time
4210 if not cached:
4211 cache_write("Relax", object, bm, self.input, False, False, loops,
4212 derived, mapping)
4214 for iteration in range(int(self.iterations)):
4215 # calculate splines and new positions
4216 tknots, tpoints = relax_calculate_t(bm_mod, knots, points,
4217 self.regular)
4218 splines = []
4219 for i in range(len(knots)):
4220 splines.append(calculate_splines(self.interpolation, bm_mod,
4221 tknots[i], knots[i]))
4222 move = [relax_calculate_verts(bm_mod, self.interpolation,
4223 tknots, knots, tpoints, points, splines)]
4224 move_verts(object, bm, mapping, move, False, -1)
4226 # cleaning up
4227 if derived:
4228 bm_mod.free()
4229 terminate()
4231 return{'FINISHED'}
4234 # space operator
4235 class Space(Operator):
4236 bl_idname = "mesh.looptools_space"
4237 bl_label = "Space"
4238 bl_description = "Space the vertices in a regular distribution on the loop"
4239 bl_options = {'REGISTER', 'UNDO'}
4241 influence: FloatProperty(
4242 name="Influence",
4243 description="Force of the tool",
4244 default=100.0,
4245 min=0.0,
4246 max=100.0,
4247 precision=1,
4248 subtype='PERCENTAGE'
4250 input: EnumProperty(
4251 name="Input",
4252 items=(("all", "Parallel (all)", "Also use non-selected "
4253 "parallel loops as input"),
4254 ("selected", "Selection", "Only use selected vertices as input")),
4255 description="Loops that are spaced",
4256 default='selected'
4258 interpolation: EnumProperty(
4259 name="Interpolation",
4260 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4261 ("linear", "Linear", "Vertices are projected on existing edges")),
4262 description="Algorithm used for interpolation",
4263 default='cubic'
4265 lock_x: BoolProperty(
4266 name="Lock X",
4267 description="Lock editing of the x-coordinate",
4268 default=False
4270 lock_y: BoolProperty(
4271 name="Lock Y",
4272 description="Lock editing of the y-coordinate",
4273 default=False
4275 lock_z: BoolProperty(
4276 name="Lock Z",
4277 description="Lock editing of the z-coordinate",
4278 default=False
4281 @classmethod
4282 def poll(cls, context):
4283 ob = context.active_object
4284 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4286 def draw(self, context):
4287 layout = self.layout
4288 col = layout.column()
4290 col.prop(self, "interpolation")
4291 col.prop(self, "input")
4292 col.separator()
4294 col_move = col.column(align=True)
4295 row = col_move.row(align=True)
4296 if self.lock_x:
4297 row.prop(self, "lock_x", text="X", icon='LOCKED')
4298 else:
4299 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
4300 if self.lock_y:
4301 row.prop(self, "lock_y", text="Y", icon='LOCKED')
4302 else:
4303 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
4304 if self.lock_z:
4305 row.prop(self, "lock_z", text="Z", icon='LOCKED')
4306 else:
4307 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
4308 col_move.prop(self, "influence")
4310 def invoke(self, context, event):
4311 # load custom settings
4312 settings_load(self)
4313 return self.execute(context)
4315 def execute(self, context):
4316 # initialise
4317 object, bm = initialise()
4318 settings_write(self)
4319 # check cache to see if we can save time
4320 cached, single_loops, loops, derived, mapping = cache_read("Space",
4321 object, bm, self.input, False)
4322 if cached:
4323 derived, bm_mod = get_derived_bmesh(object, bm)
4324 else:
4325 # find loops
4326 derived, bm_mod, loops = get_connected_input(object, bm, self.input)
4327 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4328 loops = check_loops(loops, mapping, bm_mod)
4330 # saving cache for faster execution next time
4331 if not cached:
4332 cache_write("Space", object, bm, self.input, False, False, loops,
4333 derived, mapping)
4335 move = []
4336 for loop in loops:
4337 # calculate splines and new positions
4338 if loop[1]: # circular
4339 loop[0].append(loop[0][0])
4340 tknots, tpoints = space_calculate_t(bm_mod, loop[0][:])
4341 splines = calculate_splines(self.interpolation, bm_mod,
4342 tknots, loop[0][:])
4343 move.append(space_calculate_verts(bm_mod, self.interpolation,
4344 tknots, tpoints, loop[0][:-1], splines))
4345 # move vertices to new locations
4346 if self.lock_x or self.lock_y or self.lock_z:
4347 lock = [self.lock_x, self.lock_y, self.lock_z]
4348 else:
4349 lock = False
4350 move_verts(object, bm, mapping, move, lock, self.influence)
4352 # cleaning up
4353 if derived:
4354 bm_mod.free()
4355 terminate()
4357 return{'FINISHED'}
4360 # ########################################
4361 # ##### GUI and registration #############
4362 # ########################################
4364 # menu containing all tools
4365 class VIEW3D_MT_edit_mesh_looptools(Menu):
4366 bl_label = "LoopTools"
4368 def draw(self, context):
4369 layout = self.layout
4371 layout.operator("mesh.looptools_bridge", text="Bridge").loft = False
4372 layout.operator("mesh.looptools_circle")
4373 layout.operator("mesh.looptools_curve")
4374 layout.operator("mesh.looptools_flatten")
4375 layout.operator("mesh.looptools_gstretch")
4376 layout.operator("mesh.looptools_bridge", text="Loft").loft = True
4377 layout.operator("mesh.looptools_relax")
4378 layout.operator("mesh.looptools_space")
4381 # panel containing all tools
4382 class VIEW3D_PT_tools_looptools(Panel):
4383 bl_space_type = 'VIEW_3D'
4384 bl_region_type = 'UI'
4385 bl_category = 'Edit'
4386 bl_context = "mesh_edit"
4387 bl_label = "LoopTools"
4388 bl_options = {'DEFAULT_CLOSED'}
4390 def draw(self, context):
4391 layout = self.layout
4392 col = layout.column(align=True)
4393 lt = context.window_manager.looptools
4395 # bridge - first line
4396 split = col.split(factor=0.15, align=True)
4397 if lt.display_bridge:
4398 split.prop(lt, "display_bridge", text="", icon='DOWNARROW_HLT')
4399 else:
4400 split.prop(lt, "display_bridge", text="", icon='RIGHTARROW')
4401 split.operator("mesh.looptools_bridge", text="Bridge").loft = False
4402 # bridge - settings
4403 if lt.display_bridge:
4404 box = col.column(align=True).box().column()
4405 # box.prop(self, "mode")
4407 # top row
4408 col_top = box.column(align=True)
4409 row = col_top.row(align=True)
4410 col_left = row.column(align=True)
4411 col_right = row.column(align=True)
4412 col_right.active = lt.bridge_segments != 1
4413 col_left.prop(lt, "bridge_segments")
4414 col_right.prop(lt, "bridge_min_width", text="")
4415 # bottom row
4416 bottom_left = col_left.row()
4417 bottom_left.active = lt.bridge_segments != 1
4418 bottom_left.prop(lt, "bridge_interpolation", text="")
4419 bottom_right = col_right.row()
4420 bottom_right.active = lt.bridge_interpolation == 'cubic'
4421 bottom_right.prop(lt, "bridge_cubic_strength")
4422 # boolean properties
4423 col_top.prop(lt, "bridge_remove_faces")
4425 # override properties
4426 col_top.separator()
4427 row = box.row(align=True)
4428 row.prop(lt, "bridge_twist")
4429 row.prop(lt, "bridge_reverse")
4431 # circle - first line
4432 split = col.split(factor=0.15, align=True)
4433 if lt.display_circle:
4434 split.prop(lt, "display_circle", text="", icon='DOWNARROW_HLT')
4435 else:
4436 split.prop(lt, "display_circle", text="", icon='RIGHTARROW')
4437 split.operator("mesh.looptools_circle")
4438 # circle - settings
4439 if lt.display_circle:
4440 box = col.column(align=True).box().column()
4441 box.prop(lt, "circle_fit")
4442 box.separator()
4444 box.prop(lt, "circle_flatten")
4445 row = box.row(align=True)
4446 row.prop(lt, "circle_custom_radius")
4447 row_right = row.row(align=True)
4448 row_right.active = lt.circle_custom_radius
4449 row_right.prop(lt, "circle_radius", text="")
4450 box.prop(lt, "circle_regular")
4451 box.separator()
4453 col_move = box.column(align=True)
4454 row = col_move.row(align=True)
4455 if lt.circle_lock_x:
4456 row.prop(lt, "circle_lock_x", text="X", icon='LOCKED')
4457 else:
4458 row.prop(lt, "circle_lock_x", text="X", icon='UNLOCKED')
4459 if lt.circle_lock_y:
4460 row.prop(lt, "circle_lock_y", text="Y", icon='LOCKED')
4461 else:
4462 row.prop(lt, "circle_lock_y", text="Y", icon='UNLOCKED')
4463 if lt.circle_lock_z:
4464 row.prop(lt, "circle_lock_z", text="Z", icon='LOCKED')
4465 else:
4466 row.prop(lt, "circle_lock_z", text="Z", icon='UNLOCKED')
4467 col_move.prop(lt, "circle_influence")
4469 # curve - first line
4470 split = col.split(factor=0.15, align=True)
4471 if lt.display_curve:
4472 split.prop(lt, "display_curve", text="", icon='DOWNARROW_HLT')
4473 else:
4474 split.prop(lt, "display_curve", text="", icon='RIGHTARROW')
4475 split.operator("mesh.looptools_curve")
4476 # curve - settings
4477 if lt.display_curve:
4478 box = col.column(align=True).box().column()
4479 box.prop(lt, "curve_interpolation")
4480 box.prop(lt, "curve_restriction")
4481 box.prop(lt, "curve_boundaries")
4482 box.prop(lt, "curve_regular")
4483 box.separator()
4485 col_move = box.column(align=True)
4486 row = col_move.row(align=True)
4487 if lt.curve_lock_x:
4488 row.prop(lt, "curve_lock_x", text="X", icon='LOCKED')
4489 else:
4490 row.prop(lt, "curve_lock_x", text="X", icon='UNLOCKED')
4491 if lt.curve_lock_y:
4492 row.prop(lt, "curve_lock_y", text="Y", icon='LOCKED')
4493 else:
4494 row.prop(lt, "curve_lock_y", text="Y", icon='UNLOCKED')
4495 if lt.curve_lock_z:
4496 row.prop(lt, "curve_lock_z", text="Z", icon='LOCKED')
4497 else:
4498 row.prop(lt, "curve_lock_z", text="Z", icon='UNLOCKED')
4499 col_move.prop(lt, "curve_influence")
4501 # flatten - first line
4502 split = col.split(factor=0.15, align=True)
4503 if lt.display_flatten:
4504 split.prop(lt, "display_flatten", text="", icon='DOWNARROW_HLT')
4505 else:
4506 split.prop(lt, "display_flatten", text="", icon='RIGHTARROW')
4507 split.operator("mesh.looptools_flatten")
4508 # flatten - settings
4509 if lt.display_flatten:
4510 box = col.column(align=True).box().column()
4511 box.prop(lt, "flatten_plane")
4512 # box.prop(lt, "flatten_restriction")
4513 box.separator()
4515 col_move = box.column(align=True)
4516 row = col_move.row(align=True)
4517 if lt.flatten_lock_x:
4518 row.prop(lt, "flatten_lock_x", text="X", icon='LOCKED')
4519 else:
4520 row.prop(lt, "flatten_lock_x", text="X", icon='UNLOCKED')
4521 if lt.flatten_lock_y:
4522 row.prop(lt, "flatten_lock_y", text="Y", icon='LOCKED')
4523 else:
4524 row.prop(lt, "flatten_lock_y", text="Y", icon='UNLOCKED')
4525 if lt.flatten_lock_z:
4526 row.prop(lt, "flatten_lock_z", text="Z", icon='LOCKED')
4527 else:
4528 row.prop(lt, "flatten_lock_z", text="Z", icon='UNLOCKED')
4529 col_move.prop(lt, "flatten_influence")
4531 # gstretch - first line
4532 split = col.split(factor=0.15, align=True)
4533 if lt.display_gstretch:
4534 split.prop(lt, "display_gstretch", text="", icon='DOWNARROW_HLT')
4535 else:
4536 split.prop(lt, "display_gstretch", text="", icon='RIGHTARROW')
4537 split.operator("mesh.looptools_gstretch")
4538 # gstretch settings
4539 if lt.display_gstretch:
4540 box = col.column(align=True).box().column()
4541 box.prop(lt, "gstretch_use_guide")
4542 if lt.gstretch_use_guide == "GPencil":
4543 box.prop(lt, "gstretch_guide")
4544 box.prop(lt, "gstretch_method")
4546 col_conv = box.column(align=True)
4547 col_conv.prop(lt, "gstretch_conversion", text="")
4548 if lt.gstretch_conversion == 'distance':
4549 col_conv.prop(lt, "gstretch_conversion_distance")
4550 elif lt.gstretch_conversion == 'limit_vertices':
4551 row = col_conv.row(align=True)
4552 row.prop(lt, "gstretch_conversion_min", text="Min")
4553 row.prop(lt, "gstretch_conversion_max", text="Max")
4554 elif lt.gstretch_conversion == 'vertices':
4555 col_conv.prop(lt, "gstretch_conversion_vertices")
4556 box.separator()
4558 col_move = box.column(align=True)
4559 row = col_move.row(align=True)
4560 if lt.gstretch_lock_x:
4561 row.prop(lt, "gstretch_lock_x", text="X", icon='LOCKED')
4562 else:
4563 row.prop(lt, "gstretch_lock_x", text="X", icon='UNLOCKED')
4564 if lt.gstretch_lock_y:
4565 row.prop(lt, "gstretch_lock_y", text="Y", icon='LOCKED')
4566 else:
4567 row.prop(lt, "gstretch_lock_y", text="Y", icon='UNLOCKED')
4568 if lt.gstretch_lock_z:
4569 row.prop(lt, "gstretch_lock_z", text="Z", icon='LOCKED')
4570 else:
4571 row.prop(lt, "gstretch_lock_z", text="Z", icon='UNLOCKED')
4572 col_move.prop(lt, "gstretch_influence")
4573 if lt.gstretch_use_guide == "Annotation":
4574 box.operator("remove.annotation", text="Delete Annotation Strokes")
4575 if lt.gstretch_use_guide == "GPencil":
4576 box.operator("remove.gp", text="Delete GPencil Strokes")
4578 # loft - first line
4579 split = col.split(factor=0.15, align=True)
4580 if lt.display_loft:
4581 split.prop(lt, "display_loft", text="", icon='DOWNARROW_HLT')
4582 else:
4583 split.prop(lt, "display_loft", text="", icon='RIGHTARROW')
4584 split.operator("mesh.looptools_bridge", text="Loft").loft = True
4585 # loft - settings
4586 if lt.display_loft:
4587 box = col.column(align=True).box().column()
4588 # box.prop(self, "mode")
4590 # top row
4591 col_top = box.column(align=True)
4592 row = col_top.row(align=True)
4593 col_left = row.column(align=True)
4594 col_right = row.column(align=True)
4595 col_right.active = lt.bridge_segments != 1
4596 col_left.prop(lt, "bridge_segments")
4597 col_right.prop(lt, "bridge_min_width", text="")
4598 # bottom row
4599 bottom_left = col_left.row()
4600 bottom_left.active = lt.bridge_segments != 1
4601 bottom_left.prop(lt, "bridge_interpolation", text="")
4602 bottom_right = col_right.row()
4603 bottom_right.active = lt.bridge_interpolation == 'cubic'
4604 bottom_right.prop(lt, "bridge_cubic_strength")
4605 # boolean properties
4606 col_top.prop(lt, "bridge_remove_faces")
4607 col_top.prop(lt, "bridge_loft_loop")
4609 # override properties
4610 col_top.separator()
4611 row = box.row(align=True)
4612 row.prop(lt, "bridge_twist")
4613 row.prop(lt, "bridge_reverse")
4615 # relax - first line
4616 split = col.split(factor=0.15, align=True)
4617 if lt.display_relax:
4618 split.prop(lt, "display_relax", text="", icon='DOWNARROW_HLT')
4619 else:
4620 split.prop(lt, "display_relax", text="", icon='RIGHTARROW')
4621 split.operator("mesh.looptools_relax")
4622 # relax - settings
4623 if lt.display_relax:
4624 box = col.column(align=True).box().column()
4625 box.prop(lt, "relax_interpolation")
4626 box.prop(lt, "relax_input")
4627 box.prop(lt, "relax_iterations")
4628 box.prop(lt, "relax_regular")
4630 # space - first line
4631 split = col.split(factor=0.15, align=True)
4632 if lt.display_space:
4633 split.prop(lt, "display_space", text="", icon='DOWNARROW_HLT')
4634 else:
4635 split.prop(lt, "display_space", text="", icon='RIGHTARROW')
4636 split.operator("mesh.looptools_space")
4637 # space - settings
4638 if lt.display_space:
4639 box = col.column(align=True).box().column()
4640 box.prop(lt, "space_interpolation")
4641 box.prop(lt, "space_input")
4642 box.separator()
4644 col_move = box.column(align=True)
4645 row = col_move.row(align=True)
4646 if lt.space_lock_x:
4647 row.prop(lt, "space_lock_x", text="X", icon='LOCKED')
4648 else:
4649 row.prop(lt, "space_lock_x", text="X", icon='UNLOCKED')
4650 if lt.space_lock_y:
4651 row.prop(lt, "space_lock_y", text="Y", icon='LOCKED')
4652 else:
4653 row.prop(lt, "space_lock_y", text="Y", icon='UNLOCKED')
4654 if lt.space_lock_z:
4655 row.prop(lt, "space_lock_z", text="Z", icon='LOCKED')
4656 else:
4657 row.prop(lt, "space_lock_z", text="Z", icon='UNLOCKED')
4658 col_move.prop(lt, "space_influence")
4661 # property group containing all properties for the gui in the panel
4662 class LoopToolsProps(PropertyGroup):
4664 Fake module like class
4665 bpy.context.window_manager.looptools
4667 # general display properties
4668 display_bridge: BoolProperty(
4669 name="Bridge settings",
4670 description="Display settings of the Bridge tool",
4671 default=False
4673 display_circle: BoolProperty(
4674 name="Circle settings",
4675 description="Display settings of the Circle tool",
4676 default=False
4678 display_curve: BoolProperty(
4679 name="Curve settings",
4680 description="Display settings of the Curve tool",
4681 default=False
4683 display_flatten: BoolProperty(
4684 name="Flatten settings",
4685 description="Display settings of the Flatten tool",
4686 default=False
4688 display_gstretch: BoolProperty(
4689 name="Gstretch settings",
4690 description="Display settings of the Gstretch tool",
4691 default=False
4693 display_loft: BoolProperty(
4694 name="Loft settings",
4695 description="Display settings of the Loft tool",
4696 default=False
4698 display_relax: BoolProperty(
4699 name="Relax settings",
4700 description="Display settings of the Relax tool",
4701 default=False
4703 display_space: BoolProperty(
4704 name="Space settings",
4705 description="Display settings of the Space tool",
4706 default=False
4709 # bridge properties
4710 bridge_cubic_strength: FloatProperty(
4711 name="Strength",
4712 description="Higher strength results in more fluid curves",
4713 default=1.0,
4714 soft_min=-3.0,
4715 soft_max=3.0
4717 bridge_interpolation: EnumProperty(
4718 name="Interpolation mode",
4719 items=(('cubic', "Cubic", "Gives curved results"),
4720 ('linear', "Linear", "Basic, fast, straight interpolation")),
4721 description="Interpolation mode: algorithm used when creating segments",
4722 default='cubic'
4724 bridge_loft: BoolProperty(
4725 name="Loft",
4726 description="Loft multiple loops, instead of considering them as "
4727 "a multi-input for bridging",
4728 default=False
4730 bridge_loft_loop: BoolProperty(
4731 name="Loop",
4732 description="Connect the first and the last loop with each other",
4733 default=False
4735 bridge_min_width: IntProperty(
4736 name="Minimum width",
4737 description="Segments with an edge smaller than this are merged "
4738 "(compared to base edge)",
4739 default=0,
4740 min=0,
4741 max=100,
4742 subtype='PERCENTAGE'
4744 bridge_mode: EnumProperty(
4745 name="Mode",
4746 items=(('basic', "Basic", "Fast algorithm"),
4747 ('shortest', "Shortest edge", "Slower algorithm with "
4748 "better vertex matching")),
4749 description="Algorithm used for bridging",
4750 default='shortest'
4752 bridge_remove_faces: BoolProperty(
4753 name="Remove faces",
4754 description="Remove faces that are internal after bridging",
4755 default=True
4757 bridge_reverse: BoolProperty(
4758 name="Reverse",
4759 description="Manually override the direction in which the loops "
4760 "are bridged. Only use if the tool gives the wrong result",
4761 default=False
4763 bridge_segments: IntProperty(
4764 name="Segments",
4765 description="Number of segments used to bridge the gap (0=automatic)",
4766 default=1,
4767 min=0,
4768 soft_max=20
4770 bridge_twist: IntProperty(
4771 name="Twist",
4772 description="Twist what vertices are connected to each other",
4773 default=0
4776 # circle properties
4777 circle_custom_radius: BoolProperty(
4778 name="Radius",
4779 description="Force a custom radius",
4780 default=False
4782 circle_fit: EnumProperty(
4783 name="Method",
4784 items=(("best", "Best fit", "Non-linear least squares"),
4785 ("inside", "Fit inside", "Only move vertices towards the center")),
4786 description="Method used for fitting a circle to the vertices",
4787 default='best'
4789 circle_flatten: BoolProperty(
4790 name="Flatten",
4791 description="Flatten the circle, instead of projecting it on the mesh",
4792 default=True
4794 circle_influence: FloatProperty(
4795 name="Influence",
4796 description="Force of the tool",
4797 default=100.0,
4798 min=0.0,
4799 max=100.0,
4800 precision=1,
4801 subtype='PERCENTAGE'
4803 circle_lock_x: BoolProperty(
4804 name="Lock X",
4805 description="Lock editing of the x-coordinate",
4806 default=False
4808 circle_lock_y: BoolProperty(
4809 name="Lock Y",
4810 description="Lock editing of the y-coordinate",
4811 default=False
4813 circle_lock_z: BoolProperty(
4814 name="Lock Z",
4815 description="Lock editing of the z-coordinate",
4816 default=False
4818 circle_radius: FloatProperty(
4819 name="Radius",
4820 description="Custom radius for circle",
4821 default=1.0,
4822 min=0.0,
4823 soft_max=1000.0
4825 circle_regular: BoolProperty(
4826 name="Regular",
4827 description="Distribute vertices at constant distances along the circle",
4828 default=True
4830 # curve properties
4831 curve_boundaries: BoolProperty(
4832 name="Boundaries",
4833 description="Limit the tool to work within the boundaries of the "
4834 "selected vertices",
4835 default=False
4837 curve_influence: FloatProperty(
4838 name="Influence",
4839 description="Force of the tool",
4840 default=100.0,
4841 min=0.0,
4842 max=100.0,
4843 precision=1,
4844 subtype='PERCENTAGE'
4846 curve_interpolation: EnumProperty(
4847 name="Interpolation",
4848 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4849 ("linear", "Linear", "Simple and fast linear algorithm")),
4850 description="Algorithm used for interpolation",
4851 default='cubic'
4853 curve_lock_x: BoolProperty(
4854 name="Lock X",
4855 description="Lock editing of the x-coordinate",
4856 default=False
4858 curve_lock_y: BoolProperty(
4859 name="Lock Y",
4860 description="Lock editing of the y-coordinate",
4861 default=False
4863 curve_lock_z: BoolProperty(
4864 name="Lock Z",
4865 description="Lock editing of the z-coordinate",
4866 default=False
4868 curve_regular: BoolProperty(
4869 name="Regular",
4870 description="Distribute vertices at constant distances along the curve",
4871 default=True
4873 curve_restriction: EnumProperty(
4874 name="Restriction",
4875 items=(("none", "None", "No restrictions on vertex movement"),
4876 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
4877 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
4878 description="Restrictions on how the vertices can be moved",
4879 default='none'
4882 # flatten properties
4883 flatten_influence: FloatProperty(
4884 name="Influence",
4885 description="Force of the tool",
4886 default=100.0,
4887 min=0.0,
4888 max=100.0,
4889 precision=1,
4890 subtype='PERCENTAGE'
4892 flatten_lock_x: BoolProperty(
4893 name="Lock X",
4894 description="Lock editing of the x-coordinate",
4895 default=False)
4896 flatten_lock_y: BoolProperty(name="Lock Y",
4897 description="Lock editing of the y-coordinate",
4898 default=False
4900 flatten_lock_z: BoolProperty(
4901 name="Lock Z",
4902 description="Lock editing of the z-coordinate",
4903 default=False
4905 flatten_plane: EnumProperty(
4906 name="Plane",
4907 items=(("best_fit", "Best fit", "Calculate a best fitting plane"),
4908 ("normal", "Normal", "Derive plane from averaging vertex "
4909 "normals"),
4910 ("view", "View", "Flatten on a plane perpendicular to the "
4911 "viewing angle")),
4912 description="Plane on which vertices are flattened",
4913 default='best_fit'
4915 flatten_restriction: EnumProperty(
4916 name="Restriction",
4917 items=(("none", "None", "No restrictions on vertex movement"),
4918 ("bounding_box", "Bounding box", "Vertices are restricted to "
4919 "movement inside the bounding box of the selection")),
4920 description="Restrictions on how the vertices can be moved",
4921 default='none'
4924 # gstretch properties
4925 gstretch_conversion: EnumProperty(
4926 name="Conversion",
4927 items=(("distance", "Distance", "Set the distance between vertices "
4928 "of the converted stroke"),
4929 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
4930 "number of vertices that converted GP strokes will have"),
4931 ("vertices", "Exact vertices", "Set the exact number of vertices "
4932 "that converted strokes will have. Short strokes "
4933 "with few points may contain less vertices than this number."),
4934 ("none", "No simplification", "Convert each point "
4935 "to a vertex")),
4936 description="If strokes are converted to geometry, "
4937 "use this simplification method",
4938 default='limit_vertices'
4940 gstretch_conversion_distance: FloatProperty(
4941 name="Distance",
4942 description="Absolute distance between vertices along the converted "
4943 "stroke",
4944 default=0.1,
4945 min=0.000001,
4946 soft_min=0.01,
4947 soft_max=100
4949 gstretch_conversion_max: IntProperty(
4950 name="Max Vertices",
4951 description="Maximum number of vertices strokes will "
4952 "have, when they are converted to geomtery",
4953 default=32,
4954 min=3,
4955 soft_max=500,
4956 update=gstretch_update_min
4958 gstretch_conversion_min: IntProperty(
4959 name="Min Vertices",
4960 description="Minimum number of vertices strokes will "
4961 "have, when they are converted to geomtery",
4962 default=8,
4963 min=3,
4964 soft_max=500,
4965 update=gstretch_update_max
4967 gstretch_conversion_vertices: IntProperty(
4968 name="Vertices",
4969 description="Number of vertices strokes will "
4970 "have, when they are converted to geometry. If strokes have less "
4971 "points than required, the 'Spread evenly' method is used",
4972 default=32,
4973 min=3,
4974 soft_max=500
4976 gstretch_delete_strokes: BoolProperty(
4977 name="Delete strokes",
4978 description="Remove Grease Pencil strokes if they have been used "
4979 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
4980 default=False
4982 gstretch_influence: FloatProperty(
4983 name="Influence",
4984 description="Force of the tool",
4985 default=100.0,
4986 min=0.0,
4987 max=100.0,
4988 precision=1,
4989 subtype='PERCENTAGE'
4991 gstretch_lock_x: BoolProperty(
4992 name="Lock X",
4993 description="Lock editing of the x-coordinate",
4994 default=False
4996 gstretch_lock_y: BoolProperty(
4997 name="Lock Y",
4998 description="Lock editing of the y-coordinate",
4999 default=False
5001 gstretch_lock_z: BoolProperty(
5002 name="Lock Z",
5003 description="Lock editing of the z-coordinate",
5004 default=False
5006 gstretch_method: EnumProperty(
5007 name="Method",
5008 items=(("project", "Project", "Project vertices onto the stroke, "
5009 "using vertex normals and connected edges"),
5010 ("irregular", "Spread", "Distribute vertices along the full "
5011 "stroke, retaining relative distances between the vertices"),
5012 ("regular", "Spread evenly", "Distribute vertices at regular "
5013 "distances along the full stroke")),
5014 description="Method of distributing the vertices over the Grease "
5015 "Pencil stroke",
5016 default='regular'
5018 gstretch_use_guide: EnumProperty(
5019 name="Use guides",
5020 items=(("None", "None", "None"),
5021 ("Annotation", "Annotation", "Annotation"),
5022 ("GPencil", "GPencil", "GPencil")),
5023 default="None"
5025 gstretch_guide: PointerProperty(
5026 name="GPencil object",
5027 description="Set GPencil object",
5028 type=bpy.types.Object
5031 # relax properties
5032 relax_input: EnumProperty(name="Input",
5033 items=(("all", "Parallel (all)", "Also use non-selected "
5034 "parallel loops as input"),
5035 ("selected", "Selection", "Only use selected vertices as input")),
5036 description="Loops that are relaxed",
5037 default='selected'
5039 relax_interpolation: EnumProperty(
5040 name="Interpolation",
5041 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5042 ("linear", "Linear", "Simple and fast linear algorithm")),
5043 description="Algorithm used for interpolation",
5044 default='cubic'
5046 relax_iterations: EnumProperty(name="Iterations",
5047 items=(("1", "1", "One"),
5048 ("3", "3", "Three"),
5049 ("5", "5", "Five"),
5050 ("10", "10", "Ten"),
5051 ("25", "25", "Twenty-five")),
5052 description="Number of times the loop is relaxed",
5053 default="1"
5055 relax_regular: BoolProperty(
5056 name="Regular",
5057 description="Distribute vertices at constant distances along the loop",
5058 default=True
5061 # space properties
5062 space_influence: FloatProperty(
5063 name="Influence",
5064 description="Force of the tool",
5065 default=100.0,
5066 min=0.0,
5067 max=100.0,
5068 precision=1,
5069 subtype='PERCENTAGE'
5071 space_input: EnumProperty(
5072 name="Input",
5073 items=(("all", "Parallel (all)", "Also use non-selected "
5074 "parallel loops as input"),
5075 ("selected", "Selection", "Only use selected vertices as input")),
5076 description="Loops that are spaced",
5077 default='selected'
5079 space_interpolation: EnumProperty(
5080 name="Interpolation",
5081 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5082 ("linear", "Linear", "Vertices are projected on existing edges")),
5083 description="Algorithm used for interpolation",
5084 default='cubic'
5086 space_lock_x: BoolProperty(
5087 name="Lock X",
5088 description="Lock editing of the x-coordinate",
5089 default=False
5091 space_lock_y: BoolProperty(
5092 name="Lock Y",
5093 description="Lock editing of the y-coordinate",
5094 default=False
5096 space_lock_z: BoolProperty(
5097 name="Lock Z",
5098 description="Lock editing of the z-coordinate",
5099 default=False
5102 # draw function for integration in menus
5103 def menu_func(self, context):
5104 self.layout.menu("VIEW3D_MT_edit_mesh_looptools")
5105 self.layout.separator()
5108 # Add-ons Preferences Update Panel
5110 # Define Panel classes for updating
5111 panels = (
5112 VIEW3D_PT_tools_looptools,
5116 def update_panel(self, context):
5117 message = "LoopTools: Updating Panel locations has failed"
5118 try:
5119 for panel in panels:
5120 if "bl_rna" in panel.__dict__:
5121 bpy.utils.unregister_class(panel)
5123 for panel in panels:
5124 panel.bl_category = context.preferences.addons[__name__].preferences.category
5125 bpy.utils.register_class(panel)
5127 except Exception as e:
5128 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
5129 pass
5132 class LoopPreferences(AddonPreferences):
5133 # this must match the addon name, use '__package__'
5134 # when defining this in a submodule of a python package.
5135 bl_idname = __name__
5137 category: StringProperty(
5138 name="Tab Category",
5139 description="Choose a name for the category of the panel",
5140 default="Edit",
5141 update=update_panel
5144 def draw(self, context):
5145 layout = self.layout
5147 row = layout.row()
5148 col = row.column()
5149 col.label(text="Tab Category:")
5150 col.prop(self, "category", text="")
5153 # define classes for registration
5154 classes = (
5155 VIEW3D_MT_edit_mesh_looptools,
5156 VIEW3D_PT_tools_looptools,
5157 LoopToolsProps,
5158 Bridge,
5159 Circle,
5160 Curve,
5161 Flatten,
5162 GStretch,
5163 Relax,
5164 Space,
5165 LoopPreferences,
5166 RemoveAnnotation,
5167 RemoveGPencil,
5171 # registering and menu integration
5172 def register():
5173 for cls in classes:
5174 bpy.utils.register_class(cls)
5175 bpy.types.VIEW3D_MT_edit_mesh_context_menu.prepend(menu_func)
5176 bpy.types.WindowManager.looptools = PointerProperty(type=LoopToolsProps)
5177 update_panel(None, bpy.context)
5180 # unregistering and removing menus
5181 def unregister():
5182 for cls in reversed(classes):
5183 bpy.utils.unregister_class(cls)
5184 bpy.types.VIEW3D_MT_edit_mesh_context_menu.remove(menu_func)
5185 try:
5186 del bpy.types.WindowManager.looptools
5187 except Exception as e:
5188 print('unregister fail:\n', e)
5189 pass
5192 if __name__ == "__main__":
5193 register()