Merge branch 'blender-v2.81-release'
[blender-addons.git] / mesh_looptools.py
blob67855477c4e8d2dc8fef252d4d321b84c9e69d92
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, 8),
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 # Calculate length with double precision to avoid problems with `inf`
294 vec2_length = math.sqrt(vec2[0] ** 2 + vec2[1] ** 2 + vec2[2] ** 2)
295 if vec2_length != 0:
296 vec2 /= vec2_length
297 if vec2 == vec:
298 break
299 if vec2.length == 0:
300 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
301 normal = vec2
303 elif method == 'normal':
304 # averaging the vertex normals
305 v_normals = [bm_mod.verts[v].normal for v in loop[0]]
306 normal = mathutils.Vector()
307 for v_normal in v_normals:
308 normal += v_normal
309 normal /= len(v_normals)
310 normal.normalize()
312 elif method == 'view':
313 # calculate view normal
314 rotation = bpy.context.space_data.region_3d.view_matrix.to_3x3().\
315 inverted()
316 normal = rotation @ mathutils.Vector((0.0, 0.0, 1.0))
317 if object:
318 normal = object.matrix_world.inverted().to_euler().to_matrix() @ \
319 normal
321 return(com, normal)
324 # calculate splines based on given interpolation method (controller function)
325 def calculate_splines(interpolation, bm_mod, tknots, knots):
326 if interpolation == 'cubic':
327 splines = calculate_cubic_splines(bm_mod, tknots, knots[:])
328 else: # interpolations == 'linear'
329 splines = calculate_linear_splines(bm_mod, tknots, knots[:])
331 return(splines)
334 # check loops and only return valid ones
335 def check_loops(loops, mapping, bm_mod):
336 valid_loops = []
337 for loop, circular in loops:
338 # loop needs to have at least 3 vertices
339 if len(loop) < 3:
340 continue
341 # loop needs at least 1 vertex in the original, non-mirrored mesh
342 if mapping:
343 all_virtual = True
344 for vert in loop:
345 if mapping[vert] > -1:
346 all_virtual = False
347 break
348 if all_virtual:
349 continue
350 # vertices can not all be at the same location
351 stacked = True
352 for i in range(len(loop) - 1):
353 if (bm_mod.verts[loop[i]].co - bm_mod.verts[loop[i + 1]].co).length > 1e-6:
354 stacked = False
355 break
356 if stacked:
357 continue
358 # passed all tests, loop is valid
359 valid_loops.append([loop, circular])
361 return(valid_loops)
364 # input: bmesh, output: dict with the edge-key as key and face-index as value
365 def dict_edge_faces(bm):
366 edge_faces = dict([[edgekey(edge), []] for edge in bm.edges if not edge.hide])
367 for face in bm.faces:
368 if face.hide:
369 continue
370 for key in face_edgekeys(face):
371 edge_faces[key].append(face.index)
373 return(edge_faces)
376 # input: bmesh (edge-faces optional), output: dict with face-face connections
377 def dict_face_faces(bm, edge_faces=False):
378 if not edge_faces:
379 edge_faces = dict_edge_faces(bm)
381 connected_faces = dict([[face.index, []] for face in bm.faces if not face.hide])
382 for face in bm.faces:
383 if face.hide:
384 continue
385 for edge_key in face_edgekeys(face):
386 for connected_face in edge_faces[edge_key]:
387 if connected_face == face.index:
388 continue
389 connected_faces[face.index].append(connected_face)
391 return(connected_faces)
394 # input: bmesh, output: dict with the vert index as key and edge-keys as value
395 def dict_vert_edges(bm):
396 vert_edges = dict([[v.index, []] for v in bm.verts if not v.hide])
397 for edge in bm.edges:
398 if edge.hide:
399 continue
400 ek = edgekey(edge)
401 for vert in ek:
402 vert_edges[vert].append(ek)
404 return(vert_edges)
407 # input: bmesh, output: dict with the vert index as key and face index as value
408 def dict_vert_faces(bm):
409 vert_faces = dict([[v.index, []] for v in bm.verts if not v.hide])
410 for face in bm.faces:
411 if not face.hide:
412 for vert in face.verts:
413 vert_faces[vert.index].append(face.index)
415 return(vert_faces)
418 # input: list of edge-keys, output: dictionary with vertex-vertex connections
419 def dict_vert_verts(edge_keys):
420 # create connection data
421 vert_verts = {}
422 for ek in edge_keys:
423 for i in range(2):
424 if ek[i] in vert_verts:
425 vert_verts[ek[i]].append(ek[1 - i])
426 else:
427 vert_verts[ek[i]] = [ek[1 - i]]
429 return(vert_verts)
432 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
433 def edgekey(edge):
434 return(tuple(sorted([edge.verts[0].index, edge.verts[1].index])))
437 # returns the edgekeys of a bmesh face
438 def face_edgekeys(face):
439 return([tuple(sorted([edge.verts[0].index, edge.verts[1].index])) for edge in face.edges])
442 # calculate input loops
443 def get_connected_input(object, bm, input):
444 # get mesh with modifiers applied
445 derived, bm_mod = get_derived_bmesh(object, bm)
447 # calculate selected loops
448 edge_keys = [edgekey(edge) for edge in bm_mod.edges if edge.select and not edge.hide]
449 loops = get_connected_selections(edge_keys)
451 # if only selected loops are needed, we're done
452 if input == 'selected':
453 return(derived, bm_mod, loops)
454 # elif input == 'all':
455 loops = get_parallel_loops(bm_mod, loops)
457 return(derived, bm_mod, loops)
460 # sorts all edge-keys into a list of loops
461 def get_connected_selections(edge_keys):
462 # create connection data
463 vert_verts = dict_vert_verts(edge_keys)
465 # find loops consisting of connected selected edges
466 loops = []
467 while len(vert_verts) > 0:
468 loop = [iter(vert_verts.keys()).__next__()]
469 growing = True
470 flipped = False
472 # extend loop
473 while growing:
474 # no more connection data for current vertex
475 if loop[-1] not in vert_verts:
476 if not flipped:
477 loop.reverse()
478 flipped = True
479 else:
480 growing = False
481 else:
482 extended = False
483 for i, next_vert in enumerate(vert_verts[loop[-1]]):
484 if next_vert not in loop:
485 vert_verts[loop[-1]].pop(i)
486 if len(vert_verts[loop[-1]]) == 0:
487 del vert_verts[loop[-1]]
488 # remove connection both ways
489 if next_vert in vert_verts:
490 if len(vert_verts[next_vert]) == 1:
491 del vert_verts[next_vert]
492 else:
493 vert_verts[next_vert].remove(loop[-1])
494 loop.append(next_vert)
495 extended = True
496 break
497 if not extended:
498 # found one end of the loop, continue with next
499 if not flipped:
500 loop.reverse()
501 flipped = True
502 # found both ends of the loop, stop growing
503 else:
504 growing = False
506 # check if loop is circular
507 if loop[0] in vert_verts:
508 if loop[-1] in vert_verts[loop[0]]:
509 # is circular
510 if len(vert_verts[loop[0]]) == 1:
511 del vert_verts[loop[0]]
512 else:
513 vert_verts[loop[0]].remove(loop[-1])
514 if len(vert_verts[loop[-1]]) == 1:
515 del vert_verts[loop[-1]]
516 else:
517 vert_verts[loop[-1]].remove(loop[0])
518 loop = [loop, True]
519 else:
520 # not circular
521 loop = [loop, False]
522 else:
523 # not circular
524 loop = [loop, False]
526 loops.append(loop)
528 return(loops)
531 # get the derived mesh data, if there is a mirror modifier
532 def get_derived_bmesh(object, bm):
533 # check for mirror modifiers
534 if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
535 derived = True
536 # disable other modifiers
537 show_viewport = [mod.name for mod in object.modifiers if mod.show_viewport]
538 for mod in object.modifiers:
539 if mod.type != 'MIRROR':
540 mod.show_viewport = False
541 # get derived mesh
542 bm_mod = bmesh.new()
543 depsgraph = bpy.context.evaluated_depsgraph_get()
544 object_eval = object.evaluated_get(depsgraph)
545 mesh_mod = object_eval.to_mesh()
546 bm_mod.from_mesh(mesh_mod)
547 object_eval.to_mesh_clear()
548 # re-enable other modifiers
549 for mod_name in show_viewport:
550 object.modifiers[mod_name].show_viewport = True
551 # no mirror modifiers, so no derived mesh necessary
552 else:
553 derived = False
554 bm_mod = bm
556 bm_mod.verts.ensure_lookup_table()
557 bm_mod.edges.ensure_lookup_table()
558 bm_mod.faces.ensure_lookup_table()
560 return(derived, bm_mod)
563 # return a mapping of derived indices to indices
564 def get_mapping(derived, bm, bm_mod, single_vertices, full_search, loops):
565 if not derived:
566 return(False)
568 if full_search:
569 verts = [v for v in bm.verts if not v.hide]
570 else:
571 verts = [v for v in bm.verts if v.select and not v.hide]
573 # non-selected vertices around single vertices also need to be mapped
574 if single_vertices:
575 mapping = dict([[vert, -1] for vert in single_vertices])
576 verts_mod = [bm_mod.verts[vert] for vert in single_vertices]
577 for v in verts:
578 for v_mod in verts_mod:
579 if (v.co - v_mod.co).length < 1e-6:
580 mapping[v_mod.index] = v.index
581 break
582 real_singles = [v_real for v_real in mapping.values() if v_real > -1]
584 verts_indices = [vert.index for vert in verts]
585 for face in [face for face in bm.faces if not face.select and not face.hide]:
586 for vert in face.verts:
587 if vert.index in real_singles:
588 for v in face.verts:
589 if v.index not in verts_indices:
590 if v not in verts:
591 verts.append(v)
592 break
594 # create mapping of derived indices to indices
595 mapping = dict([[vert, -1] for loop in loops for vert in loop[0]])
596 if single_vertices:
597 for single in single_vertices:
598 mapping[single] = -1
599 verts_mod = [bm_mod.verts[i] for i in mapping.keys()]
600 for v in verts:
601 for v_mod in verts_mod:
602 if (v.co - v_mod.co).length < 1e-6:
603 mapping[v_mod.index] = v.index
604 verts_mod.remove(v_mod)
605 break
607 return(mapping)
610 # calculate the determinant of a matrix
611 def matrix_determinant(m):
612 determinant = m[0][0] * m[1][1] * m[2][2] + m[0][1] * m[1][2] * m[2][0] \
613 + m[0][2] * m[1][0] * m[2][1] - m[0][2] * m[1][1] * m[2][0] \
614 - m[0][1] * m[1][0] * m[2][2] - m[0][0] * m[1][2] * m[2][1]
616 return(determinant)
619 # custom matrix inversion, to provide higher precision than the built-in one
620 def matrix_invert(m):
621 r = mathutils.Matrix((
622 (m[1][1] * m[2][2] - m[1][2] * m[2][1], m[0][2] * m[2][1] - m[0][1] * m[2][2],
623 m[0][1] * m[1][2] - m[0][2] * m[1][1]),
624 (m[1][2] * m[2][0] - m[1][0] * m[2][2], m[0][0] * m[2][2] - m[0][2] * m[2][0],
625 m[0][2] * m[1][0] - m[0][0] * m[1][2]),
626 (m[1][0] * m[2][1] - m[1][1] * m[2][0], m[0][1] * m[2][0] - m[0][0] * m[2][1],
627 m[0][0] * m[1][1] - m[0][1] * m[1][0])))
629 return (r * (1 / matrix_determinant(m)))
632 # returns a list of all loops parallel to the input, input included
633 def get_parallel_loops(bm_mod, loops):
634 # get required dictionaries
635 edge_faces = dict_edge_faces(bm_mod)
636 connected_faces = dict_face_faces(bm_mod, edge_faces)
637 # turn vertex loops into edge loops
638 edgeloops = []
639 for loop in loops:
640 edgeloop = [[sorted([loop[0][i], loop[0][i + 1]]) for i in
641 range(len(loop[0]) - 1)], loop[1]]
642 if loop[1]: # circular
643 edgeloop[0].append(sorted([loop[0][-1], loop[0][0]]))
644 edgeloops.append(edgeloop[:])
645 # variables to keep track while iterating
646 all_edgeloops = []
647 has_branches = False
649 for loop in edgeloops:
650 # initialise with original loop
651 all_edgeloops.append(loop[0])
652 newloops = [loop[0]]
653 verts_used = []
654 for edge in loop[0]:
655 if edge[0] not in verts_used:
656 verts_used.append(edge[0])
657 if edge[1] not in verts_used:
658 verts_used.append(edge[1])
660 # find parallel loops
661 while len(newloops) > 0:
662 side_a = []
663 side_b = []
664 for i in newloops[-1]:
665 i = tuple(i)
666 forbidden_side = False
667 if i not in edge_faces:
668 # weird input with branches
669 has_branches = True
670 break
671 for face in edge_faces[i]:
672 if len(side_a) == 0 and forbidden_side != "a":
673 side_a.append(face)
674 if forbidden_side:
675 break
676 forbidden_side = "a"
677 continue
678 elif side_a[-1] in connected_faces[face] and \
679 forbidden_side != "a":
680 side_a.append(face)
681 if forbidden_side:
682 break
683 forbidden_side = "a"
684 continue
685 if len(side_b) == 0 and forbidden_side != "b":
686 side_b.append(face)
687 if forbidden_side:
688 break
689 forbidden_side = "b"
690 continue
691 elif side_b[-1] in connected_faces[face] and \
692 forbidden_side != "b":
693 side_b.append(face)
694 if forbidden_side:
695 break
696 forbidden_side = "b"
697 continue
699 if has_branches:
700 # weird input with branches
701 break
703 newloops.pop(-1)
704 sides = []
705 if side_a:
706 sides.append(side_a)
707 if side_b:
708 sides.append(side_b)
710 for side in sides:
711 extraloop = []
712 for fi in side:
713 for key in face_edgekeys(bm_mod.faces[fi]):
714 if key[0] not in verts_used and key[1] not in \
715 verts_used:
716 extraloop.append(key)
717 break
718 if extraloop:
719 for key in extraloop:
720 for new_vert in key:
721 if new_vert not in verts_used:
722 verts_used.append(new_vert)
723 newloops.append(extraloop)
724 all_edgeloops.append(extraloop)
726 # input contains branches, only return selected loop
727 if has_branches:
728 return(loops)
730 # change edgeloops into normal loops
731 loops = []
732 for edgeloop in all_edgeloops:
733 loop = []
734 # grow loop by comparing vertices between consecutive edge-keys
735 for i in range(len(edgeloop) - 1):
736 for vert in range(2):
737 if edgeloop[i][vert] in edgeloop[i + 1]:
738 loop.append(edgeloop[i][vert])
739 break
740 if loop:
741 # add starting vertex
742 for vert in range(2):
743 if edgeloop[0][vert] != loop[0]:
744 loop = [edgeloop[0][vert]] + loop
745 break
746 # add ending vertex
747 for vert in range(2):
748 if edgeloop[-1][vert] != loop[-1]:
749 loop.append(edgeloop[-1][vert])
750 break
751 # check if loop is circular
752 if loop[0] == loop[-1]:
753 circular = True
754 loop = loop[:-1]
755 else:
756 circular = False
757 loops.append([loop, circular])
759 return(loops)
762 # gather initial data
763 def initialise():
764 object = bpy.context.active_object
765 if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
766 # ensure that selection is synced for the derived mesh
767 bpy.ops.object.mode_set(mode='OBJECT')
768 bpy.ops.object.mode_set(mode='EDIT')
769 bm = bmesh.from_edit_mesh(object.data)
771 bm.verts.ensure_lookup_table()
772 bm.edges.ensure_lookup_table()
773 bm.faces.ensure_lookup_table()
775 return(object, bm)
778 # move the vertices to their new locations
779 def move_verts(object, bm, mapping, move, lock, influence):
780 if lock:
781 lock_x, lock_y, lock_z = lock
782 orient_slot = bpy.context.scene.transform_orientation_slots[0]
783 custom = orient_slot.custom_orientation
784 if custom:
785 mat = custom.matrix.to_4x4().inverted() @ object.matrix_world.copy()
786 elif orient_slot.type == 'LOCAL':
787 mat = mathutils.Matrix.Identity(4)
788 elif orient_slot.type == 'VIEW':
789 mat = bpy.context.region_data.view_matrix.copy() @ \
790 object.matrix_world.copy()
791 else: # orientation == 'GLOBAL'
792 mat = object.matrix_world.copy()
793 mat_inv = mat.inverted()
795 for loop in move:
796 for index, loc in loop:
797 if mapping:
798 if mapping[index] == -1:
799 continue
800 else:
801 index = mapping[index]
802 if lock:
803 delta = (loc - bm.verts[index].co) @ mat_inv
804 if lock_x:
805 delta[0] = 0
806 if lock_y:
807 delta[1] = 0
808 if lock_z:
809 delta[2] = 0
810 delta = delta @ mat
811 loc = bm.verts[index].co + delta
812 if influence < 0:
813 new_loc = loc
814 else:
815 new_loc = loc * (influence / 100) + \
816 bm.verts[index].co * ((100 - influence) / 100)
817 bm.verts[index].co = new_loc
818 bm.normal_update()
819 object.data.update()
821 bm.verts.ensure_lookup_table()
822 bm.edges.ensure_lookup_table()
823 bm.faces.ensure_lookup_table()
826 # load custom tool settings
827 def settings_load(self):
828 lt = bpy.context.window_manager.looptools
829 tool = self.name.split()[0].lower()
830 keys = self.as_keywords().keys()
831 for key in keys:
832 setattr(self, key, getattr(lt, tool + "_" + key))
835 # store custom tool settings
836 def settings_write(self):
837 lt = bpy.context.window_manager.looptools
838 tool = self.name.split()[0].lower()
839 keys = self.as_keywords().keys()
840 for key in keys:
841 setattr(lt, tool + "_" + key, getattr(self, key))
844 # clean up and set settings back to original state
845 def terminate():
846 # update editmesh cached data
847 obj = bpy.context.active_object
848 if obj.mode == 'EDIT':
849 bmesh.update_edit_mesh(obj.data, loop_triangles=True, destructive=True)
852 # ########################################
853 # ##### Bridge functions #################
854 # ########################################
856 # calculate a cubic spline through the middle section of 4 given coordinates
857 def bridge_calculate_cubic_spline(bm, coordinates):
858 result = []
859 x = [0, 1, 2, 3]
861 for j in range(3):
862 a = []
863 for i in coordinates:
864 a.append(float(i[j]))
865 h = []
866 for i in range(3):
867 h.append(x[i + 1] - x[i])
868 q = [False]
869 for i in range(1, 3):
870 q.append(3.0 / h[i] * (a[i + 1] - a[i]) - 3.0 / h[i - 1] * (a[i] - a[i - 1]))
871 l = [1.0]
872 u = [0.0]
873 z = [0.0]
874 for i in range(1, 3):
875 l.append(2.0 * (x[i + 1] - x[i - 1]) - h[i - 1] * u[i - 1])
876 u.append(h[i] / l[i])
877 z.append((q[i] - h[i - 1] * z[i - 1]) / l[i])
878 l.append(1.0)
879 z.append(0.0)
880 b = [False for i in range(3)]
881 c = [False for i in range(4)]
882 d = [False for i in range(3)]
883 c[3] = 0.0
884 for i in range(2, -1, -1):
885 c[i] = z[i] - u[i] * c[i + 1]
886 b[i] = (a[i + 1] - a[i]) / h[i] - h[i] * (c[i + 1] + 2.0 * c[i]) / 3.0
887 d[i] = (c[i + 1] - c[i]) / (3.0 * h[i])
888 for i in range(3):
889 result.append([a[i], b[i], c[i], d[i], x[i]])
890 spline = [result[1], result[4], result[7]]
892 return(spline)
895 # return a list with new vertex location vectors, a list with face vertex
896 # integers, and the highest vertex integer in the virtual mesh
897 def bridge_calculate_geometry(bm, lines, vertex_normals, segments,
898 interpolation, cubic_strength, min_width, max_vert_index):
899 new_verts = []
900 faces = []
902 # calculate location based on interpolation method
903 def get_location(line, segment, splines):
904 v1 = bm.verts[lines[line][0]].co
905 v2 = bm.verts[lines[line][1]].co
906 if interpolation == 'linear':
907 return v1 + (segment / segments) * (v2 - v1)
908 else: # interpolation == 'cubic'
909 m = (segment / segments)
910 ax, bx, cx, dx, tx = splines[line][0]
911 x = ax + bx * m + cx * m ** 2 + dx * m ** 3
912 ay, by, cy, dy, ty = splines[line][1]
913 y = ay + by * m + cy * m ** 2 + dy * m ** 3
914 az, bz, cz, dz, tz = splines[line][2]
915 z = az + bz * m + cz * m ** 2 + dz * m ** 3
916 return mathutils.Vector((x, y, z))
918 # no interpolation needed
919 if segments == 1:
920 for i, line in enumerate(lines):
921 if i < len(lines) - 1:
922 faces.append([line[0], lines[i + 1][0], lines[i + 1][1], line[1]])
923 # more than 1 segment, interpolate
924 else:
925 # calculate splines (if necessary) once, so no recalculations needed
926 if interpolation == 'cubic':
927 splines = []
928 for line in lines:
929 v1 = bm.verts[line[0]].co
930 v2 = bm.verts[line[1]].co
931 size = (v2 - v1).length * cubic_strength
932 splines.append(bridge_calculate_cubic_spline(bm,
933 [v1 + size * vertex_normals[line[0]], v1, v2,
934 v2 + size * vertex_normals[line[1]]]))
935 else:
936 splines = False
938 # create starting situation
939 virtual_width = [(bm.verts[lines[i][0]].co -
940 bm.verts[lines[i + 1][0]].co).length for i
941 in range(len(lines) - 1)]
942 new_verts = [get_location(0, seg, splines) for seg in range(1,
943 segments)]
944 first_line_indices = [i for i in range(max_vert_index + 1,
945 max_vert_index + segments)]
947 prev_verts = new_verts[:] # vertex locations of verts on previous line
948 prev_vert_indices = first_line_indices[:]
949 max_vert_index += segments - 1 # highest vertex index in virtual mesh
950 next_verts = [] # vertex locations of verts on current line
951 next_vert_indices = []
953 for i, line in enumerate(lines):
954 if i < len(lines) - 1:
955 v1 = line[0]
956 v2 = lines[i + 1][0]
957 end_face = True
958 for seg in range(1, segments):
959 loc1 = prev_verts[seg - 1]
960 loc2 = get_location(i + 1, seg, splines)
961 if (loc1 - loc2).length < (min_width / 100) * virtual_width[i] \
962 and line[1] == lines[i + 1][1]:
963 # triangle, no new vertex
964 faces.append([v1, v2, prev_vert_indices[seg - 1],
965 prev_vert_indices[seg - 1]])
966 next_verts += prev_verts[seg - 1:]
967 next_vert_indices += prev_vert_indices[seg - 1:]
968 end_face = False
969 break
970 else:
971 if i == len(lines) - 2 and lines[0] == lines[-1]:
972 # quad with first line, no new vertex
973 faces.append([v1, v2, first_line_indices[seg - 1],
974 prev_vert_indices[seg - 1]])
975 v2 = first_line_indices[seg - 1]
976 v1 = prev_vert_indices[seg - 1]
977 else:
978 # quad, add new vertex
979 max_vert_index += 1
980 faces.append([v1, v2, max_vert_index,
981 prev_vert_indices[seg - 1]])
982 v2 = max_vert_index
983 v1 = prev_vert_indices[seg - 1]
984 new_verts.append(loc2)
985 next_verts.append(loc2)
986 next_vert_indices.append(max_vert_index)
987 if end_face:
988 faces.append([v1, v2, lines[i + 1][1], line[1]])
990 prev_verts = next_verts[:]
991 prev_vert_indices = next_vert_indices[:]
992 next_verts = []
993 next_vert_indices = []
995 return(new_verts, faces, max_vert_index)
998 # calculate lines (list of lists, vertex indices) that are used for bridging
999 def bridge_calculate_lines(bm, loops, mode, twist, reverse):
1000 lines = []
1001 loop1, loop2 = [i[0] for i in loops]
1002 loop1_circular, loop2_circular = [i[1] for i in loops]
1003 circular = loop1_circular or loop2_circular
1004 circle_full = False
1006 # calculate loop centers
1007 centers = []
1008 for loop in [loop1, loop2]:
1009 center = mathutils.Vector()
1010 for vertex in loop:
1011 center += bm.verts[vertex].co
1012 center /= len(loop)
1013 centers.append(center)
1014 for i, loop in enumerate([loop1, loop2]):
1015 for vertex in loop:
1016 if bm.verts[vertex].co == centers[i]:
1017 # prevent zero-length vectors in angle comparisons
1018 centers[i] += mathutils.Vector((0.01, 0, 0))
1019 break
1020 center1, center2 = centers
1022 # calculate the normals of the virtual planes that the loops are on
1023 normals = []
1024 normal_plurity = False
1025 for i, loop in enumerate([loop1, loop2]):
1026 # covariance matrix
1027 mat = mathutils.Matrix(((0.0, 0.0, 0.0),
1028 (0.0, 0.0, 0.0),
1029 (0.0, 0.0, 0.0)))
1030 x, y, z = centers[i]
1031 for loc in [bm.verts[vertex].co for vertex in loop]:
1032 mat[0][0] += (loc[0] - x) ** 2
1033 mat[1][0] += (loc[0] - x) * (loc[1] - y)
1034 mat[2][0] += (loc[0] - x) * (loc[2] - z)
1035 mat[0][1] += (loc[1] - y) * (loc[0] - x)
1036 mat[1][1] += (loc[1] - y) ** 2
1037 mat[2][1] += (loc[1] - y) * (loc[2] - z)
1038 mat[0][2] += (loc[2] - z) * (loc[0] - x)
1039 mat[1][2] += (loc[2] - z) * (loc[1] - y)
1040 mat[2][2] += (loc[2] - z) ** 2
1041 # plane normal
1042 normal = False
1043 if sum(mat[0]) < 1e-6 or sum(mat[1]) < 1e-6 or sum(mat[2]) < 1e-6:
1044 normal_plurity = True
1045 try:
1046 mat.invert()
1047 except:
1048 if sum(mat[0]) == 0:
1049 normal = mathutils.Vector((1.0, 0.0, 0.0))
1050 elif sum(mat[1]) == 0:
1051 normal = mathutils.Vector((0.0, 1.0, 0.0))
1052 elif sum(mat[2]) == 0:
1053 normal = mathutils.Vector((0.0, 0.0, 1.0))
1054 if not normal:
1055 # warning! this is different from .normalize()
1056 itermax = 500
1057 iter = 0
1058 vec = mathutils.Vector((1.0, 1.0, 1.0))
1059 vec2 = (mat @ vec) / (mat @ vec).length
1060 while vec != vec2 and iter < itermax:
1061 iter += 1
1062 vec = vec2
1063 vec2 = mat @ vec
1064 if vec2.length != 0:
1065 vec2 /= vec2.length
1066 if vec2.length == 0:
1067 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
1068 normal = vec2
1069 normals.append(normal)
1070 # have plane normals face in the same direction (maximum angle: 90 degrees)
1071 if ((center1 + normals[0]) - center2).length < \
1072 ((center1 - normals[0]) - center2).length:
1073 normals[0].negate()
1074 if ((center2 + normals[1]) - center1).length > \
1075 ((center2 - normals[1]) - center1).length:
1076 normals[1].negate()
1078 # rotation matrix, representing the difference between the plane normals
1079 axis = normals[0].cross(normals[1])
1080 axis = mathutils.Vector([loc if abs(loc) > 1e-8 else 0 for loc in axis])
1081 if axis.angle(mathutils.Vector((0, 0, 1)), 0) > 1.5707964:
1082 axis.negate()
1083 angle = normals[0].dot(normals[1])
1084 rotation_matrix = mathutils.Matrix.Rotation(angle, 4, axis)
1086 # if circular, rotate loops so they are aligned
1087 if circular:
1088 # make sure loop1 is the circular one (or both are circular)
1089 if loop2_circular and not loop1_circular:
1090 loop1_circular, loop2_circular = True, False
1091 loop1, loop2 = loop2, loop1
1093 # match start vertex of loop1 with loop2
1094 target_vector = bm.verts[loop2[0]].co - center2
1095 dif_angles = [[(rotation_matrix @ (bm.verts[vertex].co - center1)
1096 ).angle(target_vector, 0), False, i] for
1097 i, vertex in enumerate(loop1)]
1098 dif_angles.sort()
1099 if len(loop1) != len(loop2):
1100 angle_limit = dif_angles[0][0] * 1.2 # 20% margin
1101 dif_angles = [
1102 [(bm.verts[loop2[0]].co -
1103 bm.verts[loop1[index]].co).length, angle, index] for
1104 angle, distance, index in dif_angles if angle <= angle_limit
1106 dif_angles.sort()
1107 loop1 = loop1[dif_angles[0][2]:] + loop1[:dif_angles[0][2]]
1109 # have both loops face the same way
1110 if normal_plurity and not circular:
1111 second_to_first, second_to_second, second_to_last = [
1112 (bm.verts[loop1[1]].co - center1).angle(
1113 bm.verts[loop2[i]].co - center2) for i in [0, 1, -1]
1115 last_to_first, last_to_second = [
1116 (bm.verts[loop1[-1]].co -
1117 center1).angle(bm.verts[loop2[i]].co - center2) for
1118 i in [0, 1]
1120 if (min(last_to_first, last_to_second) * 1.1 < min(second_to_first,
1121 second_to_second)) or (loop2_circular and second_to_last * 1.1 <
1122 min(second_to_first, second_to_second)):
1123 loop1.reverse()
1124 if circular:
1125 loop1 = [loop1[-1]] + loop1[:-1]
1126 else:
1127 angle = (bm.verts[loop1[0]].co - center1).\
1128 cross(bm.verts[loop1[1]].co - center1).angle(normals[0], 0)
1129 target_angle = (bm.verts[loop2[0]].co - center2).\
1130 cross(bm.verts[loop2[1]].co - center2).angle(normals[1], 0)
1131 limit = 1.5707964 # 0.5*pi, 90 degrees
1132 if not ((angle > limit and target_angle > limit) or
1133 (angle < limit and target_angle < limit)):
1134 loop1.reverse()
1135 if circular:
1136 loop1 = [loop1[-1]] + loop1[:-1]
1137 elif normals[0].angle(normals[1]) > limit:
1138 loop1.reverse()
1139 if circular:
1140 loop1 = [loop1[-1]] + loop1[:-1]
1142 # both loops have the same length
1143 if len(loop1) == len(loop2):
1144 # manual override
1145 if twist:
1146 if abs(twist) < len(loop1):
1147 loop1 = loop1[twist:] + loop1[:twist]
1148 if reverse:
1149 loop1.reverse()
1151 lines.append([loop1[0], loop2[0]])
1152 for i in range(1, len(loop1)):
1153 lines.append([loop1[i], loop2[i]])
1155 # loops of different lengths
1156 else:
1157 # make loop1 longest loop
1158 if len(loop2) > len(loop1):
1159 loop1, loop2 = loop2, loop1
1160 loop1_circular, loop2_circular = loop2_circular, loop1_circular
1162 # manual override
1163 if twist:
1164 if abs(twist) < len(loop1):
1165 loop1 = loop1[twist:] + loop1[:twist]
1166 if reverse:
1167 loop1.reverse()
1169 # shortest angle difference doesn't always give correct start vertex
1170 if loop1_circular and not loop2_circular:
1171 shifting = 1
1172 while shifting:
1173 if len(loop1) - shifting < len(loop2):
1174 shifting = False
1175 break
1176 to_last, to_first = [
1177 (rotation_matrix @ (bm.verts[loop1[-1]].co - center1)).angle(
1178 (bm.verts[loop2[i]].co - center2), 0) for i in [-1, 0]
1180 if to_first < to_last:
1181 loop1 = [loop1[-1]] + loop1[:-1]
1182 shifting += 1
1183 else:
1184 shifting = False
1185 break
1187 # basic shortest side first
1188 if mode == 'basic':
1189 lines.append([loop1[0], loop2[0]])
1190 for i in range(1, len(loop1)):
1191 if i >= len(loop2) - 1:
1192 # triangles
1193 lines.append([loop1[i], loop2[-1]])
1194 else:
1195 # quads
1196 lines.append([loop1[i], loop2[i]])
1198 # shortest edge algorithm
1199 else: # mode == 'shortest'
1200 lines.append([loop1[0], loop2[0]])
1201 prev_vert2 = 0
1202 for i in range(len(loop1) - 1):
1203 if prev_vert2 == len(loop2) - 1 and not loop2_circular:
1204 # force triangles, reached end of loop2
1205 tri, quad = 0, 1
1206 elif prev_vert2 == len(loop2) - 1 and loop2_circular:
1207 # at end of loop2, but circular, so check with first vert
1208 tri, quad = [(bm.verts[loop1[i + 1]].co -
1209 bm.verts[loop2[j]].co).length
1210 for j in [prev_vert2, 0]]
1211 circle_full = 2
1212 elif len(loop1) - 1 - i == len(loop2) - 1 - prev_vert2 and \
1213 not circle_full:
1214 # force quads, otherwise won't make it to end of loop2
1215 tri, quad = 1, 0
1216 else:
1217 # calculate if tri or quad gives shortest edge
1218 tri, quad = [(bm.verts[loop1[i + 1]].co -
1219 bm.verts[loop2[j]].co).length
1220 for j in range(prev_vert2, prev_vert2 + 2)]
1222 # triangle
1223 if tri < quad:
1224 lines.append([loop1[i + 1], loop2[prev_vert2]])
1225 if circle_full == 2:
1226 circle_full = False
1227 # quad
1228 elif not circle_full:
1229 lines.append([loop1[i + 1], loop2[prev_vert2 + 1]])
1230 prev_vert2 += 1
1231 # quad to first vertex of loop2
1232 else:
1233 lines.append([loop1[i + 1], loop2[0]])
1234 prev_vert2 = 0
1235 circle_full = True
1237 # final face for circular loops
1238 if loop1_circular and loop2_circular:
1239 lines.append([loop1[0], loop2[0]])
1241 return(lines)
1244 # calculate number of segments needed
1245 def bridge_calculate_segments(bm, lines, loops, segments):
1246 # return if amount of segments is set by user
1247 if segments != 0:
1248 return segments
1250 # edge lengths
1251 average_edge_length = [
1252 (bm.verts[vertex].co -
1253 bm.verts[loop[0][i + 1]].co).length for loop in loops for
1254 i, vertex in enumerate(loop[0][:-1])
1256 # closing edges of circular loops
1257 average_edge_length += [
1258 (bm.verts[loop[0][-1]].co -
1259 bm.verts[loop[0][0]].co).length for loop in loops if loop[1]
1262 # average lengths
1263 average_edge_length = sum(average_edge_length) / len(average_edge_length)
1264 average_bridge_length = sum(
1265 [(bm.verts[v1].co -
1266 bm.verts[v2].co).length for v1, v2 in lines]
1267 ) / len(lines)
1269 segments = max(1, round(average_bridge_length / average_edge_length))
1271 return(segments)
1274 # return dictionary with vertex index as key, and the normal vector as value
1275 def bridge_calculate_virtual_vertex_normals(bm, lines, loops, edge_faces,
1276 edgekey_to_edge):
1277 if not edge_faces: # interpolation isn't set to cubic
1278 return False
1280 # pity reduce() isn't one of the basic functions in python anymore
1281 def average_vector_dictionary(dic):
1282 for key, vectors in dic.items():
1283 # if type(vectors) == type([]) and len(vectors) > 1:
1284 if len(vectors) > 1:
1285 average = mathutils.Vector()
1286 for vector in vectors:
1287 average += vector
1288 average /= len(vectors)
1289 dic[key] = [average]
1290 return dic
1292 # get all edges of the loop
1293 edges = [
1294 [edgekey_to_edge[tuple(sorted([loops[j][0][i],
1295 loops[j][0][i + 1]]))] for i in range(len(loops[j][0]) - 1)] for
1296 j in [0, 1]
1298 edges = edges[0] + edges[1]
1299 for j in [0, 1]:
1300 if loops[j][1]: # circular
1301 edges.append(edgekey_to_edge[tuple(sorted([loops[j][0][0],
1302 loops[j][0][-1]]))])
1305 calculation based on face topology (assign edge-normals to vertices)
1307 edge_normal = face_normal x edge_vector
1308 vertex_normal = average(edge_normals)
1310 vertex_normals = dict([(vertex, []) for vertex in loops[0][0] + loops[1][0]])
1311 for edge in edges:
1312 faces = edge_faces[edgekey(edge)] # valid faces connected to edge
1314 if faces:
1315 # get edge coordinates
1316 v1, v2 = [bm.verts[edgekey(edge)[i]].co for i in [0, 1]]
1317 edge_vector = v1 - v2
1318 if edge_vector.length < 1e-4:
1319 # zero-length edge, vertices at same location
1320 continue
1321 edge_center = (v1 + v2) / 2
1323 # average face coordinates, if connected to more than 1 valid face
1324 if len(faces) > 1:
1325 face_normal = mathutils.Vector()
1326 face_center = mathutils.Vector()
1327 for face in faces:
1328 face_normal += face.normal
1329 face_center += face.calc_center_median()
1330 face_normal /= len(faces)
1331 face_center /= len(faces)
1332 else:
1333 face_normal = faces[0].normal
1334 face_center = faces[0].calc_center_median()
1335 if face_normal.length < 1e-4:
1336 # faces with a surface of 0 have no face normal
1337 continue
1339 # calculate virtual edge normal
1340 edge_normal = edge_vector.cross(face_normal)
1341 edge_normal.length = 0.01
1342 if (face_center - (edge_center + edge_normal)).length > \
1343 (face_center - (edge_center - edge_normal)).length:
1344 # make normal face the correct way
1345 edge_normal.negate()
1346 edge_normal.normalize()
1347 # add virtual edge normal as entry for both vertices it connects
1348 for vertex in edgekey(edge):
1349 vertex_normals[vertex].append(edge_normal)
1352 calculation based on connection with other loop (vertex focused method)
1353 - used for vertices that aren't connected to any valid faces
1355 plane_normal = edge_vector x connection_vector
1356 vertex_normal = plane_normal x edge_vector
1358 vertices = [
1359 vertex for vertex, normal in vertex_normals.items() if not normal
1362 if vertices:
1363 # edge vectors connected to vertices
1364 edge_vectors = dict([[vertex, []] for vertex in vertices])
1365 for edge in edges:
1366 for v in edgekey(edge):
1367 if v in edge_vectors:
1368 edge_vector = bm.verts[edgekey(edge)[0]].co - \
1369 bm.verts[edgekey(edge)[1]].co
1370 if edge_vector.length < 1e-4:
1371 # zero-length edge, vertices at same location
1372 continue
1373 edge_vectors[v].append(edge_vector)
1375 # connection vectors between vertices of both loops
1376 connection_vectors = dict([[vertex, []] for vertex in vertices])
1377 connections = dict([[vertex, []] for vertex in vertices])
1378 for v1, v2 in lines:
1379 if v1 in connection_vectors or v2 in connection_vectors:
1380 new_vector = bm.verts[v1].co - bm.verts[v2].co
1381 if new_vector.length < 1e-4:
1382 # zero-length connection vector,
1383 # vertices in different loops at same location
1384 continue
1385 if v1 in connection_vectors:
1386 connection_vectors[v1].append(new_vector)
1387 connections[v1].append(v2)
1388 if v2 in connection_vectors:
1389 connection_vectors[v2].append(new_vector)
1390 connections[v2].append(v1)
1391 connection_vectors = average_vector_dictionary(connection_vectors)
1392 connection_vectors = dict(
1393 [[vertex, vector[0]] if vector else
1394 [vertex, []] for vertex, vector in connection_vectors.items()]
1397 for vertex, values in edge_vectors.items():
1398 # vertex normal doesn't matter, just assign a random vector to it
1399 if not connection_vectors[vertex]:
1400 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1401 continue
1403 # calculate to what location the vertex is connected,
1404 # used to determine what way to flip the normal
1405 connected_center = mathutils.Vector()
1406 for v in connections[vertex]:
1407 connected_center += bm.verts[v].co
1408 if len(connections[vertex]) > 1:
1409 connected_center /= len(connections[vertex])
1410 if len(connections[vertex]) == 0:
1411 # shouldn't be possible, but better safe than sorry
1412 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1413 continue
1415 # can't do proper calculations, because of zero-length vector
1416 if not values:
1417 if (connected_center - (bm.verts[vertex].co +
1418 connection_vectors[vertex])).length < (connected_center -
1419 (bm.verts[vertex].co - connection_vectors[vertex])).length:
1420 connection_vectors[vertex].negate()
1421 vertex_normals[vertex] = [connection_vectors[vertex].normalized()]
1422 continue
1424 # calculate vertex normals using edge-vectors,
1425 # connection-vectors and the derived plane normal
1426 for edge_vector in values:
1427 plane_normal = edge_vector.cross(connection_vectors[vertex])
1428 vertex_normal = edge_vector.cross(plane_normal)
1429 vertex_normal.length = 0.1
1430 if (connected_center - (bm.verts[vertex].co +
1431 vertex_normal)).length < (connected_center -
1432 (bm.verts[vertex].co - vertex_normal)).length:
1433 # make normal face the correct way
1434 vertex_normal.negate()
1435 vertex_normal.normalize()
1436 vertex_normals[vertex].append(vertex_normal)
1438 # average virtual vertex normals, based on all edges it's connected to
1439 vertex_normals = average_vector_dictionary(vertex_normals)
1440 vertex_normals = dict([[vertex, vector[0]] for vertex, vector in vertex_normals.items()])
1442 return(vertex_normals)
1445 # add vertices to mesh
1446 def bridge_create_vertices(bm, vertices):
1447 for i in range(len(vertices)):
1448 bm.verts.new(vertices[i])
1449 bm.verts.ensure_lookup_table()
1452 # add faces to mesh
1453 def bridge_create_faces(object, bm, faces, twist):
1454 # have the normal point the correct way
1455 if twist < 0:
1456 [face.reverse() for face in faces]
1457 faces = [face[2:] + face[:2] if face[0] == face[1] else face for face in faces]
1459 # eekadoodle prevention
1460 for i in range(len(faces)):
1461 if not faces[i][-1]:
1462 if faces[i][0] == faces[i][-1]:
1463 faces[i] = [faces[i][1], faces[i][2], faces[i][3], faces[i][1]]
1464 else:
1465 faces[i] = [faces[i][-1]] + faces[i][:-1]
1466 # result of converting from pre-bmesh period
1467 if faces[i][-1] == faces[i][-2]:
1468 faces[i] = faces[i][:-1]
1470 new_faces = []
1471 for i in range(len(faces)):
1472 new_faces.append(bm.faces.new([bm.verts[v] for v in faces[i]]))
1473 bm.normal_update()
1474 object.data.update(calc_edges=True) # calc_edges prevents memory-corruption
1476 bm.verts.ensure_lookup_table()
1477 bm.edges.ensure_lookup_table()
1478 bm.faces.ensure_lookup_table()
1480 return(new_faces)
1483 # calculate input loops
1484 def bridge_get_input(bm):
1485 # create list of internal edges, which should be skipped
1486 eks_of_selected_faces = [
1487 item for sublist in [face_edgekeys(face) for
1488 face in bm.faces if face.select and not face.hide] for item in sublist
1490 edge_count = {}
1491 for ek in eks_of_selected_faces:
1492 if ek in edge_count:
1493 edge_count[ek] += 1
1494 else:
1495 edge_count[ek] = 1
1496 internal_edges = [ek for ek in edge_count if edge_count[ek] > 1]
1498 # sort correct edges into loops
1499 selected_edges = [
1500 edgekey(edge) for edge in bm.edges if edge.select and
1501 not edge.hide and edgekey(edge) not in internal_edges
1503 loops = get_connected_selections(selected_edges)
1505 return(loops)
1508 # return values needed by the bridge operator
1509 def bridge_initialise(bm, interpolation):
1510 if interpolation == 'cubic':
1511 # dict with edge-key as key and list of connected valid faces as value
1512 face_blacklist = [
1513 face.index for face in bm.faces if face.select or
1514 face.hide
1516 edge_faces = dict(
1517 [[edgekey(edge), []] for edge in bm.edges if not edge.hide]
1519 for face in bm.faces:
1520 if face.index in face_blacklist:
1521 continue
1522 for key in face_edgekeys(face):
1523 edge_faces[key].append(face)
1524 # dictionary with the edge-key as key and edge as value
1525 edgekey_to_edge = dict(
1526 [[edgekey(edge), edge] for edge in bm.edges if edge.select and not edge.hide]
1528 else:
1529 edge_faces = False
1530 edgekey_to_edge = False
1532 # selected faces input
1533 old_selected_faces = [
1534 face.index for face in bm.faces if face.select and not face.hide
1537 # find out if faces created by bridging should be smoothed
1538 smooth = False
1539 if bm.faces:
1540 if sum([face.smooth for face in bm.faces]) / len(bm.faces) >= 0.5:
1541 smooth = True
1543 return(edge_faces, edgekey_to_edge, old_selected_faces, smooth)
1546 # return a string with the input method
1547 def bridge_input_method(loft, loft_loop):
1548 method = ""
1549 if loft:
1550 if loft_loop:
1551 method = "Loft loop"
1552 else:
1553 method = "Loft no-loop"
1554 else:
1555 method = "Bridge"
1557 return(method)
1560 # match up loops in pairs, used for multi-input bridging
1561 def bridge_match_loops(bm, loops):
1562 # calculate average loop normals and centers
1563 normals = []
1564 centers = []
1565 for vertices, circular in loops:
1566 normal = mathutils.Vector()
1567 center = mathutils.Vector()
1568 for vertex in vertices:
1569 normal += bm.verts[vertex].normal
1570 center += bm.verts[vertex].co
1571 normals.append(normal / len(vertices) / 10)
1572 centers.append(center / len(vertices))
1574 # possible matches if loop normals are faced towards the center
1575 # of the other loop
1576 matches = dict([[i, []] for i in range(len(loops))])
1577 matches_amount = 0
1578 for i in range(len(loops) + 1):
1579 for j in range(i + 1, len(loops)):
1580 if (centers[i] - centers[j]).length > \
1581 (centers[i] - (centers[j] + normals[j])).length and \
1582 (centers[j] - centers[i]).length > \
1583 (centers[j] - (centers[i] + normals[i])).length:
1584 matches_amount += 1
1585 matches[i].append([(centers[i] - centers[j]).length, i, j])
1586 matches[j].append([(centers[i] - centers[j]).length, j, i])
1587 # if no loops face each other, just make matches between all the loops
1588 if matches_amount == 0:
1589 for i in range(len(loops) + 1):
1590 for j in range(i + 1, len(loops)):
1591 matches[i].append([(centers[i] - centers[j]).length, i, j])
1592 matches[j].append([(centers[i] - centers[j]).length, j, i])
1593 for key, value in matches.items():
1594 value.sort()
1596 # matches based on distance between centers and number of vertices in loops
1597 new_order = []
1598 for loop_index in range(len(loops)):
1599 if loop_index in new_order:
1600 continue
1601 loop_matches = matches[loop_index]
1602 if not loop_matches:
1603 continue
1604 shortest_distance = loop_matches[0][0]
1605 shortest_distance *= 1.1
1606 loop_matches = [
1607 [abs(len(loops[loop_index][0]) -
1608 len(loops[loop[2]][0])), loop[0], loop[1], loop[2]] for loop in
1609 loop_matches if loop[0] < shortest_distance
1611 loop_matches.sort()
1612 for match in loop_matches:
1613 if match[3] not in new_order:
1614 new_order += [loop_index, match[3]]
1615 break
1617 # reorder loops based on matches
1618 if len(new_order) >= 2:
1619 loops = [loops[i] for i in new_order]
1621 return(loops)
1624 # remove old_selected_faces
1625 def bridge_remove_internal_faces(bm, old_selected_faces):
1626 # collect bmesh faces and internal bmesh edges
1627 remove_faces = [bm.faces[face] for face in old_selected_faces]
1628 edges = collections.Counter(
1629 [edge.index for face in remove_faces for edge in face.edges]
1631 remove_edges = [bm.edges[edge] for edge in edges if edges[edge] > 1]
1633 # remove internal faces and edges
1634 for face in remove_faces:
1635 bm.faces.remove(face)
1636 for edge in remove_edges:
1637 bm.edges.remove(edge)
1639 bm.faces.ensure_lookup_table()
1640 bm.edges.ensure_lookup_table()
1641 bm.verts.ensure_lookup_table()
1644 # update list of internal faces that are flagged for removal
1645 def bridge_save_unused_faces(bm, old_selected_faces, loops):
1646 # key: vertex index, value: lists of selected faces using it
1648 vertex_to_face = dict([[i, []] for i in range(len(bm.verts))])
1649 [[vertex_to_face[vertex.index].append(face) for vertex in
1650 bm.faces[face].verts] for face in old_selected_faces]
1652 # group selected faces that are connected
1653 groups = []
1654 grouped_faces = []
1655 for face in old_selected_faces:
1656 if face in grouped_faces:
1657 continue
1658 grouped_faces.append(face)
1659 group = [face]
1660 new_faces = [face]
1661 while new_faces:
1662 grow_face = new_faces[0]
1663 for vertex in bm.faces[grow_face].verts:
1664 vertex_face_group = [
1665 face for face in vertex_to_face[vertex.index] if
1666 face not in grouped_faces
1668 new_faces += vertex_face_group
1669 grouped_faces += vertex_face_group
1670 group += vertex_face_group
1671 new_faces.pop(0)
1672 groups.append(group)
1674 # key: vertex index, value: True/False (is it in a loop that is used)
1675 used_vertices = dict([[i, 0] for i in range(len(bm.verts))])
1676 for loop in loops:
1677 for vertex in loop[0]:
1678 used_vertices[vertex] = True
1680 # check if group is bridged, if not remove faces from internal faces list
1681 for group in groups:
1682 used = False
1683 for face in group:
1684 if used:
1685 break
1686 for vertex in bm.faces[face].verts:
1687 if used_vertices[vertex.index]:
1688 used = True
1689 break
1690 if not used:
1691 for face in group:
1692 old_selected_faces.remove(face)
1695 # add the newly created faces to the selection
1696 def bridge_select_new_faces(new_faces, smooth):
1697 for face in new_faces:
1698 face.select_set(True)
1699 face.smooth = smooth
1702 # sort loops, so they are connected in the correct order when lofting
1703 def bridge_sort_loops(bm, loops, loft_loop):
1704 # simplify loops to single points, and prepare for pathfinding
1705 x, y, z = [
1706 [sum([bm.verts[i].co[j] for i in loop[0]]) /
1707 len(loop[0]) for loop in loops] for j in range(3)
1709 nodes = [mathutils.Vector((x[i], y[i], z[i])) for i in range(len(loops))]
1711 active_node = 0
1712 open = [i for i in range(1, len(loops))]
1713 path = [[0, 0]]
1714 # connect node to path, that is shortest to active_node
1715 while len(open) > 0:
1716 distances = [(nodes[active_node] - nodes[i]).length for i in open]
1717 active_node = open[distances.index(min(distances))]
1718 open.remove(active_node)
1719 path.append([active_node, min(distances)])
1720 # check if we didn't start in the middle of the path
1721 for i in range(2, len(path)):
1722 if (nodes[path[i][0]] - nodes[0]).length < path[i][1]:
1723 temp = path[:i]
1724 path.reverse()
1725 path = path[:-i] + temp
1726 break
1728 # reorder loops
1729 loops = [loops[i[0]] for i in path]
1730 # if requested, duplicate first loop at last position, so loft can loop
1731 if loft_loop:
1732 loops = loops + [loops[0]]
1734 return(loops)
1737 # remapping old indices to new position in list
1738 def bridge_update_old_selection(bm, old_selected_faces):
1740 old_indices = old_selected_faces[:]
1741 old_selected_faces = []
1742 for i, face in enumerate(bm.faces):
1743 if face.index in old_indices:
1744 old_selected_faces.append(i)
1746 old_selected_faces = [
1747 i for i, face in enumerate(bm.faces) if face.index in old_selected_faces
1750 return(old_selected_faces)
1753 # ########################################
1754 # ##### Circle functions #################
1755 # ########################################
1757 # convert 3d coordinates to 2d coordinates on plane
1758 def circle_3d_to_2d(bm_mod, loop, com, normal):
1759 # project vertices onto the plane
1760 verts = [bm_mod.verts[v] for v in loop[0]]
1761 verts_projected = [[v.co - (v.co - com).dot(normal) * normal, v.index]
1762 for v in verts]
1764 # calculate two vectors (p and q) along the plane
1765 m = mathutils.Vector((normal[0] + 1.0, normal[1], normal[2]))
1766 p = m - (m.dot(normal) * normal)
1767 if p.dot(p) < 1e-6:
1768 m = mathutils.Vector((normal[0], normal[1] + 1.0, normal[2]))
1769 p = m - (m.dot(normal) * normal)
1770 q = p.cross(normal)
1772 # change to 2d coordinates using perpendicular projection
1773 locs_2d = []
1774 for loc, vert in verts_projected:
1775 vloc = loc - com
1776 x = p.dot(vloc) / p.dot(p)
1777 y = q.dot(vloc) / q.dot(q)
1778 locs_2d.append([x, y, vert])
1780 return(locs_2d, p, q)
1783 # calculate a best-fit circle to the 2d locations on the plane
1784 def circle_calculate_best_fit(locs_2d):
1785 # initial guess
1786 x0 = 0.0
1787 y0 = 0.0
1788 r = 1.0
1790 # calculate center and radius (non-linear least squares solution)
1791 for iter in range(500):
1792 jmat = []
1793 k = []
1794 for v in locs_2d:
1795 d = (v[0] ** 2 - 2.0 * x0 * v[0] + v[1] ** 2 - 2.0 * y0 * v[1] + x0 ** 2 + y0 ** 2) ** 0.5
1796 jmat.append([(x0 - v[0]) / d, (y0 - v[1]) / d, -1.0])
1797 k.append(-(((v[0] - x0) ** 2 + (v[1] - y0) ** 2) ** 0.5 - r))
1798 jmat2 = mathutils.Matrix(((0.0, 0.0, 0.0),
1799 (0.0, 0.0, 0.0),
1800 (0.0, 0.0, 0.0),
1802 k2 = mathutils.Vector((0.0, 0.0, 0.0))
1803 for i in range(len(jmat)):
1804 k2 += mathutils.Vector(jmat[i]) * k[i]
1805 jmat2[0][0] += jmat[i][0] ** 2
1806 jmat2[1][0] += jmat[i][0] * jmat[i][1]
1807 jmat2[2][0] += jmat[i][0] * jmat[i][2]
1808 jmat2[1][1] += jmat[i][1] ** 2
1809 jmat2[2][1] += jmat[i][1] * jmat[i][2]
1810 jmat2[2][2] += jmat[i][2] ** 2
1811 jmat2[0][1] = jmat2[1][0]
1812 jmat2[0][2] = jmat2[2][0]
1813 jmat2[1][2] = jmat2[2][1]
1814 try:
1815 jmat2.invert()
1816 except:
1817 pass
1818 dx0, dy0, dr = jmat2 @ k2
1819 x0 += dx0
1820 y0 += dy0
1821 r += dr
1822 # stop iterating if we're close enough to optimal solution
1823 if abs(dx0) < 1e-6 and abs(dy0) < 1e-6 and abs(dr) < 1e-6:
1824 break
1826 # return center of circle and radius
1827 return(x0, y0, r)
1830 # calculate circle so no vertices have to be moved away from the center
1831 def circle_calculate_min_fit(locs_2d):
1832 # center of circle
1833 x0 = (min([i[0] for i in locs_2d]) + max([i[0] for i in locs_2d])) / 2.0
1834 y0 = (min([i[1] for i in locs_2d]) + max([i[1] for i in locs_2d])) / 2.0
1835 center = mathutils.Vector([x0, y0])
1836 # radius of circle
1837 r = min([(mathutils.Vector([i[0], i[1]]) - center).length for i in locs_2d])
1839 # return center of circle and radius
1840 return(x0, y0, r)
1843 # calculate the new locations of the vertices that need to be moved
1844 def circle_calculate_verts(flatten, bm_mod, locs_2d, com, p, q, normal):
1845 # changing 2d coordinates back to 3d coordinates
1846 locs_3d = []
1847 for loc in locs_2d:
1848 locs_3d.append([loc[2], loc[0] * p + loc[1] * q + com])
1850 if flatten: # flat circle
1851 return(locs_3d)
1853 else: # project the locations on the existing mesh
1854 vert_edges = dict_vert_edges(bm_mod)
1855 vert_faces = dict_vert_faces(bm_mod)
1856 faces = [f for f in bm_mod.faces if not f.hide]
1857 rays = [normal, -normal]
1858 new_locs = []
1859 for loc in locs_3d:
1860 projection = False
1861 if bm_mod.verts[loc[0]].co == loc[1]: # vertex hasn't moved
1862 projection = loc[1]
1863 else:
1864 dif = normal.angle(loc[1] - bm_mod.verts[loc[0]].co)
1865 if -1e-6 < dif < 1e-6 or math.pi - 1e-6 < dif < math.pi + 1e-6:
1866 # original location is already along projection normal
1867 projection = bm_mod.verts[loc[0]].co
1868 else:
1869 # quick search through adjacent faces
1870 for face in vert_faces[loc[0]]:
1871 verts = [v.co for v in bm_mod.faces[face].verts]
1872 if len(verts) == 3: # triangle
1873 v1, v2, v3 = verts
1874 v4 = False
1875 else: # assume quad
1876 v1, v2, v3, v4 = verts[:4]
1877 for ray in rays:
1878 intersect = mathutils.geometry.\
1879 intersect_ray_tri(v1, v2, v3, ray, loc[1])
1880 if intersect:
1881 projection = intersect
1882 break
1883 elif v4:
1884 intersect = mathutils.geometry.\
1885 intersect_ray_tri(v1, v3, v4, ray, loc[1])
1886 if intersect:
1887 projection = intersect
1888 break
1889 if projection:
1890 break
1891 if not projection:
1892 # check if projection is on adjacent edges
1893 for edgekey in vert_edges[loc[0]]:
1894 line1 = bm_mod.verts[edgekey[0]].co
1895 line2 = bm_mod.verts[edgekey[1]].co
1896 intersect, dist = mathutils.geometry.intersect_point_line(
1897 loc[1], line1, line2
1899 if 1e-6 < dist < 1 - 1e-6:
1900 projection = intersect
1901 break
1902 if not projection:
1903 # full search through the entire mesh
1904 hits = []
1905 for face in faces:
1906 verts = [v.co for v in face.verts]
1907 if len(verts) == 3: # triangle
1908 v1, v2, v3 = verts
1909 v4 = False
1910 else: # assume quad
1911 v1, v2, v3, v4 = verts[:4]
1912 for ray in rays:
1913 intersect = mathutils.geometry.intersect_ray_tri(
1914 v1, v2, v3, ray, loc[1]
1916 if intersect:
1917 hits.append([(loc[1] - intersect).length,
1918 intersect])
1919 break
1920 elif v4:
1921 intersect = mathutils.geometry.intersect_ray_tri(
1922 v1, v3, v4, ray, loc[1]
1924 if intersect:
1925 hits.append([(loc[1] - intersect).length,
1926 intersect])
1927 break
1928 if len(hits) >= 1:
1929 # if more than 1 hit with mesh, closest hit is new loc
1930 hits.sort()
1931 projection = hits[0][1]
1932 if not projection:
1933 # nothing to project on, remain at flat location
1934 projection = loc[1]
1935 new_locs.append([loc[0], projection])
1937 # return new positions of projected circle
1938 return(new_locs)
1941 # check loops and only return valid ones
1942 def circle_check_loops(single_loops, loops, mapping, bm_mod):
1943 valid_single_loops = {}
1944 valid_loops = []
1945 for i, [loop, circular] in enumerate(loops):
1946 # loop needs to have at least 3 vertices
1947 if len(loop) < 3:
1948 continue
1949 # loop needs at least 1 vertex in the original, non-mirrored mesh
1950 if mapping:
1951 all_virtual = True
1952 for vert in loop:
1953 if mapping[vert] > -1:
1954 all_virtual = False
1955 break
1956 if all_virtual:
1957 continue
1958 # loop has to be non-collinear
1959 collinear = True
1960 loc0 = mathutils.Vector(bm_mod.verts[loop[0]].co[:])
1961 loc1 = mathutils.Vector(bm_mod.verts[loop[1]].co[:])
1962 for v in loop[2:]:
1963 locn = mathutils.Vector(bm_mod.verts[v].co[:])
1964 if loc0 == loc1 or loc1 == locn:
1965 loc0 = loc1
1966 loc1 = locn
1967 continue
1968 d1 = loc1 - loc0
1969 d2 = locn - loc1
1970 if -1e-6 < d1.angle(d2, 0) < 1e-6:
1971 loc0 = loc1
1972 loc1 = locn
1973 continue
1974 collinear = False
1975 break
1976 if collinear:
1977 continue
1978 # passed all tests, loop is valid
1979 valid_loops.append([loop, circular])
1980 valid_single_loops[len(valid_loops) - 1] = single_loops[i]
1982 return(valid_single_loops, valid_loops)
1985 # calculate the location of single input vertices that need to be flattened
1986 def circle_flatten_singles(bm_mod, com, p, q, normal, single_loop):
1987 new_locs = []
1988 for vert in single_loop:
1989 loc = mathutils.Vector(bm_mod.verts[vert].co[:])
1990 new_locs.append([vert, loc - (loc - com).dot(normal) * normal])
1992 return(new_locs)
1995 # calculate input loops
1996 def circle_get_input(object, bm):
1997 # get mesh with modifiers applied
1998 derived, bm_mod = get_derived_bmesh(object, bm)
2000 # create list of edge-keys based on selection state
2001 faces = False
2002 for face in bm.faces:
2003 if face.select and not face.hide:
2004 faces = True
2005 break
2006 if faces:
2007 # get selected, non-hidden , non-internal edge-keys
2008 eks_selected = [
2009 key for keys in [face_edgekeys(face) for face in
2010 bm_mod.faces if face.select and not face.hide] for key in keys
2012 edge_count = {}
2013 for ek in eks_selected:
2014 if ek in edge_count:
2015 edge_count[ek] += 1
2016 else:
2017 edge_count[ek] = 1
2018 edge_keys = [
2019 edgekey(edge) for edge in bm_mod.edges if edge.select and
2020 not edge.hide and edge_count.get(edgekey(edge), 1) == 1
2022 else:
2023 # no faces, so no internal edges either
2024 edge_keys = [
2025 edgekey(edge) for edge in bm_mod.edges if edge.select and not edge.hide
2028 # add edge-keys around single vertices
2029 verts_connected = dict(
2030 [[vert, 1] for edge in [edge for edge in
2031 bm_mod.edges if edge.select and not edge.hide] for vert in
2032 edgekey(edge)]
2034 single_vertices = [
2035 vert.index for vert in bm_mod.verts if
2036 vert.select and not vert.hide and
2037 not verts_connected.get(vert.index, False)
2040 if single_vertices and len(bm.faces) > 0:
2041 vert_to_single = dict(
2042 [[v.index, []] for v in bm_mod.verts if not v.hide]
2044 for face in [face for face in bm_mod.faces if not face.select and not face.hide]:
2045 for vert in face.verts:
2046 vert = vert.index
2047 if vert in single_vertices:
2048 for ek in face_edgekeys(face):
2049 if vert not in ek:
2050 edge_keys.append(ek)
2051 if vert not in vert_to_single[ek[0]]:
2052 vert_to_single[ek[0]].append(vert)
2053 if vert not in vert_to_single[ek[1]]:
2054 vert_to_single[ek[1]].append(vert)
2055 break
2057 # sort edge-keys into loops
2058 loops = get_connected_selections(edge_keys)
2060 # find out to which loops the single vertices belong
2061 single_loops = dict([[i, []] for i in range(len(loops))])
2062 if single_vertices and len(bm.faces) > 0:
2063 for i, [loop, circular] in enumerate(loops):
2064 for vert in loop:
2065 if vert_to_single[vert]:
2066 for single in vert_to_single[vert]:
2067 if single not in single_loops[i]:
2068 single_loops[i].append(single)
2070 return(derived, bm_mod, single_vertices, single_loops, loops)
2073 # recalculate positions based on the influence of the circle shape
2074 def circle_influence_locs(locs_2d, new_locs_2d, influence):
2075 for i in range(len(locs_2d)):
2076 oldx, oldy, j = locs_2d[i]
2077 newx, newy, k = new_locs_2d[i]
2078 altx = newx * (influence / 100) + oldx * ((100 - influence) / 100)
2079 alty = newy * (influence / 100) + oldy * ((100 - influence) / 100)
2080 locs_2d[i] = [altx, alty, j]
2082 return(locs_2d)
2085 # project 2d locations on circle, respecting distance relations between verts
2086 def circle_project_non_regular(locs_2d, x0, y0, r):
2087 for i in range(len(locs_2d)):
2088 x, y, j = locs_2d[i]
2089 loc = mathutils.Vector([x - x0, y - y0])
2090 loc.length = r
2091 locs_2d[i] = [loc[0], loc[1], j]
2093 return(locs_2d)
2096 # project 2d locations on circle, with equal distance between all vertices
2097 def circle_project_regular(locs_2d, x0, y0, r):
2098 # find offset angle and circling direction
2099 x, y, i = locs_2d[0]
2100 loc = mathutils.Vector([x - x0, y - y0])
2101 loc.length = r
2102 offset_angle = loc.angle(mathutils.Vector([1.0, 0.0]), 0.0)
2103 loca = mathutils.Vector([x - x0, y - y0, 0.0])
2104 if loc[1] < -1e-6:
2105 offset_angle *= -1
2106 x, y, j = locs_2d[1]
2107 locb = mathutils.Vector([x - x0, y - y0, 0.0])
2108 if loca.cross(locb)[2] >= 0:
2109 ccw = 1
2110 else:
2111 ccw = -1
2112 # distribute vertices along the circle
2113 for i in range(len(locs_2d)):
2114 t = offset_angle + ccw * (i / len(locs_2d) * 2 * math.pi)
2115 x = math.cos(t) * r
2116 y = math.sin(t) * r
2117 locs_2d[i] = [x, y, locs_2d[i][2]]
2119 return(locs_2d)
2122 # shift loop, so the first vertex is closest to the center
2123 def circle_shift_loop(bm_mod, loop, com):
2124 verts, circular = loop
2125 distances = [
2126 [(bm_mod.verts[vert].co - com).length, i] for i, vert in enumerate(verts)
2128 distances.sort()
2129 shift = distances[0][1]
2130 loop = [verts[shift:] + verts[:shift], circular]
2132 return(loop)
2135 # ########################################
2136 # ##### Curve functions ##################
2137 # ########################################
2139 # create lists with knots and points, all correctly sorted
2140 def curve_calculate_knots(loop, verts_selected):
2141 knots = [v for v in loop[0] if v in verts_selected]
2142 points = loop[0][:]
2143 # circular loop, potential for weird splines
2144 if loop[1]:
2145 offset = int(len(loop[0]) / 4)
2146 kpos = []
2147 for k in knots:
2148 kpos.append(loop[0].index(k))
2149 kdif = []
2150 for i in range(len(kpos) - 1):
2151 kdif.append(kpos[i + 1] - kpos[i])
2152 kdif.append(len(loop[0]) - kpos[-1] + kpos[0])
2153 kadd = []
2154 for k in kdif:
2155 if k > 2 * offset:
2156 kadd.append([kdif.index(k), True])
2157 # next 2 lines are optional, they insert
2158 # an extra control point in small gaps
2159 # elif k > offset:
2160 # kadd.append([kdif.index(k), False])
2161 kins = []
2162 krot = False
2163 for k in kadd: # extra knots to be added
2164 if k[1]: # big gap (break circular spline)
2165 kpos = loop[0].index(knots[k[0]]) + offset
2166 if kpos > len(loop[0]) - 1:
2167 kpos -= len(loop[0])
2168 kins.append([knots[k[0]], loop[0][kpos]])
2169 kpos2 = k[0] + 1
2170 if kpos2 > len(knots) - 1:
2171 kpos2 -= len(knots)
2172 kpos2 = loop[0].index(knots[kpos2]) - offset
2173 if kpos2 < 0:
2174 kpos2 += len(loop[0])
2175 kins.append([loop[0][kpos], loop[0][kpos2]])
2176 krot = loop[0][kpos2]
2177 else: # small gap (keep circular spline)
2178 k1 = loop[0].index(knots[k[0]])
2179 k2 = k[0] + 1
2180 if k2 > len(knots) - 1:
2181 k2 -= len(knots)
2182 k2 = loop[0].index(knots[k2])
2183 if k2 < k1:
2184 dif = len(loop[0]) - 1 - k1 + k2
2185 else:
2186 dif = k2 - k1
2187 kn = k1 + int(dif / 2)
2188 if kn > len(loop[0]) - 1:
2189 kn -= len(loop[0])
2190 kins.append([loop[0][k1], loop[0][kn]])
2191 for j in kins: # insert new knots
2192 knots.insert(knots.index(j[0]) + 1, j[1])
2193 if not krot: # circular loop
2194 knots.append(knots[0])
2195 points = loop[0][loop[0].index(knots[0]):]
2196 points += loop[0][0:loop[0].index(knots[0]) + 1]
2197 else: # non-circular loop (broken by script)
2198 krot = knots.index(krot)
2199 knots = knots[krot:] + knots[0:krot]
2200 if loop[0].index(knots[0]) > loop[0].index(knots[-1]):
2201 points = loop[0][loop[0].index(knots[0]):]
2202 points += loop[0][0:loop[0].index(knots[-1]) + 1]
2203 else:
2204 points = loop[0][loop[0].index(knots[0]):loop[0].index(knots[-1]) + 1]
2205 # non-circular loop, add first and last point as knots
2206 else:
2207 if loop[0][0] not in knots:
2208 knots.insert(0, loop[0][0])
2209 if loop[0][-1] not in knots:
2210 knots.append(loop[0][-1])
2212 return(knots, points)
2215 # calculate relative positions compared to first knot
2216 def curve_calculate_t(bm_mod, knots, points, pknots, regular, circular):
2217 tpoints = []
2218 loc_prev = False
2219 len_total = 0
2221 for p in points:
2222 if p in knots:
2223 loc = pknots[knots.index(p)] # use projected knot location
2224 else:
2225 loc = mathutils.Vector(bm_mod.verts[p].co[:])
2226 if not loc_prev:
2227 loc_prev = loc
2228 len_total += (loc - loc_prev).length
2229 tpoints.append(len_total)
2230 loc_prev = loc
2231 tknots = []
2232 for p in points:
2233 if p in knots:
2234 tknots.append(tpoints[points.index(p)])
2235 if circular:
2236 tknots[-1] = tpoints[-1]
2238 # regular option
2239 if regular:
2240 tpoints_average = tpoints[-1] / (len(tpoints) - 1)
2241 for i in range(1, len(tpoints) - 1):
2242 tpoints[i] = i * tpoints_average
2243 for i in range(len(knots)):
2244 tknots[i] = tpoints[points.index(knots[i])]
2245 if circular:
2246 tknots[-1] = tpoints[-1]
2248 return(tknots, tpoints)
2251 # change the location of non-selected points to their place on the spline
2252 def curve_calculate_vertices(bm_mod, knots, tknots, points, tpoints, splines,
2253 interpolation, restriction):
2254 newlocs = {}
2255 move = []
2257 for p in points:
2258 if p in knots:
2259 continue
2260 m = tpoints[points.index(p)]
2261 if m in tknots:
2262 n = tknots.index(m)
2263 else:
2264 t = tknots[:]
2265 t.append(m)
2266 t.sort()
2267 n = t.index(m) - 1
2268 if n > len(splines) - 1:
2269 n = len(splines) - 1
2270 elif n < 0:
2271 n = 0
2273 if interpolation == 'cubic':
2274 ax, bx, cx, dx, tx = splines[n][0]
2275 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
2276 ay, by, cy, dy, ty = splines[n][1]
2277 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
2278 az, bz, cz, dz, tz = splines[n][2]
2279 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
2280 newloc = mathutils.Vector([x, y, z])
2281 else: # interpolation == 'linear'
2282 a, d, t, u = splines[n]
2283 newloc = ((m - t) / u) * d + a
2285 if restriction != 'none': # vertex movement is restricted
2286 newlocs[p] = newloc
2287 else: # set the vertex to its new location
2288 move.append([p, newloc])
2290 if restriction != 'none': # vertex movement is restricted
2291 for p in points:
2292 if p in newlocs:
2293 newloc = newlocs[p]
2294 else:
2295 move.append([p, bm_mod.verts[p].co])
2296 continue
2297 oldloc = bm_mod.verts[p].co
2298 normal = bm_mod.verts[p].normal
2299 dloc = newloc - oldloc
2300 if dloc.length < 1e-6:
2301 move.append([p, newloc])
2302 elif restriction == 'extrude': # only extrusions
2303 if dloc.angle(normal, 0) < 0.5 * math.pi + 1e-6:
2304 move.append([p, newloc])
2305 else: # restriction == 'indent' only indentations
2306 if dloc.angle(normal) > 0.5 * math.pi - 1e-6:
2307 move.append([p, newloc])
2309 return(move)
2312 # trim loops to part between first and last selected vertices (including)
2313 def curve_cut_boundaries(bm_mod, loops):
2314 cut_loops = []
2315 for loop, circular in loops:
2316 if circular:
2317 # don't cut
2318 cut_loops.append([loop, circular])
2319 continue
2320 selected = [bm_mod.verts[v].select for v in loop]
2321 first = selected.index(True)
2322 selected.reverse()
2323 last = -selected.index(True)
2324 if last == 0:
2325 cut_loops.append([loop[first:], circular])
2326 else:
2327 cut_loops.append([loop[first:last], circular])
2329 return(cut_loops)
2332 # calculate input loops
2333 def curve_get_input(object, bm, boundaries):
2334 # get mesh with modifiers applied
2335 derived, bm_mod = get_derived_bmesh(object, bm)
2337 # vertices that still need a loop to run through it
2338 verts_unsorted = [
2339 v.index for v in bm_mod.verts if v.select and not v.hide
2341 # necessary dictionaries
2342 vert_edges = dict_vert_edges(bm_mod)
2343 edge_faces = dict_edge_faces(bm_mod)
2344 correct_loops = []
2345 # find loops through each selected vertex
2346 while len(verts_unsorted) > 0:
2347 loops = curve_vertex_loops(bm_mod, verts_unsorted[0], vert_edges,
2348 edge_faces)
2349 verts_unsorted.pop(0)
2351 # check if loop is fully selected
2352 search_perpendicular = False
2353 i = -1
2354 for loop, circular in loops:
2355 i += 1
2356 selected = [v for v in loop if bm_mod.verts[v].select]
2357 if len(selected) < 2:
2358 # only one selected vertex on loop, don't use
2359 loops.pop(i)
2360 continue
2361 elif len(selected) == len(loop):
2362 search_perpendicular = loop
2363 break
2364 # entire loop is selected, find perpendicular loops
2365 if search_perpendicular:
2366 for vert in loop:
2367 if vert in verts_unsorted:
2368 verts_unsorted.remove(vert)
2369 perp_loops = curve_perpendicular_loops(bm_mod, loop,
2370 vert_edges, edge_faces)
2371 for perp_loop in perp_loops:
2372 correct_loops.append(perp_loop)
2373 # normal input
2374 else:
2375 for loop, circular in loops:
2376 correct_loops.append([loop, circular])
2378 # boundaries option
2379 if boundaries:
2380 correct_loops = curve_cut_boundaries(bm_mod, correct_loops)
2382 return(derived, bm_mod, correct_loops)
2385 # return all loops that are perpendicular to the given one
2386 def curve_perpendicular_loops(bm_mod, start_loop, vert_edges, edge_faces):
2387 # find perpendicular loops
2388 perp_loops = []
2389 for start_vert in start_loop:
2390 loops = curve_vertex_loops(bm_mod, start_vert, vert_edges,
2391 edge_faces)
2392 for loop, circular in loops:
2393 selected = [v for v in loop if bm_mod.verts[v].select]
2394 if len(selected) == len(loop):
2395 continue
2396 else:
2397 perp_loops.append([loop, circular, loop.index(start_vert)])
2399 # trim loops to same lengths
2400 shortest = [
2401 [len(loop[0]), i] for i, loop in enumerate(perp_loops) if not loop[1]
2403 if not shortest:
2404 # all loops are circular, not trimming
2405 return([[loop[0], loop[1]] for loop in perp_loops])
2406 else:
2407 shortest = min(shortest)
2408 shortest_start = perp_loops[shortest[1]][2]
2409 before_start = shortest_start
2410 after_start = shortest[0] - shortest_start - 1
2411 bigger_before = before_start > after_start
2412 trimmed_loops = []
2413 for loop in perp_loops:
2414 # have the loop face the same direction as the shortest one
2415 if bigger_before:
2416 if loop[2] < len(loop[0]) / 2:
2417 loop[0].reverse()
2418 loop[2] = len(loop[0]) - loop[2] - 1
2419 else:
2420 if loop[2] > len(loop[0]) / 2:
2421 loop[0].reverse()
2422 loop[2] = len(loop[0]) - loop[2] - 1
2423 # circular loops can shift, to prevent wrong trimming
2424 if loop[1]:
2425 shift = shortest_start - loop[2]
2426 if loop[2] + shift > 0 and loop[2] + shift < len(loop[0]):
2427 loop[0] = loop[0][-shift:] + loop[0][:-shift]
2428 loop[2] += shift
2429 if loop[2] < 0:
2430 loop[2] += len(loop[0])
2431 elif loop[2] > len(loop[0]) - 1:
2432 loop[2] -= len(loop[0])
2433 # trim
2434 start = max(0, loop[2] - before_start)
2435 end = min(len(loop[0]), loop[2] + after_start + 1)
2436 trimmed_loops.append([loop[0][start:end], False])
2438 return(trimmed_loops)
2441 # project knots on non-selected geometry
2442 def curve_project_knots(bm_mod, verts_selected, knots, points, circular):
2443 # function to project vertex on edge
2444 def project(v1, v2, v3):
2445 # v1 and v2 are part of a line
2446 # v3 is projected onto it
2447 v2 -= v1
2448 v3 -= v1
2449 p = v3.project(v2)
2450 return(p + v1)
2452 if circular: # project all knots
2453 start = 0
2454 end = len(knots)
2455 pknots = []
2456 else: # first and last knot shouldn't be projected
2457 start = 1
2458 end = -1
2459 pknots = [mathutils.Vector(bm_mod.verts[knots[0]].co[:])]
2460 for knot in knots[start:end]:
2461 if knot in verts_selected:
2462 knot_left = knot_right = False
2463 for i in range(points.index(knot) - 1, -1 * len(points), -1):
2464 if points[i] not in knots:
2465 knot_left = points[i]
2466 break
2467 for i in range(points.index(knot) + 1, 2 * len(points)):
2468 if i > len(points) - 1:
2469 i -= len(points)
2470 if points[i] not in knots:
2471 knot_right = points[i]
2472 break
2473 if knot_left and knot_right and knot_left != knot_right:
2474 knot_left = mathutils.Vector(bm_mod.verts[knot_left].co[:])
2475 knot_right = mathutils.Vector(bm_mod.verts[knot_right].co[:])
2476 knot = mathutils.Vector(bm_mod.verts[knot].co[:])
2477 pknots.append(project(knot_left, knot_right, knot))
2478 else:
2479 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2480 else: # knot isn't selected, so shouldn't be changed
2481 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2482 if not circular:
2483 pknots.append(mathutils.Vector(bm_mod.verts[knots[-1]].co[:]))
2485 return(pknots)
2488 # find all loops through a given vertex
2489 def curve_vertex_loops(bm_mod, start_vert, vert_edges, edge_faces):
2490 edges_used = []
2491 loops = []
2493 for edge in vert_edges[start_vert]:
2494 if edge in edges_used:
2495 continue
2496 loop = []
2497 circular = False
2498 for vert in edge:
2499 active_faces = edge_faces[edge]
2500 new_vert = vert
2501 growing = True
2502 while growing:
2503 growing = False
2504 new_edges = vert_edges[new_vert]
2505 loop.append(new_vert)
2506 if len(loop) > 1:
2507 edges_used.append(tuple(sorted([loop[-1], loop[-2]])))
2508 if len(new_edges) < 3 or len(new_edges) > 4:
2509 # pole
2510 break
2511 else:
2512 # find next edge
2513 for new_edge in new_edges:
2514 if new_edge in edges_used:
2515 continue
2516 eliminate = False
2517 for new_face in edge_faces[new_edge]:
2518 if new_face in active_faces:
2519 eliminate = True
2520 break
2521 if eliminate:
2522 continue
2523 # found correct new edge
2524 active_faces = edge_faces[new_edge]
2525 v1, v2 = new_edge
2526 if v1 != new_vert:
2527 new_vert = v1
2528 else:
2529 new_vert = v2
2530 if new_vert == loop[0]:
2531 circular = True
2532 else:
2533 growing = True
2534 break
2535 if circular:
2536 break
2537 loop.reverse()
2538 loops.append([loop, circular])
2540 return(loops)
2543 # ########################################
2544 # ##### Flatten functions ################
2545 # ########################################
2547 # sort input into loops
2548 def flatten_get_input(bm):
2549 vert_verts = dict_vert_verts(
2550 [edgekey(edge) for edge in bm.edges if edge.select and not edge.hide]
2552 verts = [v.index for v in bm.verts if v.select and not v.hide]
2554 # no connected verts, consider all selected verts as a single input
2555 if not vert_verts:
2556 return([[verts, False]])
2558 loops = []
2559 while len(verts) > 0:
2560 # start of loop
2561 loop = [verts[0]]
2562 verts.pop(0)
2563 if loop[-1] in vert_verts:
2564 to_grow = vert_verts[loop[-1]]
2565 else:
2566 to_grow = []
2567 # grow loop
2568 while len(to_grow) > 0:
2569 new_vert = to_grow[0]
2570 to_grow.pop(0)
2571 if new_vert in loop:
2572 continue
2573 loop.append(new_vert)
2574 verts.remove(new_vert)
2575 to_grow += vert_verts[new_vert]
2576 # add loop to loops
2577 loops.append([loop, False])
2579 return(loops)
2582 # calculate position of vertex projections on plane
2583 def flatten_project(bm, loop, com, normal):
2584 verts = [bm.verts[v] for v in loop[0]]
2585 verts_projected = [
2586 [v.index, mathutils.Vector(v.co[:]) -
2587 (mathutils.Vector(v.co[:]) - com).dot(normal) * normal] for v in verts
2590 return(verts_projected)
2593 # ########################################
2594 # ##### Gstretch functions ###############
2595 # ########################################
2597 # fake stroke class, used to create custom strokes if no GP data is found
2598 class gstretch_fake_stroke():
2599 def __init__(self, points):
2600 self.points = [gstretch_fake_stroke_point(p) for p in points]
2603 # fake stroke point class, used in fake strokes
2604 class gstretch_fake_stroke_point():
2605 def __init__(self, loc):
2606 self.co = loc
2609 # flips loops, if necessary, to obtain maximum alignment to stroke
2610 def gstretch_align_pairs(ls_pairs, object, bm_mod, method):
2611 # returns total distance between all verts in loop and corresponding stroke
2612 def distance_loop_stroke(loop, stroke, object, bm_mod, method):
2613 stroke_lengths_cache = False
2614 loop_length = len(loop[0])
2615 total_distance = 0
2617 if method != 'regular':
2618 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2620 for i, v_index in enumerate(loop[0]):
2621 if method == 'regular':
2622 relative_distance = i / (loop_length - 1)
2623 else:
2624 relative_distance = relative_lengths[i]
2626 loc1 = object.matrix_world @ bm_mod.verts[v_index].co
2627 loc2, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2628 relative_distance, stroke_lengths_cache)
2629 total_distance += (loc2 - loc1).length
2631 return(total_distance)
2633 if ls_pairs:
2634 for (loop, stroke) in ls_pairs:
2635 total_dist = distance_loop_stroke(loop, stroke, object, bm_mod,
2636 method)
2637 loop[0].reverse()
2638 total_dist_rev = distance_loop_stroke(loop, stroke, object, bm_mod,
2639 method)
2640 if total_dist_rev > total_dist:
2641 loop[0].reverse()
2643 return(ls_pairs)
2646 # calculate vertex positions on stroke
2647 def gstretch_calculate_verts(loop, stroke, object, bm_mod, method):
2648 move = []
2649 stroke_lengths_cache = False
2650 loop_length = len(loop[0])
2651 matrix_inverse = object.matrix_world.inverted()
2653 # return intersection of line with stroke, or None
2654 def intersect_line_stroke(vec1, vec2, stroke):
2655 for i, p in enumerate(stroke.points[1:]):
2656 intersections = mathutils.geometry.intersect_line_line(vec1, vec2,
2657 p.co, stroke.points[i].co)
2658 if intersections and \
2659 (intersections[0] - intersections[1]).length < 1e-2:
2660 x, dist = mathutils.geometry.intersect_point_line(
2661 intersections[0], p.co, stroke.points[i].co)
2662 if -1 < dist < 1:
2663 return(intersections[0])
2664 return(None)
2666 if method == 'project':
2667 vert_edges = dict_vert_edges(bm_mod)
2669 for v_index in loop[0]:
2670 intersection = None
2671 for ek in vert_edges[v_index]:
2672 v1, v2 = ek
2673 v1 = bm_mod.verts[v1]
2674 v2 = bm_mod.verts[v2]
2675 if v1.select + v2.select == 1 and not v1.hide and not v2.hide:
2676 vec1 = object.matrix_world @ v1.co
2677 vec2 = object.matrix_world @ v2.co
2678 intersection = intersect_line_stroke(vec1, vec2, stroke)
2679 if intersection:
2680 break
2681 if not intersection:
2682 v = bm_mod.verts[v_index]
2683 intersection = intersect_line_stroke(v.co, v.co + v.normal,
2684 stroke)
2685 if intersection:
2686 move.append([v_index, matrix_inverse @ intersection])
2688 else:
2689 if method == 'irregular':
2690 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2692 for i, v_index in enumerate(loop[0]):
2693 if method == 'regular':
2694 relative_distance = i / (loop_length - 1)
2695 else: # method == 'irregular'
2696 relative_distance = relative_lengths[i]
2697 loc, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2698 relative_distance, stroke_lengths_cache)
2699 loc = matrix_inverse @ loc
2700 move.append([v_index, loc])
2702 return(move)
2705 # create new vertices, based on GP strokes
2706 def gstretch_create_verts(object, bm_mod, strokes, method, conversion,
2707 conversion_distance, conversion_max, conversion_min, conversion_vertices):
2708 move = []
2709 stroke_verts = []
2710 mat_world = object.matrix_world.inverted()
2711 singles = gstretch_match_single_verts(bm_mod, strokes, mat_world)
2713 for stroke in strokes:
2714 stroke_verts.append([stroke, []])
2715 min_end_point = 0
2716 if conversion == 'vertices':
2717 min_end_point = conversion_vertices
2718 end_point = conversion_vertices
2719 elif conversion == 'limit_vertices':
2720 min_end_point = conversion_min
2721 end_point = conversion_max
2722 else:
2723 end_point = len(stroke.points)
2724 # creation of new vertices at fixed user-defined distances
2725 if conversion == 'distance':
2726 method = 'project'
2727 prev_point = stroke.points[0]
2728 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world @ prev_point.co))
2729 distance = 0
2730 limit = conversion_distance
2731 for point in stroke.points:
2732 new_distance = distance + (point.co - prev_point.co).length
2733 iteration = 0
2734 while new_distance > limit:
2735 to_cover = limit - distance + (limit * iteration)
2736 new_loc = prev_point.co + to_cover * \
2737 (point.co - prev_point.co).normalized()
2738 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * new_loc))
2739 new_distance -= limit
2740 iteration += 1
2741 distance = new_distance
2742 prev_point = point
2743 # creation of new vertices for other methods
2744 else:
2745 # add vertices at stroke points
2746 for point in stroke.points[:end_point]:
2747 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world @ point.co))
2748 # add more vertices, beyond the points that are available
2749 if min_end_point > min(len(stroke.points), end_point):
2750 for i in range(min_end_point -
2751 (min(len(stroke.points), end_point))):
2752 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world @ point.co))
2753 # force even spreading of points, so they are placed on stroke
2754 method = 'regular'
2755 bm_mod.verts.ensure_lookup_table()
2756 bm_mod.verts.index_update()
2757 for stroke, verts_seq in stroke_verts:
2758 if len(verts_seq) < 2:
2759 continue
2760 # spread vertices evenly over the stroke
2761 if method == 'regular':
2762 loop = [[vert.index for vert in verts_seq], False]
2763 move += gstretch_calculate_verts(loop, stroke, object, bm_mod,
2764 method)
2765 # create edges
2766 for i, vert in enumerate(verts_seq):
2767 if i > 0:
2768 bm_mod.edges.new((verts_seq[i - 1], verts_seq[i]))
2769 vert.select = True
2770 # connect single vertices to the closest stroke
2771 if singles:
2772 for vert, m_stroke, point in singles:
2773 if m_stroke != stroke:
2774 continue
2775 bm_mod.edges.new((vert, verts_seq[point]))
2776 bm_mod.edges.ensure_lookup_table()
2777 bmesh.update_edit_mesh(object.data)
2779 return(move)
2782 # erases the grease pencil stroke
2783 def gstretch_erase_stroke(stroke, context):
2784 # change 3d coordinate into a stroke-point
2785 def sp(loc, context):
2786 lib = {'name': "",
2787 'pen_flip': False,
2788 'is_start': False,
2789 'location': (0, 0, 0),
2790 'mouse': (
2791 view3d_utils.location_3d_to_region_2d(
2792 context.region, context.space_data.region_3d, loc)
2794 'pressure': 1,
2795 'size': 0,
2796 'time': 0}
2797 return(lib)
2799 if type(stroke) != bpy.types.GPencilStroke:
2800 # fake stroke, there is nothing to delete
2801 return
2803 erase_stroke = [sp(p.co, context) for p in stroke.points]
2804 if erase_stroke:
2805 erase_stroke[0]['is_start'] = True
2806 #bpy.ops.gpencil.draw(mode='ERASER', stroke=erase_stroke)
2807 bpy.ops.gpencil.data_unlink()
2811 # get point on stroke, given by relative distance (0.0 - 1.0)
2812 def gstretch_eval_stroke(stroke, distance, stroke_lengths_cache=False):
2813 # use cache if available
2814 if not stroke_lengths_cache:
2815 lengths = [0]
2816 for i, p in enumerate(stroke.points[1:]):
2817 lengths.append((p.co - stroke.points[i].co).length + lengths[-1])
2818 total_length = max(lengths[-1], 1e-7)
2819 stroke_lengths_cache = [length / total_length for length in
2820 lengths]
2821 stroke_lengths = stroke_lengths_cache[:]
2823 if distance in stroke_lengths:
2824 loc = stroke.points[stroke_lengths.index(distance)].co
2825 elif distance > stroke_lengths[-1]:
2826 # should be impossible, but better safe than sorry
2827 loc = stroke.points[-1].co
2828 else:
2829 stroke_lengths.append(distance)
2830 stroke_lengths.sort()
2831 stroke_index = stroke_lengths.index(distance)
2832 interval_length = stroke_lengths[
2833 stroke_index + 1] - stroke_lengths[stroke_index - 1
2835 distance_relative = (distance - stroke_lengths[stroke_index - 1]) / interval_length
2836 interval_vector = stroke.points[stroke_index].co - stroke.points[stroke_index - 1].co
2837 loc = stroke.points[stroke_index - 1].co + distance_relative * interval_vector
2839 return(loc, stroke_lengths_cache)
2842 # create fake grease pencil strokes for the active object
2843 def gstretch_get_fake_strokes(object, bm_mod, loops):
2844 strokes = []
2845 for loop in loops:
2846 p1 = object.matrix_world @ bm_mod.verts[loop[0][0]].co
2847 p2 = object.matrix_world @ bm_mod.verts[loop[0][-1]].co
2848 strokes.append(gstretch_fake_stroke([p1, p2]))
2850 return(strokes)
2852 # get strokes
2853 def gstretch_get_strokes(self, context):
2854 looptools = context.window_manager.looptools
2855 gp = get_strokes(self, context)
2856 if not gp:
2857 return(None)
2858 if looptools.gstretch_use_guide == "Annotation":
2859 layer = bpy.data.grease_pencils[0].layers.active
2860 if looptools.gstretch_use_guide == "GPencil" and not looptools.gstretch_guide == None:
2861 layer = looptools.gstretch_guide.data.layers.active
2862 if not layer:
2863 return(None)
2864 frame = layer.active_frame
2865 if not frame:
2866 return(None)
2867 strokes = frame.strokes
2868 if len(strokes) < 1:
2869 return(None)
2871 return(strokes)
2873 # returns a list with loop-stroke pairs
2874 def gstretch_match_loops_strokes(loops, strokes, object, bm_mod):
2875 if not loops or not strokes:
2876 return(None)
2878 # calculate loop centers
2879 loop_centers = []
2880 bm_mod.verts.ensure_lookup_table()
2881 for loop in loops:
2882 center = mathutils.Vector()
2883 for v_index in loop[0]:
2884 center += bm_mod.verts[v_index].co
2885 center /= len(loop[0])
2886 center = object.matrix_world @ center
2887 loop_centers.append([center, loop])
2889 # calculate stroke centers
2890 stroke_centers = []
2891 for stroke in strokes:
2892 center = mathutils.Vector()
2893 for p in stroke.points:
2894 center += p.co
2895 center /= len(stroke.points)
2896 stroke_centers.append([center, stroke, 0])
2898 # match, first by stroke use count, then by distance
2899 ls_pairs = []
2900 for lc in loop_centers:
2901 distances = []
2902 for i, sc in enumerate(stroke_centers):
2903 distances.append([sc[2], (lc[0] - sc[0]).length, i])
2904 distances.sort()
2905 best_stroke = distances[0][2]
2906 ls_pairs.append([lc[1], stroke_centers[best_stroke][1]])
2907 stroke_centers[best_stroke][2] += 1 # increase stroke use count
2909 return(ls_pairs)
2912 # match single selected vertices to the closest stroke endpoint
2913 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2914 def gstretch_match_single_verts(bm_mod, strokes, mat_world):
2915 # calculate stroke endpoints in object space
2916 endpoints = []
2917 for stroke in strokes:
2918 endpoints.append((mat_world @ stroke.points[0].co, stroke, 0))
2919 endpoints.append((mat_world @ stroke.points[-1].co, stroke, -1))
2921 distances = []
2922 # find single vertices (not connected to other selected verts)
2923 for vert in bm_mod.verts:
2924 if not vert.select:
2925 continue
2926 single = True
2927 for edge in vert.link_edges:
2928 if edge.other_vert(vert).select:
2929 single = False
2930 break
2931 if not single:
2932 continue
2933 # calculate distances from vertex to endpoints
2934 distance = [((vert.co - loc).length, vert, stroke, stroke_point,
2935 endpoint_index) for endpoint_index, (loc, stroke, stroke_point) in
2936 enumerate(endpoints)]
2937 distance.sort()
2938 distances.append(distance[0])
2940 # create matches, based on shortest distance first
2941 singles = []
2942 while distances:
2943 distances.sort()
2944 singles.append((distances[0][1], distances[0][2], distances[0][3]))
2945 endpoints.pop(distances[0][4])
2946 distances.pop(0)
2947 distances_new = []
2948 for (i, vert, j, k, l) in distances:
2949 distance_new = [((vert.co - loc).length, vert, stroke, stroke_point,
2950 endpoint_index) for endpoint_index, (loc, stroke,
2951 stroke_point) in enumerate(endpoints)]
2952 distance_new.sort()
2953 distances_new.append(distance_new[0])
2954 distances = distances_new
2956 return(singles)
2959 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2960 def gstretch_relative_lengths(loop, bm_mod):
2961 lengths = [0]
2962 for i, v_index in enumerate(loop[0][1:]):
2963 lengths.append(
2964 (bm_mod.verts[v_index].co -
2965 bm_mod.verts[loop[0][i]].co).length + lengths[-1]
2967 total_length = max(lengths[-1], 1e-7)
2968 relative_lengths = [length / total_length for length in
2969 lengths]
2971 return(relative_lengths)
2974 # convert cache-stored strokes into usable (fake) GP strokes
2975 def gstretch_safe_to_true_strokes(safe_strokes):
2976 strokes = []
2977 for safe_stroke in safe_strokes:
2978 strokes.append(gstretch_fake_stroke(safe_stroke))
2980 return(strokes)
2983 # convert a GP stroke into a list of points which can be stored in cache
2984 def gstretch_true_to_safe_strokes(strokes):
2985 safe_strokes = []
2986 for stroke in strokes:
2987 safe_strokes.append([p.co.copy() for p in stroke.points])
2989 return(safe_strokes)
2992 # force consistency in GUI, max value can never be lower than min value
2993 def gstretch_update_max(self, context):
2994 # called from operator settings (after execution)
2995 if 'conversion_min' in self.keys():
2996 if self.conversion_min > self.conversion_max:
2997 self.conversion_max = self.conversion_min
2998 # called from toolbar
2999 else:
3000 lt = context.window_manager.looptools
3001 if lt.gstretch_conversion_min > lt.gstretch_conversion_max:
3002 lt.gstretch_conversion_max = lt.gstretch_conversion_min
3005 # force consistency in GUI, min value can never be higher than max value
3006 def gstretch_update_min(self, context):
3007 # called from operator settings (after execution)
3008 if 'conversion_max' in self.keys():
3009 if self.conversion_max < self.conversion_min:
3010 self.conversion_min = self.conversion_max
3011 # called from toolbar
3012 else:
3013 lt = context.window_manager.looptools
3014 if lt.gstretch_conversion_max < lt.gstretch_conversion_min:
3015 lt.gstretch_conversion_min = lt.gstretch_conversion_max
3018 # ########################################
3019 # ##### Relax functions ##################
3020 # ########################################
3022 # create lists with knots and points, all correctly sorted
3023 def relax_calculate_knots(loops):
3024 all_knots = []
3025 all_points = []
3026 for loop, circular in loops:
3027 knots = [[], []]
3028 points = [[], []]
3029 if circular:
3030 if len(loop) % 2 == 1: # odd
3031 extend = [False, True, 0, 1, 0, 1]
3032 else: # even
3033 extend = [True, False, 0, 1, 1, 2]
3034 else:
3035 if len(loop) % 2 == 1: # odd
3036 extend = [False, False, 0, 1, 1, 2]
3037 else: # even
3038 extend = [False, False, 0, 1, 1, 2]
3039 for j in range(2):
3040 if extend[j]:
3041 loop = [loop[-1]] + loop + [loop[0]]
3042 for i in range(extend[2 + 2 * j], len(loop), 2):
3043 knots[j].append(loop[i])
3044 for i in range(extend[3 + 2 * j], len(loop), 2):
3045 if loop[i] == loop[-1] and not circular:
3046 continue
3047 if len(points[j]) == 0:
3048 points[j].append(loop[i])
3049 elif loop[i] != points[j][0]:
3050 points[j].append(loop[i])
3051 if circular:
3052 if knots[j][0] != knots[j][-1]:
3053 knots[j].append(knots[j][0])
3054 if len(points[1]) == 0:
3055 knots.pop(1)
3056 points.pop(1)
3057 for k in knots:
3058 all_knots.append(k)
3059 for p in points:
3060 all_points.append(p)
3062 return(all_knots, all_points)
3065 # calculate relative positions compared to first knot
3066 def relax_calculate_t(bm_mod, knots, points, regular):
3067 all_tknots = []
3068 all_tpoints = []
3069 for i in range(len(knots)):
3070 amount = len(knots[i]) + len(points[i])
3071 mix = []
3072 for j in range(amount):
3073 if j % 2 == 0:
3074 mix.append([True, knots[i][round(j / 2)]])
3075 elif j == amount - 1:
3076 mix.append([True, knots[i][-1]])
3077 else:
3078 mix.append([False, points[i][int(j / 2)]])
3079 len_total = 0
3080 loc_prev = False
3081 tknots = []
3082 tpoints = []
3083 for m in mix:
3084 loc = mathutils.Vector(bm_mod.verts[m[1]].co[:])
3085 if not loc_prev:
3086 loc_prev = loc
3087 len_total += (loc - loc_prev).length
3088 if m[0]:
3089 tknots.append(len_total)
3090 else:
3091 tpoints.append(len_total)
3092 loc_prev = loc
3093 if regular:
3094 tpoints = []
3095 for p in range(len(points[i])):
3096 tpoints.append((tknots[p] + tknots[p + 1]) / 2)
3097 all_tknots.append(tknots)
3098 all_tpoints.append(tpoints)
3100 return(all_tknots, all_tpoints)
3103 # change the location of the points to their place on the spline
3104 def relax_calculate_verts(bm_mod, interpolation, tknots, knots, tpoints,
3105 points, splines):
3106 change = []
3107 move = []
3108 for i in range(len(knots)):
3109 for p in points[i]:
3110 m = tpoints[i][points[i].index(p)]
3111 if m in tknots[i]:
3112 n = tknots[i].index(m)
3113 else:
3114 t = tknots[i][:]
3115 t.append(m)
3116 t.sort()
3117 n = t.index(m) - 1
3118 if n > len(splines[i]) - 1:
3119 n = len(splines[i]) - 1
3120 elif n < 0:
3121 n = 0
3123 if interpolation == 'cubic':
3124 ax, bx, cx, dx, tx = splines[i][n][0]
3125 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
3126 ay, by, cy, dy, ty = splines[i][n][1]
3127 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
3128 az, bz, cz, dz, tz = splines[i][n][2]
3129 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
3130 change.append([p, mathutils.Vector([x, y, z])])
3131 else: # interpolation == 'linear'
3132 a, d, t, u = splines[i][n]
3133 if u == 0:
3134 u = 1e-8
3135 change.append([p, ((m - t) / u) * d + a])
3136 for c in change:
3137 move.append([c[0], (bm_mod.verts[c[0]].co + c[1]) / 2])
3139 return(move)
3142 # ########################################
3143 # ##### Space functions ##################
3144 # ########################################
3146 # calculate relative positions compared to first knot
3147 def space_calculate_t(bm_mod, knots):
3148 tknots = []
3149 loc_prev = False
3150 len_total = 0
3151 for k in knots:
3152 loc = mathutils.Vector(bm_mod.verts[k].co[:])
3153 if not loc_prev:
3154 loc_prev = loc
3155 len_total += (loc - loc_prev).length
3156 tknots.append(len_total)
3157 loc_prev = loc
3158 amount = len(knots)
3159 t_per_segment = len_total / (amount - 1)
3160 tpoints = [i * t_per_segment for i in range(amount)]
3162 return(tknots, tpoints)
3165 # change the location of the points to their place on the spline
3166 def space_calculate_verts(bm_mod, interpolation, tknots, tpoints, points,
3167 splines):
3168 move = []
3169 for p in points:
3170 m = tpoints[points.index(p)]
3171 if m in tknots:
3172 n = tknots.index(m)
3173 else:
3174 t = tknots[:]
3175 t.append(m)
3176 t.sort()
3177 n = t.index(m) - 1
3178 if n > len(splines) - 1:
3179 n = len(splines) - 1
3180 elif n < 0:
3181 n = 0
3183 if interpolation == 'cubic':
3184 ax, bx, cx, dx, tx = splines[n][0]
3185 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
3186 ay, by, cy, dy, ty = splines[n][1]
3187 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
3188 az, bz, cz, dz, tz = splines[n][2]
3189 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
3190 move.append([p, mathutils.Vector([x, y, z])])
3191 else: # interpolation == 'linear'
3192 a, d, t, u = splines[n]
3193 move.append([p, ((m - t) / u) * d + a])
3195 return(move)
3198 # ########################################
3199 # ##### Operators ########################
3200 # ########################################
3202 # bridge operator
3203 class Bridge(Operator):
3204 bl_idname = 'mesh.looptools_bridge'
3205 bl_label = "Bridge / Loft"
3206 bl_description = "Bridge two, or loft several, loops of vertices"
3207 bl_options = {'REGISTER', 'UNDO'}
3209 cubic_strength: FloatProperty(
3210 name="Strength",
3211 description="Higher strength results in more fluid curves",
3212 default=1.0,
3213 soft_min=-3.0,
3214 soft_max=3.0
3216 interpolation: EnumProperty(
3217 name="Interpolation mode",
3218 items=(('cubic', "Cubic", "Gives curved results"),
3219 ('linear', "Linear", "Basic, fast, straight interpolation")),
3220 description="Interpolation mode: algorithm used when creating "
3221 "segments",
3222 default='cubic'
3224 loft: BoolProperty(
3225 name="Loft",
3226 description="Loft multiple loops, instead of considering them as "
3227 "a multi-input for bridging",
3228 default=False
3230 loft_loop: BoolProperty(
3231 name="Loop",
3232 description="Connect the first and the last loop with each other",
3233 default=False
3235 min_width: IntProperty(
3236 name="Minimum width",
3237 description="Segments with an edge smaller than this are merged "
3238 "(compared to base edge)",
3239 default=0,
3240 min=0,
3241 max=100,
3242 subtype='PERCENTAGE'
3244 mode: EnumProperty(
3245 name="Mode",
3246 items=(('basic', "Basic", "Fast algorithm"),
3247 ('shortest', "Shortest edge", "Slower algorithm with better vertex matching")),
3248 description="Algorithm used for bridging",
3249 default='shortest'
3251 remove_faces: BoolProperty(
3252 name="Remove faces",
3253 description="Remove faces that are internal after bridging",
3254 default=True
3256 reverse: BoolProperty(
3257 name="Reverse",
3258 description="Manually override the direction in which the loops "
3259 "are bridged. Only use if the tool gives the wrong result",
3260 default=False
3262 segments: IntProperty(
3263 name="Segments",
3264 description="Number of segments used to bridge the gap (0=automatic)",
3265 default=1,
3266 min=0,
3267 soft_max=20
3269 twist: IntProperty(
3270 name="Twist",
3271 description="Twist what vertices are connected to each other",
3272 default=0
3275 @classmethod
3276 def poll(cls, context):
3277 ob = context.active_object
3278 return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3280 def draw(self, context):
3281 layout = self.layout
3282 # layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3284 # top row
3285 col_top = layout.column(align=True)
3286 row = col_top.row(align=True)
3287 col_left = row.column(align=True)
3288 col_right = row.column(align=True)
3289 col_right.active = self.segments != 1
3290 col_left.prop(self, "segments")
3291 col_right.prop(self, "min_width", text="")
3292 # bottom row
3293 bottom_left = col_left.row()
3294 bottom_left.active = self.segments != 1
3295 bottom_left.prop(self, "interpolation", text="")
3296 bottom_right = col_right.row()
3297 bottom_right.active = self.interpolation == 'cubic'
3298 bottom_right.prop(self, "cubic_strength")
3299 # boolean properties
3300 col_top.prop(self, "remove_faces")
3301 if self.loft:
3302 col_top.prop(self, "loft_loop")
3304 # override properties
3305 col_top.separator()
3306 row = layout.row(align=True)
3307 row.prop(self, "twist")
3308 row.prop(self, "reverse")
3310 def invoke(self, context, event):
3311 # load custom settings
3312 context.window_manager.looptools.bridge_loft = self.loft
3313 settings_load(self)
3314 return self.execute(context)
3316 def execute(self, context):
3317 # initialise
3318 object, bm = initialise()
3319 edge_faces, edgekey_to_edge, old_selected_faces, smooth = \
3320 bridge_initialise(bm, self.interpolation)
3321 settings_write(self)
3323 # check cache to see if we can save time
3324 input_method = bridge_input_method(self.loft, self.loft_loop)
3325 cached, single_loops, loops, derived, mapping = cache_read("Bridge",
3326 object, bm, input_method, False)
3327 if not cached:
3328 # get loops
3329 loops = bridge_get_input(bm)
3330 if loops:
3331 # reorder loops if there are more than 2
3332 if len(loops) > 2:
3333 if self.loft:
3334 loops = bridge_sort_loops(bm, loops, self.loft_loop)
3335 else:
3336 loops = bridge_match_loops(bm, loops)
3338 # saving cache for faster execution next time
3339 if not cached:
3340 cache_write("Bridge", object, bm, input_method, False, False,
3341 loops, False, False)
3343 if loops:
3344 # calculate new geometry
3345 vertices = []
3346 faces = []
3347 max_vert_index = len(bm.verts) - 1
3348 for i in range(1, len(loops)):
3349 if not self.loft and i % 2 == 0:
3350 continue
3351 lines = bridge_calculate_lines(bm, loops[i - 1:i + 1],
3352 self.mode, self.twist, self.reverse)
3353 vertex_normals = bridge_calculate_virtual_vertex_normals(bm,
3354 lines, loops[i - 1:i + 1], edge_faces, edgekey_to_edge)
3355 segments = bridge_calculate_segments(bm, lines,
3356 loops[i - 1:i + 1], self.segments)
3357 new_verts, new_faces, max_vert_index = \
3358 bridge_calculate_geometry(
3359 bm, lines, vertex_normals,
3360 segments, self.interpolation, self.cubic_strength,
3361 self.min_width, max_vert_index
3363 if new_verts:
3364 vertices += new_verts
3365 if new_faces:
3366 faces += new_faces
3367 # make sure faces in loops that aren't used, aren't removed
3368 if self.remove_faces and old_selected_faces:
3369 bridge_save_unused_faces(bm, old_selected_faces, loops)
3370 # create vertices
3371 if vertices:
3372 bridge_create_vertices(bm, vertices)
3373 # create faces
3374 if faces:
3375 new_faces = bridge_create_faces(object, bm, faces, self.twist)
3376 old_selected_faces = [
3377 i for i, face in enumerate(bm.faces) if face.index in old_selected_faces
3378 ] # updating list
3379 bridge_select_new_faces(new_faces, smooth)
3380 # edge-data could have changed, can't use cache next run
3381 if faces and not vertices:
3382 cache_delete("Bridge")
3383 # delete internal faces
3384 if self.remove_faces and old_selected_faces:
3385 bridge_remove_internal_faces(bm, old_selected_faces)
3386 # make sure normals are facing outside
3387 bmesh.update_edit_mesh(object.data, loop_triangles=False,
3388 destructive=True)
3389 bpy.ops.mesh.normals_make_consistent()
3391 # cleaning up
3392 terminate()
3394 return{'FINISHED'}
3397 # circle operator
3398 class Circle(Operator):
3399 bl_idname = "mesh.looptools_circle"
3400 bl_label = "Circle"
3401 bl_description = "Move selected vertices into a circle shape"
3402 bl_options = {'REGISTER', 'UNDO'}
3404 custom_radius: BoolProperty(
3405 name="Radius",
3406 description="Force a custom radius",
3407 default=False
3409 fit: EnumProperty(
3410 name="Method",
3411 items=(("best", "Best fit", "Non-linear least squares"),
3412 ("inside", "Fit inside", "Only move vertices towards the center")),
3413 description="Method used for fitting a circle to the vertices",
3414 default='best'
3416 flatten: BoolProperty(
3417 name="Flatten",
3418 description="Flatten the circle, instead of projecting it on the mesh",
3419 default=True
3421 influence: FloatProperty(
3422 name="Influence",
3423 description="Force of the tool",
3424 default=100.0,
3425 min=0.0,
3426 max=100.0,
3427 precision=1,
3428 subtype='PERCENTAGE'
3430 lock_x: BoolProperty(
3431 name="Lock X",
3432 description="Lock editing of the x-coordinate",
3433 default=False
3435 lock_y: BoolProperty(
3436 name="Lock Y",
3437 description="Lock editing of the y-coordinate",
3438 default=False
3440 lock_z: BoolProperty(name="Lock Z",
3441 description="Lock editing of the z-coordinate",
3442 default=False
3444 radius: FloatProperty(
3445 name="Radius",
3446 description="Custom radius for circle",
3447 default=1.0,
3448 min=0.0,
3449 soft_max=1000.0
3451 regular: BoolProperty(
3452 name="Regular",
3453 description="Distribute vertices at constant distances along the circle",
3454 default=True
3457 @classmethod
3458 def poll(cls, context):
3459 ob = context.active_object
3460 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3462 def draw(self, context):
3463 layout = self.layout
3464 col = layout.column()
3466 col.prop(self, "fit")
3467 col.separator()
3469 col.prop(self, "flatten")
3470 row = col.row(align=True)
3471 row.prop(self, "custom_radius")
3472 row_right = row.row(align=True)
3473 row_right.active = self.custom_radius
3474 row_right.prop(self, "radius", text="")
3475 col.prop(self, "regular")
3476 col.separator()
3478 col_move = col.column(align=True)
3479 row = col_move.row(align=True)
3480 if self.lock_x:
3481 row.prop(self, "lock_x", text="X", icon='LOCKED')
3482 else:
3483 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3484 if self.lock_y:
3485 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3486 else:
3487 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3488 if self.lock_z:
3489 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3490 else:
3491 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3492 col_move.prop(self, "influence")
3494 def invoke(self, context, event):
3495 # load custom settings
3496 settings_load(self)
3497 return self.execute(context)
3499 def execute(self, context):
3500 # initialise
3501 object, bm = initialise()
3502 settings_write(self)
3503 # check cache to see if we can save time
3504 cached, single_loops, loops, derived, mapping = cache_read("Circle",
3505 object, bm, False, False)
3506 if cached:
3507 derived, bm_mod = get_derived_bmesh(object, bm)
3508 else:
3509 # find loops
3510 derived, bm_mod, single_vertices, single_loops, loops = \
3511 circle_get_input(object, bm)
3512 mapping = get_mapping(derived, bm, bm_mod, single_vertices,
3513 False, loops)
3514 single_loops, loops = circle_check_loops(single_loops, loops,
3515 mapping, bm_mod)
3517 # saving cache for faster execution next time
3518 if not cached:
3519 cache_write("Circle", object, bm, False, False, single_loops,
3520 loops, derived, mapping)
3522 move = []
3523 for i, loop in enumerate(loops):
3524 # best fitting flat plane
3525 com, normal = calculate_plane(bm_mod, loop)
3526 # if circular, shift loop so we get a good starting vertex
3527 if loop[1]:
3528 loop = circle_shift_loop(bm_mod, loop, com)
3529 # flatten vertices on plane
3530 locs_2d, p, q = circle_3d_to_2d(bm_mod, loop, com, normal)
3531 # calculate circle
3532 if self.fit == 'best':
3533 x0, y0, r = circle_calculate_best_fit(locs_2d)
3534 else: # self.fit == 'inside'
3535 x0, y0, r = circle_calculate_min_fit(locs_2d)
3536 # radius override
3537 if self.custom_radius:
3538 r = self.radius / p.length
3539 # calculate positions on circle
3540 if self.regular:
3541 new_locs_2d = circle_project_regular(locs_2d[:], x0, y0, r)
3542 else:
3543 new_locs_2d = circle_project_non_regular(locs_2d[:], x0, y0, r)
3544 # take influence into account
3545 locs_2d = circle_influence_locs(locs_2d, new_locs_2d,
3546 self.influence)
3547 # calculate 3d positions of the created 2d input
3548 move.append(circle_calculate_verts(self.flatten, bm_mod,
3549 locs_2d, com, p, q, normal))
3550 # flatten single input vertices on plane defined by loop
3551 if self.flatten and single_loops:
3552 move.append(circle_flatten_singles(bm_mod, com, p, q,
3553 normal, single_loops[i]))
3555 # move vertices to new locations
3556 if self.lock_x or self.lock_y or self.lock_z:
3557 lock = [self.lock_x, self.lock_y, self.lock_z]
3558 else:
3559 lock = False
3560 move_verts(object, bm, mapping, move, lock, -1)
3562 # cleaning up
3563 if derived:
3564 bm_mod.free()
3565 terminate()
3567 return{'FINISHED'}
3570 # curve operator
3571 class Curve(Operator):
3572 bl_idname = "mesh.looptools_curve"
3573 bl_label = "Curve"
3574 bl_description = "Turn a loop into a smooth curve"
3575 bl_options = {'REGISTER', 'UNDO'}
3577 boundaries: BoolProperty(
3578 name="Boundaries",
3579 description="Limit the tool to work within the boundaries of the selected vertices",
3580 default=False
3582 influence: FloatProperty(
3583 name="Influence",
3584 description="Force of the tool",
3585 default=100.0,
3586 min=0.0,
3587 max=100.0,
3588 precision=1,
3589 subtype='PERCENTAGE'
3591 interpolation: EnumProperty(
3592 name="Interpolation",
3593 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
3594 ("linear", "Linear", "Simple and fast linear algorithm")),
3595 description="Algorithm used for interpolation",
3596 default='cubic'
3598 lock_x: BoolProperty(
3599 name="Lock X",
3600 description="Lock editing of the x-coordinate",
3601 default=False
3603 lock_y: BoolProperty(
3604 name="Lock Y",
3605 description="Lock editing of the y-coordinate",
3606 default=False
3608 lock_z: BoolProperty(
3609 name="Lock Z",
3610 description="Lock editing of the z-coordinate",
3611 default=False
3613 regular: BoolProperty(
3614 name="Regular",
3615 description="Distribute vertices at constant distances along the curve",
3616 default=True
3618 restriction: EnumProperty(
3619 name="Restriction",
3620 items=(("none", "None", "No restrictions on vertex movement"),
3621 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
3622 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
3623 description="Restrictions on how the vertices can be moved",
3624 default='none'
3627 @classmethod
3628 def poll(cls, context):
3629 ob = context.active_object
3630 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3632 def draw(self, context):
3633 layout = self.layout
3634 col = layout.column()
3636 col.prop(self, "interpolation")
3637 col.prop(self, "restriction")
3638 col.prop(self, "boundaries")
3639 col.prop(self, "regular")
3640 col.separator()
3642 col_move = col.column(align=True)
3643 row = col_move.row(align=True)
3644 if self.lock_x:
3645 row.prop(self, "lock_x", text="X", icon='LOCKED')
3646 else:
3647 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3648 if self.lock_y:
3649 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3650 else:
3651 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3652 if self.lock_z:
3653 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3654 else:
3655 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3656 col_move.prop(self, "influence")
3658 def invoke(self, context, event):
3659 # load custom settings
3660 settings_load(self)
3661 return self.execute(context)
3663 def execute(self, context):
3664 # initialise
3665 object, bm = initialise()
3666 settings_write(self)
3667 # check cache to see if we can save time
3668 cached, single_loops, loops, derived, mapping = cache_read("Curve",
3669 object, bm, False, self.boundaries)
3670 if cached:
3671 derived, bm_mod = get_derived_bmesh(object, bm)
3672 else:
3673 # find loops
3674 derived, bm_mod, loops = curve_get_input(object, bm, self.boundaries)
3675 mapping = get_mapping(derived, bm, bm_mod, False, True, loops)
3676 loops = check_loops(loops, mapping, bm_mod)
3677 verts_selected = [
3678 v.index for v in bm_mod.verts if v.select and not v.hide
3681 # saving cache for faster execution next time
3682 if not cached:
3683 cache_write("Curve", object, bm, False, self.boundaries, False,
3684 loops, derived, mapping)
3686 move = []
3687 for loop in loops:
3688 knots, points = curve_calculate_knots(loop, verts_selected)
3689 pknots = curve_project_knots(bm_mod, verts_selected, knots,
3690 points, loop[1])
3691 tknots, tpoints = curve_calculate_t(bm_mod, knots, points,
3692 pknots, self.regular, loop[1])
3693 splines = calculate_splines(self.interpolation, bm_mod,
3694 tknots, knots)
3695 move.append(curve_calculate_vertices(bm_mod, knots, tknots,
3696 points, tpoints, splines, self.interpolation,
3697 self.restriction))
3699 # move vertices to new locations
3700 if self.lock_x or self.lock_y or self.lock_z:
3701 lock = [self.lock_x, self.lock_y, self.lock_z]
3702 else:
3703 lock = False
3704 move_verts(object, bm, mapping, move, lock, self.influence)
3706 # cleaning up
3707 if derived:
3708 bm_mod.free()
3709 terminate()
3711 return{'FINISHED'}
3714 # flatten operator
3715 class Flatten(Operator):
3716 bl_idname = "mesh.looptools_flatten"
3717 bl_label = "Flatten"
3718 bl_description = "Flatten vertices on a best-fitting plane"
3719 bl_options = {'REGISTER', 'UNDO'}
3721 influence: FloatProperty(
3722 name="Influence",
3723 description="Force of the tool",
3724 default=100.0,
3725 min=0.0,
3726 max=100.0,
3727 precision=1,
3728 subtype='PERCENTAGE'
3730 lock_x: BoolProperty(
3731 name="Lock X",
3732 description="Lock editing of the x-coordinate",
3733 default=False
3735 lock_y: BoolProperty(
3736 name="Lock Y",
3737 description="Lock editing of the y-coordinate",
3738 default=False
3740 lock_z: BoolProperty(name="Lock Z",
3741 description="Lock editing of the z-coordinate",
3742 default=False
3744 plane: EnumProperty(
3745 name="Plane",
3746 items=(("best_fit", "Best fit", "Calculate a best fitting plane"),
3747 ("normal", "Normal", "Derive plane from averaging vertex normals"),
3748 ("view", "View", "Flatten on a plane perpendicular to the viewing angle")),
3749 description="Plane on which vertices are flattened",
3750 default='best_fit'
3752 restriction: EnumProperty(
3753 name="Restriction",
3754 items=(("none", "None", "No restrictions on vertex movement"),
3755 ("bounding_box", "Bounding box", "Vertices are restricted to "
3756 "movement inside the bounding box of the selection")),
3757 description="Restrictions on how the vertices can be moved",
3758 default='none'
3761 @classmethod
3762 def poll(cls, context):
3763 ob = context.active_object
3764 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3766 def draw(self, context):
3767 layout = self.layout
3768 col = layout.column()
3770 col.prop(self, "plane")
3771 # col.prop(self, "restriction")
3772 col.separator()
3774 col_move = col.column(align=True)
3775 row = col_move.row(align=True)
3776 if self.lock_x:
3777 row.prop(self, "lock_x", text="X", icon='LOCKED')
3778 else:
3779 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3780 if self.lock_y:
3781 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3782 else:
3783 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3784 if self.lock_z:
3785 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3786 else:
3787 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3788 col_move.prop(self, "influence")
3790 def invoke(self, context, event):
3791 # load custom settings
3792 settings_load(self)
3793 return self.execute(context)
3795 def execute(self, context):
3796 # initialise
3797 object, bm = initialise()
3798 settings_write(self)
3799 # check cache to see if we can save time
3800 cached, single_loops, loops, derived, mapping = cache_read("Flatten",
3801 object, bm, False, False)
3802 if not cached:
3803 # order input into virtual loops
3804 loops = flatten_get_input(bm)
3805 loops = check_loops(loops, mapping, bm)
3807 # saving cache for faster execution next time
3808 if not cached:
3809 cache_write("Flatten", object, bm, False, False, False, loops,
3810 False, False)
3812 move = []
3813 for loop in loops:
3814 # calculate plane and position of vertices on them
3815 com, normal = calculate_plane(bm, loop, method=self.plane,
3816 object=object)
3817 to_move = flatten_project(bm, loop, com, normal)
3818 if self.restriction == 'none':
3819 move.append(to_move)
3820 else:
3821 move.append(to_move)
3823 # move vertices to new locations
3824 if self.lock_x or self.lock_y or self.lock_z:
3825 lock = [self.lock_x, self.lock_y, self.lock_z]
3826 else:
3827 lock = False
3828 move_verts(object, bm, False, move, lock, self.influence)
3830 # cleaning up
3831 terminate()
3833 return{'FINISHED'}
3836 # Annotation operator
3837 class RemoveAnnotation(Operator):
3838 bl_idname = "remove.annotation"
3839 bl_label = "Remove Annotation"
3840 bl_description = "Remove all Annotation Strokes"
3841 bl_options = {'REGISTER', 'UNDO'}
3843 def execute(self, context):
3845 try:
3846 bpy.data.grease_pencils[0].layers.active.clear()
3847 except:
3848 self.report({'INFO'}, "No Annotation data to Unlink")
3849 return {'CANCELLED'}
3851 return{'FINISHED'}
3853 # GPencil operator
3854 class RemoveGPencil(Operator):
3855 bl_idname = "remove.gp"
3856 bl_label = "Remove GPencil"
3857 bl_description = "Remove all GPencil Strokes"
3858 bl_options = {'REGISTER', 'UNDO'}
3860 def execute(self, context):
3862 try:
3863 looptools = context.window_manager.looptools
3864 looptools.gstretch_guide.data.layers.data.clear()
3865 looptools.gstretch_guide.data.update_tag()
3866 except:
3867 self.report({'INFO'}, "No GPencil data to Unlink")
3868 return {'CANCELLED'}
3870 return{'FINISHED'}
3873 class GStretch(Operator):
3874 bl_idname = "mesh.looptools_gstretch"
3875 bl_label = "Gstretch"
3876 bl_description = "Stretch selected vertices to active stroke"
3877 bl_options = {'REGISTER', 'UNDO'}
3879 conversion: EnumProperty(
3880 name="Conversion",
3881 items=(("distance", "Distance", "Set the distance between vertices "
3882 "of the converted stroke"),
3883 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
3884 "number of vertices that converted strokes will have"),
3885 ("vertices", "Exact vertices", "Set the exact number of vertices "
3886 "that converted strokes will have. Short strokes "
3887 "with few points may contain less vertices than this number."),
3888 ("none", "No simplification", "Convert each point "
3889 "to a vertex")),
3890 description="If strokes are converted to geometry, "
3891 "use this simplification method",
3892 default='limit_vertices'
3894 conversion_distance: FloatProperty(
3895 name="Distance",
3896 description="Absolute distance between vertices along the converted "
3897 " stroke",
3898 default=0.1,
3899 min=0.000001,
3900 soft_min=0.01,
3901 soft_max=100
3903 conversion_max: IntProperty(
3904 name="Max Vertices",
3905 description="Maximum number of vertices strokes will "
3906 "have, when they are converted to geomtery",
3907 default=32,
3908 min=3,
3909 soft_max=500,
3910 update=gstretch_update_min
3912 conversion_min: IntProperty(
3913 name="Min Vertices",
3914 description="Minimum number of vertices strokes will "
3915 "have, when they are converted to geomtery",
3916 default=8,
3917 min=3,
3918 soft_max=500,
3919 update=gstretch_update_max
3921 conversion_vertices: IntProperty(
3922 name="Vertices",
3923 description="Number of vertices strokes will "
3924 "have, when they are converted to geometry. If strokes have less "
3925 "points than required, the 'Spread evenly' method is used",
3926 default=32,
3927 min=3,
3928 soft_max=500
3930 delete_strokes: BoolProperty(
3931 name="Delete strokes",
3932 description="Remove strokes if they have been used."
3933 "WARNING: DOES NOT SUPPORT UNDO",
3934 default=False
3936 influence: FloatProperty(
3937 name="Influence",
3938 description="Force of the tool",
3939 default=100.0,
3940 min=0.0,
3941 max=100.0,
3942 precision=1,
3943 subtype='PERCENTAGE'
3945 lock_x: BoolProperty(
3946 name="Lock X",
3947 description="Lock editing of the x-coordinate",
3948 default=False
3950 lock_y: BoolProperty(
3951 name="Lock Y",
3952 description="Lock editing of the y-coordinate",
3953 default=False
3955 lock_z: BoolProperty(
3956 name="Lock Z",
3957 description="Lock editing of the z-coordinate",
3958 default=False
3960 method: EnumProperty(
3961 name="Method",
3962 items=(("project", "Project", "Project vertices onto the stroke, "
3963 "using vertex normals and connected edges"),
3964 ("irregular", "Spread", "Distribute vertices along the full "
3965 "stroke, retaining relative distances between the vertices"),
3966 ("regular", "Spread evenly", "Distribute vertices at regular "
3967 "distances along the full stroke")),
3968 description="Method of distributing the vertices over the "
3969 "stroke",
3970 default='regular'
3973 @classmethod
3974 def poll(cls, context):
3975 ob = context.active_object
3976 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3978 def draw(self, context):
3979 looptools = context.window_manager.looptools
3980 layout = self.layout
3981 col = layout.column()
3983 col.prop(self, "method")
3984 col.separator()
3986 col_conv = col.column(align=True)
3987 col_conv.prop(self, "conversion", text="")
3988 if self.conversion == 'distance':
3989 col_conv.prop(self, "conversion_distance")
3990 elif self.conversion == 'limit_vertices':
3991 row = col_conv.row(align=True)
3992 row.prop(self, "conversion_min", text="Min")
3993 row.prop(self, "conversion_max", text="Max")
3994 elif self.conversion == 'vertices':
3995 col_conv.prop(self, "conversion_vertices")
3996 col.separator()
3998 col_move = col.column(align=True)
3999 row = col_move.row(align=True)
4000 if self.lock_x:
4001 row.prop(self, "lock_x", text="X", icon='LOCKED')
4002 else:
4003 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
4004 if self.lock_y:
4005 row.prop(self, "lock_y", text="Y", icon='LOCKED')
4006 else:
4007 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
4008 if self.lock_z:
4009 row.prop(self, "lock_z", text="Z", icon='LOCKED')
4010 else:
4011 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
4012 col_move.prop(self, "influence")
4013 col.separator()
4014 if looptools.gstretch_use_guide == "Annotation":
4015 col.operator("remove.annotation", text="Delete annotation strokes")
4016 if looptools.gstretch_use_guide == "GPencil":
4017 col.operator("remove.gp", text="Delete GPencil strokes")
4019 def invoke(self, context, event):
4020 # flush cached strokes
4021 if 'Gstretch' in looptools_cache:
4022 looptools_cache['Gstretch']['single_loops'] = []
4023 # load custom settings
4024 settings_load(self)
4025 return self.execute(context)
4027 def execute(self, context):
4028 # initialise
4029 object, bm = initialise()
4030 settings_write(self)
4032 # check cache to see if we can save time
4033 cached, safe_strokes, loops, derived, mapping = cache_read("Gstretch",
4034 object, bm, False, False)
4035 if cached:
4036 straightening = False
4037 if safe_strokes:
4038 strokes = gstretch_safe_to_true_strokes(safe_strokes)
4039 # cached strokes were flushed (see operator's invoke function)
4040 elif get_strokes(self, context):
4041 strokes = gstretch_get_strokes(self, context)
4042 else:
4043 # straightening function (no GP) -> loops ignore modifiers
4044 straightening = True
4045 derived = False
4046 bm_mod = bm.copy()
4047 bm_mod.verts.ensure_lookup_table()
4048 bm_mod.edges.ensure_lookup_table()
4049 bm_mod.faces.ensure_lookup_table()
4050 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
4051 if not straightening:
4052 derived, bm_mod = get_derived_bmesh(object, bm)
4053 else:
4054 # get loops and strokes
4055 if get_strokes(self, context):
4056 # find loops
4057 derived, bm_mod, loops = get_connected_input(object, bm, input='selected')
4058 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4059 loops = check_loops(loops, mapping, bm_mod)
4060 # get strokes
4061 strokes = gstretch_get_strokes(self, context)
4062 else:
4063 # straightening function (no GP) -> loops ignore modifiers
4064 derived = False
4065 mapping = False
4066 bm_mod = bm.copy()
4067 bm_mod.verts.ensure_lookup_table()
4068 bm_mod.edges.ensure_lookup_table()
4069 bm_mod.faces.ensure_lookup_table()
4070 edge_keys = [
4071 edgekey(edge) for edge in bm_mod.edges if
4072 edge.select and not edge.hide
4074 loops = get_connected_selections(edge_keys)
4075 loops = check_loops(loops, mapping, bm_mod)
4076 # create fake strokes
4077 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
4079 # saving cache for faster execution next time
4080 if not cached:
4081 if strokes:
4082 safe_strokes = gstretch_true_to_safe_strokes(strokes)
4083 else:
4084 safe_strokes = []
4085 cache_write("Gstretch", object, bm, False, False,
4086 safe_strokes, loops, derived, mapping)
4088 # pair loops and strokes
4089 ls_pairs = gstretch_match_loops_strokes(loops, strokes, object, bm_mod)
4090 ls_pairs = gstretch_align_pairs(ls_pairs, object, bm_mod, self.method)
4092 move = []
4093 if not loops:
4094 # no selected geometry, convert GP to verts
4095 if strokes:
4096 move.append(gstretch_create_verts(object, bm, strokes,
4097 self.method, self.conversion, self.conversion_distance,
4098 self.conversion_max, self.conversion_min,
4099 self.conversion_vertices))
4100 for stroke in strokes:
4101 gstretch_erase_stroke(stroke, context)
4102 elif ls_pairs:
4103 for (loop, stroke) in ls_pairs:
4104 move.append(gstretch_calculate_verts(loop, stroke, object,
4105 bm_mod, self.method))
4106 if self.delete_strokes:
4107 if type(stroke) != bpy.types.GPencilStroke:
4108 # in case of cached fake stroke, get the real one
4109 if get_strokes(self, context):
4110 strokes = gstretch_get_strokes(self, context)
4111 if loops and strokes:
4112 ls_pairs = gstretch_match_loops_strokes(loops,
4113 strokes, object, bm_mod)
4114 ls_pairs = gstretch_align_pairs(ls_pairs,
4115 object, bm_mod, self.method)
4116 for (l, s) in ls_pairs:
4117 if l == loop:
4118 stroke = s
4119 break
4120 gstretch_erase_stroke(stroke, context)
4122 # move vertices to new locations
4123 if self.lock_x or self.lock_y or self.lock_z:
4124 lock = [self.lock_x, self.lock_y, self.lock_z]
4125 else:
4126 lock = False
4127 bmesh.update_edit_mesh(object.data, loop_triangles=True, destructive=True)
4128 move_verts(object, bm, mapping, move, lock, self.influence)
4130 # cleaning up
4131 if derived:
4132 bm_mod.free()
4133 terminate()
4135 return{'FINISHED'}
4138 # relax operator
4139 class Relax(Operator):
4140 bl_idname = "mesh.looptools_relax"
4141 bl_label = "Relax"
4142 bl_description = "Relax the loop, so it is smoother"
4143 bl_options = {'REGISTER', 'UNDO'}
4145 input: EnumProperty(
4146 name="Input",
4147 items=(("all", "Parallel (all)", "Also use non-selected "
4148 "parallel loops as input"),
4149 ("selected", "Selection", "Only use selected vertices as input")),
4150 description="Loops that are relaxed",
4151 default='selected'
4153 interpolation: EnumProperty(
4154 name="Interpolation",
4155 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4156 ("linear", "Linear", "Simple and fast linear algorithm")),
4157 description="Algorithm used for interpolation",
4158 default='cubic'
4160 iterations: EnumProperty(
4161 name="Iterations",
4162 items=(("1", "1", "One"),
4163 ("3", "3", "Three"),
4164 ("5", "5", "Five"),
4165 ("10", "10", "Ten"),
4166 ("25", "25", "Twenty-five")),
4167 description="Number of times the loop is relaxed",
4168 default="1"
4170 regular: BoolProperty(
4171 name="Regular",
4172 description="Distribute vertices at constant distances along the loop",
4173 default=True
4176 @classmethod
4177 def poll(cls, context):
4178 ob = context.active_object
4179 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4181 def draw(self, context):
4182 layout = self.layout
4183 col = layout.column()
4185 col.prop(self, "interpolation")
4186 col.prop(self, "input")
4187 col.prop(self, "iterations")
4188 col.prop(self, "regular")
4190 def invoke(self, context, event):
4191 # load custom settings
4192 settings_load(self)
4193 return self.execute(context)
4195 def execute(self, context):
4196 # initialise
4197 object, bm = initialise()
4198 settings_write(self)
4199 # check cache to see if we can save time
4200 cached, single_loops, loops, derived, mapping = cache_read("Relax",
4201 object, bm, self.input, False)
4202 if cached:
4203 derived, bm_mod = get_derived_bmesh(object, bm)
4204 else:
4205 # find loops
4206 derived, bm_mod, loops = get_connected_input(object, bm, self.input)
4207 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4208 loops = check_loops(loops, mapping, bm_mod)
4209 knots, points = relax_calculate_knots(loops)
4211 # saving cache for faster execution next time
4212 if not cached:
4213 cache_write("Relax", object, bm, self.input, False, False, loops,
4214 derived, mapping)
4216 for iteration in range(int(self.iterations)):
4217 # calculate splines and new positions
4218 tknots, tpoints = relax_calculate_t(bm_mod, knots, points,
4219 self.regular)
4220 splines = []
4221 for i in range(len(knots)):
4222 splines.append(calculate_splines(self.interpolation, bm_mod,
4223 tknots[i], knots[i]))
4224 move = [relax_calculate_verts(bm_mod, self.interpolation,
4225 tknots, knots, tpoints, points, splines)]
4226 move_verts(object, bm, mapping, move, False, -1)
4228 # cleaning up
4229 if derived:
4230 bm_mod.free()
4231 terminate()
4233 return{'FINISHED'}
4236 # space operator
4237 class Space(Operator):
4238 bl_idname = "mesh.looptools_space"
4239 bl_label = "Space"
4240 bl_description = "Space the vertices in a regular distribution on the loop"
4241 bl_options = {'REGISTER', 'UNDO'}
4243 influence: FloatProperty(
4244 name="Influence",
4245 description="Force of the tool",
4246 default=100.0,
4247 min=0.0,
4248 max=100.0,
4249 precision=1,
4250 subtype='PERCENTAGE'
4252 input: EnumProperty(
4253 name="Input",
4254 items=(("all", "Parallel (all)", "Also use non-selected "
4255 "parallel loops as input"),
4256 ("selected", "Selection", "Only use selected vertices as input")),
4257 description="Loops that are spaced",
4258 default='selected'
4260 interpolation: EnumProperty(
4261 name="Interpolation",
4262 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4263 ("linear", "Linear", "Vertices are projected on existing edges")),
4264 description="Algorithm used for interpolation",
4265 default='cubic'
4267 lock_x: BoolProperty(
4268 name="Lock X",
4269 description="Lock editing of the x-coordinate",
4270 default=False
4272 lock_y: BoolProperty(
4273 name="Lock Y",
4274 description="Lock editing of the y-coordinate",
4275 default=False
4277 lock_z: BoolProperty(
4278 name="Lock Z",
4279 description="Lock editing of the z-coordinate",
4280 default=False
4283 @classmethod
4284 def poll(cls, context):
4285 ob = context.active_object
4286 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4288 def draw(self, context):
4289 layout = self.layout
4290 col = layout.column()
4292 col.prop(self, "interpolation")
4293 col.prop(self, "input")
4294 col.separator()
4296 col_move = col.column(align=True)
4297 row = col_move.row(align=True)
4298 if self.lock_x:
4299 row.prop(self, "lock_x", text="X", icon='LOCKED')
4300 else:
4301 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
4302 if self.lock_y:
4303 row.prop(self, "lock_y", text="Y", icon='LOCKED')
4304 else:
4305 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
4306 if self.lock_z:
4307 row.prop(self, "lock_z", text="Z", icon='LOCKED')
4308 else:
4309 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
4310 col_move.prop(self, "influence")
4312 def invoke(self, context, event):
4313 # load custom settings
4314 settings_load(self)
4315 return self.execute(context)
4317 def execute(self, context):
4318 # initialise
4319 object, bm = initialise()
4320 settings_write(self)
4321 # check cache to see if we can save time
4322 cached, single_loops, loops, derived, mapping = cache_read("Space",
4323 object, bm, self.input, False)
4324 if cached:
4325 derived, bm_mod = get_derived_bmesh(object, bm)
4326 else:
4327 # find loops
4328 derived, bm_mod, loops = get_connected_input(object, bm, self.input)
4329 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4330 loops = check_loops(loops, mapping, bm_mod)
4332 # saving cache for faster execution next time
4333 if not cached:
4334 cache_write("Space", object, bm, self.input, False, False, loops,
4335 derived, mapping)
4337 move = []
4338 for loop in loops:
4339 # calculate splines and new positions
4340 if loop[1]: # circular
4341 loop[0].append(loop[0][0])
4342 tknots, tpoints = space_calculate_t(bm_mod, loop[0][:])
4343 splines = calculate_splines(self.interpolation, bm_mod,
4344 tknots, loop[0][:])
4345 move.append(space_calculate_verts(bm_mod, self.interpolation,
4346 tknots, tpoints, loop[0][:-1], splines))
4347 # move vertices to new locations
4348 if self.lock_x or self.lock_y or self.lock_z:
4349 lock = [self.lock_x, self.lock_y, self.lock_z]
4350 else:
4351 lock = False
4352 move_verts(object, bm, mapping, move, lock, self.influence)
4354 # cleaning up
4355 if derived:
4356 bm_mod.free()
4357 terminate()
4359 return{'FINISHED'}
4362 # ########################################
4363 # ##### GUI and registration #############
4364 # ########################################
4366 # menu containing all tools
4367 class VIEW3D_MT_edit_mesh_looptools(Menu):
4368 bl_label = "LoopTools"
4370 def draw(self, context):
4371 layout = self.layout
4373 layout.operator("mesh.looptools_bridge", text="Bridge").loft = False
4374 layout.operator("mesh.looptools_circle")
4375 layout.operator("mesh.looptools_curve")
4376 layout.operator("mesh.looptools_flatten")
4377 layout.operator("mesh.looptools_gstretch")
4378 layout.operator("mesh.looptools_bridge", text="Loft").loft = True
4379 layout.operator("mesh.looptools_relax")
4380 layout.operator("mesh.looptools_space")
4383 # panel containing all tools
4384 class VIEW3D_PT_tools_looptools(Panel):
4385 bl_space_type = 'VIEW_3D'
4386 bl_region_type = 'UI'
4387 bl_category = 'Edit'
4388 bl_context = "mesh_edit"
4389 bl_label = "LoopTools"
4390 bl_options = {'DEFAULT_CLOSED'}
4392 def draw(self, context):
4393 layout = self.layout
4394 col = layout.column(align=True)
4395 lt = context.window_manager.looptools
4397 # bridge - first line
4398 split = col.split(factor=0.15, align=True)
4399 if lt.display_bridge:
4400 split.prop(lt, "display_bridge", text="", icon='DOWNARROW_HLT')
4401 else:
4402 split.prop(lt, "display_bridge", text="", icon='RIGHTARROW')
4403 split.operator("mesh.looptools_bridge", text="Bridge").loft = False
4404 # bridge - settings
4405 if lt.display_bridge:
4406 box = col.column(align=True).box().column()
4407 # box.prop(self, "mode")
4409 # top row
4410 col_top = box.column(align=True)
4411 row = col_top.row(align=True)
4412 col_left = row.column(align=True)
4413 col_right = row.column(align=True)
4414 col_right.active = lt.bridge_segments != 1
4415 col_left.prop(lt, "bridge_segments")
4416 col_right.prop(lt, "bridge_min_width", text="")
4417 # bottom row
4418 bottom_left = col_left.row()
4419 bottom_left.active = lt.bridge_segments != 1
4420 bottom_left.prop(lt, "bridge_interpolation", text="")
4421 bottom_right = col_right.row()
4422 bottom_right.active = lt.bridge_interpolation == 'cubic'
4423 bottom_right.prop(lt, "bridge_cubic_strength")
4424 # boolean properties
4425 col_top.prop(lt, "bridge_remove_faces")
4427 # override properties
4428 col_top.separator()
4429 row = box.row(align=True)
4430 row.prop(lt, "bridge_twist")
4431 row.prop(lt, "bridge_reverse")
4433 # circle - first line
4434 split = col.split(factor=0.15, align=True)
4435 if lt.display_circle:
4436 split.prop(lt, "display_circle", text="", icon='DOWNARROW_HLT')
4437 else:
4438 split.prop(lt, "display_circle", text="", icon='RIGHTARROW')
4439 split.operator("mesh.looptools_circle")
4440 # circle - settings
4441 if lt.display_circle:
4442 box = col.column(align=True).box().column()
4443 box.prop(lt, "circle_fit")
4444 box.separator()
4446 box.prop(lt, "circle_flatten")
4447 row = box.row(align=True)
4448 row.prop(lt, "circle_custom_radius")
4449 row_right = row.row(align=True)
4450 row_right.active = lt.circle_custom_radius
4451 row_right.prop(lt, "circle_radius", text="")
4452 box.prop(lt, "circle_regular")
4453 box.separator()
4455 col_move = box.column(align=True)
4456 row = col_move.row(align=True)
4457 if lt.circle_lock_x:
4458 row.prop(lt, "circle_lock_x", text="X", icon='LOCKED')
4459 else:
4460 row.prop(lt, "circle_lock_x", text="X", icon='UNLOCKED')
4461 if lt.circle_lock_y:
4462 row.prop(lt, "circle_lock_y", text="Y", icon='LOCKED')
4463 else:
4464 row.prop(lt, "circle_lock_y", text="Y", icon='UNLOCKED')
4465 if lt.circle_lock_z:
4466 row.prop(lt, "circle_lock_z", text="Z", icon='LOCKED')
4467 else:
4468 row.prop(lt, "circle_lock_z", text="Z", icon='UNLOCKED')
4469 col_move.prop(lt, "circle_influence")
4471 # curve - first line
4472 split = col.split(factor=0.15, align=True)
4473 if lt.display_curve:
4474 split.prop(lt, "display_curve", text="", icon='DOWNARROW_HLT')
4475 else:
4476 split.prop(lt, "display_curve", text="", icon='RIGHTARROW')
4477 split.operator("mesh.looptools_curve")
4478 # curve - settings
4479 if lt.display_curve:
4480 box = col.column(align=True).box().column()
4481 box.prop(lt, "curve_interpolation")
4482 box.prop(lt, "curve_restriction")
4483 box.prop(lt, "curve_boundaries")
4484 box.prop(lt, "curve_regular")
4485 box.separator()
4487 col_move = box.column(align=True)
4488 row = col_move.row(align=True)
4489 if lt.curve_lock_x:
4490 row.prop(lt, "curve_lock_x", text="X", icon='LOCKED')
4491 else:
4492 row.prop(lt, "curve_lock_x", text="X", icon='UNLOCKED')
4493 if lt.curve_lock_y:
4494 row.prop(lt, "curve_lock_y", text="Y", icon='LOCKED')
4495 else:
4496 row.prop(lt, "curve_lock_y", text="Y", icon='UNLOCKED')
4497 if lt.curve_lock_z:
4498 row.prop(lt, "curve_lock_z", text="Z", icon='LOCKED')
4499 else:
4500 row.prop(lt, "curve_lock_z", text="Z", icon='UNLOCKED')
4501 col_move.prop(lt, "curve_influence")
4503 # flatten - first line
4504 split = col.split(factor=0.15, align=True)
4505 if lt.display_flatten:
4506 split.prop(lt, "display_flatten", text="", icon='DOWNARROW_HLT')
4507 else:
4508 split.prop(lt, "display_flatten", text="", icon='RIGHTARROW')
4509 split.operator("mesh.looptools_flatten")
4510 # flatten - settings
4511 if lt.display_flatten:
4512 box = col.column(align=True).box().column()
4513 box.prop(lt, "flatten_plane")
4514 # box.prop(lt, "flatten_restriction")
4515 box.separator()
4517 col_move = box.column(align=True)
4518 row = col_move.row(align=True)
4519 if lt.flatten_lock_x:
4520 row.prop(lt, "flatten_lock_x", text="X", icon='LOCKED')
4521 else:
4522 row.prop(lt, "flatten_lock_x", text="X", icon='UNLOCKED')
4523 if lt.flatten_lock_y:
4524 row.prop(lt, "flatten_lock_y", text="Y", icon='LOCKED')
4525 else:
4526 row.prop(lt, "flatten_lock_y", text="Y", icon='UNLOCKED')
4527 if lt.flatten_lock_z:
4528 row.prop(lt, "flatten_lock_z", text="Z", icon='LOCKED')
4529 else:
4530 row.prop(lt, "flatten_lock_z", text="Z", icon='UNLOCKED')
4531 col_move.prop(lt, "flatten_influence")
4533 # gstretch - first line
4534 split = col.split(factor=0.15, align=True)
4535 if lt.display_gstretch:
4536 split.prop(lt, "display_gstretch", text="", icon='DOWNARROW_HLT')
4537 else:
4538 split.prop(lt, "display_gstretch", text="", icon='RIGHTARROW')
4539 split.operator("mesh.looptools_gstretch")
4540 # gstretch settings
4541 if lt.display_gstretch:
4542 box = col.column(align=True).box().column()
4543 box.prop(lt, "gstretch_use_guide")
4544 if lt.gstretch_use_guide == "GPencil":
4545 box.prop(lt, "gstretch_guide")
4546 box.prop(lt, "gstretch_method")
4548 col_conv = box.column(align=True)
4549 col_conv.prop(lt, "gstretch_conversion", text="")
4550 if lt.gstretch_conversion == 'distance':
4551 col_conv.prop(lt, "gstretch_conversion_distance")
4552 elif lt.gstretch_conversion == 'limit_vertices':
4553 row = col_conv.row(align=True)
4554 row.prop(lt, "gstretch_conversion_min", text="Min")
4555 row.prop(lt, "gstretch_conversion_max", text="Max")
4556 elif lt.gstretch_conversion == 'vertices':
4557 col_conv.prop(lt, "gstretch_conversion_vertices")
4558 box.separator()
4560 col_move = box.column(align=True)
4561 row = col_move.row(align=True)
4562 if lt.gstretch_lock_x:
4563 row.prop(lt, "gstretch_lock_x", text="X", icon='LOCKED')
4564 else:
4565 row.prop(lt, "gstretch_lock_x", text="X", icon='UNLOCKED')
4566 if lt.gstretch_lock_y:
4567 row.prop(lt, "gstretch_lock_y", text="Y", icon='LOCKED')
4568 else:
4569 row.prop(lt, "gstretch_lock_y", text="Y", icon='UNLOCKED')
4570 if lt.gstretch_lock_z:
4571 row.prop(lt, "gstretch_lock_z", text="Z", icon='LOCKED')
4572 else:
4573 row.prop(lt, "gstretch_lock_z", text="Z", icon='UNLOCKED')
4574 col_move.prop(lt, "gstretch_influence")
4575 if lt.gstretch_use_guide == "Annotation":
4576 box.operator("remove.annotation", text="Delete Annotation Strokes")
4577 if lt.gstretch_use_guide == "GPencil":
4578 box.operator("remove.gp", text="Delete GPencil Strokes")
4580 # loft - first line
4581 split = col.split(factor=0.15, align=True)
4582 if lt.display_loft:
4583 split.prop(lt, "display_loft", text="", icon='DOWNARROW_HLT')
4584 else:
4585 split.prop(lt, "display_loft", text="", icon='RIGHTARROW')
4586 split.operator("mesh.looptools_bridge", text="Loft").loft = True
4587 # loft - settings
4588 if lt.display_loft:
4589 box = col.column(align=True).box().column()
4590 # box.prop(self, "mode")
4592 # top row
4593 col_top = box.column(align=True)
4594 row = col_top.row(align=True)
4595 col_left = row.column(align=True)
4596 col_right = row.column(align=True)
4597 col_right.active = lt.bridge_segments != 1
4598 col_left.prop(lt, "bridge_segments")
4599 col_right.prop(lt, "bridge_min_width", text="")
4600 # bottom row
4601 bottom_left = col_left.row()
4602 bottom_left.active = lt.bridge_segments != 1
4603 bottom_left.prop(lt, "bridge_interpolation", text="")
4604 bottom_right = col_right.row()
4605 bottom_right.active = lt.bridge_interpolation == 'cubic'
4606 bottom_right.prop(lt, "bridge_cubic_strength")
4607 # boolean properties
4608 col_top.prop(lt, "bridge_remove_faces")
4609 col_top.prop(lt, "bridge_loft_loop")
4611 # override properties
4612 col_top.separator()
4613 row = box.row(align=True)
4614 row.prop(lt, "bridge_twist")
4615 row.prop(lt, "bridge_reverse")
4617 # relax - first line
4618 split = col.split(factor=0.15, align=True)
4619 if lt.display_relax:
4620 split.prop(lt, "display_relax", text="", icon='DOWNARROW_HLT')
4621 else:
4622 split.prop(lt, "display_relax", text="", icon='RIGHTARROW')
4623 split.operator("mesh.looptools_relax")
4624 # relax - settings
4625 if lt.display_relax:
4626 box = col.column(align=True).box().column()
4627 box.prop(lt, "relax_interpolation")
4628 box.prop(lt, "relax_input")
4629 box.prop(lt, "relax_iterations")
4630 box.prop(lt, "relax_regular")
4632 # space - first line
4633 split = col.split(factor=0.15, align=True)
4634 if lt.display_space:
4635 split.prop(lt, "display_space", text="", icon='DOWNARROW_HLT')
4636 else:
4637 split.prop(lt, "display_space", text="", icon='RIGHTARROW')
4638 split.operator("mesh.looptools_space")
4639 # space - settings
4640 if lt.display_space:
4641 box = col.column(align=True).box().column()
4642 box.prop(lt, "space_interpolation")
4643 box.prop(lt, "space_input")
4644 box.separator()
4646 col_move = box.column(align=True)
4647 row = col_move.row(align=True)
4648 if lt.space_lock_x:
4649 row.prop(lt, "space_lock_x", text="X", icon='LOCKED')
4650 else:
4651 row.prop(lt, "space_lock_x", text="X", icon='UNLOCKED')
4652 if lt.space_lock_y:
4653 row.prop(lt, "space_lock_y", text="Y", icon='LOCKED')
4654 else:
4655 row.prop(lt, "space_lock_y", text="Y", icon='UNLOCKED')
4656 if lt.space_lock_z:
4657 row.prop(lt, "space_lock_z", text="Z", icon='LOCKED')
4658 else:
4659 row.prop(lt, "space_lock_z", text="Z", icon='UNLOCKED')
4660 col_move.prop(lt, "space_influence")
4663 # property group containing all properties for the gui in the panel
4664 class LoopToolsProps(PropertyGroup):
4666 Fake module like class
4667 bpy.context.window_manager.looptools
4669 # general display properties
4670 display_bridge: BoolProperty(
4671 name="Bridge settings",
4672 description="Display settings of the Bridge tool",
4673 default=False
4675 display_circle: BoolProperty(
4676 name="Circle settings",
4677 description="Display settings of the Circle tool",
4678 default=False
4680 display_curve: BoolProperty(
4681 name="Curve settings",
4682 description="Display settings of the Curve tool",
4683 default=False
4685 display_flatten: BoolProperty(
4686 name="Flatten settings",
4687 description="Display settings of the Flatten tool",
4688 default=False
4690 display_gstretch: BoolProperty(
4691 name="Gstretch settings",
4692 description="Display settings of the Gstretch tool",
4693 default=False
4695 display_loft: BoolProperty(
4696 name="Loft settings",
4697 description="Display settings of the Loft tool",
4698 default=False
4700 display_relax: BoolProperty(
4701 name="Relax settings",
4702 description="Display settings of the Relax tool",
4703 default=False
4705 display_space: BoolProperty(
4706 name="Space settings",
4707 description="Display settings of the Space tool",
4708 default=False
4711 # bridge properties
4712 bridge_cubic_strength: FloatProperty(
4713 name="Strength",
4714 description="Higher strength results in more fluid curves",
4715 default=1.0,
4716 soft_min=-3.0,
4717 soft_max=3.0
4719 bridge_interpolation: EnumProperty(
4720 name="Interpolation mode",
4721 items=(('cubic', "Cubic", "Gives curved results"),
4722 ('linear', "Linear", "Basic, fast, straight interpolation")),
4723 description="Interpolation mode: algorithm used when creating segments",
4724 default='cubic'
4726 bridge_loft: BoolProperty(
4727 name="Loft",
4728 description="Loft multiple loops, instead of considering them as "
4729 "a multi-input for bridging",
4730 default=False
4732 bridge_loft_loop: BoolProperty(
4733 name="Loop",
4734 description="Connect the first and the last loop with each other",
4735 default=False
4737 bridge_min_width: IntProperty(
4738 name="Minimum width",
4739 description="Segments with an edge smaller than this are merged "
4740 "(compared to base edge)",
4741 default=0,
4742 min=0,
4743 max=100,
4744 subtype='PERCENTAGE'
4746 bridge_mode: EnumProperty(
4747 name="Mode",
4748 items=(('basic', "Basic", "Fast algorithm"),
4749 ('shortest', "Shortest edge", "Slower algorithm with "
4750 "better vertex matching")),
4751 description="Algorithm used for bridging",
4752 default='shortest'
4754 bridge_remove_faces: BoolProperty(
4755 name="Remove faces",
4756 description="Remove faces that are internal after bridging",
4757 default=True
4759 bridge_reverse: BoolProperty(
4760 name="Reverse",
4761 description="Manually override the direction in which the loops "
4762 "are bridged. Only use if the tool gives the wrong result",
4763 default=False
4765 bridge_segments: IntProperty(
4766 name="Segments",
4767 description="Number of segments used to bridge the gap (0=automatic)",
4768 default=1,
4769 min=0,
4770 soft_max=20
4772 bridge_twist: IntProperty(
4773 name="Twist",
4774 description="Twist what vertices are connected to each other",
4775 default=0
4778 # circle properties
4779 circle_custom_radius: BoolProperty(
4780 name="Radius",
4781 description="Force a custom radius",
4782 default=False
4784 circle_fit: EnumProperty(
4785 name="Method",
4786 items=(("best", "Best fit", "Non-linear least squares"),
4787 ("inside", "Fit inside", "Only move vertices towards the center")),
4788 description="Method used for fitting a circle to the vertices",
4789 default='best'
4791 circle_flatten: BoolProperty(
4792 name="Flatten",
4793 description="Flatten the circle, instead of projecting it on the mesh",
4794 default=True
4796 circle_influence: FloatProperty(
4797 name="Influence",
4798 description="Force of the tool",
4799 default=100.0,
4800 min=0.0,
4801 max=100.0,
4802 precision=1,
4803 subtype='PERCENTAGE'
4805 circle_lock_x: BoolProperty(
4806 name="Lock X",
4807 description="Lock editing of the x-coordinate",
4808 default=False
4810 circle_lock_y: BoolProperty(
4811 name="Lock Y",
4812 description="Lock editing of the y-coordinate",
4813 default=False
4815 circle_lock_z: BoolProperty(
4816 name="Lock Z",
4817 description="Lock editing of the z-coordinate",
4818 default=False
4820 circle_radius: FloatProperty(
4821 name="Radius",
4822 description="Custom radius for circle",
4823 default=1.0,
4824 min=0.0,
4825 soft_max=1000.0
4827 circle_regular: BoolProperty(
4828 name="Regular",
4829 description="Distribute vertices at constant distances along the circle",
4830 default=True
4832 # curve properties
4833 curve_boundaries: BoolProperty(
4834 name="Boundaries",
4835 description="Limit the tool to work within the boundaries of the "
4836 "selected vertices",
4837 default=False
4839 curve_influence: FloatProperty(
4840 name="Influence",
4841 description="Force of the tool",
4842 default=100.0,
4843 min=0.0,
4844 max=100.0,
4845 precision=1,
4846 subtype='PERCENTAGE'
4848 curve_interpolation: EnumProperty(
4849 name="Interpolation",
4850 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4851 ("linear", "Linear", "Simple and fast linear algorithm")),
4852 description="Algorithm used for interpolation",
4853 default='cubic'
4855 curve_lock_x: BoolProperty(
4856 name="Lock X",
4857 description="Lock editing of the x-coordinate",
4858 default=False
4860 curve_lock_y: BoolProperty(
4861 name="Lock Y",
4862 description="Lock editing of the y-coordinate",
4863 default=False
4865 curve_lock_z: BoolProperty(
4866 name="Lock Z",
4867 description="Lock editing of the z-coordinate",
4868 default=False
4870 curve_regular: BoolProperty(
4871 name="Regular",
4872 description="Distribute vertices at constant distances along the curve",
4873 default=True
4875 curve_restriction: EnumProperty(
4876 name="Restriction",
4877 items=(("none", "None", "No restrictions on vertex movement"),
4878 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
4879 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
4880 description="Restrictions on how the vertices can be moved",
4881 default='none'
4884 # flatten properties
4885 flatten_influence: FloatProperty(
4886 name="Influence",
4887 description="Force of the tool",
4888 default=100.0,
4889 min=0.0,
4890 max=100.0,
4891 precision=1,
4892 subtype='PERCENTAGE'
4894 flatten_lock_x: BoolProperty(
4895 name="Lock X",
4896 description="Lock editing of the x-coordinate",
4897 default=False)
4898 flatten_lock_y: BoolProperty(name="Lock Y",
4899 description="Lock editing of the y-coordinate",
4900 default=False
4902 flatten_lock_z: BoolProperty(
4903 name="Lock Z",
4904 description="Lock editing of the z-coordinate",
4905 default=False
4907 flatten_plane: EnumProperty(
4908 name="Plane",
4909 items=(("best_fit", "Best fit", "Calculate a best fitting plane"),
4910 ("normal", "Normal", "Derive plane from averaging vertex "
4911 "normals"),
4912 ("view", "View", "Flatten on a plane perpendicular to the "
4913 "viewing angle")),
4914 description="Plane on which vertices are flattened",
4915 default='best_fit'
4917 flatten_restriction: EnumProperty(
4918 name="Restriction",
4919 items=(("none", "None", "No restrictions on vertex movement"),
4920 ("bounding_box", "Bounding box", "Vertices are restricted to "
4921 "movement inside the bounding box of the selection")),
4922 description="Restrictions on how the vertices can be moved",
4923 default='none'
4926 # gstretch properties
4927 gstretch_conversion: EnumProperty(
4928 name="Conversion",
4929 items=(("distance", "Distance", "Set the distance between vertices "
4930 "of the converted stroke"),
4931 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
4932 "number of vertices that converted GP strokes will have"),
4933 ("vertices", "Exact vertices", "Set the exact number of vertices "
4934 "that converted strokes will have. Short strokes "
4935 "with few points may contain less vertices than this number."),
4936 ("none", "No simplification", "Convert each point "
4937 "to a vertex")),
4938 description="If strokes are converted to geometry, "
4939 "use this simplification method",
4940 default='limit_vertices'
4942 gstretch_conversion_distance: FloatProperty(
4943 name="Distance",
4944 description="Absolute distance between vertices along the converted "
4945 "stroke",
4946 default=0.1,
4947 min=0.000001,
4948 soft_min=0.01,
4949 soft_max=100
4951 gstretch_conversion_max: IntProperty(
4952 name="Max Vertices",
4953 description="Maximum number of vertices strokes will "
4954 "have, when they are converted to geomtery",
4955 default=32,
4956 min=3,
4957 soft_max=500,
4958 update=gstretch_update_min
4960 gstretch_conversion_min: IntProperty(
4961 name="Min Vertices",
4962 description="Minimum number of vertices strokes will "
4963 "have, when they are converted to geomtery",
4964 default=8,
4965 min=3,
4966 soft_max=500,
4967 update=gstretch_update_max
4969 gstretch_conversion_vertices: IntProperty(
4970 name="Vertices",
4971 description="Number of vertices strokes will "
4972 "have, when they are converted to geometry. If strokes have less "
4973 "points than required, the 'Spread evenly' method is used",
4974 default=32,
4975 min=3,
4976 soft_max=500
4978 gstretch_delete_strokes: BoolProperty(
4979 name="Delete strokes",
4980 description="Remove Grease Pencil strokes if they have been used "
4981 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
4982 default=False
4984 gstretch_influence: FloatProperty(
4985 name="Influence",
4986 description="Force of the tool",
4987 default=100.0,
4988 min=0.0,
4989 max=100.0,
4990 precision=1,
4991 subtype='PERCENTAGE'
4993 gstretch_lock_x: BoolProperty(
4994 name="Lock X",
4995 description="Lock editing of the x-coordinate",
4996 default=False
4998 gstretch_lock_y: BoolProperty(
4999 name="Lock Y",
5000 description="Lock editing of the y-coordinate",
5001 default=False
5003 gstretch_lock_z: BoolProperty(
5004 name="Lock Z",
5005 description="Lock editing of the z-coordinate",
5006 default=False
5008 gstretch_method: EnumProperty(
5009 name="Method",
5010 items=(("project", "Project", "Project vertices onto the stroke, "
5011 "using vertex normals and connected edges"),
5012 ("irregular", "Spread", "Distribute vertices along the full "
5013 "stroke, retaining relative distances between the vertices"),
5014 ("regular", "Spread evenly", "Distribute vertices at regular "
5015 "distances along the full stroke")),
5016 description="Method of distributing the vertices over the Grease "
5017 "Pencil stroke",
5018 default='regular'
5020 gstretch_use_guide: EnumProperty(
5021 name="Use guides",
5022 items=(("None", "None", "None"),
5023 ("Annotation", "Annotation", "Annotation"),
5024 ("GPencil", "GPencil", "GPencil")),
5025 default="None"
5027 gstretch_guide: PointerProperty(
5028 name="GPencil object",
5029 description="Set GPencil object",
5030 type=bpy.types.Object
5033 # relax properties
5034 relax_input: EnumProperty(name="Input",
5035 items=(("all", "Parallel (all)", "Also use non-selected "
5036 "parallel loops as input"),
5037 ("selected", "Selection", "Only use selected vertices as input")),
5038 description="Loops that are relaxed",
5039 default='selected'
5041 relax_interpolation: EnumProperty(
5042 name="Interpolation",
5043 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5044 ("linear", "Linear", "Simple and fast linear algorithm")),
5045 description="Algorithm used for interpolation",
5046 default='cubic'
5048 relax_iterations: EnumProperty(name="Iterations",
5049 items=(("1", "1", "One"),
5050 ("3", "3", "Three"),
5051 ("5", "5", "Five"),
5052 ("10", "10", "Ten"),
5053 ("25", "25", "Twenty-five")),
5054 description="Number of times the loop is relaxed",
5055 default="1"
5057 relax_regular: BoolProperty(
5058 name="Regular",
5059 description="Distribute vertices at constant distances along the loop",
5060 default=True
5063 # space properties
5064 space_influence: FloatProperty(
5065 name="Influence",
5066 description="Force of the tool",
5067 default=100.0,
5068 min=0.0,
5069 max=100.0,
5070 precision=1,
5071 subtype='PERCENTAGE'
5073 space_input: EnumProperty(
5074 name="Input",
5075 items=(("all", "Parallel (all)", "Also use non-selected "
5076 "parallel loops as input"),
5077 ("selected", "Selection", "Only use selected vertices as input")),
5078 description="Loops that are spaced",
5079 default='selected'
5081 space_interpolation: EnumProperty(
5082 name="Interpolation",
5083 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5084 ("linear", "Linear", "Vertices are projected on existing edges")),
5085 description="Algorithm used for interpolation",
5086 default='cubic'
5088 space_lock_x: BoolProperty(
5089 name="Lock X",
5090 description="Lock editing of the x-coordinate",
5091 default=False
5093 space_lock_y: BoolProperty(
5094 name="Lock Y",
5095 description="Lock editing of the y-coordinate",
5096 default=False
5098 space_lock_z: BoolProperty(
5099 name="Lock Z",
5100 description="Lock editing of the z-coordinate",
5101 default=False
5104 # draw function for integration in menus
5105 def menu_func(self, context):
5106 self.layout.menu("VIEW3D_MT_edit_mesh_looptools")
5107 self.layout.separator()
5110 # Add-ons Preferences Update Panel
5112 # Define Panel classes for updating
5113 panels = (
5114 VIEW3D_PT_tools_looptools,
5118 def update_panel(self, context):
5119 message = "LoopTools: Updating Panel locations has failed"
5120 try:
5121 for panel in panels:
5122 if "bl_rna" in panel.__dict__:
5123 bpy.utils.unregister_class(panel)
5125 for panel in panels:
5126 panel.bl_category = context.preferences.addons[__name__].preferences.category
5127 bpy.utils.register_class(panel)
5129 except Exception as e:
5130 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
5131 pass
5134 class LoopPreferences(AddonPreferences):
5135 # this must match the addon name, use '__package__'
5136 # when defining this in a submodule of a python package.
5137 bl_idname = __name__
5139 category: StringProperty(
5140 name="Tab Category",
5141 description="Choose a name for the category of the panel",
5142 default="Edit",
5143 update=update_panel
5146 def draw(self, context):
5147 layout = self.layout
5149 row = layout.row()
5150 col = row.column()
5151 col.label(text="Tab Category:")
5152 col.prop(self, "category", text="")
5155 # define classes for registration
5156 classes = (
5157 VIEW3D_MT_edit_mesh_looptools,
5158 VIEW3D_PT_tools_looptools,
5159 LoopToolsProps,
5160 Bridge,
5161 Circle,
5162 Curve,
5163 Flatten,
5164 GStretch,
5165 Relax,
5166 Space,
5167 LoopPreferences,
5168 RemoveAnnotation,
5169 RemoveGPencil,
5173 # registering and menu integration
5174 def register():
5175 for cls in classes:
5176 bpy.utils.register_class(cls)
5177 bpy.types.VIEW3D_MT_edit_mesh_context_menu.prepend(menu_func)
5178 bpy.types.WindowManager.looptools = PointerProperty(type=LoopToolsProps)
5179 update_panel(None, bpy.context)
5182 # unregistering and removing menus
5183 def unregister():
5184 for cls in reversed(classes):
5185 bpy.utils.unregister_class(cls)
5186 bpy.types.VIEW3D_MT_edit_mesh_context_menu.remove(menu_func)
5187 try:
5188 del bpy.types.WindowManager.looptools
5189 except Exception as e:
5190 print('unregister fail:\n', e)
5191 pass
5194 if __name__ == "__main__":
5195 register()