Fix T77568: turnaround camera crashes undoing
[blender-addons.git] / mesh_looptools.py
blobaf84f61fd14a82c61486c8b65c3879ddef4bfe9a
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
18 # Contributed to Germano Cavalcante (mano-wii), Florian Meyer (testscreenings),
19 # Brendon Murphy (meta-androcto),
20 # Maintainer: Vladimir Spivak (cwolf3d)
21 # Originally an addon by Bart Crouch
23 bl_info = {
24 "name": "LoopTools",
25 "author": "Bart Crouch, Vladimir Spivak (cwolf3d)",
26 "version": (4, 7, 3),
27 "blender": (2, 80, 0),
28 "location": "View3D > Sidebar > Edit Tab / Edit Mode Context Menu",
29 "warning": "",
30 "description": "Mesh modelling toolkit. Several tools to aid modelling",
31 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/looptools.html",
32 "category": "Mesh",
36 import bmesh
37 import bpy
38 import collections
39 import mathutils
40 import math
41 from bpy_extras import view3d_utils
42 from bpy.types import (
43 Operator,
44 Menu,
45 Panel,
46 PropertyGroup,
47 AddonPreferences,
49 from bpy.props import (
50 BoolProperty,
51 EnumProperty,
52 FloatProperty,
53 IntProperty,
54 PointerProperty,
55 StringProperty,
58 # ########################################
59 # ##### General functions ################
60 # ########################################
62 # used by all tools to improve speed on reruns Unlink
63 looptools_cache = {}
66 def get_strokes(self, context):
67 looptools = context.window_manager.looptools
68 if looptools.gstretch_use_guide == "Annotation":
69 try:
70 strokes = bpy.data.grease_pencils[0].layers.active.active_frame.strokes
71 return True
72 except:
73 self.report({'WARNING'}, "active Annotation strokes not found")
74 return False
75 if looptools.gstretch_use_guide == "GPencil" and not looptools.gstretch_guide == None:
76 try:
77 strokes = looptools.gstretch_guide.data.layers.active.active_frame.strokes
78 return True
79 except:
80 self.report({'WARNING'}, "active GPencil strokes not found")
81 return False
82 else:
83 return False
85 # force a full recalculation next time
86 def cache_delete(tool):
87 if tool in looptools_cache:
88 del looptools_cache[tool]
91 # check cache for stored information
92 def cache_read(tool, object, bm, input_method, boundaries):
93 # current tool not cached yet
94 if tool not in looptools_cache:
95 return(False, False, False, False, False)
96 # check if selected object didn't change
97 if object.name != looptools_cache[tool]["object"]:
98 return(False, False, False, False, False)
99 # check if input didn't change
100 if input_method != looptools_cache[tool]["input_method"]:
101 return(False, False, False, False, False)
102 if boundaries != looptools_cache[tool]["boundaries"]:
103 return(False, False, False, False, False)
104 modifiers = [mod.name for mod in object.modifiers if mod.show_viewport and
105 mod.type == 'MIRROR']
106 if modifiers != looptools_cache[tool]["modifiers"]:
107 return(False, False, False, False, False)
108 input = [v.index for v in bm.verts if v.select and not v.hide]
109 if input != looptools_cache[tool]["input"]:
110 return(False, False, False, False, False)
111 # reading values
112 single_loops = looptools_cache[tool]["single_loops"]
113 loops = looptools_cache[tool]["loops"]
114 derived = looptools_cache[tool]["derived"]
115 mapping = looptools_cache[tool]["mapping"]
117 return(True, single_loops, loops, derived, mapping)
120 # store information in the cache
121 def cache_write(tool, object, bm, input_method, boundaries, single_loops,
122 loops, derived, mapping):
123 # clear cache of current tool
124 if tool in looptools_cache:
125 del looptools_cache[tool]
126 # prepare values to be saved to cache
127 input = [v.index for v in bm.verts if v.select and not v.hide]
128 modifiers = [mod.name for mod in object.modifiers if mod.show_viewport
129 and mod.type == 'MIRROR']
130 # update cache
131 looptools_cache[tool] = {
132 "input": input, "object": object.name,
133 "input_method": input_method, "boundaries": boundaries,
134 "single_loops": single_loops, "loops": loops,
135 "derived": derived, "mapping": mapping, "modifiers": modifiers}
138 # calculates natural cubic splines through all given knots
139 def calculate_cubic_splines(bm_mod, tknots, knots):
140 # hack for circular loops
141 if knots[0] == knots[-1] and len(knots) > 1:
142 circular = True
143 k_new1 = []
144 for k in range(-1, -5, -1):
145 if k - 1 < -len(knots):
146 k += len(knots)
147 k_new1.append(knots[k - 1])
148 k_new2 = []
149 for k in range(4):
150 if k + 1 > len(knots) - 1:
151 k -= len(knots)
152 k_new2.append(knots[k + 1])
153 for k in k_new1:
154 knots.insert(0, k)
155 for k in k_new2:
156 knots.append(k)
157 t_new1 = []
158 total1 = 0
159 for t in range(-1, -5, -1):
160 if t - 1 < -len(tknots):
161 t += len(tknots)
162 total1 += tknots[t] - tknots[t - 1]
163 t_new1.append(tknots[0] - total1)
164 t_new2 = []
165 total2 = 0
166 for t in range(4):
167 if t + 1 > len(tknots) - 1:
168 t -= len(tknots)
169 total2 += tknots[t + 1] - tknots[t]
170 t_new2.append(tknots[-1] + total2)
171 for t in t_new1:
172 tknots.insert(0, t)
173 for t in t_new2:
174 tknots.append(t)
175 else:
176 circular = False
177 # end of hack
179 n = len(knots)
180 if n < 2:
181 return False
182 x = tknots[:]
183 locs = [bm_mod.verts[k].co[:] for k in knots]
184 result = []
185 for j in range(3):
186 a = []
187 for i in locs:
188 a.append(i[j])
189 h = []
190 for i in range(n - 1):
191 if x[i + 1] - x[i] == 0:
192 h.append(1e-8)
193 else:
194 h.append(x[i + 1] - x[i])
195 q = [False]
196 for i in range(1, n - 1):
197 q.append(3 / h[i] * (a[i + 1] - a[i]) - 3 / h[i - 1] * (a[i] - a[i - 1]))
198 l = [1.0]
199 u = [0.0]
200 z = [0.0]
201 for i in range(1, n - 1):
202 l.append(2 * (x[i + 1] - x[i - 1]) - h[i - 1] * u[i - 1])
203 if l[i] == 0:
204 l[i] = 1e-8
205 u.append(h[i] / l[i])
206 z.append((q[i] - h[i - 1] * z[i - 1]) / l[i])
207 l.append(1.0)
208 z.append(0.0)
209 b = [False for i in range(n - 1)]
210 c = [False for i in range(n)]
211 d = [False for i in range(n - 1)]
212 c[n - 1] = 0.0
213 for i in range(n - 2, -1, -1):
214 c[i] = z[i] - u[i] * c[i + 1]
215 b[i] = (a[i + 1] - a[i]) / h[i] - h[i] * (c[i + 1] + 2 * c[i]) / 3
216 d[i] = (c[i + 1] - c[i]) / (3 * h[i])
217 for i in range(n - 1):
218 result.append([a[i], b[i], c[i], d[i], x[i]])
219 splines = []
220 for i in range(len(knots) - 1):
221 splines.append([result[i], result[i + n - 1], result[i + (n - 1) * 2]])
222 if circular: # cleaning up after hack
223 knots = knots[4:-4]
224 tknots = tknots[4:-4]
226 return(splines)
229 # calculates linear splines through all given knots
230 def calculate_linear_splines(bm_mod, tknots, knots):
231 splines = []
232 for i in range(len(knots) - 1):
233 a = bm_mod.verts[knots[i]].co
234 b = bm_mod.verts[knots[i + 1]].co
235 d = b - a
236 t = tknots[i]
237 u = tknots[i + 1] - t
238 splines.append([a, d, t, u]) # [locStart, locDif, tStart, tDif]
240 return(splines)
243 # calculate a best-fit plane to the given vertices
244 def calculate_plane(bm_mod, loop, method="best_fit", object=False):
245 # getting the vertex locations
246 locs = [bm_mod.verts[v].co.copy() for v in loop[0]]
248 # calculating the center of masss
249 com = mathutils.Vector()
250 for loc in locs:
251 com += loc
252 com /= len(locs)
253 x, y, z = com
255 if method == 'best_fit':
256 # creating the covariance matrix
257 mat = mathutils.Matrix(((0.0, 0.0, 0.0),
258 (0.0, 0.0, 0.0),
259 (0.0, 0.0, 0.0),
261 for loc in locs:
262 mat[0][0] += (loc[0] - x) ** 2
263 mat[1][0] += (loc[0] - x) * (loc[1] - y)
264 mat[2][0] += (loc[0] - x) * (loc[2] - z)
265 mat[0][1] += (loc[1] - y) * (loc[0] - x)
266 mat[1][1] += (loc[1] - y) ** 2
267 mat[2][1] += (loc[1] - y) * (loc[2] - z)
268 mat[0][2] += (loc[2] - z) * (loc[0] - x)
269 mat[1][2] += (loc[2] - z) * (loc[1] - y)
270 mat[2][2] += (loc[2] - z) ** 2
272 # calculating the normal to the plane
273 normal = False
274 try:
275 mat = matrix_invert(mat)
276 except:
277 ax = 2
278 if math.fabs(sum(mat[0])) < math.fabs(sum(mat[1])):
279 if math.fabs(sum(mat[0])) < math.fabs(sum(mat[2])):
280 ax = 0
281 elif math.fabs(sum(mat[1])) < math.fabs(sum(mat[2])):
282 ax = 1
283 if ax == 0:
284 normal = mathutils.Vector((1.0, 0.0, 0.0))
285 elif ax == 1:
286 normal = mathutils.Vector((0.0, 1.0, 0.0))
287 else:
288 normal = mathutils.Vector((0.0, 0.0, 1.0))
289 if not normal:
290 # warning! this is different from .normalize()
291 itermax = 500
292 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
293 for i in range(itermax):
294 vec = vec2
295 vec2 = mat @ vec
296 # Calculate length with double precision to avoid problems with `inf`
297 vec2_length = math.sqrt(vec2[0] ** 2 + vec2[1] ** 2 + vec2[2] ** 2)
298 if vec2_length != 0:
299 vec2 /= vec2_length
300 if vec2 == vec:
301 break
302 if vec2.length == 0:
303 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
304 normal = vec2
306 elif method == 'normal':
307 # averaging the vertex normals
308 v_normals = [bm_mod.verts[v].normal for v in loop[0]]
309 normal = mathutils.Vector()
310 for v_normal in v_normals:
311 normal += v_normal
312 normal /= len(v_normals)
313 normal.normalize()
315 elif method == 'view':
316 # calculate view normal
317 rotation = bpy.context.space_data.region_3d.view_matrix.to_3x3().\
318 inverted()
319 normal = rotation @ mathutils.Vector((0.0, 0.0, 1.0))
320 if object:
321 normal = object.matrix_world.inverted().to_euler().to_matrix() @ \
322 normal
324 return(com, normal)
327 # calculate splines based on given interpolation method (controller function)
328 def calculate_splines(interpolation, bm_mod, tknots, knots):
329 if interpolation == 'cubic':
330 splines = calculate_cubic_splines(bm_mod, tknots, knots[:])
331 else: # interpolations == 'linear'
332 splines = calculate_linear_splines(bm_mod, tknots, knots[:])
334 return(splines)
337 # check loops and only return valid ones
338 def check_loops(loops, mapping, bm_mod):
339 valid_loops = []
340 for loop, circular in loops:
341 # loop needs to have at least 3 vertices
342 if len(loop) < 3:
343 continue
344 # loop needs at least 1 vertex in the original, non-mirrored mesh
345 if mapping:
346 all_virtual = True
347 for vert in loop:
348 if mapping[vert] > -1:
349 all_virtual = False
350 break
351 if all_virtual:
352 continue
353 # vertices can not all be at the same location
354 stacked = True
355 for i in range(len(loop) - 1):
356 if (bm_mod.verts[loop[i]].co - bm_mod.verts[loop[i + 1]].co).length > 1e-6:
357 stacked = False
358 break
359 if stacked:
360 continue
361 # passed all tests, loop is valid
362 valid_loops.append([loop, circular])
364 return(valid_loops)
367 # input: bmesh, output: dict with the edge-key as key and face-index as value
368 def dict_edge_faces(bm):
369 edge_faces = dict([[edgekey(edge), []] for edge in bm.edges if not edge.hide])
370 for face in bm.faces:
371 if face.hide:
372 continue
373 for key in face_edgekeys(face):
374 edge_faces[key].append(face.index)
376 return(edge_faces)
379 # input: bmesh (edge-faces optional), output: dict with face-face connections
380 def dict_face_faces(bm, edge_faces=False):
381 if not edge_faces:
382 edge_faces = dict_edge_faces(bm)
384 connected_faces = dict([[face.index, []] for face in bm.faces if not face.hide])
385 for face in bm.faces:
386 if face.hide:
387 continue
388 for edge_key in face_edgekeys(face):
389 for connected_face in edge_faces[edge_key]:
390 if connected_face == face.index:
391 continue
392 connected_faces[face.index].append(connected_face)
394 return(connected_faces)
397 # input: bmesh, output: dict with the vert index as key and edge-keys as value
398 def dict_vert_edges(bm):
399 vert_edges = dict([[v.index, []] for v in bm.verts if not v.hide])
400 for edge in bm.edges:
401 if edge.hide:
402 continue
403 ek = edgekey(edge)
404 for vert in ek:
405 vert_edges[vert].append(ek)
407 return(vert_edges)
410 # input: bmesh, output: dict with the vert index as key and face index as value
411 def dict_vert_faces(bm):
412 vert_faces = dict([[v.index, []] for v in bm.verts if not v.hide])
413 for face in bm.faces:
414 if not face.hide:
415 for vert in face.verts:
416 vert_faces[vert.index].append(face.index)
418 return(vert_faces)
421 # input: list of edge-keys, output: dictionary with vertex-vertex connections
422 def dict_vert_verts(edge_keys):
423 # create connection data
424 vert_verts = {}
425 for ek in edge_keys:
426 for i in range(2):
427 if ek[i] in vert_verts:
428 vert_verts[ek[i]].append(ek[1 - i])
429 else:
430 vert_verts[ek[i]] = [ek[1 - i]]
432 return(vert_verts)
435 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
436 def edgekey(edge):
437 return(tuple(sorted([edge.verts[0].index, edge.verts[1].index])))
440 # returns the edgekeys of a bmesh face
441 def face_edgekeys(face):
442 return([tuple(sorted([edge.verts[0].index, edge.verts[1].index])) for edge in face.edges])
445 # calculate input loops
446 def get_connected_input(object, bm, not_use_mirror, input):
447 # get mesh with modifiers applied
448 derived, bm_mod = get_derived_bmesh(object, bm, not_use_mirror)
450 # calculate selected loops
451 edge_keys = [edgekey(edge) for edge in bm_mod.edges if edge.select and not edge.hide]
452 loops = get_connected_selections(edge_keys)
454 # if only selected loops are needed, we're done
455 if input == 'selected':
456 return(derived, bm_mod, loops)
457 # elif input == 'all':
458 loops = get_parallel_loops(bm_mod, loops)
460 return(derived, bm_mod, loops)
463 # sorts all edge-keys into a list of loops
464 def get_connected_selections(edge_keys):
465 # create connection data
466 vert_verts = dict_vert_verts(edge_keys)
468 # find loops consisting of connected selected edges
469 loops = []
470 while len(vert_verts) > 0:
471 loop = [iter(vert_verts.keys()).__next__()]
472 growing = True
473 flipped = False
475 # extend loop
476 while growing:
477 # no more connection data for current vertex
478 if loop[-1] not in vert_verts:
479 if not flipped:
480 loop.reverse()
481 flipped = True
482 else:
483 growing = False
484 else:
485 extended = False
486 for i, next_vert in enumerate(vert_verts[loop[-1]]):
487 if next_vert not in loop:
488 vert_verts[loop[-1]].pop(i)
489 if len(vert_verts[loop[-1]]) == 0:
490 del vert_verts[loop[-1]]
491 # remove connection both ways
492 if next_vert in vert_verts:
493 if len(vert_verts[next_vert]) == 1:
494 del vert_verts[next_vert]
495 else:
496 vert_verts[next_vert].remove(loop[-1])
497 loop.append(next_vert)
498 extended = True
499 break
500 if not extended:
501 # found one end of the loop, continue with next
502 if not flipped:
503 loop.reverse()
504 flipped = True
505 # found both ends of the loop, stop growing
506 else:
507 growing = False
509 # check if loop is circular
510 if loop[0] in vert_verts:
511 if loop[-1] in vert_verts[loop[0]]:
512 # is circular
513 if len(vert_verts[loop[0]]) == 1:
514 del vert_verts[loop[0]]
515 else:
516 vert_verts[loop[0]].remove(loop[-1])
517 if len(vert_verts[loop[-1]]) == 1:
518 del vert_verts[loop[-1]]
519 else:
520 vert_verts[loop[-1]].remove(loop[0])
521 loop = [loop, True]
522 else:
523 # not circular
524 loop = [loop, False]
525 else:
526 # not circular
527 loop = [loop, False]
529 loops.append(loop)
531 return(loops)
534 # get the derived mesh data, if there is a mirror modifier
535 def get_derived_bmesh(object, bm, not_use_mirror):
536 # check for mirror modifiers
537 if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
538 derived = True
539 # disable other modifiers
540 show_viewport = [mod.name for mod in object.modifiers if mod.show_viewport]
541 merge = []
542 for mod in object.modifiers:
543 if mod.type != 'MIRROR':
544 mod.show_viewport = False
545 #leave the merge points untouched
546 if mod.type == 'MIRROR':
547 merge.append(mod.use_mirror_merge)
548 if not_use_mirror:
549 mod.use_mirror_merge = False
550 # get derived mesh
551 bm_mod = bmesh.new()
552 depsgraph = bpy.context.evaluated_depsgraph_get()
553 object_eval = object.evaluated_get(depsgraph)
554 mesh_mod = object_eval.to_mesh()
555 bm_mod.from_mesh(mesh_mod)
556 object_eval.to_mesh_clear()
557 # re-enable other modifiers
558 for mod_name in show_viewport:
559 object.modifiers[mod_name].show_viewport = True
560 merge.reverse()
561 for mod in object.modifiers:
562 if mod.type == 'MIRROR':
563 mod.use_mirror_merge = merge.pop()
564 # no mirror modifiers, so no derived mesh necessary
565 else:
566 derived = False
567 bm_mod = bm
569 bm_mod.verts.ensure_lookup_table()
570 bm_mod.edges.ensure_lookup_table()
571 bm_mod.faces.ensure_lookup_table()
573 return(derived, bm_mod)
576 # return a mapping of derived indices to indices
577 def get_mapping(derived, bm, bm_mod, single_vertices, full_search, loops):
578 if not derived:
579 return(False)
581 if full_search:
582 verts = [v for v in bm.verts if not v.hide]
583 else:
584 verts = [v for v in bm.verts if v.select and not v.hide]
586 # non-selected vertices around single vertices also need to be mapped
587 if single_vertices:
588 mapping = dict([[vert, -1] for vert in single_vertices])
589 verts_mod = [bm_mod.verts[vert] for vert in single_vertices]
590 for v in verts:
591 for v_mod in verts_mod:
592 if (v.co - v_mod.co).length < 1e-6:
593 mapping[v_mod.index] = v.index
594 break
595 real_singles = [v_real for v_real in mapping.values() if v_real > -1]
597 verts_indices = [vert.index for vert in verts]
598 for face in [face for face in bm.faces if not face.select and not face.hide]:
599 for vert in face.verts:
600 if vert.index in real_singles:
601 for v in face.verts:
602 if v.index not in verts_indices:
603 if v not in verts:
604 verts.append(v)
605 break
607 # create mapping of derived indices to indices
608 mapping = dict([[vert, -1] for loop in loops for vert in loop[0]])
609 if single_vertices:
610 for single in single_vertices:
611 mapping[single] = -1
612 verts_mod = [bm_mod.verts[i] for i in mapping.keys()]
613 for v in verts:
614 for v_mod in verts_mod:
615 if (v.co - v_mod.co).length < 1e-6:
616 mapping[v_mod.index] = v.index
617 verts_mod.remove(v_mod)
618 break
620 return(mapping)
623 # calculate the determinant of a matrix
624 def matrix_determinant(m):
625 determinant = m[0][0] * m[1][1] * m[2][2] + m[0][1] * m[1][2] * m[2][0] \
626 + m[0][2] * m[1][0] * m[2][1] - m[0][2] * m[1][1] * m[2][0] \
627 - m[0][1] * m[1][0] * m[2][2] - m[0][0] * m[1][2] * m[2][1]
629 return(determinant)
632 # custom matrix inversion, to provide higher precision than the built-in one
633 def matrix_invert(m):
634 r = mathutils.Matrix((
635 (m[1][1] * m[2][2] - m[1][2] * m[2][1], m[0][2] * m[2][1] - m[0][1] * m[2][2],
636 m[0][1] * m[1][2] - m[0][2] * m[1][1]),
637 (m[1][2] * m[2][0] - m[1][0] * m[2][2], m[0][0] * m[2][2] - m[0][2] * m[2][0],
638 m[0][2] * m[1][0] - m[0][0] * m[1][2]),
639 (m[1][0] * m[2][1] - m[1][1] * m[2][0], m[0][1] * m[2][0] - m[0][0] * m[2][1],
640 m[0][0] * m[1][1] - m[0][1] * m[1][0])))
642 return (r * (1 / matrix_determinant(m)))
645 # returns a list of all loops parallel to the input, input included
646 def get_parallel_loops(bm_mod, loops):
647 # get required dictionaries
648 edge_faces = dict_edge_faces(bm_mod)
649 connected_faces = dict_face_faces(bm_mod, edge_faces)
650 # turn vertex loops into edge loops
651 edgeloops = []
652 for loop in loops:
653 edgeloop = [[sorted([loop[0][i], loop[0][i + 1]]) for i in
654 range(len(loop[0]) - 1)], loop[1]]
655 if loop[1]: # circular
656 edgeloop[0].append(sorted([loop[0][-1], loop[0][0]]))
657 edgeloops.append(edgeloop[:])
658 # variables to keep track while iterating
659 all_edgeloops = []
660 has_branches = False
662 for loop in edgeloops:
663 # initialise with original loop
664 all_edgeloops.append(loop[0])
665 newloops = [loop[0]]
666 verts_used = []
667 for edge in loop[0]:
668 if edge[0] not in verts_used:
669 verts_used.append(edge[0])
670 if edge[1] not in verts_used:
671 verts_used.append(edge[1])
673 # find parallel loops
674 while len(newloops) > 0:
675 side_a = []
676 side_b = []
677 for i in newloops[-1]:
678 i = tuple(i)
679 forbidden_side = False
680 if i not in edge_faces:
681 # weird input with branches
682 has_branches = True
683 break
684 for face in edge_faces[i]:
685 if len(side_a) == 0 and forbidden_side != "a":
686 side_a.append(face)
687 if forbidden_side:
688 break
689 forbidden_side = "a"
690 continue
691 elif side_a[-1] in connected_faces[face] and \
692 forbidden_side != "a":
693 side_a.append(face)
694 if forbidden_side:
695 break
696 forbidden_side = "a"
697 continue
698 if len(side_b) == 0 and forbidden_side != "b":
699 side_b.append(face)
700 if forbidden_side:
701 break
702 forbidden_side = "b"
703 continue
704 elif side_b[-1] in connected_faces[face] and \
705 forbidden_side != "b":
706 side_b.append(face)
707 if forbidden_side:
708 break
709 forbidden_side = "b"
710 continue
712 if has_branches:
713 # weird input with branches
714 break
716 newloops.pop(-1)
717 sides = []
718 if side_a:
719 sides.append(side_a)
720 if side_b:
721 sides.append(side_b)
723 for side in sides:
724 extraloop = []
725 for fi in side:
726 for key in face_edgekeys(bm_mod.faces[fi]):
727 if key[0] not in verts_used and key[1] not in \
728 verts_used:
729 extraloop.append(key)
730 break
731 if extraloop:
732 for key in extraloop:
733 for new_vert in key:
734 if new_vert not in verts_used:
735 verts_used.append(new_vert)
736 newloops.append(extraloop)
737 all_edgeloops.append(extraloop)
739 # input contains branches, only return selected loop
740 if has_branches:
741 return(loops)
743 # change edgeloops into normal loops
744 loops = []
745 for edgeloop in all_edgeloops:
746 loop = []
747 # grow loop by comparing vertices between consecutive edge-keys
748 for i in range(len(edgeloop) - 1):
749 for vert in range(2):
750 if edgeloop[i][vert] in edgeloop[i + 1]:
751 loop.append(edgeloop[i][vert])
752 break
753 if loop:
754 # add starting vertex
755 for vert in range(2):
756 if edgeloop[0][vert] != loop[0]:
757 loop = [edgeloop[0][vert]] + loop
758 break
759 # add ending vertex
760 for vert in range(2):
761 if edgeloop[-1][vert] != loop[-1]:
762 loop.append(edgeloop[-1][vert])
763 break
764 # check if loop is circular
765 if loop[0] == loop[-1]:
766 circular = True
767 loop = loop[:-1]
768 else:
769 circular = False
770 loops.append([loop, circular])
772 return(loops)
775 # gather initial data
776 def initialise():
777 object = bpy.context.active_object
778 if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
779 # ensure that selection is synced for the derived mesh
780 bpy.ops.object.mode_set(mode='OBJECT')
781 bpy.ops.object.mode_set(mode='EDIT')
782 bm = bmesh.from_edit_mesh(object.data)
784 bm.verts.ensure_lookup_table()
785 bm.edges.ensure_lookup_table()
786 bm.faces.ensure_lookup_table()
788 return(object, bm)
791 # move the vertices to their new locations
792 def move_verts(object, bm, mapping, move, lock, influence):
793 if lock:
794 lock_x, lock_y, lock_z = lock
795 orient_slot = bpy.context.scene.transform_orientation_slots[0]
796 custom = orient_slot.custom_orientation
797 if custom:
798 mat = custom.matrix.to_4x4().inverted() @ object.matrix_world.copy()
799 elif orient_slot.type == 'LOCAL':
800 mat = mathutils.Matrix.Identity(4)
801 elif orient_slot.type == 'VIEW':
802 mat = bpy.context.region_data.view_matrix.copy() @ \
803 object.matrix_world.copy()
804 else: # orientation == 'GLOBAL'
805 mat = object.matrix_world.copy()
806 mat_inv = mat.inverted()
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)
830 bm.verts[index].co = new_loc
831 bm.normal_update()
832 object.data.update()
834 bm.verts.ensure_lookup_table()
835 bm.edges.ensure_lookup_table()
836 bm.faces.ensure_lookup_table()
839 # load custom tool settings
840 def settings_load(self):
841 lt = bpy.context.window_manager.looptools
842 tool = self.name.split()[0].lower()
843 keys = self.as_keywords().keys()
844 for key in keys:
845 setattr(self, key, getattr(lt, tool + "_" + key))
848 # store custom tool settings
849 def settings_write(self):
850 lt = bpy.context.window_manager.looptools
851 tool = self.name.split()[0].lower()
852 keys = self.as_keywords().keys()
853 for key in keys:
854 setattr(lt, tool + "_" + key, getattr(self, key))
857 # clean up and set settings back to original state
858 def terminate():
859 # update editmesh cached data
860 obj = bpy.context.active_object
861 if obj.mode == 'EDIT':
862 bmesh.update_edit_mesh(obj.data, loop_triangles=True, destructive=True)
865 # ########################################
866 # ##### Bridge functions #################
867 # ########################################
869 # calculate a cubic spline through the middle section of 4 given coordinates
870 def bridge_calculate_cubic_spline(bm, coordinates):
871 result = []
872 x = [0, 1, 2, 3]
874 for j in range(3):
875 a = []
876 for i in coordinates:
877 a.append(float(i[j]))
878 h = []
879 for i in range(3):
880 h.append(x[i + 1] - x[i])
881 q = [False]
882 for i in range(1, 3):
883 q.append(3.0 / h[i] * (a[i + 1] - a[i]) - 3.0 / h[i - 1] * (a[i] - a[i - 1]))
884 l = [1.0]
885 u = [0.0]
886 z = [0.0]
887 for i in range(1, 3):
888 l.append(2.0 * (x[i + 1] - x[i - 1]) - h[i - 1] * u[i - 1])
889 u.append(h[i] / l[i])
890 z.append((q[i] - h[i - 1] * z[i - 1]) / l[i])
891 l.append(1.0)
892 z.append(0.0)
893 b = [False for i in range(3)]
894 c = [False for i in range(4)]
895 d = [False for i in range(3)]
896 c[3] = 0.0
897 for i in range(2, -1, -1):
898 c[i] = z[i] - u[i] * c[i + 1]
899 b[i] = (a[i + 1] - a[i]) / h[i] - h[i] * (c[i + 1] + 2.0 * c[i]) / 3.0
900 d[i] = (c[i + 1] - c[i]) / (3.0 * h[i])
901 for i in range(3):
902 result.append([a[i], b[i], c[i], d[i], x[i]])
903 spline = [result[1], result[4], result[7]]
905 return(spline)
908 # return a list with new vertex location vectors, a list with face vertex
909 # integers, and the highest vertex integer in the virtual mesh
910 def bridge_calculate_geometry(bm, lines, vertex_normals, segments,
911 interpolation, cubic_strength, min_width, max_vert_index):
912 new_verts = []
913 faces = []
915 # calculate location based on interpolation method
916 def get_location(line, segment, splines):
917 v1 = bm.verts[lines[line][0]].co
918 v2 = bm.verts[lines[line][1]].co
919 if interpolation == 'linear':
920 return v1 + (segment / segments) * (v2 - v1)
921 else: # interpolation == 'cubic'
922 m = (segment / segments)
923 ax, bx, cx, dx, tx = splines[line][0]
924 x = ax + bx * m + cx * m ** 2 + dx * m ** 3
925 ay, by, cy, dy, ty = splines[line][1]
926 y = ay + by * m + cy * m ** 2 + dy * m ** 3
927 az, bz, cz, dz, tz = splines[line][2]
928 z = az + bz * m + cz * m ** 2 + dz * m ** 3
929 return mathutils.Vector((x, y, z))
931 # no interpolation needed
932 if segments == 1:
933 for i, line in enumerate(lines):
934 if i < len(lines) - 1:
935 faces.append([line[0], lines[i + 1][0], lines[i + 1][1], line[1]])
936 # more than 1 segment, interpolate
937 else:
938 # calculate splines (if necessary) once, so no recalculations needed
939 if interpolation == 'cubic':
940 splines = []
941 for line in lines:
942 v1 = bm.verts[line[0]].co
943 v2 = bm.verts[line[1]].co
944 size = (v2 - v1).length * cubic_strength
945 splines.append(bridge_calculate_cubic_spline(bm,
946 [v1 + size * vertex_normals[line[0]], v1, v2,
947 v2 + size * vertex_normals[line[1]]]))
948 else:
949 splines = False
951 # create starting situation
952 virtual_width = [(bm.verts[lines[i][0]].co -
953 bm.verts[lines[i + 1][0]].co).length for i
954 in range(len(lines) - 1)]
955 new_verts = [get_location(0, seg, splines) for seg in range(1,
956 segments)]
957 first_line_indices = [i for i in range(max_vert_index + 1,
958 max_vert_index + segments)]
960 prev_verts = new_verts[:] # vertex locations of verts on previous line
961 prev_vert_indices = first_line_indices[:]
962 max_vert_index += segments - 1 # highest vertex index in virtual mesh
963 next_verts = [] # vertex locations of verts on current line
964 next_vert_indices = []
966 for i, line in enumerate(lines):
967 if i < len(lines) - 1:
968 v1 = line[0]
969 v2 = lines[i + 1][0]
970 end_face = True
971 for seg in range(1, segments):
972 loc1 = prev_verts[seg - 1]
973 loc2 = get_location(i + 1, seg, splines)
974 if (loc1 - loc2).length < (min_width / 100) * virtual_width[i] \
975 and line[1] == lines[i + 1][1]:
976 # triangle, no new vertex
977 faces.append([v1, v2, prev_vert_indices[seg - 1],
978 prev_vert_indices[seg - 1]])
979 next_verts += prev_verts[seg - 1:]
980 next_vert_indices += prev_vert_indices[seg - 1:]
981 end_face = False
982 break
983 else:
984 if i == len(lines) - 2 and lines[0] == lines[-1]:
985 # quad with first line, no new vertex
986 faces.append([v1, v2, first_line_indices[seg - 1],
987 prev_vert_indices[seg - 1]])
988 v2 = first_line_indices[seg - 1]
989 v1 = prev_vert_indices[seg - 1]
990 else:
991 # quad, add new vertex
992 max_vert_index += 1
993 faces.append([v1, v2, max_vert_index,
994 prev_vert_indices[seg - 1]])
995 v2 = max_vert_index
996 v1 = prev_vert_indices[seg - 1]
997 new_verts.append(loc2)
998 next_verts.append(loc2)
999 next_vert_indices.append(max_vert_index)
1000 if end_face:
1001 faces.append([v1, v2, lines[i + 1][1], line[1]])
1003 prev_verts = next_verts[:]
1004 prev_vert_indices = next_vert_indices[:]
1005 next_verts = []
1006 next_vert_indices = []
1008 return(new_verts, faces, max_vert_index)
1011 # calculate lines (list of lists, vertex indices) that are used for bridging
1012 def bridge_calculate_lines(bm, loops, mode, twist, reverse):
1013 lines = []
1014 loop1, loop2 = [i[0] for i in loops]
1015 loop1_circular, loop2_circular = [i[1] for i in loops]
1016 circular = loop1_circular or loop2_circular
1017 circle_full = False
1019 # calculate loop centers
1020 centers = []
1021 for loop in [loop1, loop2]:
1022 center = mathutils.Vector()
1023 for vertex in loop:
1024 center += bm.verts[vertex].co
1025 center /= len(loop)
1026 centers.append(center)
1027 for i, loop in enumerate([loop1, loop2]):
1028 for vertex in loop:
1029 if bm.verts[vertex].co == centers[i]:
1030 # prevent zero-length vectors in angle comparisons
1031 centers[i] += mathutils.Vector((0.01, 0, 0))
1032 break
1033 center1, center2 = centers
1035 # calculate the normals of the virtual planes that the loops are on
1036 normals = []
1037 normal_plurity = False
1038 for i, loop in enumerate([loop1, loop2]):
1039 # covariance matrix
1040 mat = mathutils.Matrix(((0.0, 0.0, 0.0),
1041 (0.0, 0.0, 0.0),
1042 (0.0, 0.0, 0.0)))
1043 x, y, z = centers[i]
1044 for loc in [bm.verts[vertex].co for vertex in loop]:
1045 mat[0][0] += (loc[0] - x) ** 2
1046 mat[1][0] += (loc[0] - x) * (loc[1] - y)
1047 mat[2][0] += (loc[0] - x) * (loc[2] - z)
1048 mat[0][1] += (loc[1] - y) * (loc[0] - x)
1049 mat[1][1] += (loc[1] - y) ** 2
1050 mat[2][1] += (loc[1] - y) * (loc[2] - z)
1051 mat[0][2] += (loc[2] - z) * (loc[0] - x)
1052 mat[1][2] += (loc[2] - z) * (loc[1] - y)
1053 mat[2][2] += (loc[2] - z) ** 2
1054 # plane normal
1055 normal = False
1056 if sum(mat[0]) < 1e-6 or sum(mat[1]) < 1e-6 or sum(mat[2]) < 1e-6:
1057 normal_plurity = True
1058 try:
1059 mat.invert()
1060 except:
1061 if sum(mat[0]) == 0:
1062 normal = mathutils.Vector((1.0, 0.0, 0.0))
1063 elif sum(mat[1]) == 0:
1064 normal = mathutils.Vector((0.0, 1.0, 0.0))
1065 elif sum(mat[2]) == 0:
1066 normal = mathutils.Vector((0.0, 0.0, 1.0))
1067 if not normal:
1068 # warning! this is different from .normalize()
1069 itermax = 500
1070 iter = 0
1071 vec = mathutils.Vector((1.0, 1.0, 1.0))
1072 vec2 = (mat @ vec) / (mat @ vec).length
1073 while vec != vec2 and iter < itermax:
1074 iter += 1
1075 vec = vec2
1076 vec2 = mat @ vec
1077 if vec2.length != 0:
1078 vec2 /= vec2.length
1079 if vec2.length == 0:
1080 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
1081 normal = vec2
1082 normals.append(normal)
1083 # have plane normals face in the same direction (maximum angle: 90 degrees)
1084 if ((center1 + normals[0]) - center2).length < \
1085 ((center1 - normals[0]) - center2).length:
1086 normals[0].negate()
1087 if ((center2 + normals[1]) - center1).length > \
1088 ((center2 - normals[1]) - center1).length:
1089 normals[1].negate()
1091 # rotation matrix, representing the difference between the plane normals
1092 axis = normals[0].cross(normals[1])
1093 axis = mathutils.Vector([loc if abs(loc) > 1e-8 else 0 for loc in axis])
1094 if axis.angle(mathutils.Vector((0, 0, 1)), 0) > 1.5707964:
1095 axis.negate()
1096 angle = normals[0].dot(normals[1])
1097 rotation_matrix = mathutils.Matrix.Rotation(angle, 4, axis)
1099 # if circular, rotate loops so they are aligned
1100 if circular:
1101 # make sure loop1 is the circular one (or both are circular)
1102 if loop2_circular and not loop1_circular:
1103 loop1_circular, loop2_circular = True, False
1104 loop1, loop2 = loop2, loop1
1106 # match start vertex of loop1 with loop2
1107 target_vector = bm.verts[loop2[0]].co - center2
1108 dif_angles = [[(rotation_matrix @ (bm.verts[vertex].co - center1)
1109 ).angle(target_vector, 0), False, i] for
1110 i, vertex in enumerate(loop1)]
1111 dif_angles.sort()
1112 if len(loop1) != len(loop2):
1113 angle_limit = dif_angles[0][0] * 1.2 # 20% margin
1114 dif_angles = [
1115 [(bm.verts[loop2[0]].co -
1116 bm.verts[loop1[index]].co).length, angle, index] for
1117 angle, distance, index in dif_angles if angle <= angle_limit
1119 dif_angles.sort()
1120 loop1 = loop1[dif_angles[0][2]:] + loop1[:dif_angles[0][2]]
1122 # have both loops face the same way
1123 if normal_plurity and not circular:
1124 second_to_first, second_to_second, second_to_last = [
1125 (bm.verts[loop1[1]].co - center1).angle(
1126 bm.verts[loop2[i]].co - center2) for i in [0, 1, -1]
1128 last_to_first, last_to_second = [
1129 (bm.verts[loop1[-1]].co -
1130 center1).angle(bm.verts[loop2[i]].co - center2) for
1131 i in [0, 1]
1133 if (min(last_to_first, last_to_second) * 1.1 < min(second_to_first,
1134 second_to_second)) or (loop2_circular and second_to_last * 1.1 <
1135 min(second_to_first, second_to_second)):
1136 loop1.reverse()
1137 if circular:
1138 loop1 = [loop1[-1]] + loop1[:-1]
1139 else:
1140 angle = (bm.verts[loop1[0]].co - center1).\
1141 cross(bm.verts[loop1[1]].co - center1).angle(normals[0], 0)
1142 target_angle = (bm.verts[loop2[0]].co - center2).\
1143 cross(bm.verts[loop2[1]].co - center2).angle(normals[1], 0)
1144 limit = 1.5707964 # 0.5*pi, 90 degrees
1145 if not ((angle > limit and target_angle > limit) or
1146 (angle < limit and target_angle < limit)):
1147 loop1.reverse()
1148 if circular:
1149 loop1 = [loop1[-1]] + loop1[:-1]
1150 elif normals[0].angle(normals[1]) > limit:
1151 loop1.reverse()
1152 if circular:
1153 loop1 = [loop1[-1]] + loop1[:-1]
1155 # both loops have the same length
1156 if len(loop1) == len(loop2):
1157 # manual override
1158 if twist:
1159 if abs(twist) < len(loop1):
1160 loop1 = loop1[twist:] + loop1[:twist]
1161 if reverse:
1162 loop1.reverse()
1164 lines.append([loop1[0], loop2[0]])
1165 for i in range(1, len(loop1)):
1166 lines.append([loop1[i], loop2[i]])
1168 # loops of different lengths
1169 else:
1170 # make loop1 longest loop
1171 if len(loop2) > len(loop1):
1172 loop1, loop2 = loop2, loop1
1173 loop1_circular, loop2_circular = loop2_circular, loop1_circular
1175 # manual override
1176 if twist:
1177 if abs(twist) < len(loop1):
1178 loop1 = loop1[twist:] + loop1[:twist]
1179 if reverse:
1180 loop1.reverse()
1182 # shortest angle difference doesn't always give correct start vertex
1183 if loop1_circular and not loop2_circular:
1184 shifting = 1
1185 while shifting:
1186 if len(loop1) - shifting < len(loop2):
1187 shifting = False
1188 break
1189 to_last, to_first = [
1190 (rotation_matrix @ (bm.verts[loop1[-1]].co - center1)).angle(
1191 (bm.verts[loop2[i]].co - center2), 0) for i in [-1, 0]
1193 if to_first < to_last:
1194 loop1 = [loop1[-1]] + loop1[:-1]
1195 shifting += 1
1196 else:
1197 shifting = False
1198 break
1200 # basic shortest side first
1201 if mode == 'basic':
1202 lines.append([loop1[0], loop2[0]])
1203 for i in range(1, len(loop1)):
1204 if i >= len(loop2) - 1:
1205 # triangles
1206 lines.append([loop1[i], loop2[-1]])
1207 else:
1208 # quads
1209 lines.append([loop1[i], loop2[i]])
1211 # shortest edge algorithm
1212 else: # mode == 'shortest'
1213 lines.append([loop1[0], loop2[0]])
1214 prev_vert2 = 0
1215 for i in range(len(loop1) - 1):
1216 if prev_vert2 == len(loop2) - 1 and not loop2_circular:
1217 # force triangles, reached end of loop2
1218 tri, quad = 0, 1
1219 elif prev_vert2 == len(loop2) - 1 and loop2_circular:
1220 # at end of loop2, but circular, so check with first vert
1221 tri, quad = [(bm.verts[loop1[i + 1]].co -
1222 bm.verts[loop2[j]].co).length
1223 for j in [prev_vert2, 0]]
1224 circle_full = 2
1225 elif len(loop1) - 1 - i == len(loop2) - 1 - prev_vert2 and \
1226 not circle_full:
1227 # force quads, otherwise won't make it to end of loop2
1228 tri, quad = 1, 0
1229 else:
1230 # calculate if tri or quad gives shortest edge
1231 tri, quad = [(bm.verts[loop1[i + 1]].co -
1232 bm.verts[loop2[j]].co).length
1233 for j in range(prev_vert2, prev_vert2 + 2)]
1235 # triangle
1236 if tri < quad:
1237 lines.append([loop1[i + 1], loop2[prev_vert2]])
1238 if circle_full == 2:
1239 circle_full = False
1240 # quad
1241 elif not circle_full:
1242 lines.append([loop1[i + 1], loop2[prev_vert2 + 1]])
1243 prev_vert2 += 1
1244 # quad to first vertex of loop2
1245 else:
1246 lines.append([loop1[i + 1], loop2[0]])
1247 prev_vert2 = 0
1248 circle_full = True
1250 # final face for circular loops
1251 if loop1_circular and loop2_circular:
1252 lines.append([loop1[0], loop2[0]])
1254 return(lines)
1257 # calculate number of segments needed
1258 def bridge_calculate_segments(bm, lines, loops, segments):
1259 # return if amount of segments is set by user
1260 if segments != 0:
1261 return segments
1263 # edge lengths
1264 average_edge_length = [
1265 (bm.verts[vertex].co -
1266 bm.verts[loop[0][i + 1]].co).length for loop in loops for
1267 i, vertex in enumerate(loop[0][:-1])
1269 # closing edges of circular loops
1270 average_edge_length += [
1271 (bm.verts[loop[0][-1]].co -
1272 bm.verts[loop[0][0]].co).length for loop in loops if loop[1]
1275 # average lengths
1276 average_edge_length = sum(average_edge_length) / len(average_edge_length)
1277 average_bridge_length = sum(
1278 [(bm.verts[v1].co -
1279 bm.verts[v2].co).length for v1, v2 in lines]
1280 ) / len(lines)
1282 segments = max(1, round(average_bridge_length / average_edge_length))
1284 return(segments)
1287 # return dictionary with vertex index as key, and the normal vector as value
1288 def bridge_calculate_virtual_vertex_normals(bm, lines, loops, edge_faces,
1289 edgekey_to_edge):
1290 if not edge_faces: # interpolation isn't set to cubic
1291 return False
1293 # pity reduce() isn't one of the basic functions in python anymore
1294 def average_vector_dictionary(dic):
1295 for key, vectors in dic.items():
1296 # if type(vectors) == type([]) and len(vectors) > 1:
1297 if len(vectors) > 1:
1298 average = mathutils.Vector()
1299 for vector in vectors:
1300 average += vector
1301 average /= len(vectors)
1302 dic[key] = [average]
1303 return dic
1305 # get all edges of the loop
1306 edges = [
1307 [edgekey_to_edge[tuple(sorted([loops[j][0][i],
1308 loops[j][0][i + 1]]))] for i in range(len(loops[j][0]) - 1)] for
1309 j in [0, 1]
1311 edges = edges[0] + edges[1]
1312 for j in [0, 1]:
1313 if loops[j][1]: # circular
1314 edges.append(edgekey_to_edge[tuple(sorted([loops[j][0][0],
1315 loops[j][0][-1]]))])
1318 calculation based on face topology (assign edge-normals to vertices)
1320 edge_normal = face_normal x edge_vector
1321 vertex_normal = average(edge_normals)
1323 vertex_normals = dict([(vertex, []) for vertex in loops[0][0] + loops[1][0]])
1324 for edge in edges:
1325 faces = edge_faces[edgekey(edge)] # valid faces connected to edge
1327 if faces:
1328 # get edge coordinates
1329 v1, v2 = [bm.verts[edgekey(edge)[i]].co for i in [0, 1]]
1330 edge_vector = v1 - v2
1331 if edge_vector.length < 1e-4:
1332 # zero-length edge, vertices at same location
1333 continue
1334 edge_center = (v1 + v2) / 2
1336 # average face coordinates, if connected to more than 1 valid face
1337 if len(faces) > 1:
1338 face_normal = mathutils.Vector()
1339 face_center = mathutils.Vector()
1340 for face in faces:
1341 face_normal += face.normal
1342 face_center += face.calc_center_median()
1343 face_normal /= len(faces)
1344 face_center /= len(faces)
1345 else:
1346 face_normal = faces[0].normal
1347 face_center = faces[0].calc_center_median()
1348 if face_normal.length < 1e-4:
1349 # faces with a surface of 0 have no face normal
1350 continue
1352 # calculate virtual edge normal
1353 edge_normal = edge_vector.cross(face_normal)
1354 edge_normal.length = 0.01
1355 if (face_center - (edge_center + edge_normal)).length > \
1356 (face_center - (edge_center - edge_normal)).length:
1357 # make normal face the correct way
1358 edge_normal.negate()
1359 edge_normal.normalize()
1360 # add virtual edge normal as entry for both vertices it connects
1361 for vertex in edgekey(edge):
1362 vertex_normals[vertex].append(edge_normal)
1365 calculation based on connection with other loop (vertex focused method)
1366 - used for vertices that aren't connected to any valid faces
1368 plane_normal = edge_vector x connection_vector
1369 vertex_normal = plane_normal x edge_vector
1371 vertices = [
1372 vertex for vertex, normal in vertex_normals.items() if not normal
1375 if vertices:
1376 # edge vectors connected to vertices
1377 edge_vectors = dict([[vertex, []] for vertex in vertices])
1378 for edge in edges:
1379 for v in edgekey(edge):
1380 if v in edge_vectors:
1381 edge_vector = bm.verts[edgekey(edge)[0]].co - \
1382 bm.verts[edgekey(edge)[1]].co
1383 if edge_vector.length < 1e-4:
1384 # zero-length edge, vertices at same location
1385 continue
1386 edge_vectors[v].append(edge_vector)
1388 # connection vectors between vertices of both loops
1389 connection_vectors = dict([[vertex, []] for vertex in vertices])
1390 connections = dict([[vertex, []] for vertex in vertices])
1391 for v1, v2 in lines:
1392 if v1 in connection_vectors or v2 in connection_vectors:
1393 new_vector = bm.verts[v1].co - bm.verts[v2].co
1394 if new_vector.length < 1e-4:
1395 # zero-length connection vector,
1396 # vertices in different loops at same location
1397 continue
1398 if v1 in connection_vectors:
1399 connection_vectors[v1].append(new_vector)
1400 connections[v1].append(v2)
1401 if v2 in connection_vectors:
1402 connection_vectors[v2].append(new_vector)
1403 connections[v2].append(v1)
1404 connection_vectors = average_vector_dictionary(connection_vectors)
1405 connection_vectors = dict(
1406 [[vertex, vector[0]] if vector else
1407 [vertex, []] for vertex, vector in connection_vectors.items()]
1410 for vertex, values in edge_vectors.items():
1411 # vertex normal doesn't matter, just assign a random vector to it
1412 if not connection_vectors[vertex]:
1413 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1414 continue
1416 # calculate to what location the vertex is connected,
1417 # used to determine what way to flip the normal
1418 connected_center = mathutils.Vector()
1419 for v in connections[vertex]:
1420 connected_center += bm.verts[v].co
1421 if len(connections[vertex]) > 1:
1422 connected_center /= len(connections[vertex])
1423 if len(connections[vertex]) == 0:
1424 # shouldn't be possible, but better safe than sorry
1425 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1426 continue
1428 # can't do proper calculations, because of zero-length vector
1429 if not values:
1430 if (connected_center - (bm.verts[vertex].co +
1431 connection_vectors[vertex])).length < (connected_center -
1432 (bm.verts[vertex].co - connection_vectors[vertex])).length:
1433 connection_vectors[vertex].negate()
1434 vertex_normals[vertex] = [connection_vectors[vertex].normalized()]
1435 continue
1437 # calculate vertex normals using edge-vectors,
1438 # connection-vectors and the derived plane normal
1439 for edge_vector in values:
1440 plane_normal = edge_vector.cross(connection_vectors[vertex])
1441 vertex_normal = edge_vector.cross(plane_normal)
1442 vertex_normal.length = 0.1
1443 if (connected_center - (bm.verts[vertex].co +
1444 vertex_normal)).length < (connected_center -
1445 (bm.verts[vertex].co - vertex_normal)).length:
1446 # make normal face the correct way
1447 vertex_normal.negate()
1448 vertex_normal.normalize()
1449 vertex_normals[vertex].append(vertex_normal)
1451 # average virtual vertex normals, based on all edges it's connected to
1452 vertex_normals = average_vector_dictionary(vertex_normals)
1453 vertex_normals = dict([[vertex, vector[0]] for vertex, vector in vertex_normals.items()])
1455 return(vertex_normals)
1458 # add vertices to mesh
1459 def bridge_create_vertices(bm, vertices):
1460 for i in range(len(vertices)):
1461 bm.verts.new(vertices[i])
1462 bm.verts.ensure_lookup_table()
1465 # add faces to mesh
1466 def bridge_create_faces(object, bm, faces, twist):
1467 # have the normal point the correct way
1468 if twist < 0:
1469 [face.reverse() for face in faces]
1470 faces = [face[2:] + face[:2] if face[0] == face[1] else face for face in faces]
1472 # eekadoodle prevention
1473 for i in range(len(faces)):
1474 if not faces[i][-1]:
1475 if faces[i][0] == faces[i][-1]:
1476 faces[i] = [faces[i][1], faces[i][2], faces[i][3], faces[i][1]]
1477 else:
1478 faces[i] = [faces[i][-1]] + faces[i][:-1]
1479 # result of converting from pre-bmesh period
1480 if faces[i][-1] == faces[i][-2]:
1481 faces[i] = faces[i][:-1]
1483 new_faces = []
1484 for i in range(len(faces)):
1485 new_faces.append(bm.faces.new([bm.verts[v] for v in faces[i]]))
1486 bm.normal_update()
1487 object.data.update(calc_edges=True) # calc_edges prevents memory-corruption
1489 bm.verts.ensure_lookup_table()
1490 bm.edges.ensure_lookup_table()
1491 bm.faces.ensure_lookup_table()
1493 return(new_faces)
1496 # calculate input loops
1497 def bridge_get_input(bm):
1498 # create list of internal edges, which should be skipped
1499 eks_of_selected_faces = [
1500 item for sublist in [face_edgekeys(face) for
1501 face in bm.faces if face.select and not face.hide] for item in sublist
1503 edge_count = {}
1504 for ek in eks_of_selected_faces:
1505 if ek in edge_count:
1506 edge_count[ek] += 1
1507 else:
1508 edge_count[ek] = 1
1509 internal_edges = [ek for ek in edge_count if edge_count[ek] > 1]
1511 # sort correct edges into loops
1512 selected_edges = [
1513 edgekey(edge) for edge in bm.edges if edge.select and
1514 not edge.hide and edgekey(edge) not in internal_edges
1516 loops = get_connected_selections(selected_edges)
1518 return(loops)
1521 # return values needed by the bridge operator
1522 def bridge_initialise(bm, interpolation):
1523 if interpolation == 'cubic':
1524 # dict with edge-key as key and list of connected valid faces as value
1525 face_blacklist = [
1526 face.index for face in bm.faces if face.select or
1527 face.hide
1529 edge_faces = dict(
1530 [[edgekey(edge), []] for edge in bm.edges if not edge.hide]
1532 for face in bm.faces:
1533 if face.index in face_blacklist:
1534 continue
1535 for key in face_edgekeys(face):
1536 edge_faces[key].append(face)
1537 # dictionary with the edge-key as key and edge as value
1538 edgekey_to_edge = dict(
1539 [[edgekey(edge), edge] for edge in bm.edges if edge.select and not edge.hide]
1541 else:
1542 edge_faces = False
1543 edgekey_to_edge = False
1545 # selected faces input
1546 old_selected_faces = [
1547 face.index for face in bm.faces if face.select and not face.hide
1550 # find out if faces created by bridging should be smoothed
1551 smooth = False
1552 if bm.faces:
1553 if sum([face.smooth for face in bm.faces]) / len(bm.faces) >= 0.5:
1554 smooth = True
1556 return(edge_faces, edgekey_to_edge, old_selected_faces, smooth)
1559 # return a string with the input method
1560 def bridge_input_method(loft, loft_loop):
1561 method = ""
1562 if loft:
1563 if loft_loop:
1564 method = "Loft loop"
1565 else:
1566 method = "Loft no-loop"
1567 else:
1568 method = "Bridge"
1570 return(method)
1573 # match up loops in pairs, used for multi-input bridging
1574 def bridge_match_loops(bm, loops):
1575 # calculate average loop normals and centers
1576 normals = []
1577 centers = []
1578 for vertices, circular in loops:
1579 normal = mathutils.Vector()
1580 center = mathutils.Vector()
1581 for vertex in vertices:
1582 normal += bm.verts[vertex].normal
1583 center += bm.verts[vertex].co
1584 normals.append(normal / len(vertices) / 10)
1585 centers.append(center / len(vertices))
1587 # possible matches if loop normals are faced towards the center
1588 # of the other loop
1589 matches = dict([[i, []] for i in range(len(loops))])
1590 matches_amount = 0
1591 for i in range(len(loops) + 1):
1592 for j in range(i + 1, len(loops)):
1593 if (centers[i] - centers[j]).length > \
1594 (centers[i] - (centers[j] + normals[j])).length and \
1595 (centers[j] - centers[i]).length > \
1596 (centers[j] - (centers[i] + normals[i])).length:
1597 matches_amount += 1
1598 matches[i].append([(centers[i] - centers[j]).length, i, j])
1599 matches[j].append([(centers[i] - centers[j]).length, j, i])
1600 # if no loops face each other, just make matches between all the loops
1601 if matches_amount == 0:
1602 for i in range(len(loops) + 1):
1603 for j in range(i + 1, len(loops)):
1604 matches[i].append([(centers[i] - centers[j]).length, i, j])
1605 matches[j].append([(centers[i] - centers[j]).length, j, i])
1606 for key, value in matches.items():
1607 value.sort()
1609 # matches based on distance between centers and number of vertices in loops
1610 new_order = []
1611 for loop_index in range(len(loops)):
1612 if loop_index in new_order:
1613 continue
1614 loop_matches = matches[loop_index]
1615 if not loop_matches:
1616 continue
1617 shortest_distance = loop_matches[0][0]
1618 shortest_distance *= 1.1
1619 loop_matches = [
1620 [abs(len(loops[loop_index][0]) -
1621 len(loops[loop[2]][0])), loop[0], loop[1], loop[2]] for loop in
1622 loop_matches if loop[0] < shortest_distance
1624 loop_matches.sort()
1625 for match in loop_matches:
1626 if match[3] not in new_order:
1627 new_order += [loop_index, match[3]]
1628 break
1630 # reorder loops based on matches
1631 if len(new_order) >= 2:
1632 loops = [loops[i] for i in new_order]
1634 return(loops)
1637 # remove old_selected_faces
1638 def bridge_remove_internal_faces(bm, old_selected_faces):
1639 # collect bmesh faces and internal bmesh edges
1640 remove_faces = [bm.faces[face] for face in old_selected_faces]
1641 edges = collections.Counter(
1642 [edge.index for face in remove_faces for edge in face.edges]
1644 remove_edges = [bm.edges[edge] for edge in edges if edges[edge] > 1]
1646 # remove internal faces and edges
1647 for face in remove_faces:
1648 bm.faces.remove(face)
1649 for edge in remove_edges:
1650 bm.edges.remove(edge)
1652 bm.faces.ensure_lookup_table()
1653 bm.edges.ensure_lookup_table()
1654 bm.verts.ensure_lookup_table()
1657 # update list of internal faces that are flagged for removal
1658 def bridge_save_unused_faces(bm, old_selected_faces, loops):
1659 # key: vertex index, value: lists of selected faces using it
1661 vertex_to_face = dict([[i, []] for i in range(len(bm.verts))])
1662 [[vertex_to_face[vertex.index].append(face) for vertex in
1663 bm.faces[face].verts] for face in old_selected_faces]
1665 # group selected faces that are connected
1666 groups = []
1667 grouped_faces = []
1668 for face in old_selected_faces:
1669 if face in grouped_faces:
1670 continue
1671 grouped_faces.append(face)
1672 group = [face]
1673 new_faces = [face]
1674 while new_faces:
1675 grow_face = new_faces[0]
1676 for vertex in bm.faces[grow_face].verts:
1677 vertex_face_group = [
1678 face for face in vertex_to_face[vertex.index] if
1679 face not in grouped_faces
1681 new_faces += vertex_face_group
1682 grouped_faces += vertex_face_group
1683 group += vertex_face_group
1684 new_faces.pop(0)
1685 groups.append(group)
1687 # key: vertex index, value: True/False (is it in a loop that is used)
1688 used_vertices = dict([[i, 0] for i in range(len(bm.verts))])
1689 for loop in loops:
1690 for vertex in loop[0]:
1691 used_vertices[vertex] = True
1693 # check if group is bridged, if not remove faces from internal faces list
1694 for group in groups:
1695 used = False
1696 for face in group:
1697 if used:
1698 break
1699 for vertex in bm.faces[face].verts:
1700 if used_vertices[vertex.index]:
1701 used = True
1702 break
1703 if not used:
1704 for face in group:
1705 old_selected_faces.remove(face)
1708 # add the newly created faces to the selection
1709 def bridge_select_new_faces(new_faces, smooth):
1710 for face in new_faces:
1711 face.select_set(True)
1712 face.smooth = smooth
1715 # sort loops, so they are connected in the correct order when lofting
1716 def bridge_sort_loops(bm, loops, loft_loop):
1717 # simplify loops to single points, and prepare for pathfinding
1718 x, y, z = [
1719 [sum([bm.verts[i].co[j] for i in loop[0]]) /
1720 len(loop[0]) for loop in loops] for j in range(3)
1722 nodes = [mathutils.Vector((x[i], y[i], z[i])) for i in range(len(loops))]
1724 active_node = 0
1725 open = [i for i in range(1, len(loops))]
1726 path = [[0, 0]]
1727 # connect node to path, that is shortest to active_node
1728 while len(open) > 0:
1729 distances = [(nodes[active_node] - nodes[i]).length for i in open]
1730 active_node = open[distances.index(min(distances))]
1731 open.remove(active_node)
1732 path.append([active_node, min(distances)])
1733 # check if we didn't start in the middle of the path
1734 for i in range(2, len(path)):
1735 if (nodes[path[i][0]] - nodes[0]).length < path[i][1]:
1736 temp = path[:i]
1737 path.reverse()
1738 path = path[:-i] + temp
1739 break
1741 # reorder loops
1742 loops = [loops[i[0]] for i in path]
1743 # if requested, duplicate first loop at last position, so loft can loop
1744 if loft_loop:
1745 loops = loops + [loops[0]]
1747 return(loops)
1750 # remapping old indices to new position in list
1751 def bridge_update_old_selection(bm, old_selected_faces):
1753 old_indices = old_selected_faces[:]
1754 old_selected_faces = []
1755 for i, face in enumerate(bm.faces):
1756 if face.index in old_indices:
1757 old_selected_faces.append(i)
1759 old_selected_faces = [
1760 i for i, face in enumerate(bm.faces) if face.index in old_selected_faces
1763 return(old_selected_faces)
1766 # ########################################
1767 # ##### Circle functions #################
1768 # ########################################
1770 # convert 3d coordinates to 2d coordinates on plane
1771 def circle_3d_to_2d(bm_mod, loop, com, normal):
1772 # project vertices onto the plane
1773 verts = [bm_mod.verts[v] for v in loop[0]]
1774 verts_projected = [[v.co - (v.co - com).dot(normal) * normal, v.index]
1775 for v in verts]
1777 # calculate two vectors (p and q) along the plane
1778 m = mathutils.Vector((normal[0] + 1.0, normal[1], normal[2]))
1779 p = m - (m.dot(normal) * normal)
1780 if p.dot(p) < 1e-6:
1781 m = mathutils.Vector((normal[0], normal[1] + 1.0, normal[2]))
1782 p = m - (m.dot(normal) * normal)
1783 q = p.cross(normal)
1785 # change to 2d coordinates using perpendicular projection
1786 locs_2d = []
1787 for loc, vert in verts_projected:
1788 vloc = loc - com
1789 x = p.dot(vloc) / p.dot(p)
1790 y = q.dot(vloc) / q.dot(q)
1791 locs_2d.append([x, y, vert])
1793 return(locs_2d, p, q)
1796 # calculate a best-fit circle to the 2d locations on the plane
1797 def circle_calculate_best_fit(locs_2d):
1798 # initial guess
1799 x0 = 0.0
1800 y0 = 0.0
1801 r = 1.0
1803 # calculate center and radius (non-linear least squares solution)
1804 for iter in range(500):
1805 jmat = []
1806 k = []
1807 for v in locs_2d:
1808 d = (v[0] ** 2 - 2.0 * x0 * v[0] + v[1] ** 2 - 2.0 * y0 * v[1] + x0 ** 2 + y0 ** 2) ** 0.5
1809 jmat.append([(x0 - v[0]) / d, (y0 - v[1]) / d, -1.0])
1810 k.append(-(((v[0] - x0) ** 2 + (v[1] - y0) ** 2) ** 0.5 - r))
1811 jmat2 = mathutils.Matrix(((0.0, 0.0, 0.0),
1812 (0.0, 0.0, 0.0),
1813 (0.0, 0.0, 0.0),
1815 k2 = mathutils.Vector((0.0, 0.0, 0.0))
1816 for i in range(len(jmat)):
1817 k2 += mathutils.Vector(jmat[i]) * k[i]
1818 jmat2[0][0] += jmat[i][0] ** 2
1819 jmat2[1][0] += jmat[i][0] * jmat[i][1]
1820 jmat2[2][0] += jmat[i][0] * jmat[i][2]
1821 jmat2[1][1] += jmat[i][1] ** 2
1822 jmat2[2][1] += jmat[i][1] * jmat[i][2]
1823 jmat2[2][2] += jmat[i][2] ** 2
1824 jmat2[0][1] = jmat2[1][0]
1825 jmat2[0][2] = jmat2[2][0]
1826 jmat2[1][2] = jmat2[2][1]
1827 try:
1828 jmat2.invert()
1829 except:
1830 pass
1831 dx0, dy0, dr = jmat2 @ k2
1832 x0 += dx0
1833 y0 += dy0
1834 r += dr
1835 # stop iterating if we're close enough to optimal solution
1836 if abs(dx0) < 1e-6 and abs(dy0) < 1e-6 and abs(dr) < 1e-6:
1837 break
1839 # return center of circle and radius
1840 return(x0, y0, r)
1843 # calculate circle so no vertices have to be moved away from the center
1844 def circle_calculate_min_fit(locs_2d):
1845 # center of circle
1846 x0 = (min([i[0] for i in locs_2d]) + max([i[0] for i in locs_2d])) / 2.0
1847 y0 = (min([i[1] for i in locs_2d]) + max([i[1] for i in locs_2d])) / 2.0
1848 center = mathutils.Vector([x0, y0])
1849 # radius of circle
1850 r = min([(mathutils.Vector([i[0], i[1]]) - center).length for i in locs_2d])
1852 # return center of circle and radius
1853 return(x0, y0, r)
1856 # calculate the new locations of the vertices that need to be moved
1857 def circle_calculate_verts(flatten, bm_mod, locs_2d, com, p, q, normal):
1858 # changing 2d coordinates back to 3d coordinates
1859 locs_3d = []
1860 for loc in locs_2d:
1861 locs_3d.append([loc[2], loc[0] * p + loc[1] * q + com])
1863 if flatten: # flat circle
1864 return(locs_3d)
1866 else: # project the locations on the existing mesh
1867 vert_edges = dict_vert_edges(bm_mod)
1868 vert_faces = dict_vert_faces(bm_mod)
1869 faces = [f for f in bm_mod.faces if not f.hide]
1870 rays = [normal, -normal]
1871 new_locs = []
1872 for loc in locs_3d:
1873 projection = False
1874 if bm_mod.verts[loc[0]].co == loc[1]: # vertex hasn't moved
1875 projection = loc[1]
1876 else:
1877 dif = normal.angle(loc[1] - bm_mod.verts[loc[0]].co)
1878 if -1e-6 < dif < 1e-6 or math.pi - 1e-6 < dif < math.pi + 1e-6:
1879 # original location is already along projection normal
1880 projection = bm_mod.verts[loc[0]].co
1881 else:
1882 # quick search through adjacent faces
1883 for face in vert_faces[loc[0]]:
1884 verts = [v.co for v in bm_mod.faces[face].verts]
1885 if len(verts) == 3: # triangle
1886 v1, v2, v3 = verts
1887 v4 = False
1888 else: # assume quad
1889 v1, v2, v3, v4 = verts[:4]
1890 for ray in rays:
1891 intersect = mathutils.geometry.\
1892 intersect_ray_tri(v1, v2, v3, ray, loc[1])
1893 if intersect:
1894 projection = intersect
1895 break
1896 elif v4:
1897 intersect = mathutils.geometry.\
1898 intersect_ray_tri(v1, v3, v4, ray, loc[1])
1899 if intersect:
1900 projection = intersect
1901 break
1902 if projection:
1903 break
1904 if not projection:
1905 # check if projection is on adjacent edges
1906 for edgekey in vert_edges[loc[0]]:
1907 line1 = bm_mod.verts[edgekey[0]].co
1908 line2 = bm_mod.verts[edgekey[1]].co
1909 intersect, dist = mathutils.geometry.intersect_point_line(
1910 loc[1], line1, line2
1912 if 1e-6 < dist < 1 - 1e-6:
1913 projection = intersect
1914 break
1915 if not projection:
1916 # full search through the entire mesh
1917 hits = []
1918 for face in faces:
1919 verts = [v.co for v in face.verts]
1920 if len(verts) == 3: # triangle
1921 v1, v2, v3 = verts
1922 v4 = False
1923 else: # assume quad
1924 v1, v2, v3, v4 = verts[:4]
1925 for ray in rays:
1926 intersect = mathutils.geometry.intersect_ray_tri(
1927 v1, v2, v3, ray, loc[1]
1929 if intersect:
1930 hits.append([(loc[1] - intersect).length,
1931 intersect])
1932 break
1933 elif v4:
1934 intersect = mathutils.geometry.intersect_ray_tri(
1935 v1, v3, v4, ray, loc[1]
1937 if intersect:
1938 hits.append([(loc[1] - intersect).length,
1939 intersect])
1940 break
1941 if len(hits) >= 1:
1942 # if more than 1 hit with mesh, closest hit is new loc
1943 hits.sort()
1944 projection = hits[0][1]
1945 if not projection:
1946 # nothing to project on, remain at flat location
1947 projection = loc[1]
1948 new_locs.append([loc[0], projection])
1950 # return new positions of projected circle
1951 return(new_locs)
1954 # check loops and only return valid ones
1955 def circle_check_loops(single_loops, loops, mapping, bm_mod):
1956 valid_single_loops = {}
1957 valid_loops = []
1958 for i, [loop, circular] in enumerate(loops):
1959 # loop needs to have at least 3 vertices
1960 if len(loop) < 3:
1961 continue
1962 # loop needs at least 1 vertex in the original, non-mirrored mesh
1963 if mapping:
1964 all_virtual = True
1965 for vert in loop:
1966 if mapping[vert] > -1:
1967 all_virtual = False
1968 break
1969 if all_virtual:
1970 continue
1971 # loop has to be non-collinear
1972 collinear = True
1973 loc0 = mathutils.Vector(bm_mod.verts[loop[0]].co[:])
1974 loc1 = mathutils.Vector(bm_mod.verts[loop[1]].co[:])
1975 for v in loop[2:]:
1976 locn = mathutils.Vector(bm_mod.verts[v].co[:])
1977 if loc0 == loc1 or loc1 == locn:
1978 loc0 = loc1
1979 loc1 = locn
1980 continue
1981 d1 = loc1 - loc0
1982 d2 = locn - loc1
1983 if -1e-6 < d1.angle(d2, 0) < 1e-6:
1984 loc0 = loc1
1985 loc1 = locn
1986 continue
1987 collinear = False
1988 break
1989 if collinear:
1990 continue
1991 # passed all tests, loop is valid
1992 valid_loops.append([loop, circular])
1993 valid_single_loops[len(valid_loops) - 1] = single_loops[i]
1995 return(valid_single_loops, valid_loops)
1998 # calculate the location of single input vertices that need to be flattened
1999 def circle_flatten_singles(bm_mod, com, p, q, normal, single_loop):
2000 new_locs = []
2001 for vert in single_loop:
2002 loc = mathutils.Vector(bm_mod.verts[vert].co[:])
2003 new_locs.append([vert, loc - (loc - com).dot(normal) * normal])
2005 return(new_locs)
2008 # calculate input loops
2009 def circle_get_input(object, bm):
2010 # get mesh with modifiers applied
2011 derived, bm_mod = get_derived_bmesh(object, bm, False)
2013 # create list of edge-keys based on selection state
2014 faces = False
2015 for face in bm.faces:
2016 if face.select and not face.hide:
2017 faces = True
2018 break
2019 if faces:
2020 # get selected, non-hidden , non-internal edge-keys
2021 eks_selected = [
2022 key for keys in [face_edgekeys(face) for face in
2023 bm_mod.faces if face.select and not face.hide] for key in keys
2025 edge_count = {}
2026 for ek in eks_selected:
2027 if ek in edge_count:
2028 edge_count[ek] += 1
2029 else:
2030 edge_count[ek] = 1
2031 edge_keys = [
2032 edgekey(edge) for edge in bm_mod.edges if edge.select and
2033 not edge.hide and edge_count.get(edgekey(edge), 1) == 1
2035 else:
2036 # no faces, so no internal edges either
2037 edge_keys = [
2038 edgekey(edge) for edge in bm_mod.edges if edge.select and not edge.hide
2041 # add edge-keys around single vertices
2042 verts_connected = dict(
2043 [[vert, 1] for edge in [edge for edge in
2044 bm_mod.edges if edge.select and not edge.hide] for vert in
2045 edgekey(edge)]
2047 single_vertices = [
2048 vert.index for vert in bm_mod.verts if
2049 vert.select and not vert.hide and
2050 not verts_connected.get(vert.index, False)
2053 if single_vertices and len(bm.faces) > 0:
2054 vert_to_single = dict(
2055 [[v.index, []] for v in bm_mod.verts if not v.hide]
2057 for face in [face for face in bm_mod.faces if not face.select and not face.hide]:
2058 for vert in face.verts:
2059 vert = vert.index
2060 if vert in single_vertices:
2061 for ek in face_edgekeys(face):
2062 if vert not in ek:
2063 edge_keys.append(ek)
2064 if vert not in vert_to_single[ek[0]]:
2065 vert_to_single[ek[0]].append(vert)
2066 if vert not in vert_to_single[ek[1]]:
2067 vert_to_single[ek[1]].append(vert)
2068 break
2070 # sort edge-keys into loops
2071 loops = get_connected_selections(edge_keys)
2073 # find out to which loops the single vertices belong
2074 single_loops = dict([[i, []] for i in range(len(loops))])
2075 if single_vertices and len(bm.faces) > 0:
2076 for i, [loop, circular] in enumerate(loops):
2077 for vert in loop:
2078 if vert_to_single[vert]:
2079 for single in vert_to_single[vert]:
2080 if single not in single_loops[i]:
2081 single_loops[i].append(single)
2083 return(derived, bm_mod, single_vertices, single_loops, loops)
2086 # recalculate positions based on the influence of the circle shape
2087 def circle_influence_locs(locs_2d, new_locs_2d, influence):
2088 for i in range(len(locs_2d)):
2089 oldx, oldy, j = locs_2d[i]
2090 newx, newy, k = new_locs_2d[i]
2091 altx = newx * (influence / 100) + oldx * ((100 - influence) / 100)
2092 alty = newy * (influence / 100) + oldy * ((100 - influence) / 100)
2093 locs_2d[i] = [altx, alty, j]
2095 return(locs_2d)
2098 # project 2d locations on circle, respecting distance relations between verts
2099 def circle_project_non_regular(locs_2d, x0, y0, r):
2100 for i in range(len(locs_2d)):
2101 x, y, j = locs_2d[i]
2102 loc = mathutils.Vector([x - x0, y - y0])
2103 loc.length = r
2104 locs_2d[i] = [loc[0], loc[1], j]
2106 return(locs_2d)
2109 # project 2d locations on circle, with equal distance between all vertices
2110 def circle_project_regular(locs_2d, x0, y0, r):
2111 # find offset angle and circling direction
2112 x, y, i = locs_2d[0]
2113 loc = mathutils.Vector([x - x0, y - y0])
2114 loc.length = r
2115 offset_angle = loc.angle(mathutils.Vector([1.0, 0.0]), 0.0)
2116 loca = mathutils.Vector([x - x0, y - y0, 0.0])
2117 if loc[1] < -1e-6:
2118 offset_angle *= -1
2119 x, y, j = locs_2d[1]
2120 locb = mathutils.Vector([x - x0, y - y0, 0.0])
2121 if loca.cross(locb)[2] >= 0:
2122 ccw = 1
2123 else:
2124 ccw = -1
2125 # distribute vertices along the circle
2126 for i in range(len(locs_2d)):
2127 t = offset_angle + ccw * (i / len(locs_2d) * 2 * math.pi)
2128 x = math.cos(t) * r
2129 y = math.sin(t) * r
2130 locs_2d[i] = [x, y, locs_2d[i][2]]
2132 return(locs_2d)
2135 # shift loop, so the first vertex is closest to the center
2136 def circle_shift_loop(bm_mod, loop, com):
2137 verts, circular = loop
2138 distances = [
2139 [(bm_mod.verts[vert].co - com).length, i] for i, vert in enumerate(verts)
2141 distances.sort()
2142 shift = distances[0][1]
2143 loop = [verts[shift:] + verts[:shift], circular]
2145 return(loop)
2148 # ########################################
2149 # ##### Curve functions ##################
2150 # ########################################
2152 # create lists with knots and points, all correctly sorted
2153 def curve_calculate_knots(loop, verts_selected):
2154 knots = [v for v in loop[0] if v in verts_selected]
2155 points = loop[0][:]
2156 # circular loop, potential for weird splines
2157 if loop[1]:
2158 offset = int(len(loop[0]) / 4)
2159 kpos = []
2160 for k in knots:
2161 kpos.append(loop[0].index(k))
2162 kdif = []
2163 for i in range(len(kpos) - 1):
2164 kdif.append(kpos[i + 1] - kpos[i])
2165 kdif.append(len(loop[0]) - kpos[-1] + kpos[0])
2166 kadd = []
2167 for k in kdif:
2168 if k > 2 * offset:
2169 kadd.append([kdif.index(k), True])
2170 # next 2 lines are optional, they insert
2171 # an extra control point in small gaps
2172 # elif k > offset:
2173 # kadd.append([kdif.index(k), False])
2174 kins = []
2175 krot = False
2176 for k in kadd: # extra knots to be added
2177 if k[1]: # big gap (break circular spline)
2178 kpos = loop[0].index(knots[k[0]]) + offset
2179 if kpos > len(loop[0]) - 1:
2180 kpos -= len(loop[0])
2181 kins.append([knots[k[0]], loop[0][kpos]])
2182 kpos2 = k[0] + 1
2183 if kpos2 > len(knots) - 1:
2184 kpos2 -= len(knots)
2185 kpos2 = loop[0].index(knots[kpos2]) - offset
2186 if kpos2 < 0:
2187 kpos2 += len(loop[0])
2188 kins.append([loop[0][kpos], loop[0][kpos2]])
2189 krot = loop[0][kpos2]
2190 else: # small gap (keep circular spline)
2191 k1 = loop[0].index(knots[k[0]])
2192 k2 = k[0] + 1
2193 if k2 > len(knots) - 1:
2194 k2 -= len(knots)
2195 k2 = loop[0].index(knots[k2])
2196 if k2 < k1:
2197 dif = len(loop[0]) - 1 - k1 + k2
2198 else:
2199 dif = k2 - k1
2200 kn = k1 + int(dif / 2)
2201 if kn > len(loop[0]) - 1:
2202 kn -= len(loop[0])
2203 kins.append([loop[0][k1], loop[0][kn]])
2204 for j in kins: # insert new knots
2205 knots.insert(knots.index(j[0]) + 1, j[1])
2206 if not krot: # circular loop
2207 knots.append(knots[0])
2208 points = loop[0][loop[0].index(knots[0]):]
2209 points += loop[0][0:loop[0].index(knots[0]) + 1]
2210 else: # non-circular loop (broken by script)
2211 krot = knots.index(krot)
2212 knots = knots[krot:] + knots[0:krot]
2213 if loop[0].index(knots[0]) > loop[0].index(knots[-1]):
2214 points = loop[0][loop[0].index(knots[0]):]
2215 points += loop[0][0:loop[0].index(knots[-1]) + 1]
2216 else:
2217 points = loop[0][loop[0].index(knots[0]):loop[0].index(knots[-1]) + 1]
2218 # non-circular loop, add first and last point as knots
2219 else:
2220 if loop[0][0] not in knots:
2221 knots.insert(0, loop[0][0])
2222 if loop[0][-1] not in knots:
2223 knots.append(loop[0][-1])
2225 return(knots, points)
2228 # calculate relative positions compared to first knot
2229 def curve_calculate_t(bm_mod, knots, points, pknots, regular, circular):
2230 tpoints = []
2231 loc_prev = False
2232 len_total = 0
2234 for p in points:
2235 if p in knots:
2236 loc = pknots[knots.index(p)] # use projected knot location
2237 else:
2238 loc = mathutils.Vector(bm_mod.verts[p].co[:])
2239 if not loc_prev:
2240 loc_prev = loc
2241 len_total += (loc - loc_prev).length
2242 tpoints.append(len_total)
2243 loc_prev = loc
2244 tknots = []
2245 for p in points:
2246 if p in knots:
2247 tknots.append(tpoints[points.index(p)])
2248 if circular:
2249 tknots[-1] = tpoints[-1]
2251 # regular option
2252 if regular:
2253 tpoints_average = tpoints[-1] / (len(tpoints) - 1)
2254 for i in range(1, len(tpoints) - 1):
2255 tpoints[i] = i * tpoints_average
2256 for i in range(len(knots)):
2257 tknots[i] = tpoints[points.index(knots[i])]
2258 if circular:
2259 tknots[-1] = tpoints[-1]
2261 return(tknots, tpoints)
2264 # change the location of non-selected points to their place on the spline
2265 def curve_calculate_vertices(bm_mod, knots, tknots, points, tpoints, splines,
2266 interpolation, restriction):
2267 newlocs = {}
2268 move = []
2270 for p in points:
2271 if p in knots:
2272 continue
2273 m = tpoints[points.index(p)]
2274 if m in tknots:
2275 n = tknots.index(m)
2276 else:
2277 t = tknots[:]
2278 t.append(m)
2279 t.sort()
2280 n = t.index(m) - 1
2281 if n > len(splines) - 1:
2282 n = len(splines) - 1
2283 elif n < 0:
2284 n = 0
2286 if interpolation == 'cubic':
2287 ax, bx, cx, dx, tx = splines[n][0]
2288 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
2289 ay, by, cy, dy, ty = splines[n][1]
2290 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
2291 az, bz, cz, dz, tz = splines[n][2]
2292 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
2293 newloc = mathutils.Vector([x, y, z])
2294 else: # interpolation == 'linear'
2295 a, d, t, u = splines[n]
2296 newloc = ((m - t) / u) * d + a
2298 if restriction != 'none': # vertex movement is restricted
2299 newlocs[p] = newloc
2300 else: # set the vertex to its new location
2301 move.append([p, newloc])
2303 if restriction != 'none': # vertex movement is restricted
2304 for p in points:
2305 if p in newlocs:
2306 newloc = newlocs[p]
2307 else:
2308 move.append([p, bm_mod.verts[p].co])
2309 continue
2310 oldloc = bm_mod.verts[p].co
2311 normal = bm_mod.verts[p].normal
2312 dloc = newloc - oldloc
2313 if dloc.length < 1e-6:
2314 move.append([p, newloc])
2315 elif restriction == 'extrude': # only extrusions
2316 if dloc.angle(normal, 0) < 0.5 * math.pi + 1e-6:
2317 move.append([p, newloc])
2318 else: # restriction == 'indent' only indentations
2319 if dloc.angle(normal) > 0.5 * math.pi - 1e-6:
2320 move.append([p, newloc])
2322 return(move)
2325 # trim loops to part between first and last selected vertices (including)
2326 def curve_cut_boundaries(bm_mod, loops):
2327 cut_loops = []
2328 for loop, circular in loops:
2329 if circular:
2330 selected = [bm_mod.verts[v].select for v in loop]
2331 first = selected.index(True)
2332 selected.reverse()
2333 last = -selected.index(True)
2334 if last == 0:
2335 if len(loop[first:]) < len(loop)/2:
2336 cut_loops.append([loop[first:], False])
2337 else:
2338 if len(loop[first:last]) < len(loop)/2:
2339 cut_loops.append([loop[first:last], False])
2340 continue
2341 selected = [bm_mod.verts[v].select for v in loop]
2342 first = selected.index(True)
2343 selected.reverse()
2344 last = -selected.index(True)
2345 if last == 0:
2346 cut_loops.append([loop[first:], circular])
2347 else:
2348 cut_loops.append([loop[first:last], circular])
2350 return(cut_loops)
2353 # calculate input loops
2354 def curve_get_input(object, bm, boundaries):
2355 # get mesh with modifiers applied
2356 derived, bm_mod = get_derived_bmesh(object, bm, False)
2358 # vertices that still need a loop to run through it
2359 verts_unsorted = [
2360 v.index for v in bm_mod.verts if v.select and not v.hide
2362 # necessary dictionaries
2363 vert_edges = dict_vert_edges(bm_mod)
2364 edge_faces = dict_edge_faces(bm_mod)
2365 correct_loops = []
2366 # find loops through each selected vertex
2367 while len(verts_unsorted) > 0:
2368 loops = curve_vertex_loops(bm_mod, verts_unsorted[0], vert_edges,
2369 edge_faces)
2370 verts_unsorted.pop(0)
2372 # check if loop is fully selected
2373 search_perpendicular = False
2374 i = -1
2375 for loop, circular in loops:
2376 i += 1
2377 selected = [v for v in loop if bm_mod.verts[v].select]
2378 if len(selected) < 2:
2379 # only one selected vertex on loop, don't use
2380 loops.pop(i)
2381 continue
2382 elif len(selected) == len(loop):
2383 search_perpendicular = loop
2384 break
2385 # entire loop is selected, find perpendicular loops
2386 if search_perpendicular:
2387 for vert in loop:
2388 if vert in verts_unsorted:
2389 verts_unsorted.remove(vert)
2390 perp_loops = curve_perpendicular_loops(bm_mod, loop,
2391 vert_edges, edge_faces)
2392 for perp_loop in perp_loops:
2393 correct_loops.append(perp_loop)
2394 # normal input
2395 else:
2396 for loop, circular in loops:
2397 correct_loops.append([loop, circular])
2399 # boundaries option
2400 if boundaries:
2401 correct_loops = curve_cut_boundaries(bm_mod, correct_loops)
2403 return(derived, bm_mod, correct_loops)
2406 # return all loops that are perpendicular to the given one
2407 def curve_perpendicular_loops(bm_mod, start_loop, vert_edges, edge_faces):
2408 # find perpendicular loops
2409 perp_loops = []
2410 for start_vert in start_loop:
2411 loops = curve_vertex_loops(bm_mod, start_vert, vert_edges,
2412 edge_faces)
2413 for loop, circular in loops:
2414 selected = [v for v in loop if bm_mod.verts[v].select]
2415 if len(selected) == len(loop):
2416 continue
2417 else:
2418 perp_loops.append([loop, circular, loop.index(start_vert)])
2420 # trim loops to same lengths
2421 shortest = [
2422 [len(loop[0]), i] for i, loop in enumerate(perp_loops) if not loop[1]
2424 if not shortest:
2425 # all loops are circular, not trimming
2426 return([[loop[0], loop[1]] for loop in perp_loops])
2427 else:
2428 shortest = min(shortest)
2429 shortest_start = perp_loops[shortest[1]][2]
2430 before_start = shortest_start
2431 after_start = shortest[0] - shortest_start - 1
2432 bigger_before = before_start > after_start
2433 trimmed_loops = []
2434 for loop in perp_loops:
2435 # have the loop face the same direction as the shortest one
2436 if bigger_before:
2437 if loop[2] < len(loop[0]) / 2:
2438 loop[0].reverse()
2439 loop[2] = len(loop[0]) - loop[2] - 1
2440 else:
2441 if loop[2] > len(loop[0]) / 2:
2442 loop[0].reverse()
2443 loop[2] = len(loop[0]) - loop[2] - 1
2444 # circular loops can shift, to prevent wrong trimming
2445 if loop[1]:
2446 shift = shortest_start - loop[2]
2447 if loop[2] + shift > 0 and loop[2] + shift < len(loop[0]):
2448 loop[0] = loop[0][-shift:] + loop[0][:-shift]
2449 loop[2] += shift
2450 if loop[2] < 0:
2451 loop[2] += len(loop[0])
2452 elif loop[2] > len(loop[0]) - 1:
2453 loop[2] -= len(loop[0])
2454 # trim
2455 start = max(0, loop[2] - before_start)
2456 end = min(len(loop[0]), loop[2] + after_start + 1)
2457 trimmed_loops.append([loop[0][start:end], False])
2459 return(trimmed_loops)
2462 # project knots on non-selected geometry
2463 def curve_project_knots(bm_mod, verts_selected, knots, points, circular):
2464 # function to project vertex on edge
2465 def project(v1, v2, v3):
2466 # v1 and v2 are part of a line
2467 # v3 is projected onto it
2468 v2 -= v1
2469 v3 -= v1
2470 p = v3.project(v2)
2471 return(p + v1)
2473 if circular: # project all knots
2474 start = 0
2475 end = len(knots)
2476 pknots = []
2477 else: # first and last knot shouldn't be projected
2478 start = 1
2479 end = -1
2480 pknots = [mathutils.Vector(bm_mod.verts[knots[0]].co[:])]
2481 for knot in knots[start:end]:
2482 if knot in verts_selected:
2483 knot_left = knot_right = False
2484 for i in range(points.index(knot) - 1, -1 * len(points), -1):
2485 if points[i] not in knots:
2486 knot_left = points[i]
2487 break
2488 for i in range(points.index(knot) + 1, 2 * len(points)):
2489 if i > len(points) - 1:
2490 i -= len(points)
2491 if points[i] not in knots:
2492 knot_right = points[i]
2493 break
2494 if knot_left and knot_right and knot_left != knot_right:
2495 knot_left = mathutils.Vector(bm_mod.verts[knot_left].co[:])
2496 knot_right = mathutils.Vector(bm_mod.verts[knot_right].co[:])
2497 knot = mathutils.Vector(bm_mod.verts[knot].co[:])
2498 pknots.append(project(knot_left, knot_right, knot))
2499 else:
2500 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2501 else: # knot isn't selected, so shouldn't be changed
2502 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2503 if not circular:
2504 pknots.append(mathutils.Vector(bm_mod.verts[knots[-1]].co[:]))
2506 return(pknots)
2509 # find all loops through a given vertex
2510 def curve_vertex_loops(bm_mod, start_vert, vert_edges, edge_faces):
2511 edges_used = []
2512 loops = []
2514 for edge in vert_edges[start_vert]:
2515 if edge in edges_used:
2516 continue
2517 loop = []
2518 circular = False
2519 for vert in edge:
2520 active_faces = edge_faces[edge]
2521 new_vert = vert
2522 growing = True
2523 while growing:
2524 growing = False
2525 new_edges = vert_edges[new_vert]
2526 loop.append(new_vert)
2527 if len(loop) > 1:
2528 edges_used.append(tuple(sorted([loop[-1], loop[-2]])))
2529 if len(new_edges) < 3 or len(new_edges) > 4:
2530 # pole
2531 break
2532 else:
2533 # find next edge
2534 for new_edge in new_edges:
2535 if new_edge in edges_used:
2536 continue
2537 eliminate = False
2538 for new_face in edge_faces[new_edge]:
2539 if new_face in active_faces:
2540 eliminate = True
2541 break
2542 if eliminate:
2543 continue
2544 # found correct new edge
2545 active_faces = edge_faces[new_edge]
2546 v1, v2 = new_edge
2547 if v1 != new_vert:
2548 new_vert = v1
2549 else:
2550 new_vert = v2
2551 if new_vert == loop[0]:
2552 circular = True
2553 else:
2554 growing = True
2555 break
2556 if circular:
2557 break
2558 loop.reverse()
2559 loops.append([loop, circular])
2561 return(loops)
2564 # ########################################
2565 # ##### Flatten functions ################
2566 # ########################################
2568 # sort input into loops
2569 def flatten_get_input(bm):
2570 vert_verts = dict_vert_verts(
2571 [edgekey(edge) for edge in bm.edges if edge.select and not edge.hide]
2573 verts = [v.index for v in bm.verts if v.select and not v.hide]
2575 # no connected verts, consider all selected verts as a single input
2576 if not vert_verts:
2577 return([[verts, False]])
2579 loops = []
2580 while len(verts) > 0:
2581 # start of loop
2582 loop = [verts[0]]
2583 verts.pop(0)
2584 if loop[-1] in vert_verts:
2585 to_grow = vert_verts[loop[-1]]
2586 else:
2587 to_grow = []
2588 # grow loop
2589 while len(to_grow) > 0:
2590 new_vert = to_grow[0]
2591 to_grow.pop(0)
2592 if new_vert in loop:
2593 continue
2594 loop.append(new_vert)
2595 verts.remove(new_vert)
2596 to_grow += vert_verts[new_vert]
2597 # add loop to loops
2598 loops.append([loop, False])
2600 return(loops)
2603 # calculate position of vertex projections on plane
2604 def flatten_project(bm, loop, com, normal):
2605 verts = [bm.verts[v] for v in loop[0]]
2606 verts_projected = [
2607 [v.index, mathutils.Vector(v.co[:]) -
2608 (mathutils.Vector(v.co[:]) - com).dot(normal) * normal] for v in verts
2611 return(verts_projected)
2614 # ########################################
2615 # ##### Gstretch functions ###############
2616 # ########################################
2618 # fake stroke class, used to create custom strokes if no GP data is found
2619 class gstretch_fake_stroke():
2620 def __init__(self, points):
2621 self.points = [gstretch_fake_stroke_point(p) for p in points]
2624 # fake stroke point class, used in fake strokes
2625 class gstretch_fake_stroke_point():
2626 def __init__(self, loc):
2627 self.co = loc
2630 # flips loops, if necessary, to obtain maximum alignment to stroke
2631 def gstretch_align_pairs(ls_pairs, object, bm_mod, method):
2632 # returns total distance between all verts in loop and corresponding stroke
2633 def distance_loop_stroke(loop, stroke, object, bm_mod, method):
2634 stroke_lengths_cache = False
2635 loop_length = len(loop[0])
2636 total_distance = 0
2638 if method != 'regular':
2639 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2641 for i, v_index in enumerate(loop[0]):
2642 if method == 'regular':
2643 relative_distance = i / (loop_length - 1)
2644 else:
2645 relative_distance = relative_lengths[i]
2647 loc1 = object.matrix_world @ bm_mod.verts[v_index].co
2648 loc2, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2649 relative_distance, stroke_lengths_cache)
2650 total_distance += (loc2 - loc1).length
2652 return(total_distance)
2654 if ls_pairs:
2655 for (loop, stroke) in ls_pairs:
2656 total_dist = distance_loop_stroke(loop, stroke, object, bm_mod,
2657 method)
2658 loop[0].reverse()
2659 total_dist_rev = distance_loop_stroke(loop, stroke, object, bm_mod,
2660 method)
2661 if total_dist_rev > total_dist:
2662 loop[0].reverse()
2664 return(ls_pairs)
2667 # calculate vertex positions on stroke
2668 def gstretch_calculate_verts(loop, stroke, object, bm_mod, method):
2669 move = []
2670 stroke_lengths_cache = False
2671 loop_length = len(loop[0])
2672 matrix_inverse = object.matrix_world.inverted()
2674 # return intersection of line with stroke, or None
2675 def intersect_line_stroke(vec1, vec2, stroke):
2676 for i, p in enumerate(stroke.points[1:]):
2677 intersections = mathutils.geometry.intersect_line_line(vec1, vec2,
2678 p.co, stroke.points[i].co)
2679 if intersections and \
2680 (intersections[0] - intersections[1]).length < 1e-2:
2681 x, dist = mathutils.geometry.intersect_point_line(
2682 intersections[0], p.co, stroke.points[i].co)
2683 if -1 < dist < 1:
2684 return(intersections[0])
2685 return(None)
2687 if method == 'project':
2688 vert_edges = dict_vert_edges(bm_mod)
2690 for v_index in loop[0]:
2691 intersection = None
2692 for ek in vert_edges[v_index]:
2693 v1, v2 = ek
2694 v1 = bm_mod.verts[v1]
2695 v2 = bm_mod.verts[v2]
2696 if v1.select + v2.select == 1 and not v1.hide and not v2.hide:
2697 vec1 = object.matrix_world @ v1.co
2698 vec2 = object.matrix_world @ v2.co
2699 intersection = intersect_line_stroke(vec1, vec2, stroke)
2700 if intersection:
2701 break
2702 if not intersection:
2703 v = bm_mod.verts[v_index]
2704 intersection = intersect_line_stroke(v.co, v.co + v.normal,
2705 stroke)
2706 if intersection:
2707 move.append([v_index, matrix_inverse @ intersection])
2709 else:
2710 if method == 'irregular':
2711 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2713 for i, v_index in enumerate(loop[0]):
2714 if method == 'regular':
2715 relative_distance = i / (loop_length - 1)
2716 else: # method == 'irregular'
2717 relative_distance = relative_lengths[i]
2718 loc, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2719 relative_distance, stroke_lengths_cache)
2720 loc = matrix_inverse @ loc
2721 move.append([v_index, loc])
2723 return(move)
2726 # create new vertices, based on GP strokes
2727 def gstretch_create_verts(object, bm_mod, strokes, method, conversion,
2728 conversion_distance, conversion_max, conversion_min, conversion_vertices):
2729 move = []
2730 stroke_verts = []
2731 mat_world = object.matrix_world.inverted()
2732 singles = gstretch_match_single_verts(bm_mod, strokes, mat_world)
2734 for stroke in strokes:
2735 stroke_verts.append([stroke, []])
2736 min_end_point = 0
2737 if conversion == 'vertices':
2738 min_end_point = conversion_vertices
2739 end_point = conversion_vertices
2740 elif conversion == 'limit_vertices':
2741 min_end_point = conversion_min
2742 end_point = conversion_max
2743 else:
2744 end_point = len(stroke.points)
2745 # creation of new vertices at fixed user-defined distances
2746 if conversion == 'distance':
2747 method = 'project'
2748 prev_point = stroke.points[0]
2749 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world @ prev_point.co))
2750 distance = 0
2751 limit = conversion_distance
2752 for point in stroke.points:
2753 new_distance = distance + (point.co - prev_point.co).length
2754 iteration = 0
2755 while new_distance > limit:
2756 to_cover = limit - distance + (limit * iteration)
2757 new_loc = prev_point.co + to_cover * \
2758 (point.co - prev_point.co).normalized()
2759 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * new_loc))
2760 new_distance -= limit
2761 iteration += 1
2762 distance = new_distance
2763 prev_point = point
2764 # creation of new vertices for other methods
2765 else:
2766 # add vertices at stroke points
2767 for point in stroke.points[:end_point]:
2768 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world @ point.co))
2769 # add more vertices, beyond the points that are available
2770 if min_end_point > min(len(stroke.points), end_point):
2771 for i in range(min_end_point -
2772 (min(len(stroke.points), end_point))):
2773 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world @ point.co))
2774 # force even spreading of points, so they are placed on stroke
2775 method = 'regular'
2776 bm_mod.verts.ensure_lookup_table()
2777 bm_mod.verts.index_update()
2778 for stroke, verts_seq in stroke_verts:
2779 if len(verts_seq) < 2:
2780 continue
2781 # spread vertices evenly over the stroke
2782 if method == 'regular':
2783 loop = [[vert.index for vert in verts_seq], False]
2784 move += gstretch_calculate_verts(loop, stroke, object, bm_mod,
2785 method)
2786 # create edges
2787 for i, vert in enumerate(verts_seq):
2788 if i > 0:
2789 bm_mod.edges.new((verts_seq[i - 1], verts_seq[i]))
2790 vert.select = True
2791 # connect single vertices to the closest stroke
2792 if singles:
2793 for vert, m_stroke, point in singles:
2794 if m_stroke != stroke:
2795 continue
2796 bm_mod.edges.new((vert, verts_seq[point]))
2797 bm_mod.edges.ensure_lookup_table()
2798 bmesh.update_edit_mesh(object.data)
2800 return(move)
2803 # erases the grease pencil stroke
2804 def gstretch_erase_stroke(stroke, context):
2805 # change 3d coordinate into a stroke-point
2806 def sp(loc, context):
2807 lib = {'name': "",
2808 'pen_flip': False,
2809 'is_start': False,
2810 'location': (0, 0, 0),
2811 'mouse': (
2812 view3d_utils.location_3d_to_region_2d(
2813 context.region, context.space_data.region_3d, loc)
2815 'pressure': 1,
2816 'size': 0,
2817 'time': 0}
2818 return(lib)
2820 if type(stroke) != bpy.types.GPencilStroke:
2821 # fake stroke, there is nothing to delete
2822 return
2824 erase_stroke = [sp(p.co, context) for p in stroke.points]
2825 if erase_stroke:
2826 erase_stroke[0]['is_start'] = True
2827 #bpy.ops.gpencil.draw(mode='ERASER', stroke=erase_stroke)
2828 bpy.ops.gpencil.data_unlink()
2832 # get point on stroke, given by relative distance (0.0 - 1.0)
2833 def gstretch_eval_stroke(stroke, distance, stroke_lengths_cache=False):
2834 # use cache if available
2835 if not stroke_lengths_cache:
2836 lengths = [0]
2837 for i, p in enumerate(stroke.points[1:]):
2838 lengths.append((p.co - stroke.points[i].co).length + lengths[-1])
2839 total_length = max(lengths[-1], 1e-7)
2840 stroke_lengths_cache = [length / total_length for length in
2841 lengths]
2842 stroke_lengths = stroke_lengths_cache[:]
2844 if distance in stroke_lengths:
2845 loc = stroke.points[stroke_lengths.index(distance)].co
2846 elif distance > stroke_lengths[-1]:
2847 # should be impossible, but better safe than sorry
2848 loc = stroke.points[-1].co
2849 else:
2850 stroke_lengths.append(distance)
2851 stroke_lengths.sort()
2852 stroke_index = stroke_lengths.index(distance)
2853 interval_length = stroke_lengths[
2854 stroke_index + 1] - stroke_lengths[stroke_index - 1
2856 distance_relative = (distance - stroke_lengths[stroke_index - 1]) / interval_length
2857 interval_vector = stroke.points[stroke_index].co - stroke.points[stroke_index - 1].co
2858 loc = stroke.points[stroke_index - 1].co + distance_relative * interval_vector
2860 return(loc, stroke_lengths_cache)
2863 # create fake grease pencil strokes for the active object
2864 def gstretch_get_fake_strokes(object, bm_mod, loops):
2865 strokes = []
2866 for loop in loops:
2867 p1 = object.matrix_world @ bm_mod.verts[loop[0][0]].co
2868 p2 = object.matrix_world @ bm_mod.verts[loop[0][-1]].co
2869 strokes.append(gstretch_fake_stroke([p1, p2]))
2871 return(strokes)
2873 # get strokes
2874 def gstretch_get_strokes(self, context):
2875 looptools = context.window_manager.looptools
2876 gp = get_strokes(self, context)
2877 if not gp:
2878 return(None)
2879 if looptools.gstretch_use_guide == "Annotation":
2880 layer = bpy.data.grease_pencils[0].layers.active
2881 if looptools.gstretch_use_guide == "GPencil" and not looptools.gstretch_guide == None:
2882 layer = looptools.gstretch_guide.data.layers.active
2883 if not layer:
2884 return(None)
2885 frame = layer.active_frame
2886 if not frame:
2887 return(None)
2888 strokes = frame.strokes
2889 if len(strokes) < 1:
2890 return(None)
2892 return(strokes)
2894 # returns a list with loop-stroke pairs
2895 def gstretch_match_loops_strokes(loops, strokes, object, bm_mod):
2896 if not loops or not strokes:
2897 return(None)
2899 # calculate loop centers
2900 loop_centers = []
2901 bm_mod.verts.ensure_lookup_table()
2902 for loop in loops:
2903 center = mathutils.Vector()
2904 for v_index in loop[0]:
2905 center += bm_mod.verts[v_index].co
2906 center /= len(loop[0])
2907 center = object.matrix_world @ center
2908 loop_centers.append([center, loop])
2910 # calculate stroke centers
2911 stroke_centers = []
2912 for stroke in strokes:
2913 center = mathutils.Vector()
2914 for p in stroke.points:
2915 center += p.co
2916 center /= len(stroke.points)
2917 stroke_centers.append([center, stroke, 0])
2919 # match, first by stroke use count, then by distance
2920 ls_pairs = []
2921 for lc in loop_centers:
2922 distances = []
2923 for i, sc in enumerate(stroke_centers):
2924 distances.append([sc[2], (lc[0] - sc[0]).length, i])
2925 distances.sort()
2926 best_stroke = distances[0][2]
2927 ls_pairs.append([lc[1], stroke_centers[best_stroke][1]])
2928 stroke_centers[best_stroke][2] += 1 # increase stroke use count
2930 return(ls_pairs)
2933 # match single selected vertices to the closest stroke endpoint
2934 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2935 def gstretch_match_single_verts(bm_mod, strokes, mat_world):
2936 # calculate stroke endpoints in object space
2937 endpoints = []
2938 for stroke in strokes:
2939 endpoints.append((mat_world @ stroke.points[0].co, stroke, 0))
2940 endpoints.append((mat_world @ stroke.points[-1].co, stroke, -1))
2942 distances = []
2943 # find single vertices (not connected to other selected verts)
2944 for vert in bm_mod.verts:
2945 if not vert.select:
2946 continue
2947 single = True
2948 for edge in vert.link_edges:
2949 if edge.other_vert(vert).select:
2950 single = False
2951 break
2952 if not single:
2953 continue
2954 # calculate distances from vertex to endpoints
2955 distance = [((vert.co - loc).length, vert, stroke, stroke_point,
2956 endpoint_index) for endpoint_index, (loc, stroke, stroke_point) in
2957 enumerate(endpoints)]
2958 distance.sort()
2959 distances.append(distance[0])
2961 # create matches, based on shortest distance first
2962 singles = []
2963 while distances:
2964 distances.sort()
2965 singles.append((distances[0][1], distances[0][2], distances[0][3]))
2966 endpoints.pop(distances[0][4])
2967 distances.pop(0)
2968 distances_new = []
2969 for (i, vert, j, k, l) in distances:
2970 distance_new = [((vert.co - loc).length, vert, stroke, stroke_point,
2971 endpoint_index) for endpoint_index, (loc, stroke,
2972 stroke_point) in enumerate(endpoints)]
2973 distance_new.sort()
2974 distances_new.append(distance_new[0])
2975 distances = distances_new
2977 return(singles)
2980 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2981 def gstretch_relative_lengths(loop, bm_mod):
2982 lengths = [0]
2983 for i, v_index in enumerate(loop[0][1:]):
2984 lengths.append(
2985 (bm_mod.verts[v_index].co -
2986 bm_mod.verts[loop[0][i]].co).length + lengths[-1]
2988 total_length = max(lengths[-1], 1e-7)
2989 relative_lengths = [length / total_length for length in
2990 lengths]
2992 return(relative_lengths)
2995 # convert cache-stored strokes into usable (fake) GP strokes
2996 def gstretch_safe_to_true_strokes(safe_strokes):
2997 strokes = []
2998 for safe_stroke in safe_strokes:
2999 strokes.append(gstretch_fake_stroke(safe_stroke))
3001 return(strokes)
3004 # convert a GP stroke into a list of points which can be stored in cache
3005 def gstretch_true_to_safe_strokes(strokes):
3006 safe_strokes = []
3007 for stroke in strokes:
3008 safe_strokes.append([p.co.copy() for p in stroke.points])
3010 return(safe_strokes)
3013 # force consistency in GUI, max value can never be lower than min value
3014 def gstretch_update_max(self, context):
3015 # called from operator settings (after execution)
3016 if 'conversion_min' in self.keys():
3017 if self.conversion_min > self.conversion_max:
3018 self.conversion_max = self.conversion_min
3019 # called from toolbar
3020 else:
3021 lt = context.window_manager.looptools
3022 if lt.gstretch_conversion_min > lt.gstretch_conversion_max:
3023 lt.gstretch_conversion_max = lt.gstretch_conversion_min
3026 # force consistency in GUI, min value can never be higher than max value
3027 def gstretch_update_min(self, context):
3028 # called from operator settings (after execution)
3029 if 'conversion_max' in self.keys():
3030 if self.conversion_max < self.conversion_min:
3031 self.conversion_min = self.conversion_max
3032 # called from toolbar
3033 else:
3034 lt = context.window_manager.looptools
3035 if lt.gstretch_conversion_max < lt.gstretch_conversion_min:
3036 lt.gstretch_conversion_min = lt.gstretch_conversion_max
3039 # ########################################
3040 # ##### Relax functions ##################
3041 # ########################################
3043 # create lists with knots and points, all correctly sorted
3044 def relax_calculate_knots(loops):
3045 all_knots = []
3046 all_points = []
3047 for loop, circular in loops:
3048 knots = [[], []]
3049 points = [[], []]
3050 if circular:
3051 if len(loop) % 2 == 1: # odd
3052 extend = [False, True, 0, 1, 0, 1]
3053 else: # even
3054 extend = [True, False, 0, 1, 1, 2]
3055 else:
3056 if len(loop) % 2 == 1: # odd
3057 extend = [False, False, 0, 1, 1, 2]
3058 else: # even
3059 extend = [False, False, 0, 1, 1, 2]
3060 for j in range(2):
3061 if extend[j]:
3062 loop = [loop[-1]] + loop + [loop[0]]
3063 for i in range(extend[2 + 2 * j], len(loop), 2):
3064 knots[j].append(loop[i])
3065 for i in range(extend[3 + 2 * j], len(loop), 2):
3066 if loop[i] == loop[-1] and not circular:
3067 continue
3068 if len(points[j]) == 0:
3069 points[j].append(loop[i])
3070 elif loop[i] != points[j][0]:
3071 points[j].append(loop[i])
3072 if circular:
3073 if knots[j][0] != knots[j][-1]:
3074 knots[j].append(knots[j][0])
3075 if len(points[1]) == 0:
3076 knots.pop(1)
3077 points.pop(1)
3078 for k in knots:
3079 all_knots.append(k)
3080 for p in points:
3081 all_points.append(p)
3083 return(all_knots, all_points)
3086 # calculate relative positions compared to first knot
3087 def relax_calculate_t(bm_mod, knots, points, regular):
3088 all_tknots = []
3089 all_tpoints = []
3090 for i in range(len(knots)):
3091 amount = len(knots[i]) + len(points[i])
3092 mix = []
3093 for j in range(amount):
3094 if j % 2 == 0:
3095 mix.append([True, knots[i][round(j / 2)]])
3096 elif j == amount - 1:
3097 mix.append([True, knots[i][-1]])
3098 else:
3099 mix.append([False, points[i][int(j / 2)]])
3100 len_total = 0
3101 loc_prev = False
3102 tknots = []
3103 tpoints = []
3104 for m in mix:
3105 loc = mathutils.Vector(bm_mod.verts[m[1]].co[:])
3106 if not loc_prev:
3107 loc_prev = loc
3108 len_total += (loc - loc_prev).length
3109 if m[0]:
3110 tknots.append(len_total)
3111 else:
3112 tpoints.append(len_total)
3113 loc_prev = loc
3114 if regular:
3115 tpoints = []
3116 for p in range(len(points[i])):
3117 tpoints.append((tknots[p] + tknots[p + 1]) / 2)
3118 all_tknots.append(tknots)
3119 all_tpoints.append(tpoints)
3121 return(all_tknots, all_tpoints)
3124 # change the location of the points to their place on the spline
3125 def relax_calculate_verts(bm_mod, interpolation, tknots, knots, tpoints,
3126 points, splines):
3127 change = []
3128 move = []
3129 for i in range(len(knots)):
3130 for p in points[i]:
3131 m = tpoints[i][points[i].index(p)]
3132 if m in tknots[i]:
3133 n = tknots[i].index(m)
3134 else:
3135 t = tknots[i][:]
3136 t.append(m)
3137 t.sort()
3138 n = t.index(m) - 1
3139 if n > len(splines[i]) - 1:
3140 n = len(splines[i]) - 1
3141 elif n < 0:
3142 n = 0
3144 if interpolation == 'cubic':
3145 ax, bx, cx, dx, tx = splines[i][n][0]
3146 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
3147 ay, by, cy, dy, ty = splines[i][n][1]
3148 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
3149 az, bz, cz, dz, tz = splines[i][n][2]
3150 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
3151 change.append([p, mathutils.Vector([x, y, z])])
3152 else: # interpolation == 'linear'
3153 a, d, t, u = splines[i][n]
3154 if u == 0:
3155 u = 1e-8
3156 change.append([p, ((m - t) / u) * d + a])
3157 for c in change:
3158 move.append([c[0], (bm_mod.verts[c[0]].co + c[1]) / 2])
3160 return(move)
3163 # ########################################
3164 # ##### Space functions ##################
3165 # ########################################
3167 # calculate relative positions compared to first knot
3168 def space_calculate_t(bm_mod, knots):
3169 tknots = []
3170 loc_prev = False
3171 len_total = 0
3172 for k in knots:
3173 loc = mathutils.Vector(bm_mod.verts[k].co[:])
3174 if not loc_prev:
3175 loc_prev = loc
3176 len_total += (loc - loc_prev).length
3177 tknots.append(len_total)
3178 loc_prev = loc
3179 amount = len(knots)
3180 t_per_segment = len_total / (amount - 1)
3181 tpoints = [i * t_per_segment for i in range(amount)]
3183 return(tknots, tpoints)
3186 # change the location of the points to their place on the spline
3187 def space_calculate_verts(bm_mod, interpolation, tknots, tpoints, points,
3188 splines):
3189 move = []
3190 for p in points:
3191 m = tpoints[points.index(p)]
3192 if m in tknots:
3193 n = tknots.index(m)
3194 else:
3195 t = tknots[:]
3196 t.append(m)
3197 t.sort()
3198 n = t.index(m) - 1
3199 if n > len(splines) - 1:
3200 n = len(splines) - 1
3201 elif n < 0:
3202 n = 0
3204 if interpolation == 'cubic':
3205 ax, bx, cx, dx, tx = splines[n][0]
3206 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
3207 ay, by, cy, dy, ty = splines[n][1]
3208 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
3209 az, bz, cz, dz, tz = splines[n][2]
3210 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
3211 move.append([p, mathutils.Vector([x, y, z])])
3212 else: # interpolation == 'linear'
3213 a, d, t, u = splines[n]
3214 move.append([p, ((m - t) / u) * d + a])
3216 return(move)
3219 # ########################################
3220 # ##### Operators ########################
3221 # ########################################
3223 # bridge operator
3224 class Bridge(Operator):
3225 bl_idname = 'mesh.looptools_bridge'
3226 bl_label = "Bridge / Loft"
3227 bl_description = "Bridge two, or loft several, loops of vertices"
3228 bl_options = {'REGISTER', 'UNDO'}
3230 cubic_strength: FloatProperty(
3231 name="Strength",
3232 description="Higher strength results in more fluid curves",
3233 default=1.0,
3234 soft_min=-3.0,
3235 soft_max=3.0
3237 interpolation: EnumProperty(
3238 name="Interpolation mode",
3239 items=(('cubic', "Cubic", "Gives curved results"),
3240 ('linear', "Linear", "Basic, fast, straight interpolation")),
3241 description="Interpolation mode: algorithm used when creating "
3242 "segments",
3243 default='cubic'
3245 loft: BoolProperty(
3246 name="Loft",
3247 description="Loft multiple loops, instead of considering them as "
3248 "a multi-input for bridging",
3249 default=False
3251 loft_loop: BoolProperty(
3252 name="Loop",
3253 description="Connect the first and the last loop with each other",
3254 default=False
3256 min_width: IntProperty(
3257 name="Minimum width",
3258 description="Segments with an edge smaller than this are merged "
3259 "(compared to base edge)",
3260 default=0,
3261 min=0,
3262 max=100,
3263 subtype='PERCENTAGE'
3265 mode: EnumProperty(
3266 name="Mode",
3267 items=(('basic', "Basic", "Fast algorithm"),
3268 ('shortest', "Shortest edge", "Slower algorithm with better vertex matching")),
3269 description="Algorithm used for bridging",
3270 default='shortest'
3272 remove_faces: BoolProperty(
3273 name="Remove faces",
3274 description="Remove faces that are internal after bridging",
3275 default=True
3277 reverse: BoolProperty(
3278 name="Reverse",
3279 description="Manually override the direction in which the loops "
3280 "are bridged. Only use if the tool gives the wrong result",
3281 default=False
3283 segments: IntProperty(
3284 name="Segments",
3285 description="Number of segments used to bridge the gap (0=automatic)",
3286 default=1,
3287 min=0,
3288 soft_max=20
3290 twist: IntProperty(
3291 name="Twist",
3292 description="Twist what vertices are connected to each other",
3293 default=0
3296 @classmethod
3297 def poll(cls, context):
3298 ob = context.active_object
3299 return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3301 def draw(self, context):
3302 layout = self.layout
3303 # layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3305 # top row
3306 col_top = layout.column(align=True)
3307 row = col_top.row(align=True)
3308 col_left = row.column(align=True)
3309 col_right = row.column(align=True)
3310 col_right.active = self.segments != 1
3311 col_left.prop(self, "segments")
3312 col_right.prop(self, "min_width", text="")
3313 # bottom row
3314 bottom_left = col_left.row()
3315 bottom_left.active = self.segments != 1
3316 bottom_left.prop(self, "interpolation", text="")
3317 bottom_right = col_right.row()
3318 bottom_right.active = self.interpolation == 'cubic'
3319 bottom_right.prop(self, "cubic_strength")
3320 # boolean properties
3321 col_top.prop(self, "remove_faces")
3322 if self.loft:
3323 col_top.prop(self, "loft_loop")
3325 # override properties
3326 col_top.separator()
3327 row = layout.row(align=True)
3328 row.prop(self, "twist")
3329 row.prop(self, "reverse")
3331 def invoke(self, context, event):
3332 # load custom settings
3333 context.window_manager.looptools.bridge_loft = self.loft
3334 settings_load(self)
3335 return self.execute(context)
3337 def execute(self, context):
3338 # initialise
3339 object, bm = initialise()
3340 edge_faces, edgekey_to_edge, old_selected_faces, smooth = \
3341 bridge_initialise(bm, self.interpolation)
3342 settings_write(self)
3344 # check cache to see if we can save time
3345 input_method = bridge_input_method(self.loft, self.loft_loop)
3346 cached, single_loops, loops, derived, mapping = cache_read("Bridge",
3347 object, bm, input_method, False)
3348 if not cached:
3349 # get loops
3350 loops = bridge_get_input(bm)
3351 if loops:
3352 # reorder loops if there are more than 2
3353 if len(loops) > 2:
3354 if self.loft:
3355 loops = bridge_sort_loops(bm, loops, self.loft_loop)
3356 else:
3357 loops = bridge_match_loops(bm, loops)
3359 # saving cache for faster execution next time
3360 if not cached:
3361 cache_write("Bridge", object, bm, input_method, False, False,
3362 loops, False, False)
3364 if loops:
3365 # calculate new geometry
3366 vertices = []
3367 faces = []
3368 max_vert_index = len(bm.verts) - 1
3369 for i in range(1, len(loops)):
3370 if not self.loft and i % 2 == 0:
3371 continue
3372 lines = bridge_calculate_lines(bm, loops[i - 1:i + 1],
3373 self.mode, self.twist, self.reverse)
3374 vertex_normals = bridge_calculate_virtual_vertex_normals(bm,
3375 lines, loops[i - 1:i + 1], edge_faces, edgekey_to_edge)
3376 segments = bridge_calculate_segments(bm, lines,
3377 loops[i - 1:i + 1], self.segments)
3378 new_verts, new_faces, max_vert_index = \
3379 bridge_calculate_geometry(
3380 bm, lines, vertex_normals,
3381 segments, self.interpolation, self.cubic_strength,
3382 self.min_width, max_vert_index
3384 if new_verts:
3385 vertices += new_verts
3386 if new_faces:
3387 faces += new_faces
3388 # make sure faces in loops that aren't used, aren't removed
3389 if self.remove_faces and old_selected_faces:
3390 bridge_save_unused_faces(bm, old_selected_faces, loops)
3391 # create vertices
3392 if vertices:
3393 bridge_create_vertices(bm, vertices)
3394 # create faces
3395 if faces:
3396 new_faces = bridge_create_faces(object, bm, faces, self.twist)
3397 old_selected_faces = [
3398 i for i, face in enumerate(bm.faces) if face.index in old_selected_faces
3399 ] # updating list
3400 bridge_select_new_faces(new_faces, smooth)
3401 # edge-data could have changed, can't use cache next run
3402 if faces and not vertices:
3403 cache_delete("Bridge")
3404 # delete internal faces
3405 if self.remove_faces and old_selected_faces:
3406 bridge_remove_internal_faces(bm, old_selected_faces)
3407 # make sure normals are facing outside
3408 bmesh.update_edit_mesh(object.data, loop_triangles=False,
3409 destructive=True)
3410 bpy.ops.mesh.normals_make_consistent()
3412 # cleaning up
3413 terminate()
3415 return{'FINISHED'}
3418 # circle operator
3419 class Circle(Operator):
3420 bl_idname = "mesh.looptools_circle"
3421 bl_label = "Circle"
3422 bl_description = "Move selected vertices into a circle shape"
3423 bl_options = {'REGISTER', 'UNDO'}
3425 custom_radius: BoolProperty(
3426 name="Radius",
3427 description="Force a custom radius",
3428 default=False
3430 fit: EnumProperty(
3431 name="Method",
3432 items=(("best", "Best fit", "Non-linear least squares"),
3433 ("inside", "Fit inside", "Only move vertices towards the center")),
3434 description="Method used for fitting a circle to the vertices",
3435 default='best'
3437 flatten: BoolProperty(
3438 name="Flatten",
3439 description="Flatten the circle, instead of projecting it on the mesh",
3440 default=True
3442 influence: FloatProperty(
3443 name="Influence",
3444 description="Force of the tool",
3445 default=100.0,
3446 min=0.0,
3447 max=100.0,
3448 precision=1,
3449 subtype='PERCENTAGE'
3451 lock_x: BoolProperty(
3452 name="Lock X",
3453 description="Lock editing of the x-coordinate",
3454 default=False
3456 lock_y: BoolProperty(
3457 name="Lock Y",
3458 description="Lock editing of the y-coordinate",
3459 default=False
3461 lock_z: BoolProperty(name="Lock Z",
3462 description="Lock editing of the z-coordinate",
3463 default=False
3465 radius: FloatProperty(
3466 name="Radius",
3467 description="Custom radius for circle",
3468 default=1.0,
3469 min=0.0,
3470 soft_max=1000.0
3472 regular: BoolProperty(
3473 name="Regular",
3474 description="Distribute vertices at constant distances along the circle",
3475 default=True
3478 @classmethod
3479 def poll(cls, context):
3480 ob = context.active_object
3481 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3483 def draw(self, context):
3484 layout = self.layout
3485 col = layout.column()
3487 col.prop(self, "fit")
3488 col.separator()
3490 col.prop(self, "flatten")
3491 row = col.row(align=True)
3492 row.prop(self, "custom_radius")
3493 row_right = row.row(align=True)
3494 row_right.active = self.custom_radius
3495 row_right.prop(self, "radius", text="")
3496 col.prop(self, "regular")
3497 col.separator()
3499 col_move = col.column(align=True)
3500 row = col_move.row(align=True)
3501 if self.lock_x:
3502 row.prop(self, "lock_x", text="X", icon='LOCKED')
3503 else:
3504 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3505 if self.lock_y:
3506 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3507 else:
3508 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3509 if self.lock_z:
3510 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3511 else:
3512 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3513 col_move.prop(self, "influence")
3515 def invoke(self, context, event):
3516 # load custom settings
3517 settings_load(self)
3518 return self.execute(context)
3520 def execute(self, context):
3521 # initialise
3522 object, bm = initialise()
3523 settings_write(self)
3524 # check cache to see if we can save time
3525 cached, single_loops, loops, derived, mapping = cache_read("Circle",
3526 object, bm, False, False)
3527 if cached:
3528 derived, bm_mod = get_derived_bmesh(object, bm, False)
3529 else:
3530 # find loops
3531 derived, bm_mod, single_vertices, single_loops, loops = \
3532 circle_get_input(object, bm)
3533 mapping = get_mapping(derived, bm, bm_mod, single_vertices,
3534 False, loops)
3535 single_loops, loops = circle_check_loops(single_loops, loops,
3536 mapping, bm_mod)
3538 # saving cache for faster execution next time
3539 if not cached:
3540 cache_write("Circle", object, bm, False, False, single_loops,
3541 loops, derived, mapping)
3543 move = []
3544 for i, loop in enumerate(loops):
3545 # best fitting flat plane
3546 com, normal = calculate_plane(bm_mod, loop)
3547 # if circular, shift loop so we get a good starting vertex
3548 if loop[1]:
3549 loop = circle_shift_loop(bm_mod, loop, com)
3550 # flatten vertices on plane
3551 locs_2d, p, q = circle_3d_to_2d(bm_mod, loop, com, normal)
3552 # calculate circle
3553 if self.fit == 'best':
3554 x0, y0, r = circle_calculate_best_fit(locs_2d)
3555 else: # self.fit == 'inside'
3556 x0, y0, r = circle_calculate_min_fit(locs_2d)
3557 # radius override
3558 if self.custom_radius:
3559 r = self.radius / p.length
3560 # calculate positions on circle
3561 if self.regular:
3562 new_locs_2d = circle_project_regular(locs_2d[:], x0, y0, r)
3563 else:
3564 new_locs_2d = circle_project_non_regular(locs_2d[:], x0, y0, r)
3565 # take influence into account
3566 locs_2d = circle_influence_locs(locs_2d, new_locs_2d,
3567 self.influence)
3568 # calculate 3d positions of the created 2d input
3569 move.append(circle_calculate_verts(self.flatten, bm_mod,
3570 locs_2d, com, p, q, normal))
3571 # flatten single input vertices on plane defined by loop
3572 if self.flatten and single_loops:
3573 move.append(circle_flatten_singles(bm_mod, com, p, q,
3574 normal, single_loops[i]))
3576 # move vertices to new locations
3577 if self.lock_x or self.lock_y or self.lock_z:
3578 lock = [self.lock_x, self.lock_y, self.lock_z]
3579 else:
3580 lock = False
3581 move_verts(object, bm, mapping, move, lock, -1)
3583 # cleaning up
3584 if derived:
3585 bm_mod.free()
3586 terminate()
3588 return{'FINISHED'}
3591 # curve operator
3592 class Curve(Operator):
3593 bl_idname = "mesh.looptools_curve"
3594 bl_label = "Curve"
3595 bl_description = "Turn a loop into a smooth curve"
3596 bl_options = {'REGISTER', 'UNDO'}
3598 boundaries: BoolProperty(
3599 name="Boundaries",
3600 description="Limit the tool to work within the boundaries of the selected vertices",
3601 default=False
3603 influence: FloatProperty(
3604 name="Influence",
3605 description="Force of the tool",
3606 default=100.0,
3607 min=0.0,
3608 max=100.0,
3609 precision=1,
3610 subtype='PERCENTAGE'
3612 interpolation: EnumProperty(
3613 name="Interpolation",
3614 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
3615 ("linear", "Linear", "Simple and fast linear algorithm")),
3616 description="Algorithm used for interpolation",
3617 default='cubic'
3619 lock_x: BoolProperty(
3620 name="Lock X",
3621 description="Lock editing of the x-coordinate",
3622 default=False
3624 lock_y: BoolProperty(
3625 name="Lock Y",
3626 description="Lock editing of the y-coordinate",
3627 default=False
3629 lock_z: BoolProperty(
3630 name="Lock Z",
3631 description="Lock editing of the z-coordinate",
3632 default=False
3634 regular: BoolProperty(
3635 name="Regular",
3636 description="Distribute vertices at constant distances along the curve",
3637 default=True
3639 restriction: EnumProperty(
3640 name="Restriction",
3641 items=(("none", "None", "No restrictions on vertex movement"),
3642 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
3643 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
3644 description="Restrictions on how the vertices can be moved",
3645 default='none'
3648 @classmethod
3649 def poll(cls, context):
3650 ob = context.active_object
3651 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3653 def draw(self, context):
3654 layout = self.layout
3655 col = layout.column()
3657 col.prop(self, "interpolation")
3658 col.prop(self, "restriction")
3659 col.prop(self, "boundaries")
3660 col.prop(self, "regular")
3661 col.separator()
3663 col_move = col.column(align=True)
3664 row = col_move.row(align=True)
3665 if self.lock_x:
3666 row.prop(self, "lock_x", text="X", icon='LOCKED')
3667 else:
3668 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3669 if self.lock_y:
3670 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3671 else:
3672 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3673 if self.lock_z:
3674 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3675 else:
3676 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3677 col_move.prop(self, "influence")
3679 def invoke(self, context, event):
3680 # load custom settings
3681 settings_load(self)
3682 return self.execute(context)
3684 def execute(self, context):
3685 # initialise
3686 object, bm = initialise()
3687 settings_write(self)
3688 # check cache to see if we can save time
3689 cached, single_loops, loops, derived, mapping = cache_read("Curve",
3690 object, bm, False, self.boundaries)
3691 if cached:
3692 derived, bm_mod = get_derived_bmesh(object, bm, False)
3693 else:
3694 # find loops
3695 derived, bm_mod, loops = curve_get_input(object, bm, self.boundaries)
3696 mapping = get_mapping(derived, bm, bm_mod, False, True, loops)
3697 loops = check_loops(loops, mapping, bm_mod)
3698 verts_selected = [
3699 v.index for v in bm_mod.verts if v.select and not v.hide
3702 # saving cache for faster execution next time
3703 if not cached:
3704 cache_write("Curve", object, bm, False, self.boundaries, False,
3705 loops, derived, mapping)
3707 move = []
3708 for loop in loops:
3709 knots, points = curve_calculate_knots(loop, verts_selected)
3710 pknots = curve_project_knots(bm_mod, verts_selected, knots,
3711 points, loop[1])
3712 tknots, tpoints = curve_calculate_t(bm_mod, knots, points,
3713 pknots, self.regular, loop[1])
3714 splines = calculate_splines(self.interpolation, bm_mod,
3715 tknots, knots)
3716 move.append(curve_calculate_vertices(bm_mod, knots, tknots,
3717 points, tpoints, splines, self.interpolation,
3718 self.restriction))
3720 # move vertices to new locations
3721 if self.lock_x or self.lock_y or self.lock_z:
3722 lock = [self.lock_x, self.lock_y, self.lock_z]
3723 else:
3724 lock = False
3725 move_verts(object, bm, mapping, move, lock, self.influence)
3727 # cleaning up
3728 if derived:
3729 bm_mod.free()
3730 terminate()
3732 return{'FINISHED'}
3735 # flatten operator
3736 class Flatten(Operator):
3737 bl_idname = "mesh.looptools_flatten"
3738 bl_label = "Flatten"
3739 bl_description = "Flatten vertices on a best-fitting plane"
3740 bl_options = {'REGISTER', 'UNDO'}
3742 influence: FloatProperty(
3743 name="Influence",
3744 description="Force of the tool",
3745 default=100.0,
3746 min=0.0,
3747 max=100.0,
3748 precision=1,
3749 subtype='PERCENTAGE'
3751 lock_x: BoolProperty(
3752 name="Lock X",
3753 description="Lock editing of the x-coordinate",
3754 default=False
3756 lock_y: BoolProperty(
3757 name="Lock Y",
3758 description="Lock editing of the y-coordinate",
3759 default=False
3761 lock_z: BoolProperty(name="Lock Z",
3762 description="Lock editing of the z-coordinate",
3763 default=False
3765 plane: EnumProperty(
3766 name="Plane",
3767 items=(("best_fit", "Best fit", "Calculate a best fitting plane"),
3768 ("normal", "Normal", "Derive plane from averaging vertex normals"),
3769 ("view", "View", "Flatten on a plane perpendicular to the viewing angle")),
3770 description="Plane on which vertices are flattened",
3771 default='best_fit'
3773 restriction: EnumProperty(
3774 name="Restriction",
3775 items=(("none", "None", "No restrictions on vertex movement"),
3776 ("bounding_box", "Bounding box", "Vertices are restricted to "
3777 "movement inside the bounding box of the selection")),
3778 description="Restrictions on how the vertices can be moved",
3779 default='none'
3782 @classmethod
3783 def poll(cls, context):
3784 ob = context.active_object
3785 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3787 def draw(self, context):
3788 layout = self.layout
3789 col = layout.column()
3791 col.prop(self, "plane")
3792 # col.prop(self, "restriction")
3793 col.separator()
3795 col_move = col.column(align=True)
3796 row = col_move.row(align=True)
3797 if self.lock_x:
3798 row.prop(self, "lock_x", text="X", icon='LOCKED')
3799 else:
3800 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3801 if self.lock_y:
3802 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3803 else:
3804 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3805 if self.lock_z:
3806 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3807 else:
3808 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3809 col_move.prop(self, "influence")
3811 def invoke(self, context, event):
3812 # load custom settings
3813 settings_load(self)
3814 return self.execute(context)
3816 def execute(self, context):
3817 # initialise
3818 object, bm = initialise()
3819 settings_write(self)
3820 # check cache to see if we can save time
3821 cached, single_loops, loops, derived, mapping = cache_read("Flatten",
3822 object, bm, False, False)
3823 if not cached:
3824 # order input into virtual loops
3825 loops = flatten_get_input(bm)
3826 loops = check_loops(loops, mapping, bm)
3828 # saving cache for faster execution next time
3829 if not cached:
3830 cache_write("Flatten", object, bm, False, False, False, loops,
3831 False, False)
3833 move = []
3834 for loop in loops:
3835 # calculate plane and position of vertices on them
3836 com, normal = calculate_plane(bm, loop, method=self.plane,
3837 object=object)
3838 to_move = flatten_project(bm, loop, com, normal)
3839 if self.restriction == 'none':
3840 move.append(to_move)
3841 else:
3842 move.append(to_move)
3844 # move vertices to new locations
3845 if self.lock_x or self.lock_y or self.lock_z:
3846 lock = [self.lock_x, self.lock_y, self.lock_z]
3847 else:
3848 lock = False
3849 move_verts(object, bm, False, move, lock, self.influence)
3851 # cleaning up
3852 terminate()
3854 return{'FINISHED'}
3857 # Annotation operator
3858 class RemoveAnnotation(Operator):
3859 bl_idname = "remove.annotation"
3860 bl_label = "Remove Annotation"
3861 bl_description = "Remove all Annotation Strokes"
3862 bl_options = {'REGISTER', 'UNDO'}
3864 def execute(self, context):
3866 try:
3867 bpy.data.grease_pencils[0].layers.active.clear()
3868 except:
3869 self.report({'INFO'}, "No Annotation data to Unlink")
3870 return {'CANCELLED'}
3872 return{'FINISHED'}
3874 # GPencil operator
3875 class RemoveGPencil(Operator):
3876 bl_idname = "remove.gp"
3877 bl_label = "Remove GPencil"
3878 bl_description = "Remove all GPencil Strokes"
3879 bl_options = {'REGISTER', 'UNDO'}
3881 def execute(self, context):
3883 try:
3884 looptools = context.window_manager.looptools
3885 looptools.gstretch_guide.data.layers.data.clear()
3886 looptools.gstretch_guide.data.update_tag()
3887 except:
3888 self.report({'INFO'}, "No GPencil data to Unlink")
3889 return {'CANCELLED'}
3891 return{'FINISHED'}
3894 class GStretch(Operator):
3895 bl_idname = "mesh.looptools_gstretch"
3896 bl_label = "Gstretch"
3897 bl_description = "Stretch selected vertices to active stroke"
3898 bl_options = {'REGISTER', 'UNDO'}
3900 conversion: EnumProperty(
3901 name="Conversion",
3902 items=(("distance", "Distance", "Set the distance between vertices "
3903 "of the converted stroke"),
3904 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
3905 "number of vertices that converted strokes will have"),
3906 ("vertices", "Exact vertices", "Set the exact number of vertices "
3907 "that converted strokes will have. Short strokes "
3908 "with few points may contain less vertices than this number."),
3909 ("none", "No simplification", "Convert each point "
3910 "to a vertex")),
3911 description="If strokes are converted to geometry, "
3912 "use this simplification method",
3913 default='limit_vertices'
3915 conversion_distance: FloatProperty(
3916 name="Distance",
3917 description="Absolute distance between vertices along the converted "
3918 " stroke",
3919 default=0.1,
3920 min=0.000001,
3921 soft_min=0.01,
3922 soft_max=100
3924 conversion_max: IntProperty(
3925 name="Max Vertices",
3926 description="Maximum number of vertices strokes will "
3927 "have, when they are converted to geomtery",
3928 default=32,
3929 min=3,
3930 soft_max=500,
3931 update=gstretch_update_min
3933 conversion_min: IntProperty(
3934 name="Min Vertices",
3935 description="Minimum number of vertices strokes will "
3936 "have, when they are converted to geomtery",
3937 default=8,
3938 min=3,
3939 soft_max=500,
3940 update=gstretch_update_max
3942 conversion_vertices: IntProperty(
3943 name="Vertices",
3944 description="Number of vertices strokes will "
3945 "have, when they are converted to geometry. If strokes have less "
3946 "points than required, the 'Spread evenly' method is used",
3947 default=32,
3948 min=3,
3949 soft_max=500
3951 delete_strokes: BoolProperty(
3952 name="Delete strokes",
3953 description="Remove strokes if they have been used."
3954 "WARNING: DOES NOT SUPPORT UNDO",
3955 default=False
3957 influence: FloatProperty(
3958 name="Influence",
3959 description="Force of the tool",
3960 default=100.0,
3961 min=0.0,
3962 max=100.0,
3963 precision=1,
3964 subtype='PERCENTAGE'
3966 lock_x: BoolProperty(
3967 name="Lock X",
3968 description="Lock editing of the x-coordinate",
3969 default=False
3971 lock_y: BoolProperty(
3972 name="Lock Y",
3973 description="Lock editing of the y-coordinate",
3974 default=False
3976 lock_z: BoolProperty(
3977 name="Lock Z",
3978 description="Lock editing of the z-coordinate",
3979 default=False
3981 method: EnumProperty(
3982 name="Method",
3983 items=(("project", "Project", "Project vertices onto the stroke, "
3984 "using vertex normals and connected edges"),
3985 ("irregular", "Spread", "Distribute vertices along the full "
3986 "stroke, retaining relative distances between the vertices"),
3987 ("regular", "Spread evenly", "Distribute vertices at regular "
3988 "distances along the full stroke")),
3989 description="Method of distributing the vertices over the "
3990 "stroke",
3991 default='regular'
3994 @classmethod
3995 def poll(cls, context):
3996 ob = context.active_object
3997 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3999 def draw(self, context):
4000 looptools = context.window_manager.looptools
4001 layout = self.layout
4002 col = layout.column()
4004 col.prop(self, "method")
4005 col.separator()
4007 col_conv = col.column(align=True)
4008 col_conv.prop(self, "conversion", text="")
4009 if self.conversion == 'distance':
4010 col_conv.prop(self, "conversion_distance")
4011 elif self.conversion == 'limit_vertices':
4012 row = col_conv.row(align=True)
4013 row.prop(self, "conversion_min", text="Min")
4014 row.prop(self, "conversion_max", text="Max")
4015 elif self.conversion == 'vertices':
4016 col_conv.prop(self, "conversion_vertices")
4017 col.separator()
4019 col_move = col.column(align=True)
4020 row = col_move.row(align=True)
4021 if self.lock_x:
4022 row.prop(self, "lock_x", text="X", icon='LOCKED')
4023 else:
4024 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
4025 if self.lock_y:
4026 row.prop(self, "lock_y", text="Y", icon='LOCKED')
4027 else:
4028 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
4029 if self.lock_z:
4030 row.prop(self, "lock_z", text="Z", icon='LOCKED')
4031 else:
4032 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
4033 col_move.prop(self, "influence")
4034 col.separator()
4035 if looptools.gstretch_use_guide == "Annotation":
4036 col.operator("remove.annotation", text="Delete annotation strokes")
4037 if looptools.gstretch_use_guide == "GPencil":
4038 col.operator("remove.gp", text="Delete GPencil strokes")
4040 def invoke(self, context, event):
4041 # flush cached strokes
4042 if 'Gstretch' in looptools_cache:
4043 looptools_cache['Gstretch']['single_loops'] = []
4044 # load custom settings
4045 settings_load(self)
4046 return self.execute(context)
4048 def execute(self, context):
4049 # initialise
4050 object, bm = initialise()
4051 settings_write(self)
4053 # check cache to see if we can save time
4054 cached, safe_strokes, loops, derived, mapping = cache_read("Gstretch",
4055 object, bm, False, False)
4056 if cached:
4057 straightening = False
4058 if safe_strokes:
4059 strokes = gstretch_safe_to_true_strokes(safe_strokes)
4060 # cached strokes were flushed (see operator's invoke function)
4061 elif get_strokes(self, context):
4062 strokes = gstretch_get_strokes(self, context)
4063 else:
4064 # straightening function (no GP) -> loops ignore modifiers
4065 straightening = True
4066 derived = False
4067 bm_mod = bm.copy()
4068 bm_mod.verts.ensure_lookup_table()
4069 bm_mod.edges.ensure_lookup_table()
4070 bm_mod.faces.ensure_lookup_table()
4071 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
4072 if not straightening:
4073 derived, bm_mod = get_derived_bmesh(object, bm, False)
4074 else:
4075 # get loops and strokes
4076 if get_strokes(self, context):
4077 # find loops
4078 derived, bm_mod, loops = get_connected_input(object, bm, False, input='selected')
4079 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4080 loops = check_loops(loops, mapping, bm_mod)
4081 # get strokes
4082 strokes = gstretch_get_strokes(self, context)
4083 else:
4084 # straightening function (no GP) -> loops ignore modifiers
4085 derived = False
4086 mapping = False
4087 bm_mod = bm.copy()
4088 bm_mod.verts.ensure_lookup_table()
4089 bm_mod.edges.ensure_lookup_table()
4090 bm_mod.faces.ensure_lookup_table()
4091 edge_keys = [
4092 edgekey(edge) for edge in bm_mod.edges if
4093 edge.select and not edge.hide
4095 loops = get_connected_selections(edge_keys)
4096 loops = check_loops(loops, mapping, bm_mod)
4097 # create fake strokes
4098 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
4100 # saving cache for faster execution next time
4101 if not cached:
4102 if strokes:
4103 safe_strokes = gstretch_true_to_safe_strokes(strokes)
4104 else:
4105 safe_strokes = []
4106 cache_write("Gstretch", object, bm, False, False,
4107 safe_strokes, loops, derived, mapping)
4109 # pair loops and strokes
4110 ls_pairs = gstretch_match_loops_strokes(loops, strokes, object, bm_mod)
4111 ls_pairs = gstretch_align_pairs(ls_pairs, object, bm_mod, self.method)
4113 move = []
4114 if not loops:
4115 # no selected geometry, convert GP to verts
4116 if strokes:
4117 move.append(gstretch_create_verts(object, bm, strokes,
4118 self.method, self.conversion, self.conversion_distance,
4119 self.conversion_max, self.conversion_min,
4120 self.conversion_vertices))
4121 for stroke in strokes:
4122 gstretch_erase_stroke(stroke, context)
4123 elif ls_pairs:
4124 for (loop, stroke) in ls_pairs:
4125 move.append(gstretch_calculate_verts(loop, stroke, object,
4126 bm_mod, self.method))
4127 if self.delete_strokes:
4128 if type(stroke) != bpy.types.GPencilStroke:
4129 # in case of cached fake stroke, get the real one
4130 if get_strokes(self, context):
4131 strokes = gstretch_get_strokes(self, context)
4132 if loops and strokes:
4133 ls_pairs = gstretch_match_loops_strokes(loops,
4134 strokes, object, bm_mod)
4135 ls_pairs = gstretch_align_pairs(ls_pairs,
4136 object, bm_mod, self.method)
4137 for (l, s) in ls_pairs:
4138 if l == loop:
4139 stroke = s
4140 break
4141 gstretch_erase_stroke(stroke, context)
4143 # move vertices to new locations
4144 if self.lock_x or self.lock_y or self.lock_z:
4145 lock = [self.lock_x, self.lock_y, self.lock_z]
4146 else:
4147 lock = False
4148 bmesh.update_edit_mesh(object.data, loop_triangles=True, destructive=True)
4149 move_verts(object, bm, mapping, move, lock, self.influence)
4151 # cleaning up
4152 if derived:
4153 bm_mod.free()
4154 terminate()
4156 return{'FINISHED'}
4159 # relax operator
4160 class Relax(Operator):
4161 bl_idname = "mesh.looptools_relax"
4162 bl_label = "Relax"
4163 bl_description = "Relax the loop, so it is smoother"
4164 bl_options = {'REGISTER', 'UNDO'}
4166 input: EnumProperty(
4167 name="Input",
4168 items=(("all", "Parallel (all)", "Also use non-selected "
4169 "parallel loops as input"),
4170 ("selected", "Selection", "Only use selected vertices as input")),
4171 description="Loops that are relaxed",
4172 default='selected'
4174 interpolation: EnumProperty(
4175 name="Interpolation",
4176 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4177 ("linear", "Linear", "Simple and fast linear algorithm")),
4178 description="Algorithm used for interpolation",
4179 default='cubic'
4181 iterations: EnumProperty(
4182 name="Iterations",
4183 items=(("1", "1", "One"),
4184 ("3", "3", "Three"),
4185 ("5", "5", "Five"),
4186 ("10", "10", "Ten"),
4187 ("25", "25", "Twenty-five")),
4188 description="Number of times the loop is relaxed",
4189 default="1"
4191 regular: BoolProperty(
4192 name="Regular",
4193 description="Distribute vertices at constant distances along the loop",
4194 default=True
4197 @classmethod
4198 def poll(cls, context):
4199 ob = context.active_object
4200 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4202 def draw(self, context):
4203 layout = self.layout
4204 col = layout.column()
4206 col.prop(self, "interpolation")
4207 col.prop(self, "input")
4208 col.prop(self, "iterations")
4209 col.prop(self, "regular")
4211 def invoke(self, context, event):
4212 # load custom settings
4213 settings_load(self)
4214 return self.execute(context)
4216 def execute(self, context):
4217 # initialise
4218 object, bm = initialise()
4219 settings_write(self)
4220 # check cache to see if we can save time
4221 cached, single_loops, loops, derived, mapping = cache_read("Relax",
4222 object, bm, self.input, False)
4223 if cached:
4224 derived, bm_mod = get_derived_bmesh(object, bm, False)
4225 else:
4226 # find loops
4227 derived, bm_mod, loops = get_connected_input(object, bm, False, self.input)
4228 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4229 loops = check_loops(loops, mapping, bm_mod)
4230 knots, points = relax_calculate_knots(loops)
4232 # saving cache for faster execution next time
4233 if not cached:
4234 cache_write("Relax", object, bm, self.input, False, False, loops,
4235 derived, mapping)
4237 for iteration in range(int(self.iterations)):
4238 # calculate splines and new positions
4239 tknots, tpoints = relax_calculate_t(bm_mod, knots, points,
4240 self.regular)
4241 splines = []
4242 for i in range(len(knots)):
4243 splines.append(calculate_splines(self.interpolation, bm_mod,
4244 tknots[i], knots[i]))
4245 move = [relax_calculate_verts(bm_mod, self.interpolation,
4246 tknots, knots, tpoints, points, splines)]
4247 move_verts(object, bm, mapping, move, False, -1)
4249 # cleaning up
4250 if derived:
4251 bm_mod.free()
4252 terminate()
4254 return{'FINISHED'}
4257 # space operator
4258 class Space(Operator):
4259 bl_idname = "mesh.looptools_space"
4260 bl_label = "Space"
4261 bl_description = "Space the vertices in a regular distribution on the loop"
4262 bl_options = {'REGISTER', 'UNDO'}
4264 influence: FloatProperty(
4265 name="Influence",
4266 description="Force of the tool",
4267 default=100.0,
4268 min=0.0,
4269 max=100.0,
4270 precision=1,
4271 subtype='PERCENTAGE'
4273 input: EnumProperty(
4274 name="Input",
4275 items=(("all", "Parallel (all)", "Also use non-selected "
4276 "parallel loops as input"),
4277 ("selected", "Selection", "Only use selected vertices as input")),
4278 description="Loops that are spaced",
4279 default='selected'
4281 interpolation: EnumProperty(
4282 name="Interpolation",
4283 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4284 ("linear", "Linear", "Vertices are projected on existing edges")),
4285 description="Algorithm used for interpolation",
4286 default='cubic'
4288 lock_x: BoolProperty(
4289 name="Lock X",
4290 description="Lock editing of the x-coordinate",
4291 default=False
4293 lock_y: BoolProperty(
4294 name="Lock Y",
4295 description="Lock editing of the y-coordinate",
4296 default=False
4298 lock_z: BoolProperty(
4299 name="Lock Z",
4300 description="Lock editing of the z-coordinate",
4301 default=False
4304 @classmethod
4305 def poll(cls, context):
4306 ob = context.active_object
4307 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4309 def draw(self, context):
4310 layout = self.layout
4311 col = layout.column()
4313 col.prop(self, "interpolation")
4314 col.prop(self, "input")
4315 col.separator()
4317 col_move = col.column(align=True)
4318 row = col_move.row(align=True)
4319 if self.lock_x:
4320 row.prop(self, "lock_x", text="X", icon='LOCKED')
4321 else:
4322 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
4323 if self.lock_y:
4324 row.prop(self, "lock_y", text="Y", icon='LOCKED')
4325 else:
4326 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
4327 if self.lock_z:
4328 row.prop(self, "lock_z", text="Z", icon='LOCKED')
4329 else:
4330 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
4331 col_move.prop(self, "influence")
4333 def invoke(self, context, event):
4334 # load custom settings
4335 settings_load(self)
4336 return self.execute(context)
4338 def execute(self, context):
4339 # initialise
4340 object, bm = initialise()
4341 settings_write(self)
4342 # check cache to see if we can save time
4343 cached, single_loops, loops, derived, mapping = cache_read("Space",
4344 object, bm, self.input, False)
4345 if cached:
4346 derived, bm_mod = get_derived_bmesh(object, bm, True)
4347 else:
4348 # find loops
4349 derived, bm_mod, loops = get_connected_input(object, bm, True, self.input)
4350 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4351 loops = check_loops(loops, mapping, bm_mod)
4353 # saving cache for faster execution next time
4354 if not cached:
4355 cache_write("Space", object, bm, self.input, False, False, loops,
4356 derived, mapping)
4358 move = []
4359 for loop in loops:
4360 # calculate splines and new positions
4361 if loop[1]: # circular
4362 loop[0].append(loop[0][0])
4363 tknots, tpoints = space_calculate_t(bm_mod, loop[0][:])
4364 splines = calculate_splines(self.interpolation, bm_mod,
4365 tknots, loop[0][:])
4366 move.append(space_calculate_verts(bm_mod, self.interpolation,
4367 tknots, tpoints, loop[0][:-1], splines))
4368 # move vertices to new locations
4369 if self.lock_x or self.lock_y or self.lock_z:
4370 lock = [self.lock_x, self.lock_y, self.lock_z]
4371 else:
4372 lock = False
4373 move_verts(object, bm, mapping, move, lock, self.influence)
4375 # cleaning up
4376 if derived:
4377 bm_mod.free()
4378 terminate()
4380 cache_delete("Space")
4382 return{'FINISHED'}
4385 # ########################################
4386 # ##### GUI and registration #############
4387 # ########################################
4389 # menu containing all tools
4390 class VIEW3D_MT_edit_mesh_looptools(Menu):
4391 bl_label = "LoopTools"
4393 def draw(self, context):
4394 layout = self.layout
4396 layout.operator("mesh.looptools_bridge", text="Bridge").loft = False
4397 layout.operator("mesh.looptools_circle")
4398 layout.operator("mesh.looptools_curve")
4399 layout.operator("mesh.looptools_flatten")
4400 layout.operator("mesh.looptools_gstretch")
4401 layout.operator("mesh.looptools_bridge", text="Loft").loft = True
4402 layout.operator("mesh.looptools_relax")
4403 layout.operator("mesh.looptools_space")
4406 # panel containing all tools
4407 class VIEW3D_PT_tools_looptools(Panel):
4408 bl_space_type = 'VIEW_3D'
4409 bl_region_type = 'UI'
4410 bl_category = 'Edit'
4411 bl_context = "mesh_edit"
4412 bl_label = "LoopTools"
4413 bl_options = {'DEFAULT_CLOSED'}
4415 def draw(self, context):
4416 layout = self.layout
4417 col = layout.column(align=True)
4418 lt = context.window_manager.looptools
4420 # bridge - first line
4421 split = col.split(factor=0.15, align=True)
4422 if lt.display_bridge:
4423 split.prop(lt, "display_bridge", text="", icon='DOWNARROW_HLT')
4424 else:
4425 split.prop(lt, "display_bridge", text="", icon='RIGHTARROW')
4426 split.operator("mesh.looptools_bridge", text="Bridge").loft = False
4427 # bridge - settings
4428 if lt.display_bridge:
4429 box = col.column(align=True).box().column()
4430 # box.prop(self, "mode")
4432 # top row
4433 col_top = box.column(align=True)
4434 row = col_top.row(align=True)
4435 col_left = row.column(align=True)
4436 col_right = row.column(align=True)
4437 col_right.active = lt.bridge_segments != 1
4438 col_left.prop(lt, "bridge_segments")
4439 col_right.prop(lt, "bridge_min_width", text="")
4440 # bottom row
4441 bottom_left = col_left.row()
4442 bottom_left.active = lt.bridge_segments != 1
4443 bottom_left.prop(lt, "bridge_interpolation", text="")
4444 bottom_right = col_right.row()
4445 bottom_right.active = lt.bridge_interpolation == 'cubic'
4446 bottom_right.prop(lt, "bridge_cubic_strength")
4447 # boolean properties
4448 col_top.prop(lt, "bridge_remove_faces")
4450 # override properties
4451 col_top.separator()
4452 row = box.row(align=True)
4453 row.prop(lt, "bridge_twist")
4454 row.prop(lt, "bridge_reverse")
4456 # circle - first line
4457 split = col.split(factor=0.15, align=True)
4458 if lt.display_circle:
4459 split.prop(lt, "display_circle", text="", icon='DOWNARROW_HLT')
4460 else:
4461 split.prop(lt, "display_circle", text="", icon='RIGHTARROW')
4462 split.operator("mesh.looptools_circle")
4463 # circle - settings
4464 if lt.display_circle:
4465 box = col.column(align=True).box().column()
4466 box.prop(lt, "circle_fit")
4467 box.separator()
4469 box.prop(lt, "circle_flatten")
4470 row = box.row(align=True)
4471 row.prop(lt, "circle_custom_radius")
4472 row_right = row.row(align=True)
4473 row_right.active = lt.circle_custom_radius
4474 row_right.prop(lt, "circle_radius", text="")
4475 box.prop(lt, "circle_regular")
4476 box.separator()
4478 col_move = box.column(align=True)
4479 row = col_move.row(align=True)
4480 if lt.circle_lock_x:
4481 row.prop(lt, "circle_lock_x", text="X", icon='LOCKED')
4482 else:
4483 row.prop(lt, "circle_lock_x", text="X", icon='UNLOCKED')
4484 if lt.circle_lock_y:
4485 row.prop(lt, "circle_lock_y", text="Y", icon='LOCKED')
4486 else:
4487 row.prop(lt, "circle_lock_y", text="Y", icon='UNLOCKED')
4488 if lt.circle_lock_z:
4489 row.prop(lt, "circle_lock_z", text="Z", icon='LOCKED')
4490 else:
4491 row.prop(lt, "circle_lock_z", text="Z", icon='UNLOCKED')
4492 col_move.prop(lt, "circle_influence")
4494 # curve - first line
4495 split = col.split(factor=0.15, align=True)
4496 if lt.display_curve:
4497 split.prop(lt, "display_curve", text="", icon='DOWNARROW_HLT')
4498 else:
4499 split.prop(lt, "display_curve", text="", icon='RIGHTARROW')
4500 split.operator("mesh.looptools_curve")
4501 # curve - settings
4502 if lt.display_curve:
4503 box = col.column(align=True).box().column()
4504 box.prop(lt, "curve_interpolation")
4505 box.prop(lt, "curve_restriction")
4506 box.prop(lt, "curve_boundaries")
4507 box.prop(lt, "curve_regular")
4508 box.separator()
4510 col_move = box.column(align=True)
4511 row = col_move.row(align=True)
4512 if lt.curve_lock_x:
4513 row.prop(lt, "curve_lock_x", text="X", icon='LOCKED')
4514 else:
4515 row.prop(lt, "curve_lock_x", text="X", icon='UNLOCKED')
4516 if lt.curve_lock_y:
4517 row.prop(lt, "curve_lock_y", text="Y", icon='LOCKED')
4518 else:
4519 row.prop(lt, "curve_lock_y", text="Y", icon='UNLOCKED')
4520 if lt.curve_lock_z:
4521 row.prop(lt, "curve_lock_z", text="Z", icon='LOCKED')
4522 else:
4523 row.prop(lt, "curve_lock_z", text="Z", icon='UNLOCKED')
4524 col_move.prop(lt, "curve_influence")
4526 # flatten - first line
4527 split = col.split(factor=0.15, align=True)
4528 if lt.display_flatten:
4529 split.prop(lt, "display_flatten", text="", icon='DOWNARROW_HLT')
4530 else:
4531 split.prop(lt, "display_flatten", text="", icon='RIGHTARROW')
4532 split.operator("mesh.looptools_flatten")
4533 # flatten - settings
4534 if lt.display_flatten:
4535 box = col.column(align=True).box().column()
4536 box.prop(lt, "flatten_plane")
4537 # box.prop(lt, "flatten_restriction")
4538 box.separator()
4540 col_move = box.column(align=True)
4541 row = col_move.row(align=True)
4542 if lt.flatten_lock_x:
4543 row.prop(lt, "flatten_lock_x", text="X", icon='LOCKED')
4544 else:
4545 row.prop(lt, "flatten_lock_x", text="X", icon='UNLOCKED')
4546 if lt.flatten_lock_y:
4547 row.prop(lt, "flatten_lock_y", text="Y", icon='LOCKED')
4548 else:
4549 row.prop(lt, "flatten_lock_y", text="Y", icon='UNLOCKED')
4550 if lt.flatten_lock_z:
4551 row.prop(lt, "flatten_lock_z", text="Z", icon='LOCKED')
4552 else:
4553 row.prop(lt, "flatten_lock_z", text="Z", icon='UNLOCKED')
4554 col_move.prop(lt, "flatten_influence")
4556 # gstretch - first line
4557 split = col.split(factor=0.15, align=True)
4558 if lt.display_gstretch:
4559 split.prop(lt, "display_gstretch", text="", icon='DOWNARROW_HLT')
4560 else:
4561 split.prop(lt, "display_gstretch", text="", icon='RIGHTARROW')
4562 split.operator("mesh.looptools_gstretch")
4563 # gstretch settings
4564 if lt.display_gstretch:
4565 box = col.column(align=True).box().column()
4566 box.prop(lt, "gstretch_use_guide")
4567 if lt.gstretch_use_guide == "GPencil":
4568 box.prop(lt, "gstretch_guide")
4569 box.prop(lt, "gstretch_method")
4571 col_conv = box.column(align=True)
4572 col_conv.prop(lt, "gstretch_conversion", text="")
4573 if lt.gstretch_conversion == 'distance':
4574 col_conv.prop(lt, "gstretch_conversion_distance")
4575 elif lt.gstretch_conversion == 'limit_vertices':
4576 row = col_conv.row(align=True)
4577 row.prop(lt, "gstretch_conversion_min", text="Min")
4578 row.prop(lt, "gstretch_conversion_max", text="Max")
4579 elif lt.gstretch_conversion == 'vertices':
4580 col_conv.prop(lt, "gstretch_conversion_vertices")
4581 box.separator()
4583 col_move = box.column(align=True)
4584 row = col_move.row(align=True)
4585 if lt.gstretch_lock_x:
4586 row.prop(lt, "gstretch_lock_x", text="X", icon='LOCKED')
4587 else:
4588 row.prop(lt, "gstretch_lock_x", text="X", icon='UNLOCKED')
4589 if lt.gstretch_lock_y:
4590 row.prop(lt, "gstretch_lock_y", text="Y", icon='LOCKED')
4591 else:
4592 row.prop(lt, "gstretch_lock_y", text="Y", icon='UNLOCKED')
4593 if lt.gstretch_lock_z:
4594 row.prop(lt, "gstretch_lock_z", text="Z", icon='LOCKED')
4595 else:
4596 row.prop(lt, "gstretch_lock_z", text="Z", icon='UNLOCKED')
4597 col_move.prop(lt, "gstretch_influence")
4598 if lt.gstretch_use_guide == "Annotation":
4599 box.operator("remove.annotation", text="Delete Annotation Strokes")
4600 if lt.gstretch_use_guide == "GPencil":
4601 box.operator("remove.gp", text="Delete GPencil Strokes")
4603 # loft - first line
4604 split = col.split(factor=0.15, align=True)
4605 if lt.display_loft:
4606 split.prop(lt, "display_loft", text="", icon='DOWNARROW_HLT')
4607 else:
4608 split.prop(lt, "display_loft", text="", icon='RIGHTARROW')
4609 split.operator("mesh.looptools_bridge", text="Loft").loft = True
4610 # loft - settings
4611 if lt.display_loft:
4612 box = col.column(align=True).box().column()
4613 # box.prop(self, "mode")
4615 # top row
4616 col_top = box.column(align=True)
4617 row = col_top.row(align=True)
4618 col_left = row.column(align=True)
4619 col_right = row.column(align=True)
4620 col_right.active = lt.bridge_segments != 1
4621 col_left.prop(lt, "bridge_segments")
4622 col_right.prop(lt, "bridge_min_width", text="")
4623 # bottom row
4624 bottom_left = col_left.row()
4625 bottom_left.active = lt.bridge_segments != 1
4626 bottom_left.prop(lt, "bridge_interpolation", text="")
4627 bottom_right = col_right.row()
4628 bottom_right.active = lt.bridge_interpolation == 'cubic'
4629 bottom_right.prop(lt, "bridge_cubic_strength")
4630 # boolean properties
4631 col_top.prop(lt, "bridge_remove_faces")
4632 col_top.prop(lt, "bridge_loft_loop")
4634 # override properties
4635 col_top.separator()
4636 row = box.row(align=True)
4637 row.prop(lt, "bridge_twist")
4638 row.prop(lt, "bridge_reverse")
4640 # relax - first line
4641 split = col.split(factor=0.15, align=True)
4642 if lt.display_relax:
4643 split.prop(lt, "display_relax", text="", icon='DOWNARROW_HLT')
4644 else:
4645 split.prop(lt, "display_relax", text="", icon='RIGHTARROW')
4646 split.operator("mesh.looptools_relax")
4647 # relax - settings
4648 if lt.display_relax:
4649 box = col.column(align=True).box().column()
4650 box.prop(lt, "relax_interpolation")
4651 box.prop(lt, "relax_input")
4652 box.prop(lt, "relax_iterations")
4653 box.prop(lt, "relax_regular")
4655 # space - first line
4656 split = col.split(factor=0.15, align=True)
4657 if lt.display_space:
4658 split.prop(lt, "display_space", text="", icon='DOWNARROW_HLT')
4659 else:
4660 split.prop(lt, "display_space", text="", icon='RIGHTARROW')
4661 split.operator("mesh.looptools_space")
4662 # space - settings
4663 if lt.display_space:
4664 box = col.column(align=True).box().column()
4665 box.prop(lt, "space_interpolation")
4666 box.prop(lt, "space_input")
4667 box.separator()
4669 col_move = box.column(align=True)
4670 row = col_move.row(align=True)
4671 if lt.space_lock_x:
4672 row.prop(lt, "space_lock_x", text="X", icon='LOCKED')
4673 else:
4674 row.prop(lt, "space_lock_x", text="X", icon='UNLOCKED')
4675 if lt.space_lock_y:
4676 row.prop(lt, "space_lock_y", text="Y", icon='LOCKED')
4677 else:
4678 row.prop(lt, "space_lock_y", text="Y", icon='UNLOCKED')
4679 if lt.space_lock_z:
4680 row.prop(lt, "space_lock_z", text="Z", icon='LOCKED')
4681 else:
4682 row.prop(lt, "space_lock_z", text="Z", icon='UNLOCKED')
4683 col_move.prop(lt, "space_influence")
4686 # property group containing all properties for the gui in the panel
4687 class LoopToolsProps(PropertyGroup):
4689 Fake module like class
4690 bpy.context.window_manager.looptools
4692 # general display properties
4693 display_bridge: BoolProperty(
4694 name="Bridge settings",
4695 description="Display settings of the Bridge tool",
4696 default=False
4698 display_circle: BoolProperty(
4699 name="Circle settings",
4700 description="Display settings of the Circle tool",
4701 default=False
4703 display_curve: BoolProperty(
4704 name="Curve settings",
4705 description="Display settings of the Curve tool",
4706 default=False
4708 display_flatten: BoolProperty(
4709 name="Flatten settings",
4710 description="Display settings of the Flatten tool",
4711 default=False
4713 display_gstretch: BoolProperty(
4714 name="Gstretch settings",
4715 description="Display settings of the Gstretch tool",
4716 default=False
4718 display_loft: BoolProperty(
4719 name="Loft settings",
4720 description="Display settings of the Loft tool",
4721 default=False
4723 display_relax: BoolProperty(
4724 name="Relax settings",
4725 description="Display settings of the Relax tool",
4726 default=False
4728 display_space: BoolProperty(
4729 name="Space settings",
4730 description="Display settings of the Space tool",
4731 default=False
4734 # bridge properties
4735 bridge_cubic_strength: FloatProperty(
4736 name="Strength",
4737 description="Higher strength results in more fluid curves",
4738 default=1.0,
4739 soft_min=-3.0,
4740 soft_max=3.0
4742 bridge_interpolation: EnumProperty(
4743 name="Interpolation mode",
4744 items=(('cubic', "Cubic", "Gives curved results"),
4745 ('linear', "Linear", "Basic, fast, straight interpolation")),
4746 description="Interpolation mode: algorithm used when creating segments",
4747 default='cubic'
4749 bridge_loft: BoolProperty(
4750 name="Loft",
4751 description="Loft multiple loops, instead of considering them as "
4752 "a multi-input for bridging",
4753 default=False
4755 bridge_loft_loop: BoolProperty(
4756 name="Loop",
4757 description="Connect the first and the last loop with each other",
4758 default=False
4760 bridge_min_width: IntProperty(
4761 name="Minimum width",
4762 description="Segments with an edge smaller than this are merged "
4763 "(compared to base edge)",
4764 default=0,
4765 min=0,
4766 max=100,
4767 subtype='PERCENTAGE'
4769 bridge_mode: EnumProperty(
4770 name="Mode",
4771 items=(('basic', "Basic", "Fast algorithm"),
4772 ('shortest', "Shortest edge", "Slower algorithm with "
4773 "better vertex matching")),
4774 description="Algorithm used for bridging",
4775 default='shortest'
4777 bridge_remove_faces: BoolProperty(
4778 name="Remove faces",
4779 description="Remove faces that are internal after bridging",
4780 default=True
4782 bridge_reverse: BoolProperty(
4783 name="Reverse",
4784 description="Manually override the direction in which the loops "
4785 "are bridged. Only use if the tool gives the wrong result",
4786 default=False
4788 bridge_segments: IntProperty(
4789 name="Segments",
4790 description="Number of segments used to bridge the gap (0=automatic)",
4791 default=1,
4792 min=0,
4793 soft_max=20
4795 bridge_twist: IntProperty(
4796 name="Twist",
4797 description="Twist what vertices are connected to each other",
4798 default=0
4801 # circle properties
4802 circle_custom_radius: BoolProperty(
4803 name="Radius",
4804 description="Force a custom radius",
4805 default=False
4807 circle_fit: EnumProperty(
4808 name="Method",
4809 items=(("best", "Best fit", "Non-linear least squares"),
4810 ("inside", "Fit inside", "Only move vertices towards the center")),
4811 description="Method used for fitting a circle to the vertices",
4812 default='best'
4814 circle_flatten: BoolProperty(
4815 name="Flatten",
4816 description="Flatten the circle, instead of projecting it on the mesh",
4817 default=True
4819 circle_influence: FloatProperty(
4820 name="Influence",
4821 description="Force of the tool",
4822 default=100.0,
4823 min=0.0,
4824 max=100.0,
4825 precision=1,
4826 subtype='PERCENTAGE'
4828 circle_lock_x: BoolProperty(
4829 name="Lock X",
4830 description="Lock editing of the x-coordinate",
4831 default=False
4833 circle_lock_y: BoolProperty(
4834 name="Lock Y",
4835 description="Lock editing of the y-coordinate",
4836 default=False
4838 circle_lock_z: BoolProperty(
4839 name="Lock Z",
4840 description="Lock editing of the z-coordinate",
4841 default=False
4843 circle_radius: FloatProperty(
4844 name="Radius",
4845 description="Custom radius for circle",
4846 default=1.0,
4847 min=0.0,
4848 soft_max=1000.0
4850 circle_regular: BoolProperty(
4851 name="Regular",
4852 description="Distribute vertices at constant distances along the circle",
4853 default=True
4855 # curve properties
4856 curve_boundaries: BoolProperty(
4857 name="Boundaries",
4858 description="Limit the tool to work within the boundaries of the "
4859 "selected vertices",
4860 default=False
4862 curve_influence: FloatProperty(
4863 name="Influence",
4864 description="Force of the tool",
4865 default=100.0,
4866 min=0.0,
4867 max=100.0,
4868 precision=1,
4869 subtype='PERCENTAGE'
4871 curve_interpolation: EnumProperty(
4872 name="Interpolation",
4873 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4874 ("linear", "Linear", "Simple and fast linear algorithm")),
4875 description="Algorithm used for interpolation",
4876 default='cubic'
4878 curve_lock_x: BoolProperty(
4879 name="Lock X",
4880 description="Lock editing of the x-coordinate",
4881 default=False
4883 curve_lock_y: BoolProperty(
4884 name="Lock Y",
4885 description="Lock editing of the y-coordinate",
4886 default=False
4888 curve_lock_z: BoolProperty(
4889 name="Lock Z",
4890 description="Lock editing of the z-coordinate",
4891 default=False
4893 curve_regular: BoolProperty(
4894 name="Regular",
4895 description="Distribute vertices at constant distances along the curve",
4896 default=True
4898 curve_restriction: EnumProperty(
4899 name="Restriction",
4900 items=(("none", "None", "No restrictions on vertex movement"),
4901 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
4902 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
4903 description="Restrictions on how the vertices can be moved",
4904 default='none'
4907 # flatten properties
4908 flatten_influence: FloatProperty(
4909 name="Influence",
4910 description="Force of the tool",
4911 default=100.0,
4912 min=0.0,
4913 max=100.0,
4914 precision=1,
4915 subtype='PERCENTAGE'
4917 flatten_lock_x: BoolProperty(
4918 name="Lock X",
4919 description="Lock editing of the x-coordinate",
4920 default=False)
4921 flatten_lock_y: BoolProperty(name="Lock Y",
4922 description="Lock editing of the y-coordinate",
4923 default=False
4925 flatten_lock_z: BoolProperty(
4926 name="Lock Z",
4927 description="Lock editing of the z-coordinate",
4928 default=False
4930 flatten_plane: EnumProperty(
4931 name="Plane",
4932 items=(("best_fit", "Best fit", "Calculate a best fitting plane"),
4933 ("normal", "Normal", "Derive plane from averaging vertex "
4934 "normals"),
4935 ("view", "View", "Flatten on a plane perpendicular to the "
4936 "viewing angle")),
4937 description="Plane on which vertices are flattened",
4938 default='best_fit'
4940 flatten_restriction: EnumProperty(
4941 name="Restriction",
4942 items=(("none", "None", "No restrictions on vertex movement"),
4943 ("bounding_box", "Bounding box", "Vertices are restricted to "
4944 "movement inside the bounding box of the selection")),
4945 description="Restrictions on how the vertices can be moved",
4946 default='none'
4949 # gstretch properties
4950 gstretch_conversion: EnumProperty(
4951 name="Conversion",
4952 items=(("distance", "Distance", "Set the distance between vertices "
4953 "of the converted stroke"),
4954 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
4955 "number of vertices that converted GP strokes will have"),
4956 ("vertices", "Exact vertices", "Set the exact number of vertices "
4957 "that converted strokes will have. Short strokes "
4958 "with few points may contain less vertices than this number."),
4959 ("none", "No simplification", "Convert each point "
4960 "to a vertex")),
4961 description="If strokes are converted to geometry, "
4962 "use this simplification method",
4963 default='limit_vertices'
4965 gstretch_conversion_distance: FloatProperty(
4966 name="Distance",
4967 description="Absolute distance between vertices along the converted "
4968 "stroke",
4969 default=0.1,
4970 min=0.000001,
4971 soft_min=0.01,
4972 soft_max=100
4974 gstretch_conversion_max: IntProperty(
4975 name="Max Vertices",
4976 description="Maximum number of vertices strokes will "
4977 "have, when they are converted to geomtery",
4978 default=32,
4979 min=3,
4980 soft_max=500,
4981 update=gstretch_update_min
4983 gstretch_conversion_min: IntProperty(
4984 name="Min Vertices",
4985 description="Minimum number of vertices strokes will "
4986 "have, when they are converted to geomtery",
4987 default=8,
4988 min=3,
4989 soft_max=500,
4990 update=gstretch_update_max
4992 gstretch_conversion_vertices: IntProperty(
4993 name="Vertices",
4994 description="Number of vertices strokes will "
4995 "have, when they are converted to geometry. If strokes have less "
4996 "points than required, the 'Spread evenly' method is used",
4997 default=32,
4998 min=3,
4999 soft_max=500
5001 gstretch_delete_strokes: BoolProperty(
5002 name="Delete strokes",
5003 description="Remove Grease Pencil strokes if they have been used "
5004 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
5005 default=False
5007 gstretch_influence: FloatProperty(
5008 name="Influence",
5009 description="Force of the tool",
5010 default=100.0,
5011 min=0.0,
5012 max=100.0,
5013 precision=1,
5014 subtype='PERCENTAGE'
5016 gstretch_lock_x: BoolProperty(
5017 name="Lock X",
5018 description="Lock editing of the x-coordinate",
5019 default=False
5021 gstretch_lock_y: BoolProperty(
5022 name="Lock Y",
5023 description="Lock editing of the y-coordinate",
5024 default=False
5026 gstretch_lock_z: BoolProperty(
5027 name="Lock Z",
5028 description="Lock editing of the z-coordinate",
5029 default=False
5031 gstretch_method: EnumProperty(
5032 name="Method",
5033 items=(("project", "Project", "Project vertices onto the stroke, "
5034 "using vertex normals and connected edges"),
5035 ("irregular", "Spread", "Distribute vertices along the full "
5036 "stroke, retaining relative distances between the vertices"),
5037 ("regular", "Spread evenly", "Distribute vertices at regular "
5038 "distances along the full stroke")),
5039 description="Method of distributing the vertices over the Grease "
5040 "Pencil stroke",
5041 default='regular'
5043 gstretch_use_guide: EnumProperty(
5044 name="Use guides",
5045 items=(("None", "None", "None"),
5046 ("Annotation", "Annotation", "Annotation"),
5047 ("GPencil", "GPencil", "GPencil")),
5048 default="None"
5050 gstretch_guide: PointerProperty(
5051 name="GPencil object",
5052 description="Set GPencil object",
5053 type=bpy.types.Object
5056 # relax properties
5057 relax_input: EnumProperty(name="Input",
5058 items=(("all", "Parallel (all)", "Also use non-selected "
5059 "parallel loops as input"),
5060 ("selected", "Selection", "Only use selected vertices as input")),
5061 description="Loops that are relaxed",
5062 default='selected'
5064 relax_interpolation: EnumProperty(
5065 name="Interpolation",
5066 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5067 ("linear", "Linear", "Simple and fast linear algorithm")),
5068 description="Algorithm used for interpolation",
5069 default='cubic'
5071 relax_iterations: EnumProperty(name="Iterations",
5072 items=(("1", "1", "One"),
5073 ("3", "3", "Three"),
5074 ("5", "5", "Five"),
5075 ("10", "10", "Ten"),
5076 ("25", "25", "Twenty-five")),
5077 description="Number of times the loop is relaxed",
5078 default="1"
5080 relax_regular: BoolProperty(
5081 name="Regular",
5082 description="Distribute vertices at constant distances along the loop",
5083 default=True
5086 # space properties
5087 space_influence: FloatProperty(
5088 name="Influence",
5089 description="Force of the tool",
5090 default=100.0,
5091 min=0.0,
5092 max=100.0,
5093 precision=1,
5094 subtype='PERCENTAGE'
5096 space_input: EnumProperty(
5097 name="Input",
5098 items=(("all", "Parallel (all)", "Also use non-selected "
5099 "parallel loops as input"),
5100 ("selected", "Selection", "Only use selected vertices as input")),
5101 description="Loops that are spaced",
5102 default='selected'
5104 space_interpolation: EnumProperty(
5105 name="Interpolation",
5106 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5107 ("linear", "Linear", "Vertices are projected on existing edges")),
5108 description="Algorithm used for interpolation",
5109 default='cubic'
5111 space_lock_x: BoolProperty(
5112 name="Lock X",
5113 description="Lock editing of the x-coordinate",
5114 default=False
5116 space_lock_y: BoolProperty(
5117 name="Lock Y",
5118 description="Lock editing of the y-coordinate",
5119 default=False
5121 space_lock_z: BoolProperty(
5122 name="Lock Z",
5123 description="Lock editing of the z-coordinate",
5124 default=False
5127 # draw function for integration in menus
5128 def menu_func(self, context):
5129 self.layout.menu("VIEW3D_MT_edit_mesh_looptools")
5130 self.layout.separator()
5133 # Add-ons Preferences Update Panel
5135 # Define Panel classes for updating
5136 panels = (
5137 VIEW3D_PT_tools_looptools,
5141 def update_panel(self, context):
5142 message = "LoopTools: Updating Panel locations has failed"
5143 try:
5144 for panel in panels:
5145 if "bl_rna" in panel.__dict__:
5146 bpy.utils.unregister_class(panel)
5148 for panel in panels:
5149 panel.bl_category = context.preferences.addons[__name__].preferences.category
5150 bpy.utils.register_class(panel)
5152 except Exception as e:
5153 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
5154 pass
5157 class LoopPreferences(AddonPreferences):
5158 # this must match the addon name, use '__package__'
5159 # when defining this in a submodule of a python package.
5160 bl_idname = __name__
5162 category: StringProperty(
5163 name="Tab Category",
5164 description="Choose a name for the category of the panel",
5165 default="Edit",
5166 update=update_panel
5169 def draw(self, context):
5170 layout = self.layout
5172 row = layout.row()
5173 col = row.column()
5174 col.label(text="Tab Category:")
5175 col.prop(self, "category", text="")
5178 # define classes for registration
5179 classes = (
5180 VIEW3D_MT_edit_mesh_looptools,
5181 VIEW3D_PT_tools_looptools,
5182 LoopToolsProps,
5183 Bridge,
5184 Circle,
5185 Curve,
5186 Flatten,
5187 GStretch,
5188 Relax,
5189 Space,
5190 LoopPreferences,
5191 RemoveAnnotation,
5192 RemoveGPencil,
5196 # registering and menu integration
5197 def register():
5198 for cls in classes:
5199 bpy.utils.register_class(cls)
5200 bpy.types.VIEW3D_MT_edit_mesh_context_menu.prepend(menu_func)
5201 bpy.types.WindowManager.looptools = PointerProperty(type=LoopToolsProps)
5202 update_panel(None, bpy.context)
5205 # unregistering and removing menus
5206 def unregister():
5207 for cls in reversed(classes):
5208 bpy.utils.unregister_class(cls)
5209 bpy.types.VIEW3D_MT_edit_mesh_context_menu.remove(menu_func)
5210 try:
5211 del bpy.types.WindowManager.looptools
5212 except Exception as e:
5213 print('unregister fail:\n', e)
5214 pass
5217 if __name__ == "__main__":
5218 register()