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