Merge branch 'blender-v2.92-release'
[blender-addons.git] / mesh_looptools.py
blobd13c4213faeae4fa9cc973190aabc4e5656efc93
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, 6),
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 # get all mirror vectors
809 mirror_Vectors = []
810 if object.data.use_mirror_x:
811 mirror_Vectors.append(mathutils.Vector((-1, 1, 1)))
812 if object.data.use_mirror_y:
813 mirror_Vectors.append(mathutils.Vector((1, -1, 1)))
814 if object.data.use_mirror_x and object.data.use_mirror_y:
815 mirror_Vectors.append(mathutils.Vector((-1, -1, 1)))
816 z_mirror_Vectors = []
817 if object.data.use_mirror_z:
818 for v in mirror_Vectors:
819 z_mirror_Vectors.append(mathutils.Vector((1, 1, -1)) * v)
820 mirror_Vectors.extend(z_mirror_Vectors)
821 mirror_Vectors.append(mathutils.Vector((1, 1, -1)))
823 for loop in move:
824 for index, loc in loop:
825 if mapping:
826 if mapping[index] == -1:
827 continue
828 else:
829 index = mapping[index]
830 if lock:
831 delta = (loc - bm.verts[index].co) @ mat_inv
832 if lock_x:
833 delta[0] = 0
834 if lock_y:
835 delta[1] = 0
836 if lock_z:
837 delta[2] = 0
838 delta = delta @ mat
839 loc = bm.verts[index].co + delta
840 if influence < 0:
841 new_loc = loc
842 else:
843 new_loc = loc * (influence / 100) + \
844 bm.verts[index].co * ((100 - influence) / 100)
846 for vert in bm.verts:
847 for mirror_Vector in mirror_Vectors:
848 if vert.co == mirror_Vector * bm.verts[index].co:
849 vert.co = mirror_Vector * new_loc
851 bm.verts[index].co = new_loc
853 bm.normal_update()
854 object.data.update()
856 bm.verts.ensure_lookup_table()
857 bm.edges.ensure_lookup_table()
858 bm.faces.ensure_lookup_table()
861 # load custom tool settings
862 def settings_load(self):
863 lt = bpy.context.window_manager.looptools
864 tool = self.name.split()[0].lower()
865 keys = self.as_keywords().keys()
866 for key in keys:
867 setattr(self, key, getattr(lt, tool + "_" + key))
870 # store custom tool settings
871 def settings_write(self):
872 lt = bpy.context.window_manager.looptools
873 tool = self.name.split()[0].lower()
874 keys = self.as_keywords().keys()
875 for key in keys:
876 setattr(lt, tool + "_" + key, getattr(self, key))
879 # clean up and set settings back to original state
880 def terminate():
881 # update editmesh cached data
882 obj = bpy.context.active_object
883 if obj.mode == 'EDIT':
884 bmesh.update_edit_mesh(obj.data, loop_triangles=True, destructive=True)
887 # ########################################
888 # ##### Bridge functions #################
889 # ########################################
891 # calculate a cubic spline through the middle section of 4 given coordinates
892 def bridge_calculate_cubic_spline(bm, coordinates):
893 result = []
894 x = [0, 1, 2, 3]
896 for j in range(3):
897 a = []
898 for i in coordinates:
899 a.append(float(i[j]))
900 h = []
901 for i in range(3):
902 h.append(x[i + 1] - x[i])
903 q = [False]
904 for i in range(1, 3):
905 q.append(3.0 / h[i] * (a[i + 1] - a[i]) - 3.0 / h[i - 1] * (a[i] - a[i - 1]))
906 l = [1.0]
907 u = [0.0]
908 z = [0.0]
909 for i in range(1, 3):
910 l.append(2.0 * (x[i + 1] - x[i - 1]) - h[i - 1] * u[i - 1])
911 u.append(h[i] / l[i])
912 z.append((q[i] - h[i - 1] * z[i - 1]) / l[i])
913 l.append(1.0)
914 z.append(0.0)
915 b = [False for i in range(3)]
916 c = [False for i in range(4)]
917 d = [False for i in range(3)]
918 c[3] = 0.0
919 for i in range(2, -1, -1):
920 c[i] = z[i] - u[i] * c[i + 1]
921 b[i] = (a[i + 1] - a[i]) / h[i] - h[i] * (c[i + 1] + 2.0 * c[i]) / 3.0
922 d[i] = (c[i + 1] - c[i]) / (3.0 * h[i])
923 for i in range(3):
924 result.append([a[i], b[i], c[i], d[i], x[i]])
925 spline = [result[1], result[4], result[7]]
927 return(spline)
930 # return a list with new vertex location vectors, a list with face vertex
931 # integers, and the highest vertex integer in the virtual mesh
932 def bridge_calculate_geometry(bm, lines, vertex_normals, segments,
933 interpolation, cubic_strength, min_width, max_vert_index):
934 new_verts = []
935 faces = []
937 # calculate location based on interpolation method
938 def get_location(line, segment, splines):
939 v1 = bm.verts[lines[line][0]].co
940 v2 = bm.verts[lines[line][1]].co
941 if interpolation == 'linear':
942 return v1 + (segment / segments) * (v2 - v1)
943 else: # interpolation == 'cubic'
944 m = (segment / segments)
945 ax, bx, cx, dx, tx = splines[line][0]
946 x = ax + bx * m + cx * m ** 2 + dx * m ** 3
947 ay, by, cy, dy, ty = splines[line][1]
948 y = ay + by * m + cy * m ** 2 + dy * m ** 3
949 az, bz, cz, dz, tz = splines[line][2]
950 z = az + bz * m + cz * m ** 2 + dz * m ** 3
951 return mathutils.Vector((x, y, z))
953 # no interpolation needed
954 if segments == 1:
955 for i, line in enumerate(lines):
956 if i < len(lines) - 1:
957 faces.append([line[0], lines[i + 1][0], lines[i + 1][1], line[1]])
958 # more than 1 segment, interpolate
959 else:
960 # calculate splines (if necessary) once, so no recalculations needed
961 if interpolation == 'cubic':
962 splines = []
963 for line in lines:
964 v1 = bm.verts[line[0]].co
965 v2 = bm.verts[line[1]].co
966 size = (v2 - v1).length * cubic_strength
967 splines.append(bridge_calculate_cubic_spline(bm,
968 [v1 + size * vertex_normals[line[0]], v1, v2,
969 v2 + size * vertex_normals[line[1]]]))
970 else:
971 splines = False
973 # create starting situation
974 virtual_width = [(bm.verts[lines[i][0]].co -
975 bm.verts[lines[i + 1][0]].co).length for i
976 in range(len(lines) - 1)]
977 new_verts = [get_location(0, seg, splines) for seg in range(1,
978 segments)]
979 first_line_indices = [i for i in range(max_vert_index + 1,
980 max_vert_index + segments)]
982 prev_verts = new_verts[:] # vertex locations of verts on previous line
983 prev_vert_indices = first_line_indices[:]
984 max_vert_index += segments - 1 # highest vertex index in virtual mesh
985 next_verts = [] # vertex locations of verts on current line
986 next_vert_indices = []
988 for i, line in enumerate(lines):
989 if i < len(lines) - 1:
990 v1 = line[0]
991 v2 = lines[i + 1][0]
992 end_face = True
993 for seg in range(1, segments):
994 loc1 = prev_verts[seg - 1]
995 loc2 = get_location(i + 1, seg, splines)
996 if (loc1 - loc2).length < (min_width / 100) * virtual_width[i] \
997 and line[1] == lines[i + 1][1]:
998 # triangle, no new vertex
999 faces.append([v1, v2, prev_vert_indices[seg - 1],
1000 prev_vert_indices[seg - 1]])
1001 next_verts += prev_verts[seg - 1:]
1002 next_vert_indices += prev_vert_indices[seg - 1:]
1003 end_face = False
1004 break
1005 else:
1006 if i == len(lines) - 2 and lines[0] == lines[-1]:
1007 # quad with first line, no new vertex
1008 faces.append([v1, v2, first_line_indices[seg - 1],
1009 prev_vert_indices[seg - 1]])
1010 v2 = first_line_indices[seg - 1]
1011 v1 = prev_vert_indices[seg - 1]
1012 else:
1013 # quad, add new vertex
1014 max_vert_index += 1
1015 faces.append([v1, v2, max_vert_index,
1016 prev_vert_indices[seg - 1]])
1017 v2 = max_vert_index
1018 v1 = prev_vert_indices[seg - 1]
1019 new_verts.append(loc2)
1020 next_verts.append(loc2)
1021 next_vert_indices.append(max_vert_index)
1022 if end_face:
1023 faces.append([v1, v2, lines[i + 1][1], line[1]])
1025 prev_verts = next_verts[:]
1026 prev_vert_indices = next_vert_indices[:]
1027 next_verts = []
1028 next_vert_indices = []
1030 return(new_verts, faces, max_vert_index)
1033 # calculate lines (list of lists, vertex indices) that are used for bridging
1034 def bridge_calculate_lines(bm, loops, mode, twist, reverse):
1035 lines = []
1036 loop1, loop2 = [i[0] for i in loops]
1037 loop1_circular, loop2_circular = [i[1] for i in loops]
1038 circular = loop1_circular or loop2_circular
1039 circle_full = False
1041 # calculate loop centers
1042 centers = []
1043 for loop in [loop1, loop2]:
1044 center = mathutils.Vector()
1045 for vertex in loop:
1046 center += bm.verts[vertex].co
1047 center /= len(loop)
1048 centers.append(center)
1049 for i, loop in enumerate([loop1, loop2]):
1050 for vertex in loop:
1051 if bm.verts[vertex].co == centers[i]:
1052 # prevent zero-length vectors in angle comparisons
1053 centers[i] += mathutils.Vector((0.01, 0, 0))
1054 break
1055 center1, center2 = centers
1057 # calculate the normals of the virtual planes that the loops are on
1058 normals = []
1059 normal_plurity = False
1060 for i, loop in enumerate([loop1, loop2]):
1061 # covariance matrix
1062 mat = mathutils.Matrix(((0.0, 0.0, 0.0),
1063 (0.0, 0.0, 0.0),
1064 (0.0, 0.0, 0.0)))
1065 x, y, z = centers[i]
1066 for loc in [bm.verts[vertex].co for vertex in loop]:
1067 mat[0][0] += (loc[0] - x) ** 2
1068 mat[1][0] += (loc[0] - x) * (loc[1] - y)
1069 mat[2][0] += (loc[0] - x) * (loc[2] - z)
1070 mat[0][1] += (loc[1] - y) * (loc[0] - x)
1071 mat[1][1] += (loc[1] - y) ** 2
1072 mat[2][1] += (loc[1] - y) * (loc[2] - z)
1073 mat[0][2] += (loc[2] - z) * (loc[0] - x)
1074 mat[1][2] += (loc[2] - z) * (loc[1] - y)
1075 mat[2][2] += (loc[2] - z) ** 2
1076 # plane normal
1077 normal = False
1078 if sum(mat[0]) < 1e-6 or sum(mat[1]) < 1e-6 or sum(mat[2]) < 1e-6:
1079 normal_plurity = True
1080 try:
1081 mat.invert()
1082 except:
1083 if sum(mat[0]) == 0:
1084 normal = mathutils.Vector((1.0, 0.0, 0.0))
1085 elif sum(mat[1]) == 0:
1086 normal = mathutils.Vector((0.0, 1.0, 0.0))
1087 elif sum(mat[2]) == 0:
1088 normal = mathutils.Vector((0.0, 0.0, 1.0))
1089 if not normal:
1090 # warning! this is different from .normalize()
1091 itermax = 500
1092 iter = 0
1093 vec = mathutils.Vector((1.0, 1.0, 1.0))
1094 vec2 = (mat @ vec) / (mat @ vec).length
1095 while vec != vec2 and iter < itermax:
1096 iter += 1
1097 vec = vec2
1098 vec2 = mat @ vec
1099 if vec2.length != 0:
1100 vec2 /= vec2.length
1101 if vec2.length == 0:
1102 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
1103 normal = vec2
1104 normals.append(normal)
1105 # have plane normals face in the same direction (maximum angle: 90 degrees)
1106 if ((center1 + normals[0]) - center2).length < \
1107 ((center1 - normals[0]) - center2).length:
1108 normals[0].negate()
1109 if ((center2 + normals[1]) - center1).length > \
1110 ((center2 - normals[1]) - center1).length:
1111 normals[1].negate()
1113 # rotation matrix, representing the difference between the plane normals
1114 axis = normals[0].cross(normals[1])
1115 axis = mathutils.Vector([loc if abs(loc) > 1e-8 else 0 for loc in axis])
1116 if axis.angle(mathutils.Vector((0, 0, 1)), 0) > 1.5707964:
1117 axis.negate()
1118 angle = normals[0].dot(normals[1])
1119 rotation_matrix = mathutils.Matrix.Rotation(angle, 4, axis)
1121 # if circular, rotate loops so they are aligned
1122 if circular:
1123 # make sure loop1 is the circular one (or both are circular)
1124 if loop2_circular and not loop1_circular:
1125 loop1_circular, loop2_circular = True, False
1126 loop1, loop2 = loop2, loop1
1128 # match start vertex of loop1 with loop2
1129 target_vector = bm.verts[loop2[0]].co - center2
1130 dif_angles = [[(rotation_matrix @ (bm.verts[vertex].co - center1)
1131 ).angle(target_vector, 0), False, i] for
1132 i, vertex in enumerate(loop1)]
1133 dif_angles.sort()
1134 if len(loop1) != len(loop2):
1135 angle_limit = dif_angles[0][0] * 1.2 # 20% margin
1136 dif_angles = [
1137 [(bm.verts[loop2[0]].co -
1138 bm.verts[loop1[index]].co).length, angle, index] for
1139 angle, distance, index in dif_angles if angle <= angle_limit
1141 dif_angles.sort()
1142 loop1 = loop1[dif_angles[0][2]:] + loop1[:dif_angles[0][2]]
1144 # have both loops face the same way
1145 if normal_plurity and not circular:
1146 second_to_first, second_to_second, second_to_last = [
1147 (bm.verts[loop1[1]].co - center1).angle(
1148 bm.verts[loop2[i]].co - center2) for i in [0, 1, -1]
1150 last_to_first, last_to_second = [
1151 (bm.verts[loop1[-1]].co -
1152 center1).angle(bm.verts[loop2[i]].co - center2) for
1153 i in [0, 1]
1155 if (min(last_to_first, last_to_second) * 1.1 < min(second_to_first,
1156 second_to_second)) or (loop2_circular and second_to_last * 1.1 <
1157 min(second_to_first, second_to_second)):
1158 loop1.reverse()
1159 if circular:
1160 loop1 = [loop1[-1]] + loop1[:-1]
1161 else:
1162 angle = (bm.verts[loop1[0]].co - center1).\
1163 cross(bm.verts[loop1[1]].co - center1).angle(normals[0], 0)
1164 target_angle = (bm.verts[loop2[0]].co - center2).\
1165 cross(bm.verts[loop2[1]].co - center2).angle(normals[1], 0)
1166 limit = 1.5707964 # 0.5*pi, 90 degrees
1167 if not ((angle > limit and target_angle > limit) or
1168 (angle < limit and target_angle < limit)):
1169 loop1.reverse()
1170 if circular:
1171 loop1 = [loop1[-1]] + loop1[:-1]
1172 elif normals[0].angle(normals[1]) > limit:
1173 loop1.reverse()
1174 if circular:
1175 loop1 = [loop1[-1]] + loop1[:-1]
1177 # both loops have the same length
1178 if len(loop1) == len(loop2):
1179 # manual override
1180 if twist:
1181 if abs(twist) < len(loop1):
1182 loop1 = loop1[twist:] + loop1[:twist]
1183 if reverse:
1184 loop1.reverse()
1186 lines.append([loop1[0], loop2[0]])
1187 for i in range(1, len(loop1)):
1188 lines.append([loop1[i], loop2[i]])
1190 # loops of different lengths
1191 else:
1192 # make loop1 longest loop
1193 if len(loop2) > len(loop1):
1194 loop1, loop2 = loop2, loop1
1195 loop1_circular, loop2_circular = loop2_circular, loop1_circular
1197 # manual override
1198 if twist:
1199 if abs(twist) < len(loop1):
1200 loop1 = loop1[twist:] + loop1[:twist]
1201 if reverse:
1202 loop1.reverse()
1204 # shortest angle difference doesn't always give correct start vertex
1205 if loop1_circular and not loop2_circular:
1206 shifting = 1
1207 while shifting:
1208 if len(loop1) - shifting < len(loop2):
1209 shifting = False
1210 break
1211 to_last, to_first = [
1212 (rotation_matrix @ (bm.verts[loop1[-1]].co - center1)).angle(
1213 (bm.verts[loop2[i]].co - center2), 0) for i in [-1, 0]
1215 if to_first < to_last:
1216 loop1 = [loop1[-1]] + loop1[:-1]
1217 shifting += 1
1218 else:
1219 shifting = False
1220 break
1222 # basic shortest side first
1223 if mode == 'basic':
1224 lines.append([loop1[0], loop2[0]])
1225 for i in range(1, len(loop1)):
1226 if i >= len(loop2) - 1:
1227 # triangles
1228 lines.append([loop1[i], loop2[-1]])
1229 else:
1230 # quads
1231 lines.append([loop1[i], loop2[i]])
1233 # shortest edge algorithm
1234 else: # mode == 'shortest'
1235 lines.append([loop1[0], loop2[0]])
1236 prev_vert2 = 0
1237 for i in range(len(loop1) - 1):
1238 if prev_vert2 == len(loop2) - 1 and not loop2_circular:
1239 # force triangles, reached end of loop2
1240 tri, quad = 0, 1
1241 elif prev_vert2 == len(loop2) - 1 and loop2_circular:
1242 # at end of loop2, but circular, so check with first vert
1243 tri, quad = [(bm.verts[loop1[i + 1]].co -
1244 bm.verts[loop2[j]].co).length
1245 for j in [prev_vert2, 0]]
1246 circle_full = 2
1247 elif len(loop1) - 1 - i == len(loop2) - 1 - prev_vert2 and \
1248 not circle_full:
1249 # force quads, otherwise won't make it to end of loop2
1250 tri, quad = 1, 0
1251 else:
1252 # calculate if tri or quad gives shortest edge
1253 tri, quad = [(bm.verts[loop1[i + 1]].co -
1254 bm.verts[loop2[j]].co).length
1255 for j in range(prev_vert2, prev_vert2 + 2)]
1257 # triangle
1258 if tri < quad:
1259 lines.append([loop1[i + 1], loop2[prev_vert2]])
1260 if circle_full == 2:
1261 circle_full = False
1262 # quad
1263 elif not circle_full:
1264 lines.append([loop1[i + 1], loop2[prev_vert2 + 1]])
1265 prev_vert2 += 1
1266 # quad to first vertex of loop2
1267 else:
1268 lines.append([loop1[i + 1], loop2[0]])
1269 prev_vert2 = 0
1270 circle_full = True
1272 # final face for circular loops
1273 if loop1_circular and loop2_circular:
1274 lines.append([loop1[0], loop2[0]])
1276 return(lines)
1279 # calculate number of segments needed
1280 def bridge_calculate_segments(bm, lines, loops, segments):
1281 # return if amount of segments is set by user
1282 if segments != 0:
1283 return segments
1285 # edge lengths
1286 average_edge_length = [
1287 (bm.verts[vertex].co -
1288 bm.verts[loop[0][i + 1]].co).length for loop in loops for
1289 i, vertex in enumerate(loop[0][:-1])
1291 # closing edges of circular loops
1292 average_edge_length += [
1293 (bm.verts[loop[0][-1]].co -
1294 bm.verts[loop[0][0]].co).length for loop in loops if loop[1]
1297 # average lengths
1298 average_edge_length = sum(average_edge_length) / len(average_edge_length)
1299 average_bridge_length = sum(
1300 [(bm.verts[v1].co -
1301 bm.verts[v2].co).length for v1, v2 in lines]
1302 ) / len(lines)
1304 segments = max(1, round(average_bridge_length / average_edge_length))
1306 return(segments)
1309 # return dictionary with vertex index as key, and the normal vector as value
1310 def bridge_calculate_virtual_vertex_normals(bm, lines, loops, edge_faces,
1311 edgekey_to_edge):
1312 if not edge_faces: # interpolation isn't set to cubic
1313 return False
1315 # pity reduce() isn't one of the basic functions in python anymore
1316 def average_vector_dictionary(dic):
1317 for key, vectors in dic.items():
1318 # if type(vectors) == type([]) and len(vectors) > 1:
1319 if len(vectors) > 1:
1320 average = mathutils.Vector()
1321 for vector in vectors:
1322 average += vector
1323 average /= len(vectors)
1324 dic[key] = [average]
1325 return dic
1327 # get all edges of the loop
1328 edges = [
1329 [edgekey_to_edge[tuple(sorted([loops[j][0][i],
1330 loops[j][0][i + 1]]))] for i in range(len(loops[j][0]) - 1)] for
1331 j in [0, 1]
1333 edges = edges[0] + edges[1]
1334 for j in [0, 1]:
1335 if loops[j][1]: # circular
1336 edges.append(edgekey_to_edge[tuple(sorted([loops[j][0][0],
1337 loops[j][0][-1]]))])
1340 calculation based on face topology (assign edge-normals to vertices)
1342 edge_normal = face_normal x edge_vector
1343 vertex_normal = average(edge_normals)
1345 vertex_normals = dict([(vertex, []) for vertex in loops[0][0] + loops[1][0]])
1346 for edge in edges:
1347 faces = edge_faces[edgekey(edge)] # valid faces connected to edge
1349 if faces:
1350 # get edge coordinates
1351 v1, v2 = [bm.verts[edgekey(edge)[i]].co for i in [0, 1]]
1352 edge_vector = v1 - v2
1353 if edge_vector.length < 1e-4:
1354 # zero-length edge, vertices at same location
1355 continue
1356 edge_center = (v1 + v2) / 2
1358 # average face coordinates, if connected to more than 1 valid face
1359 if len(faces) > 1:
1360 face_normal = mathutils.Vector()
1361 face_center = mathutils.Vector()
1362 for face in faces:
1363 face_normal += face.normal
1364 face_center += face.calc_center_median()
1365 face_normal /= len(faces)
1366 face_center /= len(faces)
1367 else:
1368 face_normal = faces[0].normal
1369 face_center = faces[0].calc_center_median()
1370 if face_normal.length < 1e-4:
1371 # faces with a surface of 0 have no face normal
1372 continue
1374 # calculate virtual edge normal
1375 edge_normal = edge_vector.cross(face_normal)
1376 edge_normal.length = 0.01
1377 if (face_center - (edge_center + edge_normal)).length > \
1378 (face_center - (edge_center - edge_normal)).length:
1379 # make normal face the correct way
1380 edge_normal.negate()
1381 edge_normal.normalize()
1382 # add virtual edge normal as entry for both vertices it connects
1383 for vertex in edgekey(edge):
1384 vertex_normals[vertex].append(edge_normal)
1387 calculation based on connection with other loop (vertex focused method)
1388 - used for vertices that aren't connected to any valid faces
1390 plane_normal = edge_vector x connection_vector
1391 vertex_normal = plane_normal x edge_vector
1393 vertices = [
1394 vertex for vertex, normal in vertex_normals.items() if not normal
1397 if vertices:
1398 # edge vectors connected to vertices
1399 edge_vectors = dict([[vertex, []] for vertex in vertices])
1400 for edge in edges:
1401 for v in edgekey(edge):
1402 if v in edge_vectors:
1403 edge_vector = bm.verts[edgekey(edge)[0]].co - \
1404 bm.verts[edgekey(edge)[1]].co
1405 if edge_vector.length < 1e-4:
1406 # zero-length edge, vertices at same location
1407 continue
1408 edge_vectors[v].append(edge_vector)
1410 # connection vectors between vertices of both loops
1411 connection_vectors = dict([[vertex, []] for vertex in vertices])
1412 connections = dict([[vertex, []] for vertex in vertices])
1413 for v1, v2 in lines:
1414 if v1 in connection_vectors or v2 in connection_vectors:
1415 new_vector = bm.verts[v1].co - bm.verts[v2].co
1416 if new_vector.length < 1e-4:
1417 # zero-length connection vector,
1418 # vertices in different loops at same location
1419 continue
1420 if v1 in connection_vectors:
1421 connection_vectors[v1].append(new_vector)
1422 connections[v1].append(v2)
1423 if v2 in connection_vectors:
1424 connection_vectors[v2].append(new_vector)
1425 connections[v2].append(v1)
1426 connection_vectors = average_vector_dictionary(connection_vectors)
1427 connection_vectors = dict(
1428 [[vertex, vector[0]] if vector else
1429 [vertex, []] for vertex, vector in connection_vectors.items()]
1432 for vertex, values in edge_vectors.items():
1433 # vertex normal doesn't matter, just assign a random vector to it
1434 if not connection_vectors[vertex]:
1435 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1436 continue
1438 # calculate to what location the vertex is connected,
1439 # used to determine what way to flip the normal
1440 connected_center = mathutils.Vector()
1441 for v in connections[vertex]:
1442 connected_center += bm.verts[v].co
1443 if len(connections[vertex]) > 1:
1444 connected_center /= len(connections[vertex])
1445 if len(connections[vertex]) == 0:
1446 # shouldn't be possible, but better safe than sorry
1447 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1448 continue
1450 # can't do proper calculations, because of zero-length vector
1451 if not values:
1452 if (connected_center - (bm.verts[vertex].co +
1453 connection_vectors[vertex])).length < (connected_center -
1454 (bm.verts[vertex].co - connection_vectors[vertex])).length:
1455 connection_vectors[vertex].negate()
1456 vertex_normals[vertex] = [connection_vectors[vertex].normalized()]
1457 continue
1459 # calculate vertex normals using edge-vectors,
1460 # connection-vectors and the derived plane normal
1461 for edge_vector in values:
1462 plane_normal = edge_vector.cross(connection_vectors[vertex])
1463 vertex_normal = edge_vector.cross(plane_normal)
1464 vertex_normal.length = 0.1
1465 if (connected_center - (bm.verts[vertex].co +
1466 vertex_normal)).length < (connected_center -
1467 (bm.verts[vertex].co - vertex_normal)).length:
1468 # make normal face the correct way
1469 vertex_normal.negate()
1470 vertex_normal.normalize()
1471 vertex_normals[vertex].append(vertex_normal)
1473 # average virtual vertex normals, based on all edges it's connected to
1474 vertex_normals = average_vector_dictionary(vertex_normals)
1475 vertex_normals = dict([[vertex, vector[0]] for vertex, vector in vertex_normals.items()])
1477 return(vertex_normals)
1480 # add vertices to mesh
1481 def bridge_create_vertices(bm, vertices):
1482 for i in range(len(vertices)):
1483 bm.verts.new(vertices[i])
1484 bm.verts.ensure_lookup_table()
1487 # add faces to mesh
1488 def bridge_create_faces(object, bm, faces, twist):
1489 # have the normal point the correct way
1490 if twist < 0:
1491 [face.reverse() for face in faces]
1492 faces = [face[2:] + face[:2] if face[0] == face[1] else face for face in faces]
1494 # eekadoodle prevention
1495 for i in range(len(faces)):
1496 if not faces[i][-1]:
1497 if faces[i][0] == faces[i][-1]:
1498 faces[i] = [faces[i][1], faces[i][2], faces[i][3], faces[i][1]]
1499 else:
1500 faces[i] = [faces[i][-1]] + faces[i][:-1]
1501 # result of converting from pre-bmesh period
1502 if faces[i][-1] == faces[i][-2]:
1503 faces[i] = faces[i][:-1]
1505 new_faces = []
1506 for i in range(len(faces)):
1507 try:
1508 new_faces.append(bm.faces.new([bm.verts[v] for v in faces[i]]))
1509 except:
1510 # face already exists
1511 pass
1512 bm.normal_update()
1513 object.data.update(calc_edges=True) # calc_edges prevents memory-corruption
1515 bm.verts.ensure_lookup_table()
1516 bm.edges.ensure_lookup_table()
1517 bm.faces.ensure_lookup_table()
1519 return(new_faces)
1522 # calculate input loops
1523 def bridge_get_input(bm):
1524 # create list of internal edges, which should be skipped
1525 eks_of_selected_faces = [
1526 item for sublist in [face_edgekeys(face) for
1527 face in bm.faces if face.select and not face.hide] for item in sublist
1529 edge_count = {}
1530 for ek in eks_of_selected_faces:
1531 if ek in edge_count:
1532 edge_count[ek] += 1
1533 else:
1534 edge_count[ek] = 1
1535 internal_edges = [ek for ek in edge_count if edge_count[ek] > 1]
1537 # sort correct edges into loops
1538 selected_edges = [
1539 edgekey(edge) for edge in bm.edges if edge.select and
1540 not edge.hide and edgekey(edge) not in internal_edges
1542 loops = get_connected_selections(selected_edges)
1544 return(loops)
1547 # return values needed by the bridge operator
1548 def bridge_initialise(bm, interpolation):
1549 if interpolation == 'cubic':
1550 # dict with edge-key as key and list of connected valid faces as value
1551 face_blacklist = [
1552 face.index for face in bm.faces if face.select or
1553 face.hide
1555 edge_faces = dict(
1556 [[edgekey(edge), []] for edge in bm.edges if not edge.hide]
1558 for face in bm.faces:
1559 if face.index in face_blacklist:
1560 continue
1561 for key in face_edgekeys(face):
1562 edge_faces[key].append(face)
1563 # dictionary with the edge-key as key and edge as value
1564 edgekey_to_edge = dict(
1565 [[edgekey(edge), edge] for edge in bm.edges if edge.select and not edge.hide]
1567 else:
1568 edge_faces = False
1569 edgekey_to_edge = False
1571 # selected faces input
1572 old_selected_faces = [
1573 face.index for face in bm.faces if face.select and not face.hide
1576 # find out if faces created by bridging should be smoothed
1577 smooth = False
1578 if bm.faces:
1579 if sum([face.smooth for face in bm.faces]) / len(bm.faces) >= 0.5:
1580 smooth = True
1582 return(edge_faces, edgekey_to_edge, old_selected_faces, smooth)
1585 # return a string with the input method
1586 def bridge_input_method(loft, loft_loop):
1587 method = ""
1588 if loft:
1589 if loft_loop:
1590 method = "Loft loop"
1591 else:
1592 method = "Loft no-loop"
1593 else:
1594 method = "Bridge"
1596 return(method)
1599 # match up loops in pairs, used for multi-input bridging
1600 def bridge_match_loops(bm, loops):
1601 # calculate average loop normals and centers
1602 normals = []
1603 centers = []
1604 for vertices, circular in loops:
1605 normal = mathutils.Vector()
1606 center = mathutils.Vector()
1607 for vertex in vertices:
1608 normal += bm.verts[vertex].normal
1609 center += bm.verts[vertex].co
1610 normals.append(normal / len(vertices) / 10)
1611 centers.append(center / len(vertices))
1613 # possible matches if loop normals are faced towards the center
1614 # of the other loop
1615 matches = dict([[i, []] for i in range(len(loops))])
1616 matches_amount = 0
1617 for i in range(len(loops) + 1):
1618 for j in range(i + 1, len(loops)):
1619 if (centers[i] - centers[j]).length > \
1620 (centers[i] - (centers[j] + normals[j])).length and \
1621 (centers[j] - centers[i]).length > \
1622 (centers[j] - (centers[i] + normals[i])).length:
1623 matches_amount += 1
1624 matches[i].append([(centers[i] - centers[j]).length, i, j])
1625 matches[j].append([(centers[i] - centers[j]).length, j, i])
1626 # if no loops face each other, just make matches between all the loops
1627 if matches_amount == 0:
1628 for i in range(len(loops) + 1):
1629 for j in range(i + 1, len(loops)):
1630 matches[i].append([(centers[i] - centers[j]).length, i, j])
1631 matches[j].append([(centers[i] - centers[j]).length, j, i])
1632 for key, value in matches.items():
1633 value.sort()
1635 # matches based on distance between centers and number of vertices in loops
1636 new_order = []
1637 for loop_index in range(len(loops)):
1638 if loop_index in new_order:
1639 continue
1640 loop_matches = matches[loop_index]
1641 if not loop_matches:
1642 continue
1643 shortest_distance = loop_matches[0][0]
1644 shortest_distance *= 1.1
1645 loop_matches = [
1646 [abs(len(loops[loop_index][0]) -
1647 len(loops[loop[2]][0])), loop[0], loop[1], loop[2]] for loop in
1648 loop_matches if loop[0] < shortest_distance
1650 loop_matches.sort()
1651 for match in loop_matches:
1652 if match[3] not in new_order:
1653 new_order += [loop_index, match[3]]
1654 break
1656 # reorder loops based on matches
1657 if len(new_order) >= 2:
1658 loops = [loops[i] for i in new_order]
1660 return(loops)
1663 # remove old_selected_faces
1664 def bridge_remove_internal_faces(bm, old_selected_faces):
1665 # collect bmesh faces and internal bmesh edges
1666 remove_faces = [bm.faces[face] for face in old_selected_faces]
1667 edges = collections.Counter(
1668 [edge.index for face in remove_faces for edge in face.edges]
1670 remove_edges = [bm.edges[edge] for edge in edges if edges[edge] > 1]
1672 # remove internal faces and edges
1673 for face in remove_faces:
1674 bm.faces.remove(face)
1675 for edge in remove_edges:
1676 bm.edges.remove(edge)
1678 bm.faces.ensure_lookup_table()
1679 bm.edges.ensure_lookup_table()
1680 bm.verts.ensure_lookup_table()
1683 # update list of internal faces that are flagged for removal
1684 def bridge_save_unused_faces(bm, old_selected_faces, loops):
1685 # key: vertex index, value: lists of selected faces using it
1687 vertex_to_face = dict([[i, []] for i in range(len(bm.verts))])
1688 [[vertex_to_face[vertex.index].append(face) for vertex in
1689 bm.faces[face].verts] for face in old_selected_faces]
1691 # group selected faces that are connected
1692 groups = []
1693 grouped_faces = []
1694 for face in old_selected_faces:
1695 if face in grouped_faces:
1696 continue
1697 grouped_faces.append(face)
1698 group = [face]
1699 new_faces = [face]
1700 while new_faces:
1701 grow_face = new_faces[0]
1702 for vertex in bm.faces[grow_face].verts:
1703 vertex_face_group = [
1704 face for face in vertex_to_face[vertex.index] if
1705 face not in grouped_faces
1707 new_faces += vertex_face_group
1708 grouped_faces += vertex_face_group
1709 group += vertex_face_group
1710 new_faces.pop(0)
1711 groups.append(group)
1713 # key: vertex index, value: True/False (is it in a loop that is used)
1714 used_vertices = dict([[i, 0] for i in range(len(bm.verts))])
1715 for loop in loops:
1716 for vertex in loop[0]:
1717 used_vertices[vertex] = True
1719 # check if group is bridged, if not remove faces from internal faces list
1720 for group in groups:
1721 used = False
1722 for face in group:
1723 if used:
1724 break
1725 for vertex in bm.faces[face].verts:
1726 if used_vertices[vertex.index]:
1727 used = True
1728 break
1729 if not used:
1730 for face in group:
1731 old_selected_faces.remove(face)
1734 # add the newly created faces to the selection
1735 def bridge_select_new_faces(new_faces, smooth):
1736 for face in new_faces:
1737 face.select_set(True)
1738 face.smooth = smooth
1741 # sort loops, so they are connected in the correct order when lofting
1742 def bridge_sort_loops(bm, loops, loft_loop):
1743 # simplify loops to single points, and prepare for pathfinding
1744 x, y, z = [
1745 [sum([bm.verts[i].co[j] for i in loop[0]]) /
1746 len(loop[0]) for loop in loops] for j in range(3)
1748 nodes = [mathutils.Vector((x[i], y[i], z[i])) for i in range(len(loops))]
1750 active_node = 0
1751 open = [i for i in range(1, len(loops))]
1752 path = [[0, 0]]
1753 # connect node to path, that is shortest to active_node
1754 while len(open) > 0:
1755 distances = [(nodes[active_node] - nodes[i]).length for i in open]
1756 active_node = open[distances.index(min(distances))]
1757 open.remove(active_node)
1758 path.append([active_node, min(distances)])
1759 # check if we didn't start in the middle of the path
1760 for i in range(2, len(path)):
1761 if (nodes[path[i][0]] - nodes[0]).length < path[i][1]:
1762 temp = path[:i]
1763 path.reverse()
1764 path = path[:-i] + temp
1765 break
1767 # reorder loops
1768 loops = [loops[i[0]] for i in path]
1769 # if requested, duplicate first loop at last position, so loft can loop
1770 if loft_loop:
1771 loops = loops + [loops[0]]
1773 return(loops)
1776 # remapping old indices to new position in list
1777 def bridge_update_old_selection(bm, old_selected_faces):
1779 old_indices = old_selected_faces[:]
1780 old_selected_faces = []
1781 for i, face in enumerate(bm.faces):
1782 if face.index in old_indices:
1783 old_selected_faces.append(i)
1785 old_selected_faces = [
1786 i for i, face in enumerate(bm.faces) if face.index in old_selected_faces
1789 return(old_selected_faces)
1792 # ########################################
1793 # ##### Circle functions #################
1794 # ########################################
1796 # convert 3d coordinates to 2d coordinates on plane
1797 def circle_3d_to_2d(bm_mod, loop, com, normal):
1798 # project vertices onto the plane
1799 verts = [bm_mod.verts[v] for v in loop[0]]
1800 verts_projected = [[v.co - (v.co - com).dot(normal) * normal, v.index]
1801 for v in verts]
1803 # calculate two vectors (p and q) along the plane
1804 m = mathutils.Vector((normal[0] + 1.0, normal[1], normal[2]))
1805 p = m - (m.dot(normal) * normal)
1806 if p.dot(p) < 1e-6:
1807 m = mathutils.Vector((normal[0], normal[1] + 1.0, normal[2]))
1808 p = m - (m.dot(normal) * normal)
1809 q = p.cross(normal)
1811 # change to 2d coordinates using perpendicular projection
1812 locs_2d = []
1813 for loc, vert in verts_projected:
1814 vloc = loc - com
1815 x = p.dot(vloc) / p.dot(p)
1816 y = q.dot(vloc) / q.dot(q)
1817 locs_2d.append([x, y, vert])
1819 return(locs_2d, p, q)
1822 # calculate a best-fit circle to the 2d locations on the plane
1823 def circle_calculate_best_fit(locs_2d):
1824 # initial guess
1825 x0 = 0.0
1826 y0 = 0.0
1827 r = 1.0
1829 # calculate center and radius (non-linear least squares solution)
1830 for iter in range(500):
1831 jmat = []
1832 k = []
1833 for v in locs_2d:
1834 d = (v[0] ** 2 - 2.0 * x0 * v[0] + v[1] ** 2 - 2.0 * y0 * v[1] + x0 ** 2 + y0 ** 2) ** 0.5
1835 jmat.append([(x0 - v[0]) / d, (y0 - v[1]) / d, -1.0])
1836 k.append(-(((v[0] - x0) ** 2 + (v[1] - y0) ** 2) ** 0.5 - r))
1837 jmat2 = mathutils.Matrix(((0.0, 0.0, 0.0),
1838 (0.0, 0.0, 0.0),
1839 (0.0, 0.0, 0.0),
1841 k2 = mathutils.Vector((0.0, 0.0, 0.0))
1842 for i in range(len(jmat)):
1843 k2 += mathutils.Vector(jmat[i]) * k[i]
1844 jmat2[0][0] += jmat[i][0] ** 2
1845 jmat2[1][0] += jmat[i][0] * jmat[i][1]
1846 jmat2[2][0] += jmat[i][0] * jmat[i][2]
1847 jmat2[1][1] += jmat[i][1] ** 2
1848 jmat2[2][1] += jmat[i][1] * jmat[i][2]
1849 jmat2[2][2] += jmat[i][2] ** 2
1850 jmat2[0][1] = jmat2[1][0]
1851 jmat2[0][2] = jmat2[2][0]
1852 jmat2[1][2] = jmat2[2][1]
1853 try:
1854 jmat2.invert()
1855 except:
1856 pass
1857 dx0, dy0, dr = jmat2 @ k2
1858 x0 += dx0
1859 y0 += dy0
1860 r += dr
1861 # stop iterating if we're close enough to optimal solution
1862 if abs(dx0) < 1e-6 and abs(dy0) < 1e-6 and abs(dr) < 1e-6:
1863 break
1865 # return center of circle and radius
1866 return(x0, y0, r)
1869 # calculate circle so no vertices have to be moved away from the center
1870 def circle_calculate_min_fit(locs_2d):
1871 # center of circle
1872 x0 = (min([i[0] for i in locs_2d]) + max([i[0] for i in locs_2d])) / 2.0
1873 y0 = (min([i[1] for i in locs_2d]) + max([i[1] for i in locs_2d])) / 2.0
1874 center = mathutils.Vector([x0, y0])
1875 # radius of circle
1876 r = min([(mathutils.Vector([i[0], i[1]]) - center).length for i in locs_2d])
1878 # return center of circle and radius
1879 return(x0, y0, r)
1882 # calculate the new locations of the vertices that need to be moved
1883 def circle_calculate_verts(flatten, bm_mod, locs_2d, com, p, q, normal):
1884 # changing 2d coordinates back to 3d coordinates
1885 locs_3d = []
1886 for loc in locs_2d:
1887 locs_3d.append([loc[2], loc[0] * p + loc[1] * q + com])
1889 if flatten: # flat circle
1890 return(locs_3d)
1892 else: # project the locations on the existing mesh
1893 vert_edges = dict_vert_edges(bm_mod)
1894 vert_faces = dict_vert_faces(bm_mod)
1895 faces = [f for f in bm_mod.faces if not f.hide]
1896 rays = [normal, -normal]
1897 new_locs = []
1898 for loc in locs_3d:
1899 projection = False
1900 if bm_mod.verts[loc[0]].co == loc[1]: # vertex hasn't moved
1901 projection = loc[1]
1902 else:
1903 dif = normal.angle(loc[1] - bm_mod.verts[loc[0]].co)
1904 if -1e-6 < dif < 1e-6 or math.pi - 1e-6 < dif < math.pi + 1e-6:
1905 # original location is already along projection normal
1906 projection = bm_mod.verts[loc[0]].co
1907 else:
1908 # quick search through adjacent faces
1909 for face in vert_faces[loc[0]]:
1910 verts = [v.co for v in bm_mod.faces[face].verts]
1911 if len(verts) == 3: # triangle
1912 v1, v2, v3 = verts
1913 v4 = False
1914 else: # assume quad
1915 v1, v2, v3, v4 = verts[:4]
1916 for ray in rays:
1917 intersect = mathutils.geometry.\
1918 intersect_ray_tri(v1, v2, v3, ray, loc[1])
1919 if intersect:
1920 projection = intersect
1921 break
1922 elif v4:
1923 intersect = mathutils.geometry.\
1924 intersect_ray_tri(v1, v3, v4, ray, loc[1])
1925 if intersect:
1926 projection = intersect
1927 break
1928 if projection:
1929 break
1930 if not projection:
1931 # check if projection is on adjacent edges
1932 for edgekey in vert_edges[loc[0]]:
1933 line1 = bm_mod.verts[edgekey[0]].co
1934 line2 = bm_mod.verts[edgekey[1]].co
1935 intersect, dist = mathutils.geometry.intersect_point_line(
1936 loc[1], line1, line2
1938 if 1e-6 < dist < 1 - 1e-6:
1939 projection = intersect
1940 break
1941 if not projection:
1942 # full search through the entire mesh
1943 hits = []
1944 for face in faces:
1945 verts = [v.co for v in face.verts]
1946 if len(verts) == 3: # triangle
1947 v1, v2, v3 = verts
1948 v4 = False
1949 else: # assume quad
1950 v1, v2, v3, v4 = verts[:4]
1951 for ray in rays:
1952 intersect = mathutils.geometry.intersect_ray_tri(
1953 v1, v2, v3, ray, loc[1]
1955 if intersect:
1956 hits.append([(loc[1] - intersect).length,
1957 intersect])
1958 break
1959 elif v4:
1960 intersect = mathutils.geometry.intersect_ray_tri(
1961 v1, v3, v4, ray, loc[1]
1963 if intersect:
1964 hits.append([(loc[1] - intersect).length,
1965 intersect])
1966 break
1967 if len(hits) >= 1:
1968 # if more than 1 hit with mesh, closest hit is new loc
1969 hits.sort()
1970 projection = hits[0][1]
1971 if not projection:
1972 # nothing to project on, remain at flat location
1973 projection = loc[1]
1974 new_locs.append([loc[0], projection])
1976 # return new positions of projected circle
1977 return(new_locs)
1980 # check loops and only return valid ones
1981 def circle_check_loops(single_loops, loops, mapping, bm_mod):
1982 valid_single_loops = {}
1983 valid_loops = []
1984 for i, [loop, circular] in enumerate(loops):
1985 # loop needs to have at least 3 vertices
1986 if len(loop) < 3:
1987 continue
1988 # loop needs at least 1 vertex in the original, non-mirrored mesh
1989 if mapping:
1990 all_virtual = True
1991 for vert in loop:
1992 if mapping[vert] > -1:
1993 all_virtual = False
1994 break
1995 if all_virtual:
1996 continue
1997 # loop has to be non-collinear
1998 collinear = True
1999 loc0 = mathutils.Vector(bm_mod.verts[loop[0]].co[:])
2000 loc1 = mathutils.Vector(bm_mod.verts[loop[1]].co[:])
2001 for v in loop[2:]:
2002 locn = mathutils.Vector(bm_mod.verts[v].co[:])
2003 if loc0 == loc1 or loc1 == locn:
2004 loc0 = loc1
2005 loc1 = locn
2006 continue
2007 d1 = loc1 - loc0
2008 d2 = locn - loc1
2009 if -1e-6 < d1.angle(d2, 0) < 1e-6:
2010 loc0 = loc1
2011 loc1 = locn
2012 continue
2013 collinear = False
2014 break
2015 if collinear:
2016 continue
2017 # passed all tests, loop is valid
2018 valid_loops.append([loop, circular])
2019 valid_single_loops[len(valid_loops) - 1] = single_loops[i]
2021 return(valid_single_loops, valid_loops)
2024 # calculate the location of single input vertices that need to be flattened
2025 def circle_flatten_singles(bm_mod, com, p, q, normal, single_loop):
2026 new_locs = []
2027 for vert in single_loop:
2028 loc = mathutils.Vector(bm_mod.verts[vert].co[:])
2029 new_locs.append([vert, loc - (loc - com).dot(normal) * normal])
2031 return(new_locs)
2034 # calculate input loops
2035 def circle_get_input(object, bm):
2036 # get mesh with modifiers applied
2037 derived, bm_mod = get_derived_bmesh(object, bm, False)
2039 # create list of edge-keys based on selection state
2040 faces = False
2041 for face in bm.faces:
2042 if face.select and not face.hide:
2043 faces = True
2044 break
2045 if faces:
2046 # get selected, non-hidden , non-internal edge-keys
2047 eks_selected = [
2048 key for keys in [face_edgekeys(face) for face in
2049 bm_mod.faces if face.select and not face.hide] for key in keys
2051 edge_count = {}
2052 for ek in eks_selected:
2053 if ek in edge_count:
2054 edge_count[ek] += 1
2055 else:
2056 edge_count[ek] = 1
2057 edge_keys = [
2058 edgekey(edge) for edge in bm_mod.edges if edge.select and
2059 not edge.hide and edge_count.get(edgekey(edge), 1) == 1
2061 else:
2062 # no faces, so no internal edges either
2063 edge_keys = [
2064 edgekey(edge) for edge in bm_mod.edges if edge.select and not edge.hide
2067 # add edge-keys around single vertices
2068 verts_connected = dict(
2069 [[vert, 1] for edge in [edge for edge in
2070 bm_mod.edges if edge.select and not edge.hide] for vert in
2071 edgekey(edge)]
2073 single_vertices = [
2074 vert.index for vert in bm_mod.verts if
2075 vert.select and not vert.hide and
2076 not verts_connected.get(vert.index, False)
2079 if single_vertices and len(bm.faces) > 0:
2080 vert_to_single = dict(
2081 [[v.index, []] for v in bm_mod.verts if not v.hide]
2083 for face in [face for face in bm_mod.faces if not face.select and not face.hide]:
2084 for vert in face.verts:
2085 vert = vert.index
2086 if vert in single_vertices:
2087 for ek in face_edgekeys(face):
2088 if vert not in ek:
2089 edge_keys.append(ek)
2090 if vert not in vert_to_single[ek[0]]:
2091 vert_to_single[ek[0]].append(vert)
2092 if vert not in vert_to_single[ek[1]]:
2093 vert_to_single[ek[1]].append(vert)
2094 break
2096 # sort edge-keys into loops
2097 loops = get_connected_selections(edge_keys)
2099 # find out to which loops the single vertices belong
2100 single_loops = dict([[i, []] for i in range(len(loops))])
2101 if single_vertices and len(bm.faces) > 0:
2102 for i, [loop, circular] in enumerate(loops):
2103 for vert in loop:
2104 if vert_to_single[vert]:
2105 for single in vert_to_single[vert]:
2106 if single not in single_loops[i]:
2107 single_loops[i].append(single)
2109 return(derived, bm_mod, single_vertices, single_loops, loops)
2112 # recalculate positions based on the influence of the circle shape
2113 def circle_influence_locs(locs_2d, new_locs_2d, influence):
2114 for i in range(len(locs_2d)):
2115 oldx, oldy, j = locs_2d[i]
2116 newx, newy, k = new_locs_2d[i]
2117 altx = newx * (influence / 100) + oldx * ((100 - influence) / 100)
2118 alty = newy * (influence / 100) + oldy * ((100 - influence) / 100)
2119 locs_2d[i] = [altx, alty, j]
2121 return(locs_2d)
2124 # project 2d locations on circle, respecting distance relations between verts
2125 def circle_project_non_regular(locs_2d, x0, y0, r, angle):
2126 for i in range(len(locs_2d)):
2127 x, y, j = locs_2d[i]
2128 loc = mathutils.Vector([x - x0, y - y0])
2129 mat_rot = mathutils.Matrix.Rotation(angle, 2, 'X')
2130 loc.rotate(mat_rot)
2131 loc.length = r
2132 locs_2d[i] = [loc[0], loc[1], j]
2134 return(locs_2d)
2137 # project 2d locations on circle, with equal distance between all vertices
2138 def circle_project_regular(locs_2d, x0, y0, r, angle):
2139 # find offset angle and circling direction
2140 x, y, i = locs_2d[0]
2141 loc = mathutils.Vector([x - x0, y - y0])
2142 loc.length = r
2143 offset_angle = loc.angle(mathutils.Vector([1.0, 0.0]), 0.0)
2144 loca = mathutils.Vector([x - x0, y - y0, 0.0])
2145 if loc[1] < -1e-6:
2146 offset_angle *= -1
2147 x, y, j = locs_2d[1]
2148 locb = mathutils.Vector([x - x0, y - y0, 0.0])
2149 if loca.cross(locb)[2] >= 0:
2150 ccw = 1
2151 else:
2152 ccw = -1
2153 # distribute vertices along the circle
2154 for i in range(len(locs_2d)):
2155 t = offset_angle + ccw * (i / len(locs_2d) * 2 * math.pi)
2156 x = math.cos(t + angle) * r
2157 y = math.sin(t + angle) * r
2158 locs_2d[i] = [x, y, locs_2d[i][2]]
2160 return(locs_2d)
2163 # shift loop, so the first vertex is closest to the center
2164 def circle_shift_loop(bm_mod, loop, com):
2165 verts, circular = loop
2166 distances = [
2167 [(bm_mod.verts[vert].co - com).length, i] for i, vert in enumerate(verts)
2169 distances.sort()
2170 shift = distances[0][1]
2171 loop = [verts[shift:] + verts[:shift], circular]
2173 return(loop)
2176 # ########################################
2177 # ##### Curve functions ##################
2178 # ########################################
2180 # create lists with knots and points, all correctly sorted
2181 def curve_calculate_knots(loop, verts_selected):
2182 knots = [v for v in loop[0] if v in verts_selected]
2183 points = loop[0][:]
2184 # circular loop, potential for weird splines
2185 if loop[1]:
2186 offset = int(len(loop[0]) / 4)
2187 kpos = []
2188 for k in knots:
2189 kpos.append(loop[0].index(k))
2190 kdif = []
2191 for i in range(len(kpos) - 1):
2192 kdif.append(kpos[i + 1] - kpos[i])
2193 kdif.append(len(loop[0]) - kpos[-1] + kpos[0])
2194 kadd = []
2195 for k in kdif:
2196 if k > 2 * offset:
2197 kadd.append([kdif.index(k), True])
2198 # next 2 lines are optional, they insert
2199 # an extra control point in small gaps
2200 # elif k > offset:
2201 # kadd.append([kdif.index(k), False])
2202 kins = []
2203 krot = False
2204 for k in kadd: # extra knots to be added
2205 if k[1]: # big gap (break circular spline)
2206 kpos = loop[0].index(knots[k[0]]) + offset
2207 if kpos > len(loop[0]) - 1:
2208 kpos -= len(loop[0])
2209 kins.append([knots[k[0]], loop[0][kpos]])
2210 kpos2 = k[0] + 1
2211 if kpos2 > len(knots) - 1:
2212 kpos2 -= len(knots)
2213 kpos2 = loop[0].index(knots[kpos2]) - offset
2214 if kpos2 < 0:
2215 kpos2 += len(loop[0])
2216 kins.append([loop[0][kpos], loop[0][kpos2]])
2217 krot = loop[0][kpos2]
2218 else: # small gap (keep circular spline)
2219 k1 = loop[0].index(knots[k[0]])
2220 k2 = k[0] + 1
2221 if k2 > len(knots) - 1:
2222 k2 -= len(knots)
2223 k2 = loop[0].index(knots[k2])
2224 if k2 < k1:
2225 dif = len(loop[0]) - 1 - k1 + k2
2226 else:
2227 dif = k2 - k1
2228 kn = k1 + int(dif / 2)
2229 if kn > len(loop[0]) - 1:
2230 kn -= len(loop[0])
2231 kins.append([loop[0][k1], loop[0][kn]])
2232 for j in kins: # insert new knots
2233 knots.insert(knots.index(j[0]) + 1, j[1])
2234 if not krot: # circular loop
2235 knots.append(knots[0])
2236 points = loop[0][loop[0].index(knots[0]):]
2237 points += loop[0][0:loop[0].index(knots[0]) + 1]
2238 else: # non-circular loop (broken by script)
2239 krot = knots.index(krot)
2240 knots = knots[krot:] + knots[0:krot]
2241 if loop[0].index(knots[0]) > loop[0].index(knots[-1]):
2242 points = loop[0][loop[0].index(knots[0]):]
2243 points += loop[0][0:loop[0].index(knots[-1]) + 1]
2244 else:
2245 points = loop[0][loop[0].index(knots[0]):loop[0].index(knots[-1]) + 1]
2246 # non-circular loop, add first and last point as knots
2247 else:
2248 if loop[0][0] not in knots:
2249 knots.insert(0, loop[0][0])
2250 if loop[0][-1] not in knots:
2251 knots.append(loop[0][-1])
2253 return(knots, points)
2256 # calculate relative positions compared to first knot
2257 def curve_calculate_t(bm_mod, knots, points, pknots, regular, circular):
2258 tpoints = []
2259 loc_prev = False
2260 len_total = 0
2262 for p in points:
2263 if p in knots:
2264 loc = pknots[knots.index(p)] # use projected knot location
2265 else:
2266 loc = mathutils.Vector(bm_mod.verts[p].co[:])
2267 if not loc_prev:
2268 loc_prev = loc
2269 len_total += (loc - loc_prev).length
2270 tpoints.append(len_total)
2271 loc_prev = loc
2272 tknots = []
2273 for p in points:
2274 if p in knots:
2275 tknots.append(tpoints[points.index(p)])
2276 if circular:
2277 tknots[-1] = tpoints[-1]
2279 # regular option
2280 if regular:
2281 tpoints_average = tpoints[-1] / (len(tpoints) - 1)
2282 for i in range(1, len(tpoints) - 1):
2283 tpoints[i] = i * tpoints_average
2284 for i in range(len(knots)):
2285 tknots[i] = tpoints[points.index(knots[i])]
2286 if circular:
2287 tknots[-1] = tpoints[-1]
2289 return(tknots, tpoints)
2292 # change the location of non-selected points to their place on the spline
2293 def curve_calculate_vertices(bm_mod, knots, tknots, points, tpoints, splines,
2294 interpolation, restriction):
2295 newlocs = {}
2296 move = []
2298 for p in points:
2299 if p in knots:
2300 continue
2301 m = tpoints[points.index(p)]
2302 if m in tknots:
2303 n = tknots.index(m)
2304 else:
2305 t = tknots[:]
2306 t.append(m)
2307 t.sort()
2308 n = t.index(m) - 1
2309 if n > len(splines) - 1:
2310 n = len(splines) - 1
2311 elif n < 0:
2312 n = 0
2314 if interpolation == 'cubic':
2315 ax, bx, cx, dx, tx = splines[n][0]
2316 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
2317 ay, by, cy, dy, ty = splines[n][1]
2318 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
2319 az, bz, cz, dz, tz = splines[n][2]
2320 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
2321 newloc = mathutils.Vector([x, y, z])
2322 else: # interpolation == 'linear'
2323 a, d, t, u = splines[n]
2324 newloc = ((m - t) / u) * d + a
2326 if restriction != 'none': # vertex movement is restricted
2327 newlocs[p] = newloc
2328 else: # set the vertex to its new location
2329 move.append([p, newloc])
2331 if restriction != 'none': # vertex movement is restricted
2332 for p in points:
2333 if p in newlocs:
2334 newloc = newlocs[p]
2335 else:
2336 move.append([p, bm_mod.verts[p].co])
2337 continue
2338 oldloc = bm_mod.verts[p].co
2339 normal = bm_mod.verts[p].normal
2340 dloc = newloc - oldloc
2341 if dloc.length < 1e-6:
2342 move.append([p, newloc])
2343 elif restriction == 'extrude': # only extrusions
2344 if dloc.angle(normal, 0) < 0.5 * math.pi + 1e-6:
2345 move.append([p, newloc])
2346 else: # restriction == 'indent' only indentations
2347 if dloc.angle(normal) > 0.5 * math.pi - 1e-6:
2348 move.append([p, newloc])
2350 return(move)
2353 # trim loops to part between first and last selected vertices (including)
2354 def curve_cut_boundaries(bm_mod, loops):
2355 cut_loops = []
2356 for loop, circular in loops:
2357 if circular:
2358 selected = [bm_mod.verts[v].select for v in loop]
2359 first = selected.index(True)
2360 selected.reverse()
2361 last = -selected.index(True)
2362 if last == 0:
2363 if len(loop[first:]) < len(loop)/2:
2364 cut_loops.append([loop[first:], False])
2365 else:
2366 if len(loop[first:last]) < len(loop)/2:
2367 cut_loops.append([loop[first:last], False])
2368 continue
2369 selected = [bm_mod.verts[v].select for v in loop]
2370 first = selected.index(True)
2371 selected.reverse()
2372 last = -selected.index(True)
2373 if last == 0:
2374 cut_loops.append([loop[first:], circular])
2375 else:
2376 cut_loops.append([loop[first:last], circular])
2378 return(cut_loops)
2381 # calculate input loops
2382 def curve_get_input(object, bm, boundaries):
2383 # get mesh with modifiers applied
2384 derived, bm_mod = get_derived_bmesh(object, bm, False)
2386 # vertices that still need a loop to run through it
2387 verts_unsorted = [
2388 v.index for v in bm_mod.verts if v.select and not v.hide
2390 # necessary dictionaries
2391 vert_edges = dict_vert_edges(bm_mod)
2392 edge_faces = dict_edge_faces(bm_mod)
2393 correct_loops = []
2394 # find loops through each selected vertex
2395 while len(verts_unsorted) > 0:
2396 loops = curve_vertex_loops(bm_mod, verts_unsorted[0], vert_edges,
2397 edge_faces)
2398 verts_unsorted.pop(0)
2400 # check if loop is fully selected
2401 search_perpendicular = False
2402 i = -1
2403 for loop, circular in loops:
2404 i += 1
2405 selected = [v for v in loop if bm_mod.verts[v].select]
2406 if len(selected) < 2:
2407 # only one selected vertex on loop, don't use
2408 loops.pop(i)
2409 continue
2410 elif len(selected) == len(loop):
2411 search_perpendicular = loop
2412 break
2413 # entire loop is selected, find perpendicular loops
2414 if search_perpendicular:
2415 for vert in loop:
2416 if vert in verts_unsorted:
2417 verts_unsorted.remove(vert)
2418 perp_loops = curve_perpendicular_loops(bm_mod, loop,
2419 vert_edges, edge_faces)
2420 for perp_loop in perp_loops:
2421 correct_loops.append(perp_loop)
2422 # normal input
2423 else:
2424 for loop, circular in loops:
2425 correct_loops.append([loop, circular])
2427 # boundaries option
2428 if boundaries:
2429 correct_loops = curve_cut_boundaries(bm_mod, correct_loops)
2431 return(derived, bm_mod, correct_loops)
2434 # return all loops that are perpendicular to the given one
2435 def curve_perpendicular_loops(bm_mod, start_loop, vert_edges, edge_faces):
2436 # find perpendicular loops
2437 perp_loops = []
2438 for start_vert in start_loop:
2439 loops = curve_vertex_loops(bm_mod, start_vert, vert_edges,
2440 edge_faces)
2441 for loop, circular in loops:
2442 selected = [v for v in loop if bm_mod.verts[v].select]
2443 if len(selected) == len(loop):
2444 continue
2445 else:
2446 perp_loops.append([loop, circular, loop.index(start_vert)])
2448 # trim loops to same lengths
2449 shortest = [
2450 [len(loop[0]), i] for i, loop in enumerate(perp_loops) if not loop[1]
2452 if not shortest:
2453 # all loops are circular, not trimming
2454 return([[loop[0], loop[1]] for loop in perp_loops])
2455 else:
2456 shortest = min(shortest)
2457 shortest_start = perp_loops[shortest[1]][2]
2458 before_start = shortest_start
2459 after_start = shortest[0] - shortest_start - 1
2460 bigger_before = before_start > after_start
2461 trimmed_loops = []
2462 for loop in perp_loops:
2463 # have the loop face the same direction as the shortest one
2464 if bigger_before:
2465 if loop[2] < len(loop[0]) / 2:
2466 loop[0].reverse()
2467 loop[2] = len(loop[0]) - loop[2] - 1
2468 else:
2469 if loop[2] > len(loop[0]) / 2:
2470 loop[0].reverse()
2471 loop[2] = len(loop[0]) - loop[2] - 1
2472 # circular loops can shift, to prevent wrong trimming
2473 if loop[1]:
2474 shift = shortest_start - loop[2]
2475 if loop[2] + shift > 0 and loop[2] + shift < len(loop[0]):
2476 loop[0] = loop[0][-shift:] + loop[0][:-shift]
2477 loop[2] += shift
2478 if loop[2] < 0:
2479 loop[2] += len(loop[0])
2480 elif loop[2] > len(loop[0]) - 1:
2481 loop[2] -= len(loop[0])
2482 # trim
2483 start = max(0, loop[2] - before_start)
2484 end = min(len(loop[0]), loop[2] + after_start + 1)
2485 trimmed_loops.append([loop[0][start:end], False])
2487 return(trimmed_loops)
2490 # project knots on non-selected geometry
2491 def curve_project_knots(bm_mod, verts_selected, knots, points, circular):
2492 # function to project vertex on edge
2493 def project(v1, v2, v3):
2494 # v1 and v2 are part of a line
2495 # v3 is projected onto it
2496 v2 -= v1
2497 v3 -= v1
2498 p = v3.project(v2)
2499 return(p + v1)
2501 if circular: # project all knots
2502 start = 0
2503 end = len(knots)
2504 pknots = []
2505 else: # first and last knot shouldn't be projected
2506 start = 1
2507 end = -1
2508 pknots = [mathutils.Vector(bm_mod.verts[knots[0]].co[:])]
2509 for knot in knots[start:end]:
2510 if knot in verts_selected:
2511 knot_left = knot_right = False
2512 for i in range(points.index(knot) - 1, -1 * len(points), -1):
2513 if points[i] not in knots:
2514 knot_left = points[i]
2515 break
2516 for i in range(points.index(knot) + 1, 2 * len(points)):
2517 if i > len(points) - 1:
2518 i -= len(points)
2519 if points[i] not in knots:
2520 knot_right = points[i]
2521 break
2522 if knot_left and knot_right and knot_left != knot_right:
2523 knot_left = mathutils.Vector(bm_mod.verts[knot_left].co[:])
2524 knot_right = mathutils.Vector(bm_mod.verts[knot_right].co[:])
2525 knot = mathutils.Vector(bm_mod.verts[knot].co[:])
2526 pknots.append(project(knot_left, knot_right, knot))
2527 else:
2528 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2529 else: # knot isn't selected, so shouldn't be changed
2530 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2531 if not circular:
2532 pknots.append(mathutils.Vector(bm_mod.verts[knots[-1]].co[:]))
2534 return(pknots)
2537 # find all loops through a given vertex
2538 def curve_vertex_loops(bm_mod, start_vert, vert_edges, edge_faces):
2539 edges_used = []
2540 loops = []
2542 for edge in vert_edges[start_vert]:
2543 if edge in edges_used:
2544 continue
2545 loop = []
2546 circular = False
2547 for vert in edge:
2548 active_faces = edge_faces[edge]
2549 new_vert = vert
2550 growing = True
2551 while growing:
2552 growing = False
2553 new_edges = vert_edges[new_vert]
2554 loop.append(new_vert)
2555 if len(loop) > 1:
2556 edges_used.append(tuple(sorted([loop[-1], loop[-2]])))
2557 if len(new_edges) < 3 or len(new_edges) > 4:
2558 # pole
2559 break
2560 else:
2561 # find next edge
2562 for new_edge in new_edges:
2563 if new_edge in edges_used:
2564 continue
2565 eliminate = False
2566 for new_face in edge_faces[new_edge]:
2567 if new_face in active_faces:
2568 eliminate = True
2569 break
2570 if eliminate:
2571 continue
2572 # found correct new edge
2573 active_faces = edge_faces[new_edge]
2574 v1, v2 = new_edge
2575 if v1 != new_vert:
2576 new_vert = v1
2577 else:
2578 new_vert = v2
2579 if new_vert == loop[0]:
2580 circular = True
2581 else:
2582 growing = True
2583 break
2584 if circular:
2585 break
2586 loop.reverse()
2587 loops.append([loop, circular])
2589 return(loops)
2592 # ########################################
2593 # ##### Flatten functions ################
2594 # ########################################
2596 # sort input into loops
2597 def flatten_get_input(bm):
2598 vert_verts = dict_vert_verts(
2599 [edgekey(edge) for edge in bm.edges if edge.select and not edge.hide]
2601 verts = [v.index for v in bm.verts if v.select and not v.hide]
2603 # no connected verts, consider all selected verts as a single input
2604 if not vert_verts:
2605 return([[verts, False]])
2607 loops = []
2608 while len(verts) > 0:
2609 # start of loop
2610 loop = [verts[0]]
2611 verts.pop(0)
2612 if loop[-1] in vert_verts:
2613 to_grow = vert_verts[loop[-1]]
2614 else:
2615 to_grow = []
2616 # grow loop
2617 while len(to_grow) > 0:
2618 new_vert = to_grow[0]
2619 to_grow.pop(0)
2620 if new_vert in loop:
2621 continue
2622 loop.append(new_vert)
2623 verts.remove(new_vert)
2624 to_grow += vert_verts[new_vert]
2625 # add loop to loops
2626 loops.append([loop, False])
2628 return(loops)
2631 # calculate position of vertex projections on plane
2632 def flatten_project(bm, loop, com, normal):
2633 verts = [bm.verts[v] for v in loop[0]]
2634 verts_projected = [
2635 [v.index, mathutils.Vector(v.co[:]) -
2636 (mathutils.Vector(v.co[:]) - com).dot(normal) * normal] for v in verts
2639 return(verts_projected)
2642 # ########################################
2643 # ##### Gstretch functions ###############
2644 # ########################################
2646 # fake stroke class, used to create custom strokes if no GP data is found
2647 class gstretch_fake_stroke():
2648 def __init__(self, points):
2649 self.points = [gstretch_fake_stroke_point(p) for p in points]
2652 # fake stroke point class, used in fake strokes
2653 class gstretch_fake_stroke_point():
2654 def __init__(self, loc):
2655 self.co = loc
2658 # flips loops, if necessary, to obtain maximum alignment to stroke
2659 def gstretch_align_pairs(ls_pairs, object, bm_mod, method):
2660 # returns total distance between all verts in loop and corresponding stroke
2661 def distance_loop_stroke(loop, stroke, object, bm_mod, method):
2662 stroke_lengths_cache = False
2663 loop_length = len(loop[0])
2664 total_distance = 0
2666 if method != 'regular':
2667 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2669 for i, v_index in enumerate(loop[0]):
2670 if method == 'regular':
2671 relative_distance = i / (loop_length - 1)
2672 else:
2673 relative_distance = relative_lengths[i]
2675 loc1 = object.matrix_world @ bm_mod.verts[v_index].co
2676 loc2, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2677 relative_distance, stroke_lengths_cache)
2678 total_distance += (loc2 - loc1).length
2680 return(total_distance)
2682 if ls_pairs:
2683 for (loop, stroke) in ls_pairs:
2684 total_dist = distance_loop_stroke(loop, stroke, object, bm_mod,
2685 method)
2686 loop[0].reverse()
2687 total_dist_rev = distance_loop_stroke(loop, stroke, object, bm_mod,
2688 method)
2689 if total_dist_rev > total_dist:
2690 loop[0].reverse()
2692 return(ls_pairs)
2695 # calculate vertex positions on stroke
2696 def gstretch_calculate_verts(loop, stroke, object, bm_mod, method):
2697 move = []
2698 stroke_lengths_cache = False
2699 loop_length = len(loop[0])
2700 matrix_inverse = object.matrix_world.inverted()
2702 # return intersection of line with stroke, or None
2703 def intersect_line_stroke(vec1, vec2, stroke):
2704 for i, p in enumerate(stroke.points[1:]):
2705 intersections = mathutils.geometry.intersect_line_line(vec1, vec2,
2706 p.co, stroke.points[i].co)
2707 if intersections and \
2708 (intersections[0] - intersections[1]).length < 1e-2:
2709 x, dist = mathutils.geometry.intersect_point_line(
2710 intersections[0], p.co, stroke.points[i].co)
2711 if -1 < dist < 1:
2712 return(intersections[0])
2713 return(None)
2715 if method == 'project':
2716 vert_edges = dict_vert_edges(bm_mod)
2718 for v_index in loop[0]:
2719 intersection = None
2720 for ek in vert_edges[v_index]:
2721 v1, v2 = ek
2722 v1 = bm_mod.verts[v1]
2723 v2 = bm_mod.verts[v2]
2724 if v1.select + v2.select == 1 and not v1.hide and not v2.hide:
2725 vec1 = object.matrix_world @ v1.co
2726 vec2 = object.matrix_world @ v2.co
2727 intersection = intersect_line_stroke(vec1, vec2, stroke)
2728 if intersection:
2729 break
2730 if not intersection:
2731 v = bm_mod.verts[v_index]
2732 intersection = intersect_line_stroke(v.co, v.co + v.normal,
2733 stroke)
2734 if intersection:
2735 move.append([v_index, matrix_inverse @ intersection])
2737 else:
2738 if method == 'irregular':
2739 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2741 for i, v_index in enumerate(loop[0]):
2742 if method == 'regular':
2743 relative_distance = i / (loop_length - 1)
2744 else: # method == 'irregular'
2745 relative_distance = relative_lengths[i]
2746 loc, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2747 relative_distance, stroke_lengths_cache)
2748 loc = matrix_inverse @ loc
2749 move.append([v_index, loc])
2751 return(move)
2754 # create new vertices, based on GP strokes
2755 def gstretch_create_verts(object, bm_mod, strokes, method, conversion,
2756 conversion_distance, conversion_max, conversion_min, conversion_vertices):
2757 move = []
2758 stroke_verts = []
2759 mat_world = object.matrix_world.inverted()
2760 singles = gstretch_match_single_verts(bm_mod, strokes, mat_world)
2762 for stroke in strokes:
2763 stroke_verts.append([stroke, []])
2764 min_end_point = 0
2765 if conversion == 'vertices':
2766 min_end_point = conversion_vertices
2767 end_point = conversion_vertices
2768 elif conversion == 'limit_vertices':
2769 min_end_point = conversion_min
2770 end_point = conversion_max
2771 else:
2772 end_point = len(stroke.points)
2773 # creation of new vertices at fixed user-defined distances
2774 if conversion == 'distance':
2775 method = 'project'
2776 prev_point = stroke.points[0]
2777 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world @ prev_point.co))
2778 distance = 0
2779 limit = conversion_distance
2780 for point in stroke.points:
2781 new_distance = distance + (point.co - prev_point.co).length
2782 iteration = 0
2783 while new_distance > limit:
2784 to_cover = limit - distance + (limit * iteration)
2785 new_loc = prev_point.co + to_cover * \
2786 (point.co - prev_point.co).normalized()
2787 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * new_loc))
2788 new_distance -= limit
2789 iteration += 1
2790 distance = new_distance
2791 prev_point = point
2792 # creation of new vertices for other methods
2793 else:
2794 # add vertices at stroke points
2795 for point in stroke.points[:end_point]:
2796 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world @ point.co))
2797 # add more vertices, beyond the points that are available
2798 if min_end_point > min(len(stroke.points), end_point):
2799 for i in range(min_end_point -
2800 (min(len(stroke.points), end_point))):
2801 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world @ point.co))
2802 # force even spreading of points, so they are placed on stroke
2803 method = 'regular'
2804 bm_mod.verts.ensure_lookup_table()
2805 bm_mod.verts.index_update()
2806 for stroke, verts_seq in stroke_verts:
2807 if len(verts_seq) < 2:
2808 continue
2809 # spread vertices evenly over the stroke
2810 if method == 'regular':
2811 loop = [[vert.index for vert in verts_seq], False]
2812 move += gstretch_calculate_verts(loop, stroke, object, bm_mod,
2813 method)
2814 # create edges
2815 for i, vert in enumerate(verts_seq):
2816 if i > 0:
2817 bm_mod.edges.new((verts_seq[i - 1], verts_seq[i]))
2818 vert.select = True
2819 # connect single vertices to the closest stroke
2820 if singles:
2821 for vert, m_stroke, point in singles:
2822 if m_stroke != stroke:
2823 continue
2824 bm_mod.edges.new((vert, verts_seq[point]))
2825 bm_mod.edges.ensure_lookup_table()
2826 bmesh.update_edit_mesh(object.data)
2828 return(move)
2831 # erases the grease pencil stroke
2832 def gstretch_erase_stroke(stroke, context):
2833 # change 3d coordinate into a stroke-point
2834 def sp(loc, context):
2835 lib = {'name': "",
2836 'pen_flip': False,
2837 'is_start': False,
2838 'location': (0, 0, 0),
2839 'mouse': (
2840 view3d_utils.location_3d_to_region_2d(
2841 context.region, context.space_data.region_3d, loc)
2843 'pressure': 1,
2844 'size': 0,
2845 'time': 0}
2846 return(lib)
2848 if type(stroke) != bpy.types.GPencilStroke:
2849 # fake stroke, there is nothing to delete
2850 return
2852 erase_stroke = [sp(p.co, context) for p in stroke.points]
2853 if erase_stroke:
2854 erase_stroke[0]['is_start'] = True
2855 #bpy.ops.gpencil.draw(mode='ERASER', stroke=erase_stroke)
2856 bpy.ops.gpencil.data_unlink()
2860 # get point on stroke, given by relative distance (0.0 - 1.0)
2861 def gstretch_eval_stroke(stroke, distance, stroke_lengths_cache=False):
2862 # use cache if available
2863 if not stroke_lengths_cache:
2864 lengths = [0]
2865 for i, p in enumerate(stroke.points[1:]):
2866 lengths.append((p.co - stroke.points[i].co).length + lengths[-1])
2867 total_length = max(lengths[-1], 1e-7)
2868 stroke_lengths_cache = [length / total_length for length in
2869 lengths]
2870 stroke_lengths = stroke_lengths_cache[:]
2872 if distance in stroke_lengths:
2873 loc = stroke.points[stroke_lengths.index(distance)].co
2874 elif distance > stroke_lengths[-1]:
2875 # should be impossible, but better safe than sorry
2876 loc = stroke.points[-1].co
2877 else:
2878 stroke_lengths.append(distance)
2879 stroke_lengths.sort()
2880 stroke_index = stroke_lengths.index(distance)
2881 interval_length = stroke_lengths[
2882 stroke_index + 1] - stroke_lengths[stroke_index - 1
2884 distance_relative = (distance - stroke_lengths[stroke_index - 1]) / interval_length
2885 interval_vector = stroke.points[stroke_index].co - stroke.points[stroke_index - 1].co
2886 loc = stroke.points[stroke_index - 1].co + distance_relative * interval_vector
2888 return(loc, stroke_lengths_cache)
2891 # create fake grease pencil strokes for the active object
2892 def gstretch_get_fake_strokes(object, bm_mod, loops):
2893 strokes = []
2894 for loop in loops:
2895 p1 = object.matrix_world @ bm_mod.verts[loop[0][0]].co
2896 p2 = object.matrix_world @ bm_mod.verts[loop[0][-1]].co
2897 strokes.append(gstretch_fake_stroke([p1, p2]))
2899 return(strokes)
2901 # get strokes
2902 def gstretch_get_strokes(self, context):
2903 looptools = context.window_manager.looptools
2904 gp = get_strokes(self, context)
2905 if not gp:
2906 return(None)
2907 if looptools.gstretch_use_guide == "Annotation":
2908 layer = bpy.data.grease_pencils[0].layers.active
2909 if looptools.gstretch_use_guide == "GPencil" and not looptools.gstretch_guide == None:
2910 layer = looptools.gstretch_guide.data.layers.active
2911 if not layer:
2912 return(None)
2913 frame = layer.active_frame
2914 if not frame:
2915 return(None)
2916 strokes = frame.strokes
2917 if len(strokes) < 1:
2918 return(None)
2920 return(strokes)
2922 # returns a list with loop-stroke pairs
2923 def gstretch_match_loops_strokes(loops, strokes, object, bm_mod):
2924 if not loops or not strokes:
2925 return(None)
2927 # calculate loop centers
2928 loop_centers = []
2929 bm_mod.verts.ensure_lookup_table()
2930 for loop in loops:
2931 center = mathutils.Vector()
2932 for v_index in loop[0]:
2933 center += bm_mod.verts[v_index].co
2934 center /= len(loop[0])
2935 center = object.matrix_world @ center
2936 loop_centers.append([center, loop])
2938 # calculate stroke centers
2939 stroke_centers = []
2940 for stroke in strokes:
2941 center = mathutils.Vector()
2942 for p in stroke.points:
2943 center += p.co
2944 center /= len(stroke.points)
2945 stroke_centers.append([center, stroke, 0])
2947 # match, first by stroke use count, then by distance
2948 ls_pairs = []
2949 for lc in loop_centers:
2950 distances = []
2951 for i, sc in enumerate(stroke_centers):
2952 distances.append([sc[2], (lc[0] - sc[0]).length, i])
2953 distances.sort()
2954 best_stroke = distances[0][2]
2955 ls_pairs.append([lc[1], stroke_centers[best_stroke][1]])
2956 stroke_centers[best_stroke][2] += 1 # increase stroke use count
2958 return(ls_pairs)
2961 # match single selected vertices to the closest stroke endpoint
2962 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2963 def gstretch_match_single_verts(bm_mod, strokes, mat_world):
2964 # calculate stroke endpoints in object space
2965 endpoints = []
2966 for stroke in strokes:
2967 endpoints.append((mat_world @ stroke.points[0].co, stroke, 0))
2968 endpoints.append((mat_world @ stroke.points[-1].co, stroke, -1))
2970 distances = []
2971 # find single vertices (not connected to other selected verts)
2972 for vert in bm_mod.verts:
2973 if not vert.select:
2974 continue
2975 single = True
2976 for edge in vert.link_edges:
2977 if edge.other_vert(vert).select:
2978 single = False
2979 break
2980 if not single:
2981 continue
2982 # calculate distances from vertex to endpoints
2983 distance = [((vert.co - loc).length, vert, stroke, stroke_point,
2984 endpoint_index) for endpoint_index, (loc, stroke, stroke_point) in
2985 enumerate(endpoints)]
2986 distance.sort()
2987 distances.append(distance[0])
2989 # create matches, based on shortest distance first
2990 singles = []
2991 while distances:
2992 distances.sort()
2993 singles.append((distances[0][1], distances[0][2], distances[0][3]))
2994 endpoints.pop(distances[0][4])
2995 distances.pop(0)
2996 distances_new = []
2997 for (i, vert, j, k, l) in distances:
2998 distance_new = [((vert.co - loc).length, vert, stroke, stroke_point,
2999 endpoint_index) for endpoint_index, (loc, stroke,
3000 stroke_point) in enumerate(endpoints)]
3001 distance_new.sort()
3002 distances_new.append(distance_new[0])
3003 distances = distances_new
3005 return(singles)
3008 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
3009 def gstretch_relative_lengths(loop, bm_mod):
3010 lengths = [0]
3011 for i, v_index in enumerate(loop[0][1:]):
3012 lengths.append(
3013 (bm_mod.verts[v_index].co -
3014 bm_mod.verts[loop[0][i]].co).length + lengths[-1]
3016 total_length = max(lengths[-1], 1e-7)
3017 relative_lengths = [length / total_length for length in
3018 lengths]
3020 return(relative_lengths)
3023 # convert cache-stored strokes into usable (fake) GP strokes
3024 def gstretch_safe_to_true_strokes(safe_strokes):
3025 strokes = []
3026 for safe_stroke in safe_strokes:
3027 strokes.append(gstretch_fake_stroke(safe_stroke))
3029 return(strokes)
3032 # convert a GP stroke into a list of points which can be stored in cache
3033 def gstretch_true_to_safe_strokes(strokes):
3034 safe_strokes = []
3035 for stroke in strokes:
3036 safe_strokes.append([p.co.copy() for p in stroke.points])
3038 return(safe_strokes)
3041 # force consistency in GUI, max value can never be lower than min value
3042 def gstretch_update_max(self, context):
3043 # called from operator settings (after execution)
3044 if 'conversion_min' in self.keys():
3045 if self.conversion_min > self.conversion_max:
3046 self.conversion_max = self.conversion_min
3047 # called from toolbar
3048 else:
3049 lt = context.window_manager.looptools
3050 if lt.gstretch_conversion_min > lt.gstretch_conversion_max:
3051 lt.gstretch_conversion_max = lt.gstretch_conversion_min
3054 # force consistency in GUI, min value can never be higher than max value
3055 def gstretch_update_min(self, context):
3056 # called from operator settings (after execution)
3057 if 'conversion_max' in self.keys():
3058 if self.conversion_max < self.conversion_min:
3059 self.conversion_min = self.conversion_max
3060 # called from toolbar
3061 else:
3062 lt = context.window_manager.looptools
3063 if lt.gstretch_conversion_max < lt.gstretch_conversion_min:
3064 lt.gstretch_conversion_min = lt.gstretch_conversion_max
3067 # ########################################
3068 # ##### Relax functions ##################
3069 # ########################################
3071 # create lists with knots and points, all correctly sorted
3072 def relax_calculate_knots(loops):
3073 all_knots = []
3074 all_points = []
3075 for loop, circular in loops:
3076 knots = [[], []]
3077 points = [[], []]
3078 if circular:
3079 if len(loop) % 2 == 1: # odd
3080 extend = [False, True, 0, 1, 0, 1]
3081 else: # even
3082 extend = [True, False, 0, 1, 1, 2]
3083 else:
3084 if len(loop) % 2 == 1: # odd
3085 extend = [False, False, 0, 1, 1, 2]
3086 else: # even
3087 extend = [False, False, 0, 1, 1, 2]
3088 for j in range(2):
3089 if extend[j]:
3090 loop = [loop[-1]] + loop + [loop[0]]
3091 for i in range(extend[2 + 2 * j], len(loop), 2):
3092 knots[j].append(loop[i])
3093 for i in range(extend[3 + 2 * j], len(loop), 2):
3094 if loop[i] == loop[-1] and not circular:
3095 continue
3096 if len(points[j]) == 0:
3097 points[j].append(loop[i])
3098 elif loop[i] != points[j][0]:
3099 points[j].append(loop[i])
3100 if circular:
3101 if knots[j][0] != knots[j][-1]:
3102 knots[j].append(knots[j][0])
3103 if len(points[1]) == 0:
3104 knots.pop(1)
3105 points.pop(1)
3106 for k in knots:
3107 all_knots.append(k)
3108 for p in points:
3109 all_points.append(p)
3111 return(all_knots, all_points)
3114 # calculate relative positions compared to first knot
3115 def relax_calculate_t(bm_mod, knots, points, regular):
3116 all_tknots = []
3117 all_tpoints = []
3118 for i in range(len(knots)):
3119 amount = len(knots[i]) + len(points[i])
3120 mix = []
3121 for j in range(amount):
3122 if j % 2 == 0:
3123 mix.append([True, knots[i][round(j / 2)]])
3124 elif j == amount - 1:
3125 mix.append([True, knots[i][-1]])
3126 else:
3127 mix.append([False, points[i][int(j / 2)]])
3128 len_total = 0
3129 loc_prev = False
3130 tknots = []
3131 tpoints = []
3132 for m in mix:
3133 loc = mathutils.Vector(bm_mod.verts[m[1]].co[:])
3134 if not loc_prev:
3135 loc_prev = loc
3136 len_total += (loc - loc_prev).length
3137 if m[0]:
3138 tknots.append(len_total)
3139 else:
3140 tpoints.append(len_total)
3141 loc_prev = loc
3142 if regular:
3143 tpoints = []
3144 for p in range(len(points[i])):
3145 tpoints.append((tknots[p] + tknots[p + 1]) / 2)
3146 all_tknots.append(tknots)
3147 all_tpoints.append(tpoints)
3149 return(all_tknots, all_tpoints)
3152 # change the location of the points to their place on the spline
3153 def relax_calculate_verts(bm_mod, interpolation, tknots, knots, tpoints,
3154 points, splines):
3155 change = []
3156 move = []
3157 for i in range(len(knots)):
3158 for p in points[i]:
3159 m = tpoints[i][points[i].index(p)]
3160 if m in tknots[i]:
3161 n = tknots[i].index(m)
3162 else:
3163 t = tknots[i][:]
3164 t.append(m)
3165 t.sort()
3166 n = t.index(m) - 1
3167 if n > len(splines[i]) - 1:
3168 n = len(splines[i]) - 1
3169 elif n < 0:
3170 n = 0
3172 if interpolation == 'cubic':
3173 ax, bx, cx, dx, tx = splines[i][n][0]
3174 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
3175 ay, by, cy, dy, ty = splines[i][n][1]
3176 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
3177 az, bz, cz, dz, tz = splines[i][n][2]
3178 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
3179 change.append([p, mathutils.Vector([x, y, z])])
3180 else: # interpolation == 'linear'
3181 a, d, t, u = splines[i][n]
3182 if u == 0:
3183 u = 1e-8
3184 change.append([p, ((m - t) / u) * d + a])
3185 for c in change:
3186 move.append([c[0], (bm_mod.verts[c[0]].co + c[1]) / 2])
3188 return(move)
3191 # ########################################
3192 # ##### Space functions ##################
3193 # ########################################
3195 # calculate relative positions compared to first knot
3196 def space_calculate_t(bm_mod, knots):
3197 tknots = []
3198 loc_prev = False
3199 len_total = 0
3200 for k in knots:
3201 loc = mathutils.Vector(bm_mod.verts[k].co[:])
3202 if not loc_prev:
3203 loc_prev = loc
3204 len_total += (loc - loc_prev).length
3205 tknots.append(len_total)
3206 loc_prev = loc
3207 amount = len(knots)
3208 t_per_segment = len_total / (amount - 1)
3209 tpoints = [i * t_per_segment for i in range(amount)]
3211 return(tknots, tpoints)
3214 # change the location of the points to their place on the spline
3215 def space_calculate_verts(bm_mod, interpolation, tknots, tpoints, points,
3216 splines):
3217 move = []
3218 for p in points:
3219 m = tpoints[points.index(p)]
3220 if m in tknots:
3221 n = tknots.index(m)
3222 else:
3223 t = tknots[:]
3224 t.append(m)
3225 t.sort()
3226 n = t.index(m) - 1
3227 if n > len(splines) - 1:
3228 n = len(splines) - 1
3229 elif n < 0:
3230 n = 0
3232 if interpolation == 'cubic':
3233 ax, bx, cx, dx, tx = splines[n][0]
3234 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
3235 ay, by, cy, dy, ty = splines[n][1]
3236 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
3237 az, bz, cz, dz, tz = splines[n][2]
3238 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
3239 move.append([p, mathutils.Vector([x, y, z])])
3240 else: # interpolation == 'linear'
3241 a, d, t, u = splines[n]
3242 move.append([p, ((m - t) / u) * d + a])
3244 return(move)
3247 # ########################################
3248 # ##### Operators ########################
3249 # ########################################
3251 # bridge operator
3252 class Bridge(Operator):
3253 bl_idname = 'mesh.looptools_bridge'
3254 bl_label = "Bridge / Loft"
3255 bl_description = "Bridge two, or loft several, loops of vertices"
3256 bl_options = {'REGISTER', 'UNDO'}
3258 cubic_strength: FloatProperty(
3259 name="Strength",
3260 description="Higher strength results in more fluid curves",
3261 default=1.0,
3262 soft_min=-3.0,
3263 soft_max=3.0
3265 interpolation: EnumProperty(
3266 name="Interpolation mode",
3267 items=(('cubic', "Cubic", "Gives curved results"),
3268 ('linear', "Linear", "Basic, fast, straight interpolation")),
3269 description="Interpolation mode: algorithm used when creating "
3270 "segments",
3271 default='cubic'
3273 loft: BoolProperty(
3274 name="Loft",
3275 description="Loft multiple loops, instead of considering them as "
3276 "a multi-input for bridging",
3277 default=False
3279 loft_loop: BoolProperty(
3280 name="Loop",
3281 description="Connect the first and the last loop with each other",
3282 default=False
3284 min_width: IntProperty(
3285 name="Minimum width",
3286 description="Segments with an edge smaller than this are merged "
3287 "(compared to base edge)",
3288 default=0,
3289 min=0,
3290 max=100,
3291 subtype='PERCENTAGE'
3293 mode: EnumProperty(
3294 name="Mode",
3295 items=(('basic', "Basic", "Fast algorithm"),
3296 ('shortest', "Shortest edge", "Slower algorithm with better vertex matching")),
3297 description="Algorithm used for bridging",
3298 default='shortest'
3300 remove_faces: BoolProperty(
3301 name="Remove faces",
3302 description="Remove faces that are internal after bridging",
3303 default=True
3305 reverse: BoolProperty(
3306 name="Reverse",
3307 description="Manually override the direction in which the loops "
3308 "are bridged. Only use if the tool gives the wrong result",
3309 default=False
3311 segments: IntProperty(
3312 name="Segments",
3313 description="Number of segments used to bridge the gap (0=automatic)",
3314 default=1,
3315 min=0,
3316 soft_max=20
3318 twist: IntProperty(
3319 name="Twist",
3320 description="Twist what vertices are connected to each other",
3321 default=0
3324 @classmethod
3325 def poll(cls, context):
3326 ob = context.active_object
3327 return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3329 def draw(self, context):
3330 layout = self.layout
3331 # layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3333 # top row
3334 col_top = layout.column(align=True)
3335 row = col_top.row(align=True)
3336 col_left = row.column(align=True)
3337 col_right = row.column(align=True)
3338 col_right.active = self.segments != 1
3339 col_left.prop(self, "segments")
3340 col_right.prop(self, "min_width", text="")
3341 # bottom row
3342 bottom_left = col_left.row()
3343 bottom_left.active = self.segments != 1
3344 bottom_left.prop(self, "interpolation", text="")
3345 bottom_right = col_right.row()
3346 bottom_right.active = self.interpolation == 'cubic'
3347 bottom_right.prop(self, "cubic_strength")
3348 # boolean properties
3349 col_top.prop(self, "remove_faces")
3350 if self.loft:
3351 col_top.prop(self, "loft_loop")
3353 # override properties
3354 col_top.separator()
3355 row = layout.row(align=True)
3356 row.prop(self, "twist")
3357 row.prop(self, "reverse")
3359 def invoke(self, context, event):
3360 # load custom settings
3361 context.window_manager.looptools.bridge_loft = self.loft
3362 settings_load(self)
3363 return self.execute(context)
3365 def execute(self, context):
3366 # initialise
3367 object, bm = initialise()
3368 edge_faces, edgekey_to_edge, old_selected_faces, smooth = \
3369 bridge_initialise(bm, self.interpolation)
3370 settings_write(self)
3372 # check cache to see if we can save time
3373 input_method = bridge_input_method(self.loft, self.loft_loop)
3374 cached, single_loops, loops, derived, mapping = cache_read("Bridge",
3375 object, bm, input_method, False)
3376 if not cached:
3377 # get loops
3378 loops = bridge_get_input(bm)
3379 if loops:
3380 # reorder loops if there are more than 2
3381 if len(loops) > 2:
3382 if self.loft:
3383 loops = bridge_sort_loops(bm, loops, self.loft_loop)
3384 else:
3385 loops = bridge_match_loops(bm, loops)
3387 # saving cache for faster execution next time
3388 if not cached:
3389 cache_write("Bridge", object, bm, input_method, False, False,
3390 loops, False, False)
3392 if loops:
3393 # calculate new geometry
3394 vertices = []
3395 faces = []
3396 max_vert_index = len(bm.verts) - 1
3397 for i in range(1, len(loops)):
3398 if not self.loft and i % 2 == 0:
3399 continue
3400 lines = bridge_calculate_lines(bm, loops[i - 1:i + 1],
3401 self.mode, self.twist, self.reverse)
3402 vertex_normals = bridge_calculate_virtual_vertex_normals(bm,
3403 lines, loops[i - 1:i + 1], edge_faces, edgekey_to_edge)
3404 segments = bridge_calculate_segments(bm, lines,
3405 loops[i - 1:i + 1], self.segments)
3406 new_verts, new_faces, max_vert_index = \
3407 bridge_calculate_geometry(
3408 bm, lines, vertex_normals,
3409 segments, self.interpolation, self.cubic_strength,
3410 self.min_width, max_vert_index
3412 if new_verts:
3413 vertices += new_verts
3414 if new_faces:
3415 faces += new_faces
3416 # make sure faces in loops that aren't used, aren't removed
3417 if self.remove_faces and old_selected_faces:
3418 bridge_save_unused_faces(bm, old_selected_faces, loops)
3419 # create vertices
3420 if vertices:
3421 bridge_create_vertices(bm, vertices)
3422 # create faces
3423 if faces:
3424 new_faces = bridge_create_faces(object, bm, faces, self.twist)
3425 old_selected_faces = [
3426 i for i, face in enumerate(bm.faces) if face.index in old_selected_faces
3427 ] # updating list
3428 bridge_select_new_faces(new_faces, smooth)
3429 # edge-data could have changed, can't use cache next run
3430 if faces and not vertices:
3431 cache_delete("Bridge")
3432 # delete internal faces
3433 if self.remove_faces and old_selected_faces:
3434 bridge_remove_internal_faces(bm, old_selected_faces)
3435 # make sure normals are facing outside
3436 bmesh.update_edit_mesh(object.data, loop_triangles=False,
3437 destructive=True)
3438 bpy.ops.mesh.normals_make_consistent()
3440 # cleaning up
3441 terminate()
3443 return{'FINISHED'}
3446 # circle operator
3447 class Circle(Operator):
3448 bl_idname = "mesh.looptools_circle"
3449 bl_label = "Circle"
3450 bl_description = "Move selected vertices into a circle shape"
3451 bl_options = {'REGISTER', 'UNDO'}
3453 custom_radius: BoolProperty(
3454 name="Radius",
3455 description="Force a custom radius",
3456 default=False
3458 fit: EnumProperty(
3459 name="Method",
3460 items=(("best", "Best fit", "Non-linear least squares"),
3461 ("inside", "Fit inside", "Only move vertices towards the center")),
3462 description="Method used for fitting a circle to the vertices",
3463 default='best'
3465 flatten: BoolProperty(
3466 name="Flatten",
3467 description="Flatten the circle, instead of projecting it on the mesh",
3468 default=True
3470 influence: FloatProperty(
3471 name="Influence",
3472 description="Force of the tool",
3473 default=100.0,
3474 min=0.0,
3475 max=100.0,
3476 precision=1,
3477 subtype='PERCENTAGE'
3479 lock_x: BoolProperty(
3480 name="Lock X",
3481 description="Lock editing of the x-coordinate",
3482 default=False
3484 lock_y: BoolProperty(
3485 name="Lock Y",
3486 description="Lock editing of the y-coordinate",
3487 default=False
3489 lock_z: BoolProperty(name="Lock Z",
3490 description="Lock editing of the z-coordinate",
3491 default=False
3493 radius: FloatProperty(
3494 name="Radius",
3495 description="Custom radius for circle",
3496 default=1.0,
3497 min=0.0,
3498 soft_max=1000.0
3500 angle: FloatProperty(
3501 name="Angle",
3502 description="Rotate a circle by an angle",
3503 unit='ROTATION',
3504 default=math.radians(0.0),
3505 soft_min=math.radians(-360.0),
3506 soft_max=math.radians(360.0)
3508 regular: BoolProperty(
3509 name="Regular",
3510 description="Distribute vertices at constant distances along the circle",
3511 default=True
3514 @classmethod
3515 def poll(cls, context):
3516 ob = context.active_object
3517 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3519 def draw(self, context):
3520 layout = self.layout
3521 col = layout.column()
3523 col.prop(self, "fit")
3524 col.separator()
3526 col.prop(self, "flatten")
3527 row = col.row(align=True)
3528 row.prop(self, "custom_radius")
3529 row_right = row.row(align=True)
3530 row_right.active = self.custom_radius
3531 row_right.prop(self, "radius", text="")
3532 col.prop(self, "regular")
3533 col.prop(self, "angle")
3534 col.separator()
3536 col_move = col.column(align=True)
3537 row = col_move.row(align=True)
3538 if self.lock_x:
3539 row.prop(self, "lock_x", text="X", icon='LOCKED')
3540 else:
3541 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3542 if self.lock_y:
3543 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3544 else:
3545 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3546 if self.lock_z:
3547 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3548 else:
3549 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3550 col_move.prop(self, "influence")
3552 def invoke(self, context, event):
3553 # load custom settings
3554 settings_load(self)
3555 return self.execute(context)
3557 def execute(self, context):
3558 # initialise
3559 object, bm = initialise()
3560 settings_write(self)
3561 # check cache to see if we can save time
3562 cached, single_loops, loops, derived, mapping = cache_read("Circle",
3563 object, bm, False, False)
3564 if cached:
3565 derived, bm_mod = get_derived_bmesh(object, bm, False)
3566 else:
3567 # find loops
3568 derived, bm_mod, single_vertices, single_loops, loops = \
3569 circle_get_input(object, bm)
3570 mapping = get_mapping(derived, bm, bm_mod, single_vertices,
3571 False, loops)
3572 single_loops, loops = circle_check_loops(single_loops, loops,
3573 mapping, bm_mod)
3575 # saving cache for faster execution next time
3576 if not cached:
3577 cache_write("Circle", object, bm, False, False, single_loops,
3578 loops, derived, mapping)
3580 move = []
3581 for i, loop in enumerate(loops):
3582 # best fitting flat plane
3583 com, normal = calculate_plane(bm_mod, loop)
3584 # if circular, shift loop so we get a good starting vertex
3585 if loop[1]:
3586 loop = circle_shift_loop(bm_mod, loop, com)
3587 # flatten vertices on plane
3588 locs_2d, p, q = circle_3d_to_2d(bm_mod, loop, com, normal)
3589 # calculate circle
3590 if self.fit == 'best':
3591 x0, y0, r = circle_calculate_best_fit(locs_2d)
3592 else: # self.fit == 'inside'
3593 x0, y0, r = circle_calculate_min_fit(locs_2d)
3594 # radius override
3595 if self.custom_radius:
3596 r = self.radius / p.length
3597 # calculate positions on circle
3598 if self.regular:
3599 new_locs_2d = circle_project_regular(locs_2d[:], x0, y0, r, self.angle)
3600 else:
3601 new_locs_2d = circle_project_non_regular(locs_2d[:], x0, y0, r, self.angle)
3602 # take influence into account
3603 locs_2d = circle_influence_locs(locs_2d, new_locs_2d,
3604 self.influence)
3605 # calculate 3d positions of the created 2d input
3606 move.append(circle_calculate_verts(self.flatten, bm_mod,
3607 locs_2d, com, p, q, normal))
3608 # flatten single input vertices on plane defined by loop
3609 if self.flatten and single_loops:
3610 move.append(circle_flatten_singles(bm_mod, com, p, q,
3611 normal, single_loops[i]))
3613 # move vertices to new locations
3614 if self.lock_x or self.lock_y or self.lock_z:
3615 lock = [self.lock_x, self.lock_y, self.lock_z]
3616 else:
3617 lock = False
3618 move_verts(object, bm, mapping, move, lock, -1)
3620 # cleaning up
3621 if derived:
3622 bm_mod.free()
3623 terminate()
3625 return{'FINISHED'}
3628 # curve operator
3629 class Curve(Operator):
3630 bl_idname = "mesh.looptools_curve"
3631 bl_label = "Curve"
3632 bl_description = "Turn a loop into a smooth curve"
3633 bl_options = {'REGISTER', 'UNDO'}
3635 boundaries: BoolProperty(
3636 name="Boundaries",
3637 description="Limit the tool to work within the boundaries of the selected vertices",
3638 default=False
3640 influence: FloatProperty(
3641 name="Influence",
3642 description="Force of the tool",
3643 default=100.0,
3644 min=0.0,
3645 max=100.0,
3646 precision=1,
3647 subtype='PERCENTAGE'
3649 interpolation: EnumProperty(
3650 name="Interpolation",
3651 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
3652 ("linear", "Linear", "Simple and fast linear algorithm")),
3653 description="Algorithm used for interpolation",
3654 default='cubic'
3656 lock_x: BoolProperty(
3657 name="Lock X",
3658 description="Lock editing of the x-coordinate",
3659 default=False
3661 lock_y: BoolProperty(
3662 name="Lock Y",
3663 description="Lock editing of the y-coordinate",
3664 default=False
3666 lock_z: BoolProperty(
3667 name="Lock Z",
3668 description="Lock editing of the z-coordinate",
3669 default=False
3671 regular: BoolProperty(
3672 name="Regular",
3673 description="Distribute vertices at constant distances along the curve",
3674 default=True
3676 restriction: EnumProperty(
3677 name="Restriction",
3678 items=(("none", "None", "No restrictions on vertex movement"),
3679 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
3680 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
3681 description="Restrictions on how the vertices can be moved",
3682 default='none'
3685 @classmethod
3686 def poll(cls, context):
3687 ob = context.active_object
3688 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3690 def draw(self, context):
3691 layout = self.layout
3692 col = layout.column()
3694 col.prop(self, "interpolation")
3695 col.prop(self, "restriction")
3696 col.prop(self, "boundaries")
3697 col.prop(self, "regular")
3698 col.separator()
3700 col_move = col.column(align=True)
3701 row = col_move.row(align=True)
3702 if self.lock_x:
3703 row.prop(self, "lock_x", text="X", icon='LOCKED')
3704 else:
3705 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3706 if self.lock_y:
3707 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3708 else:
3709 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3710 if self.lock_z:
3711 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3712 else:
3713 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3714 col_move.prop(self, "influence")
3716 def invoke(self, context, event):
3717 # load custom settings
3718 settings_load(self)
3719 return self.execute(context)
3721 def execute(self, context):
3722 # initialise
3723 object, bm = initialise()
3724 settings_write(self)
3725 # check cache to see if we can save time
3726 cached, single_loops, loops, derived, mapping = cache_read("Curve",
3727 object, bm, False, self.boundaries)
3728 if cached:
3729 derived, bm_mod = get_derived_bmesh(object, bm, False)
3730 else:
3731 # find loops
3732 derived, bm_mod, loops = curve_get_input(object, bm, self.boundaries)
3733 mapping = get_mapping(derived, bm, bm_mod, False, True, loops)
3734 loops = check_loops(loops, mapping, bm_mod)
3735 verts_selected = [
3736 v.index for v in bm_mod.verts if v.select and not v.hide
3739 # saving cache for faster execution next time
3740 if not cached:
3741 cache_write("Curve", object, bm, False, self.boundaries, False,
3742 loops, derived, mapping)
3744 move = []
3745 for loop in loops:
3746 knots, points = curve_calculate_knots(loop, verts_selected)
3747 pknots = curve_project_knots(bm_mod, verts_selected, knots,
3748 points, loop[1])
3749 tknots, tpoints = curve_calculate_t(bm_mod, knots, points,
3750 pknots, self.regular, loop[1])
3751 splines = calculate_splines(self.interpolation, bm_mod,
3752 tknots, knots)
3753 move.append(curve_calculate_vertices(bm_mod, knots, tknots,
3754 points, tpoints, splines, self.interpolation,
3755 self.restriction))
3757 # move vertices to new locations
3758 if self.lock_x or self.lock_y or self.lock_z:
3759 lock = [self.lock_x, self.lock_y, self.lock_z]
3760 else:
3761 lock = False
3762 move_verts(object, bm, mapping, move, lock, self.influence)
3764 # cleaning up
3765 if derived:
3766 bm_mod.free()
3767 terminate()
3769 return{'FINISHED'}
3772 # flatten operator
3773 class Flatten(Operator):
3774 bl_idname = "mesh.looptools_flatten"
3775 bl_label = "Flatten"
3776 bl_description = "Flatten vertices on a best-fitting plane"
3777 bl_options = {'REGISTER', 'UNDO'}
3779 influence: FloatProperty(
3780 name="Influence",
3781 description="Force of the tool",
3782 default=100.0,
3783 min=0.0,
3784 max=100.0,
3785 precision=1,
3786 subtype='PERCENTAGE'
3788 lock_x: BoolProperty(
3789 name="Lock X",
3790 description="Lock editing of the x-coordinate",
3791 default=False
3793 lock_y: BoolProperty(
3794 name="Lock Y",
3795 description="Lock editing of the y-coordinate",
3796 default=False
3798 lock_z: BoolProperty(name="Lock Z",
3799 description="Lock editing of the z-coordinate",
3800 default=False
3802 plane: EnumProperty(
3803 name="Plane",
3804 items=(("best_fit", "Best fit", "Calculate a best fitting plane"),
3805 ("normal", "Normal", "Derive plane from averaging vertex normals"),
3806 ("view", "View", "Flatten on a plane perpendicular to the viewing angle")),
3807 description="Plane on which vertices are flattened",
3808 default='best_fit'
3810 restriction: EnumProperty(
3811 name="Restriction",
3812 items=(("none", "None", "No restrictions on vertex movement"),
3813 ("bounding_box", "Bounding box", "Vertices are restricted to "
3814 "movement inside the bounding box of the selection")),
3815 description="Restrictions on how the vertices can be moved",
3816 default='none'
3819 @classmethod
3820 def poll(cls, context):
3821 ob = context.active_object
3822 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3824 def draw(self, context):
3825 layout = self.layout
3826 col = layout.column()
3828 col.prop(self, "plane")
3829 # col.prop(self, "restriction")
3830 col.separator()
3832 col_move = col.column(align=True)
3833 row = col_move.row(align=True)
3834 if self.lock_x:
3835 row.prop(self, "lock_x", text="X", icon='LOCKED')
3836 else:
3837 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3838 if self.lock_y:
3839 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3840 else:
3841 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3842 if self.lock_z:
3843 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3844 else:
3845 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3846 col_move.prop(self, "influence")
3848 def invoke(self, context, event):
3849 # load custom settings
3850 settings_load(self)
3851 return self.execute(context)
3853 def execute(self, context):
3854 # initialise
3855 object, bm = initialise()
3856 settings_write(self)
3857 # check cache to see if we can save time
3858 cached, single_loops, loops, derived, mapping = cache_read("Flatten",
3859 object, bm, False, False)
3860 if not cached:
3861 # order input into virtual loops
3862 loops = flatten_get_input(bm)
3863 loops = check_loops(loops, mapping, bm)
3865 # saving cache for faster execution next time
3866 if not cached:
3867 cache_write("Flatten", object, bm, False, False, False, loops,
3868 False, False)
3870 move = []
3871 for loop in loops:
3872 # calculate plane and position of vertices on them
3873 com, normal = calculate_plane(bm, loop, method=self.plane,
3874 object=object)
3875 to_move = flatten_project(bm, loop, com, normal)
3876 if self.restriction == 'none':
3877 move.append(to_move)
3878 else:
3879 move.append(to_move)
3881 # move vertices to new locations
3882 if self.lock_x or self.lock_y or self.lock_z:
3883 lock = [self.lock_x, self.lock_y, self.lock_z]
3884 else:
3885 lock = False
3886 move_verts(object, bm, False, move, lock, self.influence)
3888 # cleaning up
3889 terminate()
3891 return{'FINISHED'}
3894 # Annotation operator
3895 class RemoveAnnotation(Operator):
3896 bl_idname = "remove.annotation"
3897 bl_label = "Remove Annotation"
3898 bl_description = "Remove all Annotation Strokes"
3899 bl_options = {'REGISTER', 'UNDO'}
3901 def execute(self, context):
3903 try:
3904 bpy.data.grease_pencils[0].layers.active.clear()
3905 except:
3906 self.report({'INFO'}, "No Annotation data to Unlink")
3907 return {'CANCELLED'}
3909 return{'FINISHED'}
3911 # GPencil operator
3912 class RemoveGPencil(Operator):
3913 bl_idname = "remove.gp"
3914 bl_label = "Remove GPencil"
3915 bl_description = "Remove all GPencil Strokes"
3916 bl_options = {'REGISTER', 'UNDO'}
3918 def execute(self, context):
3920 try:
3921 looptools = context.window_manager.looptools
3922 looptools.gstretch_guide.data.layers.data.clear()
3923 looptools.gstretch_guide.data.update_tag()
3924 except:
3925 self.report({'INFO'}, "No GPencil data to Unlink")
3926 return {'CANCELLED'}
3928 return{'FINISHED'}
3931 class GStretch(Operator):
3932 bl_idname = "mesh.looptools_gstretch"
3933 bl_label = "Gstretch"
3934 bl_description = "Stretch selected vertices to active stroke"
3935 bl_options = {'REGISTER', 'UNDO'}
3937 conversion: EnumProperty(
3938 name="Conversion",
3939 items=(("distance", "Distance", "Set the distance between vertices "
3940 "of the converted stroke"),
3941 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
3942 "number of vertices that converted strokes will have"),
3943 ("vertices", "Exact vertices", "Set the exact number of vertices "
3944 "that converted strokes will have. Short strokes "
3945 "with few points may contain less vertices than this number."),
3946 ("none", "No simplification", "Convert each point "
3947 "to a vertex")),
3948 description="If strokes are converted to geometry, "
3949 "use this simplification method",
3950 default='limit_vertices'
3952 conversion_distance: FloatProperty(
3953 name="Distance",
3954 description="Absolute distance between vertices along the converted "
3955 " stroke",
3956 default=0.1,
3957 min=0.000001,
3958 soft_min=0.01,
3959 soft_max=100
3961 conversion_max: IntProperty(
3962 name="Max Vertices",
3963 description="Maximum number of vertices strokes will "
3964 "have, when they are converted to geomtery",
3965 default=32,
3966 min=3,
3967 soft_max=500,
3968 update=gstretch_update_min
3970 conversion_min: IntProperty(
3971 name="Min Vertices",
3972 description="Minimum number of vertices strokes will "
3973 "have, when they are converted to geomtery",
3974 default=8,
3975 min=3,
3976 soft_max=500,
3977 update=gstretch_update_max
3979 conversion_vertices: IntProperty(
3980 name="Vertices",
3981 description="Number of vertices strokes will "
3982 "have, when they are converted to geometry. If strokes have less "
3983 "points than required, the 'Spread evenly' method is used",
3984 default=32,
3985 min=3,
3986 soft_max=500
3988 delete_strokes: BoolProperty(
3989 name="Delete strokes",
3990 description="Remove strokes if they have been used."
3991 "WARNING: DOES NOT SUPPORT UNDO",
3992 default=False
3994 influence: FloatProperty(
3995 name="Influence",
3996 description="Force of the tool",
3997 default=100.0,
3998 min=0.0,
3999 max=100.0,
4000 precision=1,
4001 subtype='PERCENTAGE'
4003 lock_x: BoolProperty(
4004 name="Lock X",
4005 description="Lock editing of the x-coordinate",
4006 default=False
4008 lock_y: BoolProperty(
4009 name="Lock Y",
4010 description="Lock editing of the y-coordinate",
4011 default=False
4013 lock_z: BoolProperty(
4014 name="Lock Z",
4015 description="Lock editing of the z-coordinate",
4016 default=False
4018 method: EnumProperty(
4019 name="Method",
4020 items=(("project", "Project", "Project vertices onto the stroke, "
4021 "using vertex normals and connected edges"),
4022 ("irregular", "Spread", "Distribute vertices along the full "
4023 "stroke, retaining relative distances between the vertices"),
4024 ("regular", "Spread evenly", "Distribute vertices at regular "
4025 "distances along the full stroke")),
4026 description="Method of distributing the vertices over the "
4027 "stroke",
4028 default='regular'
4031 @classmethod
4032 def poll(cls, context):
4033 ob = context.active_object
4034 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4036 def draw(self, context):
4037 looptools = context.window_manager.looptools
4038 layout = self.layout
4039 col = layout.column()
4041 col.prop(self, "method")
4042 col.separator()
4044 col_conv = col.column(align=True)
4045 col_conv.prop(self, "conversion", text="")
4046 if self.conversion == 'distance':
4047 col_conv.prop(self, "conversion_distance")
4048 elif self.conversion == 'limit_vertices':
4049 row = col_conv.row(align=True)
4050 row.prop(self, "conversion_min", text="Min")
4051 row.prop(self, "conversion_max", text="Max")
4052 elif self.conversion == 'vertices':
4053 col_conv.prop(self, "conversion_vertices")
4054 col.separator()
4056 col_move = col.column(align=True)
4057 row = col_move.row(align=True)
4058 if self.lock_x:
4059 row.prop(self, "lock_x", text="X", icon='LOCKED')
4060 else:
4061 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
4062 if self.lock_y:
4063 row.prop(self, "lock_y", text="Y", icon='LOCKED')
4064 else:
4065 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
4066 if self.lock_z:
4067 row.prop(self, "lock_z", text="Z", icon='LOCKED')
4068 else:
4069 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
4070 col_move.prop(self, "influence")
4071 col.separator()
4072 if looptools.gstretch_use_guide == "Annotation":
4073 col.operator("remove.annotation", text="Delete annotation strokes")
4074 if looptools.gstretch_use_guide == "GPencil":
4075 col.operator("remove.gp", text="Delete GPencil strokes")
4077 def invoke(self, context, event):
4078 # flush cached strokes
4079 if 'Gstretch' in looptools_cache:
4080 looptools_cache['Gstretch']['single_loops'] = []
4081 # load custom settings
4082 settings_load(self)
4083 return self.execute(context)
4085 def execute(self, context):
4086 # initialise
4087 object, bm = initialise()
4088 settings_write(self)
4090 # check cache to see if we can save time
4091 cached, safe_strokes, loops, derived, mapping = cache_read("Gstretch",
4092 object, bm, False, False)
4093 if cached:
4094 straightening = False
4095 if safe_strokes:
4096 strokes = gstretch_safe_to_true_strokes(safe_strokes)
4097 # cached strokes were flushed (see operator's invoke function)
4098 elif get_strokes(self, context):
4099 strokes = gstretch_get_strokes(self, context)
4100 else:
4101 # straightening function (no GP) -> loops ignore modifiers
4102 straightening = True
4103 derived = False
4104 bm_mod = bm.copy()
4105 bm_mod.verts.ensure_lookup_table()
4106 bm_mod.edges.ensure_lookup_table()
4107 bm_mod.faces.ensure_lookup_table()
4108 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
4109 if not straightening:
4110 derived, bm_mod = get_derived_bmesh(object, bm, False)
4111 else:
4112 # get loops and strokes
4113 if get_strokes(self, context):
4114 # find loops
4115 derived, bm_mod, loops = get_connected_input(object, bm, False, input='selected')
4116 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4117 loops = check_loops(loops, mapping, bm_mod)
4118 # get strokes
4119 strokes = gstretch_get_strokes(self, context)
4120 else:
4121 # straightening function (no GP) -> loops ignore modifiers
4122 derived = False
4123 mapping = False
4124 bm_mod = bm.copy()
4125 bm_mod.verts.ensure_lookup_table()
4126 bm_mod.edges.ensure_lookup_table()
4127 bm_mod.faces.ensure_lookup_table()
4128 edge_keys = [
4129 edgekey(edge) for edge in bm_mod.edges if
4130 edge.select and not edge.hide
4132 loops = get_connected_selections(edge_keys)
4133 loops = check_loops(loops, mapping, bm_mod)
4134 # create fake strokes
4135 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
4137 # saving cache for faster execution next time
4138 if not cached:
4139 if strokes:
4140 safe_strokes = gstretch_true_to_safe_strokes(strokes)
4141 else:
4142 safe_strokes = []
4143 cache_write("Gstretch", object, bm, False, False,
4144 safe_strokes, loops, derived, mapping)
4146 # pair loops and strokes
4147 ls_pairs = gstretch_match_loops_strokes(loops, strokes, object, bm_mod)
4148 ls_pairs = gstretch_align_pairs(ls_pairs, object, bm_mod, self.method)
4150 move = []
4151 if not loops:
4152 # no selected geometry, convert GP to verts
4153 if strokes:
4154 move.append(gstretch_create_verts(object, bm, strokes,
4155 self.method, self.conversion, self.conversion_distance,
4156 self.conversion_max, self.conversion_min,
4157 self.conversion_vertices))
4158 for stroke in strokes:
4159 gstretch_erase_stroke(stroke, context)
4160 elif ls_pairs:
4161 for (loop, stroke) in ls_pairs:
4162 move.append(gstretch_calculate_verts(loop, stroke, object,
4163 bm_mod, self.method))
4164 if self.delete_strokes:
4165 if type(stroke) != bpy.types.GPencilStroke:
4166 # in case of cached fake stroke, get the real one
4167 if get_strokes(self, context):
4168 strokes = gstretch_get_strokes(self, context)
4169 if loops and strokes:
4170 ls_pairs = gstretch_match_loops_strokes(loops,
4171 strokes, object, bm_mod)
4172 ls_pairs = gstretch_align_pairs(ls_pairs,
4173 object, bm_mod, self.method)
4174 for (l, s) in ls_pairs:
4175 if l == loop:
4176 stroke = s
4177 break
4178 gstretch_erase_stroke(stroke, context)
4180 # move vertices to new locations
4181 if self.lock_x or self.lock_y or self.lock_z:
4182 lock = [self.lock_x, self.lock_y, self.lock_z]
4183 else:
4184 lock = False
4185 bmesh.update_edit_mesh(object.data, loop_triangles=True, destructive=True)
4186 move_verts(object, bm, mapping, move, lock, self.influence)
4188 # cleaning up
4189 if derived:
4190 bm_mod.free()
4191 terminate()
4193 return{'FINISHED'}
4196 # relax operator
4197 class Relax(Operator):
4198 bl_idname = "mesh.looptools_relax"
4199 bl_label = "Relax"
4200 bl_description = "Relax the loop, so it is smoother"
4201 bl_options = {'REGISTER', 'UNDO'}
4203 input: EnumProperty(
4204 name="Input",
4205 items=(("all", "Parallel (all)", "Also use non-selected "
4206 "parallel loops as input"),
4207 ("selected", "Selection", "Only use selected vertices as input")),
4208 description="Loops that are relaxed",
4209 default='selected'
4211 interpolation: EnumProperty(
4212 name="Interpolation",
4213 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4214 ("linear", "Linear", "Simple and fast linear algorithm")),
4215 description="Algorithm used for interpolation",
4216 default='cubic'
4218 iterations: EnumProperty(
4219 name="Iterations",
4220 items=(("1", "1", "One"),
4221 ("3", "3", "Three"),
4222 ("5", "5", "Five"),
4223 ("10", "10", "Ten"),
4224 ("25", "25", "Twenty-five")),
4225 description="Number of times the loop is relaxed",
4226 default="1"
4228 regular: BoolProperty(
4229 name="Regular",
4230 description="Distribute vertices at constant distances along the loop",
4231 default=True
4234 @classmethod
4235 def poll(cls, context):
4236 ob = context.active_object
4237 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4239 def draw(self, context):
4240 layout = self.layout
4241 col = layout.column()
4243 col.prop(self, "interpolation")
4244 col.prop(self, "input")
4245 col.prop(self, "iterations")
4246 col.prop(self, "regular")
4248 def invoke(self, context, event):
4249 # load custom settings
4250 settings_load(self)
4251 return self.execute(context)
4253 def execute(self, context):
4254 # initialise
4255 object, bm = initialise()
4256 settings_write(self)
4257 # check cache to see if we can save time
4258 cached, single_loops, loops, derived, mapping = cache_read("Relax",
4259 object, bm, self.input, False)
4260 if cached:
4261 derived, bm_mod = get_derived_bmesh(object, bm, False)
4262 else:
4263 # find loops
4264 derived, bm_mod, loops = get_connected_input(object, bm, False, self.input)
4265 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4266 loops = check_loops(loops, mapping, bm_mod)
4267 knots, points = relax_calculate_knots(loops)
4269 # saving cache for faster execution next time
4270 if not cached:
4271 cache_write("Relax", object, bm, self.input, False, False, loops,
4272 derived, mapping)
4274 for iteration in range(int(self.iterations)):
4275 # calculate splines and new positions
4276 tknots, tpoints = relax_calculate_t(bm_mod, knots, points,
4277 self.regular)
4278 splines = []
4279 for i in range(len(knots)):
4280 splines.append(calculate_splines(self.interpolation, bm_mod,
4281 tknots[i], knots[i]))
4282 move = [relax_calculate_verts(bm_mod, self.interpolation,
4283 tknots, knots, tpoints, points, splines)]
4284 move_verts(object, bm, mapping, move, False, -1)
4286 # cleaning up
4287 if derived:
4288 bm_mod.free()
4289 terminate()
4291 return{'FINISHED'}
4294 # space operator
4295 class Space(Operator):
4296 bl_idname = "mesh.looptools_space"
4297 bl_label = "Space"
4298 bl_description = "Space the vertices in a regular distribution on the loop"
4299 bl_options = {'REGISTER', 'UNDO'}
4301 influence: FloatProperty(
4302 name="Influence",
4303 description="Force of the tool",
4304 default=100.0,
4305 min=0.0,
4306 max=100.0,
4307 precision=1,
4308 subtype='PERCENTAGE'
4310 input: EnumProperty(
4311 name="Input",
4312 items=(("all", "Parallel (all)", "Also use non-selected "
4313 "parallel loops as input"),
4314 ("selected", "Selection", "Only use selected vertices as input")),
4315 description="Loops that are spaced",
4316 default='selected'
4318 interpolation: EnumProperty(
4319 name="Interpolation",
4320 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4321 ("linear", "Linear", "Vertices are projected on existing edges")),
4322 description="Algorithm used for interpolation",
4323 default='cubic'
4325 lock_x: BoolProperty(
4326 name="Lock X",
4327 description="Lock editing of the x-coordinate",
4328 default=False
4330 lock_y: BoolProperty(
4331 name="Lock Y",
4332 description="Lock editing of the y-coordinate",
4333 default=False
4335 lock_z: BoolProperty(
4336 name="Lock Z",
4337 description="Lock editing of the z-coordinate",
4338 default=False
4341 @classmethod
4342 def poll(cls, context):
4343 ob = context.active_object
4344 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4346 def draw(self, context):
4347 layout = self.layout
4348 col = layout.column()
4350 col.prop(self, "interpolation")
4351 col.prop(self, "input")
4352 col.separator()
4354 col_move = col.column(align=True)
4355 row = col_move.row(align=True)
4356 if self.lock_x:
4357 row.prop(self, "lock_x", text="X", icon='LOCKED')
4358 else:
4359 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
4360 if self.lock_y:
4361 row.prop(self, "lock_y", text="Y", icon='LOCKED')
4362 else:
4363 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
4364 if self.lock_z:
4365 row.prop(self, "lock_z", text="Z", icon='LOCKED')
4366 else:
4367 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
4368 col_move.prop(self, "influence")
4370 def invoke(self, context, event):
4371 # load custom settings
4372 settings_load(self)
4373 return self.execute(context)
4375 def execute(self, context):
4376 # initialise
4377 object, bm = initialise()
4378 settings_write(self)
4379 # check cache to see if we can save time
4380 cached, single_loops, loops, derived, mapping = cache_read("Space",
4381 object, bm, self.input, False)
4382 if cached:
4383 derived, bm_mod = get_derived_bmesh(object, bm, True)
4384 else:
4385 # find loops
4386 derived, bm_mod, loops = get_connected_input(object, bm, True, self.input)
4387 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4388 loops = check_loops(loops, mapping, bm_mod)
4390 # saving cache for faster execution next time
4391 if not cached:
4392 cache_write("Space", object, bm, self.input, False, False, loops,
4393 derived, mapping)
4395 move = []
4396 for loop in loops:
4397 # calculate splines and new positions
4398 if loop[1]: # circular
4399 loop[0].append(loop[0][0])
4400 tknots, tpoints = space_calculate_t(bm_mod, loop[0][:])
4401 splines = calculate_splines(self.interpolation, bm_mod,
4402 tknots, loop[0][:])
4403 move.append(space_calculate_verts(bm_mod, self.interpolation,
4404 tknots, tpoints, loop[0][:-1], splines))
4405 # move vertices to new locations
4406 if self.lock_x or self.lock_y or self.lock_z:
4407 lock = [self.lock_x, self.lock_y, self.lock_z]
4408 else:
4409 lock = False
4410 move_verts(object, bm, mapping, move, lock, self.influence)
4412 # cleaning up
4413 if derived:
4414 bm_mod.free()
4415 terminate()
4417 cache_delete("Space")
4419 return{'FINISHED'}
4422 # ########################################
4423 # ##### GUI and registration #############
4424 # ########################################
4426 # menu containing all tools
4427 class VIEW3D_MT_edit_mesh_looptools(Menu):
4428 bl_label = "LoopTools"
4430 def draw(self, context):
4431 layout = self.layout
4433 layout.operator("mesh.looptools_bridge", text="Bridge").loft = False
4434 layout.operator("mesh.looptools_circle")
4435 layout.operator("mesh.looptools_curve")
4436 layout.operator("mesh.looptools_flatten")
4437 layout.operator("mesh.looptools_gstretch")
4438 layout.operator("mesh.looptools_bridge", text="Loft").loft = True
4439 layout.operator("mesh.looptools_relax")
4440 layout.operator("mesh.looptools_space")
4443 # panel containing all tools
4444 class VIEW3D_PT_tools_looptools(Panel):
4445 bl_space_type = 'VIEW_3D'
4446 bl_region_type = 'UI'
4447 bl_category = 'Edit'
4448 bl_context = "mesh_edit"
4449 bl_label = "LoopTools"
4450 bl_options = {'DEFAULT_CLOSED'}
4452 def draw(self, context):
4453 layout = self.layout
4454 col = layout.column(align=True)
4455 lt = context.window_manager.looptools
4457 # bridge - first line
4458 split = col.split(factor=0.15, align=True)
4459 if lt.display_bridge:
4460 split.prop(lt, "display_bridge", text="", icon='DOWNARROW_HLT')
4461 else:
4462 split.prop(lt, "display_bridge", text="", icon='RIGHTARROW')
4463 split.operator("mesh.looptools_bridge", text="Bridge").loft = False
4464 # bridge - settings
4465 if lt.display_bridge:
4466 box = col.column(align=True).box().column()
4467 # box.prop(self, "mode")
4469 # top row
4470 col_top = box.column(align=True)
4471 row = col_top.row(align=True)
4472 col_left = row.column(align=True)
4473 col_right = row.column(align=True)
4474 col_right.active = lt.bridge_segments != 1
4475 col_left.prop(lt, "bridge_segments")
4476 col_right.prop(lt, "bridge_min_width", text="")
4477 # bottom row
4478 bottom_left = col_left.row()
4479 bottom_left.active = lt.bridge_segments != 1
4480 bottom_left.prop(lt, "bridge_interpolation", text="")
4481 bottom_right = col_right.row()
4482 bottom_right.active = lt.bridge_interpolation == 'cubic'
4483 bottom_right.prop(lt, "bridge_cubic_strength")
4484 # boolean properties
4485 col_top.prop(lt, "bridge_remove_faces")
4487 # override properties
4488 col_top.separator()
4489 row = box.row(align=True)
4490 row.prop(lt, "bridge_twist")
4491 row.prop(lt, "bridge_reverse")
4493 # circle - first line
4494 split = col.split(factor=0.15, align=True)
4495 if lt.display_circle:
4496 split.prop(lt, "display_circle", text="", icon='DOWNARROW_HLT')
4497 else:
4498 split.prop(lt, "display_circle", text="", icon='RIGHTARROW')
4499 split.operator("mesh.looptools_circle")
4500 # circle - settings
4501 if lt.display_circle:
4502 box = col.column(align=True).box().column()
4503 box.prop(lt, "circle_fit")
4504 box.separator()
4506 box.prop(lt, "circle_flatten")
4507 row = box.row(align=True)
4508 row.prop(lt, "circle_custom_radius")
4509 row_right = row.row(align=True)
4510 row_right.active = lt.circle_custom_radius
4511 row_right.prop(lt, "circle_radius", text="")
4512 box.prop(lt, "circle_regular")
4513 box.separator()
4515 col_move = box.column(align=True)
4516 row = col_move.row(align=True)
4517 if lt.circle_lock_x:
4518 row.prop(lt, "circle_lock_x", text="X", icon='LOCKED')
4519 else:
4520 row.prop(lt, "circle_lock_x", text="X", icon='UNLOCKED')
4521 if lt.circle_lock_y:
4522 row.prop(lt, "circle_lock_y", text="Y", icon='LOCKED')
4523 else:
4524 row.prop(lt, "circle_lock_y", text="Y", icon='UNLOCKED')
4525 if lt.circle_lock_z:
4526 row.prop(lt, "circle_lock_z", text="Z", icon='LOCKED')
4527 else:
4528 row.prop(lt, "circle_lock_z", text="Z", icon='UNLOCKED')
4529 col_move.prop(lt, "circle_influence")
4531 # curve - first line
4532 split = col.split(factor=0.15, align=True)
4533 if lt.display_curve:
4534 split.prop(lt, "display_curve", text="", icon='DOWNARROW_HLT')
4535 else:
4536 split.prop(lt, "display_curve", text="", icon='RIGHTARROW')
4537 split.operator("mesh.looptools_curve")
4538 # curve - settings
4539 if lt.display_curve:
4540 box = col.column(align=True).box().column()
4541 box.prop(lt, "curve_interpolation")
4542 box.prop(lt, "curve_restriction")
4543 box.prop(lt, "curve_boundaries")
4544 box.prop(lt, "curve_regular")
4545 box.separator()
4547 col_move = box.column(align=True)
4548 row = col_move.row(align=True)
4549 if lt.curve_lock_x:
4550 row.prop(lt, "curve_lock_x", text="X", icon='LOCKED')
4551 else:
4552 row.prop(lt, "curve_lock_x", text="X", icon='UNLOCKED')
4553 if lt.curve_lock_y:
4554 row.prop(lt, "curve_lock_y", text="Y", icon='LOCKED')
4555 else:
4556 row.prop(lt, "curve_lock_y", text="Y", icon='UNLOCKED')
4557 if lt.curve_lock_z:
4558 row.prop(lt, "curve_lock_z", text="Z", icon='LOCKED')
4559 else:
4560 row.prop(lt, "curve_lock_z", text="Z", icon='UNLOCKED')
4561 col_move.prop(lt, "curve_influence")
4563 # flatten - first line
4564 split = col.split(factor=0.15, align=True)
4565 if lt.display_flatten:
4566 split.prop(lt, "display_flatten", text="", icon='DOWNARROW_HLT')
4567 else:
4568 split.prop(lt, "display_flatten", text="", icon='RIGHTARROW')
4569 split.operator("mesh.looptools_flatten")
4570 # flatten - settings
4571 if lt.display_flatten:
4572 box = col.column(align=True).box().column()
4573 box.prop(lt, "flatten_plane")
4574 # box.prop(lt, "flatten_restriction")
4575 box.separator()
4577 col_move = box.column(align=True)
4578 row = col_move.row(align=True)
4579 if lt.flatten_lock_x:
4580 row.prop(lt, "flatten_lock_x", text="X", icon='LOCKED')
4581 else:
4582 row.prop(lt, "flatten_lock_x", text="X", icon='UNLOCKED')
4583 if lt.flatten_lock_y:
4584 row.prop(lt, "flatten_lock_y", text="Y", icon='LOCKED')
4585 else:
4586 row.prop(lt, "flatten_lock_y", text="Y", icon='UNLOCKED')
4587 if lt.flatten_lock_z:
4588 row.prop(lt, "flatten_lock_z", text="Z", icon='LOCKED')
4589 else:
4590 row.prop(lt, "flatten_lock_z", text="Z", icon='UNLOCKED')
4591 col_move.prop(lt, "flatten_influence")
4593 # gstretch - first line
4594 split = col.split(factor=0.15, align=True)
4595 if lt.display_gstretch:
4596 split.prop(lt, "display_gstretch", text="", icon='DOWNARROW_HLT')
4597 else:
4598 split.prop(lt, "display_gstretch", text="", icon='RIGHTARROW')
4599 split.operator("mesh.looptools_gstretch")
4600 # gstretch settings
4601 if lt.display_gstretch:
4602 box = col.column(align=True).box().column()
4603 box.prop(lt, "gstretch_use_guide")
4604 if lt.gstretch_use_guide == "GPencil":
4605 box.prop(lt, "gstretch_guide")
4606 box.prop(lt, "gstretch_method")
4608 col_conv = box.column(align=True)
4609 col_conv.prop(lt, "gstretch_conversion", text="")
4610 if lt.gstretch_conversion == 'distance':
4611 col_conv.prop(lt, "gstretch_conversion_distance")
4612 elif lt.gstretch_conversion == 'limit_vertices':
4613 row = col_conv.row(align=True)
4614 row.prop(lt, "gstretch_conversion_min", text="Min")
4615 row.prop(lt, "gstretch_conversion_max", text="Max")
4616 elif lt.gstretch_conversion == 'vertices':
4617 col_conv.prop(lt, "gstretch_conversion_vertices")
4618 box.separator()
4620 col_move = box.column(align=True)
4621 row = col_move.row(align=True)
4622 if lt.gstretch_lock_x:
4623 row.prop(lt, "gstretch_lock_x", text="X", icon='LOCKED')
4624 else:
4625 row.prop(lt, "gstretch_lock_x", text="X", icon='UNLOCKED')
4626 if lt.gstretch_lock_y:
4627 row.prop(lt, "gstretch_lock_y", text="Y", icon='LOCKED')
4628 else:
4629 row.prop(lt, "gstretch_lock_y", text="Y", icon='UNLOCKED')
4630 if lt.gstretch_lock_z:
4631 row.prop(lt, "gstretch_lock_z", text="Z", icon='LOCKED')
4632 else:
4633 row.prop(lt, "gstretch_lock_z", text="Z", icon='UNLOCKED')
4634 col_move.prop(lt, "gstretch_influence")
4635 if lt.gstretch_use_guide == "Annotation":
4636 box.operator("remove.annotation", text="Delete Annotation Strokes")
4637 if lt.gstretch_use_guide == "GPencil":
4638 box.operator("remove.gp", text="Delete GPencil Strokes")
4640 # loft - first line
4641 split = col.split(factor=0.15, align=True)
4642 if lt.display_loft:
4643 split.prop(lt, "display_loft", text="", icon='DOWNARROW_HLT')
4644 else:
4645 split.prop(lt, "display_loft", text="", icon='RIGHTARROW')
4646 split.operator("mesh.looptools_bridge", text="Loft").loft = True
4647 # loft - settings
4648 if lt.display_loft:
4649 box = col.column(align=True).box().column()
4650 # box.prop(self, "mode")
4652 # top row
4653 col_top = box.column(align=True)
4654 row = col_top.row(align=True)
4655 col_left = row.column(align=True)
4656 col_right = row.column(align=True)
4657 col_right.active = lt.bridge_segments != 1
4658 col_left.prop(lt, "bridge_segments")
4659 col_right.prop(lt, "bridge_min_width", text="")
4660 # bottom row
4661 bottom_left = col_left.row()
4662 bottom_left.active = lt.bridge_segments != 1
4663 bottom_left.prop(lt, "bridge_interpolation", text="")
4664 bottom_right = col_right.row()
4665 bottom_right.active = lt.bridge_interpolation == 'cubic'
4666 bottom_right.prop(lt, "bridge_cubic_strength")
4667 # boolean properties
4668 col_top.prop(lt, "bridge_remove_faces")
4669 col_top.prop(lt, "bridge_loft_loop")
4671 # override properties
4672 col_top.separator()
4673 row = box.row(align=True)
4674 row.prop(lt, "bridge_twist")
4675 row.prop(lt, "bridge_reverse")
4677 # relax - first line
4678 split = col.split(factor=0.15, align=True)
4679 if lt.display_relax:
4680 split.prop(lt, "display_relax", text="", icon='DOWNARROW_HLT')
4681 else:
4682 split.prop(lt, "display_relax", text="", icon='RIGHTARROW')
4683 split.operator("mesh.looptools_relax")
4684 # relax - settings
4685 if lt.display_relax:
4686 box = col.column(align=True).box().column()
4687 box.prop(lt, "relax_interpolation")
4688 box.prop(lt, "relax_input")
4689 box.prop(lt, "relax_iterations")
4690 box.prop(lt, "relax_regular")
4692 # space - first line
4693 split = col.split(factor=0.15, align=True)
4694 if lt.display_space:
4695 split.prop(lt, "display_space", text="", icon='DOWNARROW_HLT')
4696 else:
4697 split.prop(lt, "display_space", text="", icon='RIGHTARROW')
4698 split.operator("mesh.looptools_space")
4699 # space - settings
4700 if lt.display_space:
4701 box = col.column(align=True).box().column()
4702 box.prop(lt, "space_interpolation")
4703 box.prop(lt, "space_input")
4704 box.separator()
4706 col_move = box.column(align=True)
4707 row = col_move.row(align=True)
4708 if lt.space_lock_x:
4709 row.prop(lt, "space_lock_x", text="X", icon='LOCKED')
4710 else:
4711 row.prop(lt, "space_lock_x", text="X", icon='UNLOCKED')
4712 if lt.space_lock_y:
4713 row.prop(lt, "space_lock_y", text="Y", icon='LOCKED')
4714 else:
4715 row.prop(lt, "space_lock_y", text="Y", icon='UNLOCKED')
4716 if lt.space_lock_z:
4717 row.prop(lt, "space_lock_z", text="Z", icon='LOCKED')
4718 else:
4719 row.prop(lt, "space_lock_z", text="Z", icon='UNLOCKED')
4720 col_move.prop(lt, "space_influence")
4723 # property group containing all properties for the gui in the panel
4724 class LoopToolsProps(PropertyGroup):
4726 Fake module like class
4727 bpy.context.window_manager.looptools
4729 # general display properties
4730 display_bridge: BoolProperty(
4731 name="Bridge settings",
4732 description="Display settings of the Bridge tool",
4733 default=False
4735 display_circle: BoolProperty(
4736 name="Circle settings",
4737 description="Display settings of the Circle tool",
4738 default=False
4740 display_curve: BoolProperty(
4741 name="Curve settings",
4742 description="Display settings of the Curve tool",
4743 default=False
4745 display_flatten: BoolProperty(
4746 name="Flatten settings",
4747 description="Display settings of the Flatten tool",
4748 default=False
4750 display_gstretch: BoolProperty(
4751 name="Gstretch settings",
4752 description="Display settings of the Gstretch tool",
4753 default=False
4755 display_loft: BoolProperty(
4756 name="Loft settings",
4757 description="Display settings of the Loft tool",
4758 default=False
4760 display_relax: BoolProperty(
4761 name="Relax settings",
4762 description="Display settings of the Relax tool",
4763 default=False
4765 display_space: BoolProperty(
4766 name="Space settings",
4767 description="Display settings of the Space tool",
4768 default=False
4771 # bridge properties
4772 bridge_cubic_strength: FloatProperty(
4773 name="Strength",
4774 description="Higher strength results in more fluid curves",
4775 default=1.0,
4776 soft_min=-3.0,
4777 soft_max=3.0
4779 bridge_interpolation: EnumProperty(
4780 name="Interpolation mode",
4781 items=(('cubic', "Cubic", "Gives curved results"),
4782 ('linear', "Linear", "Basic, fast, straight interpolation")),
4783 description="Interpolation mode: algorithm used when creating segments",
4784 default='cubic'
4786 bridge_loft: BoolProperty(
4787 name="Loft",
4788 description="Loft multiple loops, instead of considering them as "
4789 "a multi-input for bridging",
4790 default=False
4792 bridge_loft_loop: BoolProperty(
4793 name="Loop",
4794 description="Connect the first and the last loop with each other",
4795 default=False
4797 bridge_min_width: IntProperty(
4798 name="Minimum width",
4799 description="Segments with an edge smaller than this are merged "
4800 "(compared to base edge)",
4801 default=0,
4802 min=0,
4803 max=100,
4804 subtype='PERCENTAGE'
4806 bridge_mode: EnumProperty(
4807 name="Mode",
4808 items=(('basic', "Basic", "Fast algorithm"),
4809 ('shortest', "Shortest edge", "Slower algorithm with "
4810 "better vertex matching")),
4811 description="Algorithm used for bridging",
4812 default='shortest'
4814 bridge_remove_faces: BoolProperty(
4815 name="Remove faces",
4816 description="Remove faces that are internal after bridging",
4817 default=True
4819 bridge_reverse: BoolProperty(
4820 name="Reverse",
4821 description="Manually override the direction in which the loops "
4822 "are bridged. Only use if the tool gives the wrong result",
4823 default=False
4825 bridge_segments: IntProperty(
4826 name="Segments",
4827 description="Number of segments used to bridge the gap (0=automatic)",
4828 default=1,
4829 min=0,
4830 soft_max=20
4832 bridge_twist: IntProperty(
4833 name="Twist",
4834 description="Twist what vertices are connected to each other",
4835 default=0
4838 # circle properties
4839 circle_custom_radius: BoolProperty(
4840 name="Radius",
4841 description="Force a custom radius",
4842 default=False
4844 circle_fit: EnumProperty(
4845 name="Method",
4846 items=(("best", "Best fit", "Non-linear least squares"),
4847 ("inside", "Fit inside", "Only move vertices towards the center")),
4848 description="Method used for fitting a circle to the vertices",
4849 default='best'
4851 circle_flatten: BoolProperty(
4852 name="Flatten",
4853 description="Flatten the circle, instead of projecting it on the mesh",
4854 default=True
4856 circle_influence: FloatProperty(
4857 name="Influence",
4858 description="Force of the tool",
4859 default=100.0,
4860 min=0.0,
4861 max=100.0,
4862 precision=1,
4863 subtype='PERCENTAGE'
4865 circle_lock_x: BoolProperty(
4866 name="Lock X",
4867 description="Lock editing of the x-coordinate",
4868 default=False
4870 circle_lock_y: BoolProperty(
4871 name="Lock Y",
4872 description="Lock editing of the y-coordinate",
4873 default=False
4875 circle_lock_z: BoolProperty(
4876 name="Lock Z",
4877 description="Lock editing of the z-coordinate",
4878 default=False
4880 circle_radius: FloatProperty(
4881 name="Radius",
4882 description="Custom radius for circle",
4883 default=1.0,
4884 min=0.0,
4885 soft_max=1000.0
4887 circle_regular: BoolProperty(
4888 name="Regular",
4889 description="Distribute vertices at constant distances along the circle",
4890 default=True
4892 circle_angle: FloatProperty(
4893 name="Angle",
4894 description="Rotate a circle by an angle",
4895 unit='ROTATION',
4896 default=math.radians(0.0),
4897 soft_min=math.radians(-360.0),
4898 soft_max=math.radians(360.0)
4900 # curve properties
4901 curve_boundaries: BoolProperty(
4902 name="Boundaries",
4903 description="Limit the tool to work within the boundaries of the "
4904 "selected vertices",
4905 default=False
4907 curve_influence: FloatProperty(
4908 name="Influence",
4909 description="Force of the tool",
4910 default=100.0,
4911 min=0.0,
4912 max=100.0,
4913 precision=1,
4914 subtype='PERCENTAGE'
4916 curve_interpolation: EnumProperty(
4917 name="Interpolation",
4918 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4919 ("linear", "Linear", "Simple and fast linear algorithm")),
4920 description="Algorithm used for interpolation",
4921 default='cubic'
4923 curve_lock_x: BoolProperty(
4924 name="Lock X",
4925 description="Lock editing of the x-coordinate",
4926 default=False
4928 curve_lock_y: BoolProperty(
4929 name="Lock Y",
4930 description="Lock editing of the y-coordinate",
4931 default=False
4933 curve_lock_z: BoolProperty(
4934 name="Lock Z",
4935 description="Lock editing of the z-coordinate",
4936 default=False
4938 curve_regular: BoolProperty(
4939 name="Regular",
4940 description="Distribute vertices at constant distances along the curve",
4941 default=True
4943 curve_restriction: EnumProperty(
4944 name="Restriction",
4945 items=(("none", "None", "No restrictions on vertex movement"),
4946 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
4947 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
4948 description="Restrictions on how the vertices can be moved",
4949 default='none'
4952 # flatten properties
4953 flatten_influence: FloatProperty(
4954 name="Influence",
4955 description="Force of the tool",
4956 default=100.0,
4957 min=0.0,
4958 max=100.0,
4959 precision=1,
4960 subtype='PERCENTAGE'
4962 flatten_lock_x: BoolProperty(
4963 name="Lock X",
4964 description="Lock editing of the x-coordinate",
4965 default=False)
4966 flatten_lock_y: BoolProperty(name="Lock Y",
4967 description="Lock editing of the y-coordinate",
4968 default=False
4970 flatten_lock_z: BoolProperty(
4971 name="Lock Z",
4972 description="Lock editing of the z-coordinate",
4973 default=False
4975 flatten_plane: EnumProperty(
4976 name="Plane",
4977 items=(("best_fit", "Best fit", "Calculate a best fitting plane"),
4978 ("normal", "Normal", "Derive plane from averaging vertex "
4979 "normals"),
4980 ("view", "View", "Flatten on a plane perpendicular to the "
4981 "viewing angle")),
4982 description="Plane on which vertices are flattened",
4983 default='best_fit'
4985 flatten_restriction: EnumProperty(
4986 name="Restriction",
4987 items=(("none", "None", "No restrictions on vertex movement"),
4988 ("bounding_box", "Bounding box", "Vertices are restricted to "
4989 "movement inside the bounding box of the selection")),
4990 description="Restrictions on how the vertices can be moved",
4991 default='none'
4994 # gstretch properties
4995 gstretch_conversion: EnumProperty(
4996 name="Conversion",
4997 items=(("distance", "Distance", "Set the distance between vertices "
4998 "of the converted stroke"),
4999 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
5000 "number of vertices that converted GP strokes will have"),
5001 ("vertices", "Exact vertices", "Set the exact number of vertices "
5002 "that converted strokes will have. Short strokes "
5003 "with few points may contain less vertices than this number."),
5004 ("none", "No simplification", "Convert each point "
5005 "to a vertex")),
5006 description="If strokes are converted to geometry, "
5007 "use this simplification method",
5008 default='limit_vertices'
5010 gstretch_conversion_distance: FloatProperty(
5011 name="Distance",
5012 description="Absolute distance between vertices along the converted "
5013 "stroke",
5014 default=0.1,
5015 min=0.000001,
5016 soft_min=0.01,
5017 soft_max=100
5019 gstretch_conversion_max: IntProperty(
5020 name="Max Vertices",
5021 description="Maximum number of vertices strokes will "
5022 "have, when they are converted to geomtery",
5023 default=32,
5024 min=3,
5025 soft_max=500,
5026 update=gstretch_update_min
5028 gstretch_conversion_min: IntProperty(
5029 name="Min Vertices",
5030 description="Minimum number of vertices strokes will "
5031 "have, when they are converted to geomtery",
5032 default=8,
5033 min=3,
5034 soft_max=500,
5035 update=gstretch_update_max
5037 gstretch_conversion_vertices: IntProperty(
5038 name="Vertices",
5039 description="Number of vertices strokes will "
5040 "have, when they are converted to geometry. If strokes have less "
5041 "points than required, the 'Spread evenly' method is used",
5042 default=32,
5043 min=3,
5044 soft_max=500
5046 gstretch_delete_strokes: BoolProperty(
5047 name="Delete strokes",
5048 description="Remove Grease Pencil strokes if they have been used "
5049 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
5050 default=False
5052 gstretch_influence: FloatProperty(
5053 name="Influence",
5054 description="Force of the tool",
5055 default=100.0,
5056 min=0.0,
5057 max=100.0,
5058 precision=1,
5059 subtype='PERCENTAGE'
5061 gstretch_lock_x: BoolProperty(
5062 name="Lock X",
5063 description="Lock editing of the x-coordinate",
5064 default=False
5066 gstretch_lock_y: BoolProperty(
5067 name="Lock Y",
5068 description="Lock editing of the y-coordinate",
5069 default=False
5071 gstretch_lock_z: BoolProperty(
5072 name="Lock Z",
5073 description="Lock editing of the z-coordinate",
5074 default=False
5076 gstretch_method: EnumProperty(
5077 name="Method",
5078 items=(("project", "Project", "Project vertices onto the stroke, "
5079 "using vertex normals and connected edges"),
5080 ("irregular", "Spread", "Distribute vertices along the full "
5081 "stroke, retaining relative distances between the vertices"),
5082 ("regular", "Spread evenly", "Distribute vertices at regular "
5083 "distances along the full stroke")),
5084 description="Method of distributing the vertices over the Grease "
5085 "Pencil stroke",
5086 default='regular'
5088 gstretch_use_guide: EnumProperty(
5089 name="Use guides",
5090 items=(("None", "None", "None"),
5091 ("Annotation", "Annotation", "Annotation"),
5092 ("GPencil", "GPencil", "GPencil")),
5093 default="None"
5095 gstretch_guide: PointerProperty(
5096 name="GPencil object",
5097 description="Set GPencil object",
5098 type=bpy.types.Object
5101 # relax properties
5102 relax_input: EnumProperty(name="Input",
5103 items=(("all", "Parallel (all)", "Also use non-selected "
5104 "parallel loops as input"),
5105 ("selected", "Selection", "Only use selected vertices as input")),
5106 description="Loops that are relaxed",
5107 default='selected'
5109 relax_interpolation: EnumProperty(
5110 name="Interpolation",
5111 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5112 ("linear", "Linear", "Simple and fast linear algorithm")),
5113 description="Algorithm used for interpolation",
5114 default='cubic'
5116 relax_iterations: EnumProperty(name="Iterations",
5117 items=(("1", "1", "One"),
5118 ("3", "3", "Three"),
5119 ("5", "5", "Five"),
5120 ("10", "10", "Ten"),
5121 ("25", "25", "Twenty-five")),
5122 description="Number of times the loop is relaxed",
5123 default="1"
5125 relax_regular: BoolProperty(
5126 name="Regular",
5127 description="Distribute vertices at constant distances along the loop",
5128 default=True
5131 # space properties
5132 space_influence: FloatProperty(
5133 name="Influence",
5134 description="Force of the tool",
5135 default=100.0,
5136 min=0.0,
5137 max=100.0,
5138 precision=1,
5139 subtype='PERCENTAGE'
5141 space_input: EnumProperty(
5142 name="Input",
5143 items=(("all", "Parallel (all)", "Also use non-selected "
5144 "parallel loops as input"),
5145 ("selected", "Selection", "Only use selected vertices as input")),
5146 description="Loops that are spaced",
5147 default='selected'
5149 space_interpolation: EnumProperty(
5150 name="Interpolation",
5151 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5152 ("linear", "Linear", "Vertices are projected on existing edges")),
5153 description="Algorithm used for interpolation",
5154 default='cubic'
5156 space_lock_x: BoolProperty(
5157 name="Lock X",
5158 description="Lock editing of the x-coordinate",
5159 default=False
5161 space_lock_y: BoolProperty(
5162 name="Lock Y",
5163 description="Lock editing of the y-coordinate",
5164 default=False
5166 space_lock_z: BoolProperty(
5167 name="Lock Z",
5168 description="Lock editing of the z-coordinate",
5169 default=False
5172 # draw function for integration in menus
5173 def menu_func(self, context):
5174 self.layout.menu("VIEW3D_MT_edit_mesh_looptools")
5175 self.layout.separator()
5178 # Add-ons Preferences Update Panel
5180 # Define Panel classes for updating
5181 panels = (
5182 VIEW3D_PT_tools_looptools,
5186 def update_panel(self, context):
5187 message = "LoopTools: Updating Panel locations has failed"
5188 try:
5189 for panel in panels:
5190 if "bl_rna" in panel.__dict__:
5191 bpy.utils.unregister_class(panel)
5193 for panel in panels:
5194 panel.bl_category = context.preferences.addons[__name__].preferences.category
5195 bpy.utils.register_class(panel)
5197 except Exception as e:
5198 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
5199 pass
5202 class LoopPreferences(AddonPreferences):
5203 # this must match the addon name, use '__package__'
5204 # when defining this in a submodule of a python package.
5205 bl_idname = __name__
5207 category: StringProperty(
5208 name="Tab Category",
5209 description="Choose a name for the category of the panel",
5210 default="Edit",
5211 update=update_panel
5214 def draw(self, context):
5215 layout = self.layout
5217 row = layout.row()
5218 col = row.column()
5219 col.label(text="Tab Category:")
5220 col.prop(self, "category", text="")
5223 # define classes for registration
5224 classes = (
5225 VIEW3D_MT_edit_mesh_looptools,
5226 VIEW3D_PT_tools_looptools,
5227 LoopToolsProps,
5228 Bridge,
5229 Circle,
5230 Curve,
5231 Flatten,
5232 GStretch,
5233 Relax,
5234 Space,
5235 LoopPreferences,
5236 RemoveAnnotation,
5237 RemoveGPencil,
5241 # registering and menu integration
5242 def register():
5243 for cls in classes:
5244 bpy.utils.register_class(cls)
5245 bpy.types.VIEW3D_MT_edit_mesh_context_menu.prepend(menu_func)
5246 bpy.types.WindowManager.looptools = PointerProperty(type=LoopToolsProps)
5247 update_panel(None, bpy.context)
5250 # unregistering and removing menus
5251 def unregister():
5252 for cls in reversed(classes):
5253 bpy.utils.unregister_class(cls)
5254 bpy.types.VIEW3D_MT_edit_mesh_context_menu.remove(menu_func)
5255 try:
5256 del bpy.types.WindowManager.looptools
5257 except Exception as e:
5258 print('unregister fail:\n', e)
5259 pass
5262 if __name__ == "__main__":
5263 register()