Pose Library: update for rename of asset_library to asset_library_ref
[blender-addons.git] / mesh_looptools.py
blob62f9aa47744c7f5ae3a57284ac5a9cca75fe9fd3
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, 7),
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 # delete internal faces
3423 if self.remove_faces and old_selected_faces:
3424 bridge_remove_internal_faces(bm, old_selected_faces)
3425 # create faces
3426 if faces:
3427 new_faces = bridge_create_faces(object, bm, faces, self.twist)
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 # make sure normals are facing outside
3433 bmesh.update_edit_mesh(object.data, loop_triangles=False, destructive=True)
3434 bpy.ops.mesh.normals_make_consistent()
3436 # cleaning up
3437 terminate()
3439 return{'FINISHED'}
3442 # circle operator
3443 class Circle(Operator):
3444 bl_idname = "mesh.looptools_circle"
3445 bl_label = "Circle"
3446 bl_description = "Move selected vertices into a circle shape"
3447 bl_options = {'REGISTER', 'UNDO'}
3449 custom_radius: BoolProperty(
3450 name="Radius",
3451 description="Force a custom radius",
3452 default=False
3454 fit: EnumProperty(
3455 name="Method",
3456 items=(("best", "Best fit", "Non-linear least squares"),
3457 ("inside", "Fit inside", "Only move vertices towards the center")),
3458 description="Method used for fitting a circle to the vertices",
3459 default='best'
3461 flatten: BoolProperty(
3462 name="Flatten",
3463 description="Flatten the circle, instead of projecting it on the mesh",
3464 default=True
3466 influence: FloatProperty(
3467 name="Influence",
3468 description="Force of the tool",
3469 default=100.0,
3470 min=0.0,
3471 max=100.0,
3472 precision=1,
3473 subtype='PERCENTAGE'
3475 lock_x: BoolProperty(
3476 name="Lock X",
3477 description="Lock editing of the x-coordinate",
3478 default=False
3480 lock_y: BoolProperty(
3481 name="Lock Y",
3482 description="Lock editing of the y-coordinate",
3483 default=False
3485 lock_z: BoolProperty(name="Lock Z",
3486 description="Lock editing of the z-coordinate",
3487 default=False
3489 radius: FloatProperty(
3490 name="Radius",
3491 description="Custom radius for circle",
3492 default=1.0,
3493 min=0.0,
3494 soft_max=1000.0
3496 angle: FloatProperty(
3497 name="Angle",
3498 description="Rotate a circle by an angle",
3499 unit='ROTATION',
3500 default=math.radians(0.0),
3501 soft_min=math.radians(-360.0),
3502 soft_max=math.radians(360.0)
3504 regular: BoolProperty(
3505 name="Regular",
3506 description="Distribute vertices at constant distances along the circle",
3507 default=True
3510 @classmethod
3511 def poll(cls, context):
3512 ob = context.active_object
3513 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3515 def draw(self, context):
3516 layout = self.layout
3517 col = layout.column()
3519 col.prop(self, "fit")
3520 col.separator()
3522 col.prop(self, "flatten")
3523 row = col.row(align=True)
3524 row.prop(self, "custom_radius")
3525 row_right = row.row(align=True)
3526 row_right.active = self.custom_radius
3527 row_right.prop(self, "radius", text="")
3528 col.prop(self, "regular")
3529 col.prop(self, "angle")
3530 col.separator()
3532 col_move = col.column(align=True)
3533 row = col_move.row(align=True)
3534 if self.lock_x:
3535 row.prop(self, "lock_x", text="X", icon='LOCKED')
3536 else:
3537 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3538 if self.lock_y:
3539 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3540 else:
3541 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3542 if self.lock_z:
3543 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3544 else:
3545 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3546 col_move.prop(self, "influence")
3548 def invoke(self, context, event):
3549 # load custom settings
3550 settings_load(self)
3551 return self.execute(context)
3553 def execute(self, context):
3554 # initialise
3555 object, bm = initialise()
3556 settings_write(self)
3557 # check cache to see if we can save time
3558 cached, single_loops, loops, derived, mapping = cache_read("Circle",
3559 object, bm, False, False)
3560 if cached:
3561 derived, bm_mod = get_derived_bmesh(object, bm, False)
3562 else:
3563 # find loops
3564 derived, bm_mod, single_vertices, single_loops, loops = \
3565 circle_get_input(object, bm)
3566 mapping = get_mapping(derived, bm, bm_mod, single_vertices,
3567 False, loops)
3568 single_loops, loops = circle_check_loops(single_loops, loops,
3569 mapping, bm_mod)
3571 # saving cache for faster execution next time
3572 if not cached:
3573 cache_write("Circle", object, bm, False, False, single_loops,
3574 loops, derived, mapping)
3576 move = []
3577 for i, loop in enumerate(loops):
3578 # best fitting flat plane
3579 com, normal = calculate_plane(bm_mod, loop)
3580 # if circular, shift loop so we get a good starting vertex
3581 if loop[1]:
3582 loop = circle_shift_loop(bm_mod, loop, com)
3583 # flatten vertices on plane
3584 locs_2d, p, q = circle_3d_to_2d(bm_mod, loop, com, normal)
3585 # calculate circle
3586 if self.fit == 'best':
3587 x0, y0, r = circle_calculate_best_fit(locs_2d)
3588 else: # self.fit == 'inside'
3589 x0, y0, r = circle_calculate_min_fit(locs_2d)
3590 # radius override
3591 if self.custom_radius:
3592 r = self.radius / p.length
3593 # calculate positions on circle
3594 if self.regular:
3595 new_locs_2d = circle_project_regular(locs_2d[:], x0, y0, r, self.angle)
3596 else:
3597 new_locs_2d = circle_project_non_regular(locs_2d[:], x0, y0, r, self.angle)
3598 # take influence into account
3599 locs_2d = circle_influence_locs(locs_2d, new_locs_2d,
3600 self.influence)
3601 # calculate 3d positions of the created 2d input
3602 move.append(circle_calculate_verts(self.flatten, bm_mod,
3603 locs_2d, com, p, q, normal))
3604 # flatten single input vertices on plane defined by loop
3605 if self.flatten and single_loops:
3606 move.append(circle_flatten_singles(bm_mod, com, p, q,
3607 normal, single_loops[i]))
3609 # move vertices to new locations
3610 if self.lock_x or self.lock_y or self.lock_z:
3611 lock = [self.lock_x, self.lock_y, self.lock_z]
3612 else:
3613 lock = False
3614 move_verts(object, bm, mapping, move, lock, -1)
3616 # cleaning up
3617 if derived:
3618 bm_mod.free()
3619 terminate()
3621 return{'FINISHED'}
3624 # curve operator
3625 class Curve(Operator):
3626 bl_idname = "mesh.looptools_curve"
3627 bl_label = "Curve"
3628 bl_description = "Turn a loop into a smooth curve"
3629 bl_options = {'REGISTER', 'UNDO'}
3631 boundaries: BoolProperty(
3632 name="Boundaries",
3633 description="Limit the tool to work within the boundaries of the selected vertices",
3634 default=False
3636 influence: FloatProperty(
3637 name="Influence",
3638 description="Force of the tool",
3639 default=100.0,
3640 min=0.0,
3641 max=100.0,
3642 precision=1,
3643 subtype='PERCENTAGE'
3645 interpolation: EnumProperty(
3646 name="Interpolation",
3647 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
3648 ("linear", "Linear", "Simple and fast linear algorithm")),
3649 description="Algorithm used for interpolation",
3650 default='cubic'
3652 lock_x: BoolProperty(
3653 name="Lock X",
3654 description="Lock editing of the x-coordinate",
3655 default=False
3657 lock_y: BoolProperty(
3658 name="Lock Y",
3659 description="Lock editing of the y-coordinate",
3660 default=False
3662 lock_z: BoolProperty(
3663 name="Lock Z",
3664 description="Lock editing of the z-coordinate",
3665 default=False
3667 regular: BoolProperty(
3668 name="Regular",
3669 description="Distribute vertices at constant distances along the curve",
3670 default=True
3672 restriction: EnumProperty(
3673 name="Restriction",
3674 items=(("none", "None", "No restrictions on vertex movement"),
3675 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
3676 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
3677 description="Restrictions on how the vertices can be moved",
3678 default='none'
3681 @classmethod
3682 def poll(cls, context):
3683 ob = context.active_object
3684 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3686 def draw(self, context):
3687 layout = self.layout
3688 col = layout.column()
3690 col.prop(self, "interpolation")
3691 col.prop(self, "restriction")
3692 col.prop(self, "boundaries")
3693 col.prop(self, "regular")
3694 col.separator()
3696 col_move = col.column(align=True)
3697 row = col_move.row(align=True)
3698 if self.lock_x:
3699 row.prop(self, "lock_x", text="X", icon='LOCKED')
3700 else:
3701 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3702 if self.lock_y:
3703 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3704 else:
3705 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3706 if self.lock_z:
3707 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3708 else:
3709 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3710 col_move.prop(self, "influence")
3712 def invoke(self, context, event):
3713 # load custom settings
3714 settings_load(self)
3715 return self.execute(context)
3717 def execute(self, context):
3718 # initialise
3719 object, bm = initialise()
3720 settings_write(self)
3721 # check cache to see if we can save time
3722 cached, single_loops, loops, derived, mapping = cache_read("Curve",
3723 object, bm, False, self.boundaries)
3724 if cached:
3725 derived, bm_mod = get_derived_bmesh(object, bm, False)
3726 else:
3727 # find loops
3728 derived, bm_mod, loops = curve_get_input(object, bm, self.boundaries)
3729 mapping = get_mapping(derived, bm, bm_mod, False, True, loops)
3730 loops = check_loops(loops, mapping, bm_mod)
3731 verts_selected = [
3732 v.index for v in bm_mod.verts if v.select and not v.hide
3735 # saving cache for faster execution next time
3736 if not cached:
3737 cache_write("Curve", object, bm, False, self.boundaries, False,
3738 loops, derived, mapping)
3740 move = []
3741 for loop in loops:
3742 knots, points = curve_calculate_knots(loop, verts_selected)
3743 pknots = curve_project_knots(bm_mod, verts_selected, knots,
3744 points, loop[1])
3745 tknots, tpoints = curve_calculate_t(bm_mod, knots, points,
3746 pknots, self.regular, loop[1])
3747 splines = calculate_splines(self.interpolation, bm_mod,
3748 tknots, knots)
3749 move.append(curve_calculate_vertices(bm_mod, knots, tknots,
3750 points, tpoints, splines, self.interpolation,
3751 self.restriction))
3753 # move vertices to new locations
3754 if self.lock_x or self.lock_y or self.lock_z:
3755 lock = [self.lock_x, self.lock_y, self.lock_z]
3756 else:
3757 lock = False
3758 move_verts(object, bm, mapping, move, lock, self.influence)
3760 # cleaning up
3761 if derived:
3762 bm_mod.free()
3763 terminate()
3765 return{'FINISHED'}
3768 # flatten operator
3769 class Flatten(Operator):
3770 bl_idname = "mesh.looptools_flatten"
3771 bl_label = "Flatten"
3772 bl_description = "Flatten vertices on a best-fitting plane"
3773 bl_options = {'REGISTER', 'UNDO'}
3775 influence: FloatProperty(
3776 name="Influence",
3777 description="Force of the tool",
3778 default=100.0,
3779 min=0.0,
3780 max=100.0,
3781 precision=1,
3782 subtype='PERCENTAGE'
3784 lock_x: BoolProperty(
3785 name="Lock X",
3786 description="Lock editing of the x-coordinate",
3787 default=False
3789 lock_y: BoolProperty(
3790 name="Lock Y",
3791 description="Lock editing of the y-coordinate",
3792 default=False
3794 lock_z: BoolProperty(name="Lock Z",
3795 description="Lock editing of the z-coordinate",
3796 default=False
3798 plane: EnumProperty(
3799 name="Plane",
3800 items=(("best_fit", "Best fit", "Calculate a best fitting plane"),
3801 ("normal", "Normal", "Derive plane from averaging vertex normals"),
3802 ("view", "View", "Flatten on a plane perpendicular to the viewing angle")),
3803 description="Plane on which vertices are flattened",
3804 default='best_fit'
3806 restriction: EnumProperty(
3807 name="Restriction",
3808 items=(("none", "None", "No restrictions on vertex movement"),
3809 ("bounding_box", "Bounding box", "Vertices are restricted to "
3810 "movement inside the bounding box of the selection")),
3811 description="Restrictions on how the vertices can be moved",
3812 default='none'
3815 @classmethod
3816 def poll(cls, context):
3817 ob = context.active_object
3818 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3820 def draw(self, context):
3821 layout = self.layout
3822 col = layout.column()
3824 col.prop(self, "plane")
3825 # col.prop(self, "restriction")
3826 col.separator()
3828 col_move = col.column(align=True)
3829 row = col_move.row(align=True)
3830 if self.lock_x:
3831 row.prop(self, "lock_x", text="X", icon='LOCKED')
3832 else:
3833 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3834 if self.lock_y:
3835 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3836 else:
3837 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3838 if self.lock_z:
3839 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3840 else:
3841 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3842 col_move.prop(self, "influence")
3844 def invoke(self, context, event):
3845 # load custom settings
3846 settings_load(self)
3847 return self.execute(context)
3849 def execute(self, context):
3850 # initialise
3851 object, bm = initialise()
3852 settings_write(self)
3853 # check cache to see if we can save time
3854 cached, single_loops, loops, derived, mapping = cache_read("Flatten",
3855 object, bm, False, False)
3856 if not cached:
3857 # order input into virtual loops
3858 loops = flatten_get_input(bm)
3859 loops = check_loops(loops, mapping, bm)
3861 # saving cache for faster execution next time
3862 if not cached:
3863 cache_write("Flatten", object, bm, False, False, False, loops,
3864 False, False)
3866 move = []
3867 for loop in loops:
3868 # calculate plane and position of vertices on them
3869 com, normal = calculate_plane(bm, loop, method=self.plane,
3870 object=object)
3871 to_move = flatten_project(bm, loop, com, normal)
3872 if self.restriction == 'none':
3873 move.append(to_move)
3874 else:
3875 move.append(to_move)
3877 # move vertices to new locations
3878 if self.lock_x or self.lock_y or self.lock_z:
3879 lock = [self.lock_x, self.lock_y, self.lock_z]
3880 else:
3881 lock = False
3882 move_verts(object, bm, False, move, lock, self.influence)
3884 # cleaning up
3885 terminate()
3887 return{'FINISHED'}
3890 # Annotation operator
3891 class RemoveAnnotation(Operator):
3892 bl_idname = "remove.annotation"
3893 bl_label = "Remove Annotation"
3894 bl_description = "Remove all Annotation Strokes"
3895 bl_options = {'REGISTER', 'UNDO'}
3897 def execute(self, context):
3899 try:
3900 bpy.data.grease_pencils[0].layers.active.clear()
3901 except:
3902 self.report({'INFO'}, "No Annotation data to Unlink")
3903 return {'CANCELLED'}
3905 return{'FINISHED'}
3907 # GPencil operator
3908 class RemoveGPencil(Operator):
3909 bl_idname = "remove.gp"
3910 bl_label = "Remove GPencil"
3911 bl_description = "Remove all GPencil Strokes"
3912 bl_options = {'REGISTER', 'UNDO'}
3914 def execute(self, context):
3916 try:
3917 looptools = context.window_manager.looptools
3918 looptools.gstretch_guide.data.layers.data.clear()
3919 looptools.gstretch_guide.data.update_tag()
3920 except:
3921 self.report({'INFO'}, "No GPencil data to Unlink")
3922 return {'CANCELLED'}
3924 return{'FINISHED'}
3927 class GStretch(Operator):
3928 bl_idname = "mesh.looptools_gstretch"
3929 bl_label = "Gstretch"
3930 bl_description = "Stretch selected vertices to active stroke"
3931 bl_options = {'REGISTER', 'UNDO'}
3933 conversion: EnumProperty(
3934 name="Conversion",
3935 items=(("distance", "Distance", "Set the distance between vertices "
3936 "of the converted stroke"),
3937 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
3938 "number of vertices that converted strokes will have"),
3939 ("vertices", "Exact vertices", "Set the exact number of vertices "
3940 "that converted strokes will have. Short strokes "
3941 "with few points may contain less vertices than this number."),
3942 ("none", "No simplification", "Convert each point "
3943 "to a vertex")),
3944 description="If strokes are converted to geometry, "
3945 "use this simplification method",
3946 default='limit_vertices'
3948 conversion_distance: FloatProperty(
3949 name="Distance",
3950 description="Absolute distance between vertices along the converted "
3951 " stroke",
3952 default=0.1,
3953 min=0.000001,
3954 soft_min=0.01,
3955 soft_max=100
3957 conversion_max: IntProperty(
3958 name="Max Vertices",
3959 description="Maximum number of vertices strokes will "
3960 "have, when they are converted to geomtery",
3961 default=32,
3962 min=3,
3963 soft_max=500,
3964 update=gstretch_update_min
3966 conversion_min: IntProperty(
3967 name="Min Vertices",
3968 description="Minimum number of vertices strokes will "
3969 "have, when they are converted to geomtery",
3970 default=8,
3971 min=3,
3972 soft_max=500,
3973 update=gstretch_update_max
3975 conversion_vertices: IntProperty(
3976 name="Vertices",
3977 description="Number of vertices strokes will "
3978 "have, when they are converted to geometry. If strokes have less "
3979 "points than required, the 'Spread evenly' method is used",
3980 default=32,
3981 min=3,
3982 soft_max=500
3984 delete_strokes: BoolProperty(
3985 name="Delete strokes",
3986 description="Remove strokes if they have been used."
3987 "WARNING: DOES NOT SUPPORT UNDO",
3988 default=False
3990 influence: FloatProperty(
3991 name="Influence",
3992 description="Force of the tool",
3993 default=100.0,
3994 min=0.0,
3995 max=100.0,
3996 precision=1,
3997 subtype='PERCENTAGE'
3999 lock_x: BoolProperty(
4000 name="Lock X",
4001 description="Lock editing of the x-coordinate",
4002 default=False
4004 lock_y: BoolProperty(
4005 name="Lock Y",
4006 description="Lock editing of the y-coordinate",
4007 default=False
4009 lock_z: BoolProperty(
4010 name="Lock Z",
4011 description="Lock editing of the z-coordinate",
4012 default=False
4014 method: EnumProperty(
4015 name="Method",
4016 items=(("project", "Project", "Project vertices onto the stroke, "
4017 "using vertex normals and connected edges"),
4018 ("irregular", "Spread", "Distribute vertices along the full "
4019 "stroke, retaining relative distances between the vertices"),
4020 ("regular", "Spread evenly", "Distribute vertices at regular "
4021 "distances along the full stroke")),
4022 description="Method of distributing the vertices over the "
4023 "stroke",
4024 default='regular'
4027 @classmethod
4028 def poll(cls, context):
4029 ob = context.active_object
4030 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4032 def draw(self, context):
4033 looptools = context.window_manager.looptools
4034 layout = self.layout
4035 col = layout.column()
4037 col.prop(self, "method")
4038 col.separator()
4040 col_conv = col.column(align=True)
4041 col_conv.prop(self, "conversion", text="")
4042 if self.conversion == 'distance':
4043 col_conv.prop(self, "conversion_distance")
4044 elif self.conversion == 'limit_vertices':
4045 row = col_conv.row(align=True)
4046 row.prop(self, "conversion_min", text="Min")
4047 row.prop(self, "conversion_max", text="Max")
4048 elif self.conversion == 'vertices':
4049 col_conv.prop(self, "conversion_vertices")
4050 col.separator()
4052 col_move = col.column(align=True)
4053 row = col_move.row(align=True)
4054 if self.lock_x:
4055 row.prop(self, "lock_x", text="X", icon='LOCKED')
4056 else:
4057 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
4058 if self.lock_y:
4059 row.prop(self, "lock_y", text="Y", icon='LOCKED')
4060 else:
4061 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
4062 if self.lock_z:
4063 row.prop(self, "lock_z", text="Z", icon='LOCKED')
4064 else:
4065 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
4066 col_move.prop(self, "influence")
4067 col.separator()
4068 if looptools.gstretch_use_guide == "Annotation":
4069 col.operator("remove.annotation", text="Delete annotation strokes")
4070 if looptools.gstretch_use_guide == "GPencil":
4071 col.operator("remove.gp", text="Delete GPencil strokes")
4073 def invoke(self, context, event):
4074 # flush cached strokes
4075 if 'Gstretch' in looptools_cache:
4076 looptools_cache['Gstretch']['single_loops'] = []
4077 # load custom settings
4078 settings_load(self)
4079 return self.execute(context)
4081 def execute(self, context):
4082 # initialise
4083 object, bm = initialise()
4084 settings_write(self)
4086 # check cache to see if we can save time
4087 cached, safe_strokes, loops, derived, mapping = cache_read("Gstretch",
4088 object, bm, False, False)
4089 if cached:
4090 straightening = False
4091 if safe_strokes:
4092 strokes = gstretch_safe_to_true_strokes(safe_strokes)
4093 # cached strokes were flushed (see operator's invoke function)
4094 elif get_strokes(self, context):
4095 strokes = gstretch_get_strokes(self, context)
4096 else:
4097 # straightening function (no GP) -> loops ignore modifiers
4098 straightening = True
4099 derived = False
4100 bm_mod = bm.copy()
4101 bm_mod.verts.ensure_lookup_table()
4102 bm_mod.edges.ensure_lookup_table()
4103 bm_mod.faces.ensure_lookup_table()
4104 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
4105 if not straightening:
4106 derived, bm_mod = get_derived_bmesh(object, bm, False)
4107 else:
4108 # get loops and strokes
4109 if get_strokes(self, context):
4110 # find loops
4111 derived, bm_mod, loops = get_connected_input(object, bm, False, input='selected')
4112 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4113 loops = check_loops(loops, mapping, bm_mod)
4114 # get strokes
4115 strokes = gstretch_get_strokes(self, context)
4116 else:
4117 # straightening function (no GP) -> loops ignore modifiers
4118 derived = False
4119 mapping = False
4120 bm_mod = bm.copy()
4121 bm_mod.verts.ensure_lookup_table()
4122 bm_mod.edges.ensure_lookup_table()
4123 bm_mod.faces.ensure_lookup_table()
4124 edge_keys = [
4125 edgekey(edge) for edge in bm_mod.edges if
4126 edge.select and not edge.hide
4128 loops = get_connected_selections(edge_keys)
4129 loops = check_loops(loops, mapping, bm_mod)
4130 # create fake strokes
4131 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
4133 # saving cache for faster execution next time
4134 if not cached:
4135 if strokes:
4136 safe_strokes = gstretch_true_to_safe_strokes(strokes)
4137 else:
4138 safe_strokes = []
4139 cache_write("Gstretch", object, bm, False, False,
4140 safe_strokes, loops, derived, mapping)
4142 # pair loops and strokes
4143 ls_pairs = gstretch_match_loops_strokes(loops, strokes, object, bm_mod)
4144 ls_pairs = gstretch_align_pairs(ls_pairs, object, bm_mod, self.method)
4146 move = []
4147 if not loops:
4148 # no selected geometry, convert GP to verts
4149 if strokes:
4150 move.append(gstretch_create_verts(object, bm, strokes,
4151 self.method, self.conversion, self.conversion_distance,
4152 self.conversion_max, self.conversion_min,
4153 self.conversion_vertices))
4154 for stroke in strokes:
4155 gstretch_erase_stroke(stroke, context)
4156 elif ls_pairs:
4157 for (loop, stroke) in ls_pairs:
4158 move.append(gstretch_calculate_verts(loop, stroke, object,
4159 bm_mod, self.method))
4160 if self.delete_strokes:
4161 if type(stroke) != bpy.types.GPencilStroke:
4162 # in case of cached fake stroke, get the real one
4163 if get_strokes(self, context):
4164 strokes = gstretch_get_strokes(self, context)
4165 if loops and strokes:
4166 ls_pairs = gstretch_match_loops_strokes(loops,
4167 strokes, object, bm_mod)
4168 ls_pairs = gstretch_align_pairs(ls_pairs,
4169 object, bm_mod, self.method)
4170 for (l, s) in ls_pairs:
4171 if l == loop:
4172 stroke = s
4173 break
4174 gstretch_erase_stroke(stroke, context)
4176 # move vertices to new locations
4177 if self.lock_x or self.lock_y or self.lock_z:
4178 lock = [self.lock_x, self.lock_y, self.lock_z]
4179 else:
4180 lock = False
4181 bmesh.update_edit_mesh(object.data, loop_triangles=True, destructive=True)
4182 move_verts(object, bm, mapping, move, lock, self.influence)
4184 # cleaning up
4185 if derived:
4186 bm_mod.free()
4187 terminate()
4189 return{'FINISHED'}
4192 # relax operator
4193 class Relax(Operator):
4194 bl_idname = "mesh.looptools_relax"
4195 bl_label = "Relax"
4196 bl_description = "Relax the loop, so it is smoother"
4197 bl_options = {'REGISTER', 'UNDO'}
4199 input: EnumProperty(
4200 name="Input",
4201 items=(("all", "Parallel (all)", "Also use non-selected "
4202 "parallel loops as input"),
4203 ("selected", "Selection", "Only use selected vertices as input")),
4204 description="Loops that are relaxed",
4205 default='selected'
4207 interpolation: EnumProperty(
4208 name="Interpolation",
4209 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4210 ("linear", "Linear", "Simple and fast linear algorithm")),
4211 description="Algorithm used for interpolation",
4212 default='cubic'
4214 iterations: EnumProperty(
4215 name="Iterations",
4216 items=(("1", "1", "One"),
4217 ("3", "3", "Three"),
4218 ("5", "5", "Five"),
4219 ("10", "10", "Ten"),
4220 ("25", "25", "Twenty-five")),
4221 description="Number of times the loop is relaxed",
4222 default="1"
4224 regular: BoolProperty(
4225 name="Regular",
4226 description="Distribute vertices at constant distances along the loop",
4227 default=True
4230 @classmethod
4231 def poll(cls, context):
4232 ob = context.active_object
4233 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4235 def draw(self, context):
4236 layout = self.layout
4237 col = layout.column()
4239 col.prop(self, "interpolation")
4240 col.prop(self, "input")
4241 col.prop(self, "iterations")
4242 col.prop(self, "regular")
4244 def invoke(self, context, event):
4245 # load custom settings
4246 settings_load(self)
4247 return self.execute(context)
4249 def execute(self, context):
4250 # initialise
4251 object, bm = initialise()
4252 settings_write(self)
4253 # check cache to see if we can save time
4254 cached, single_loops, loops, derived, mapping = cache_read("Relax",
4255 object, bm, self.input, False)
4256 if cached:
4257 derived, bm_mod = get_derived_bmesh(object, bm, False)
4258 else:
4259 # find loops
4260 derived, bm_mod, loops = get_connected_input(object, bm, False, self.input)
4261 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4262 loops = check_loops(loops, mapping, bm_mod)
4263 knots, points = relax_calculate_knots(loops)
4265 # saving cache for faster execution next time
4266 if not cached:
4267 cache_write("Relax", object, bm, self.input, False, False, loops,
4268 derived, mapping)
4270 for iteration in range(int(self.iterations)):
4271 # calculate splines and new positions
4272 tknots, tpoints = relax_calculate_t(bm_mod, knots, points,
4273 self.regular)
4274 splines = []
4275 for i in range(len(knots)):
4276 splines.append(calculate_splines(self.interpolation, bm_mod,
4277 tknots[i], knots[i]))
4278 move = [relax_calculate_verts(bm_mod, self.interpolation,
4279 tknots, knots, tpoints, points, splines)]
4280 move_verts(object, bm, mapping, move, False, -1)
4282 # cleaning up
4283 if derived:
4284 bm_mod.free()
4285 terminate()
4287 return{'FINISHED'}
4290 # space operator
4291 class Space(Operator):
4292 bl_idname = "mesh.looptools_space"
4293 bl_label = "Space"
4294 bl_description = "Space the vertices in a regular distribution on the loop"
4295 bl_options = {'REGISTER', 'UNDO'}
4297 influence: FloatProperty(
4298 name="Influence",
4299 description="Force of the tool",
4300 default=100.0,
4301 min=0.0,
4302 max=100.0,
4303 precision=1,
4304 subtype='PERCENTAGE'
4306 input: EnumProperty(
4307 name="Input",
4308 items=(("all", "Parallel (all)", "Also use non-selected "
4309 "parallel loops as input"),
4310 ("selected", "Selection", "Only use selected vertices as input")),
4311 description="Loops that are spaced",
4312 default='selected'
4314 interpolation: EnumProperty(
4315 name="Interpolation",
4316 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4317 ("linear", "Linear", "Vertices are projected on existing edges")),
4318 description="Algorithm used for interpolation",
4319 default='cubic'
4321 lock_x: BoolProperty(
4322 name="Lock X",
4323 description="Lock editing of the x-coordinate",
4324 default=False
4326 lock_y: BoolProperty(
4327 name="Lock Y",
4328 description="Lock editing of the y-coordinate",
4329 default=False
4331 lock_z: BoolProperty(
4332 name="Lock Z",
4333 description="Lock editing of the z-coordinate",
4334 default=False
4337 @classmethod
4338 def poll(cls, context):
4339 ob = context.active_object
4340 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4342 def draw(self, context):
4343 layout = self.layout
4344 col = layout.column()
4346 col.prop(self, "interpolation")
4347 col.prop(self, "input")
4348 col.separator()
4350 col_move = col.column(align=True)
4351 row = col_move.row(align=True)
4352 if self.lock_x:
4353 row.prop(self, "lock_x", text="X", icon='LOCKED')
4354 else:
4355 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
4356 if self.lock_y:
4357 row.prop(self, "lock_y", text="Y", icon='LOCKED')
4358 else:
4359 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
4360 if self.lock_z:
4361 row.prop(self, "lock_z", text="Z", icon='LOCKED')
4362 else:
4363 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
4364 col_move.prop(self, "influence")
4366 def invoke(self, context, event):
4367 # load custom settings
4368 settings_load(self)
4369 return self.execute(context)
4371 def execute(self, context):
4372 # initialise
4373 object, bm = initialise()
4374 settings_write(self)
4375 # check cache to see if we can save time
4376 cached, single_loops, loops, derived, mapping = cache_read("Space",
4377 object, bm, self.input, False)
4378 if cached:
4379 derived, bm_mod = get_derived_bmesh(object, bm, True)
4380 else:
4381 # find loops
4382 derived, bm_mod, loops = get_connected_input(object, bm, True, self.input)
4383 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4384 loops = check_loops(loops, mapping, bm_mod)
4386 # saving cache for faster execution next time
4387 if not cached:
4388 cache_write("Space", object, bm, self.input, False, False, loops,
4389 derived, mapping)
4391 move = []
4392 for loop in loops:
4393 # calculate splines and new positions
4394 if loop[1]: # circular
4395 loop[0].append(loop[0][0])
4396 tknots, tpoints = space_calculate_t(bm_mod, loop[0][:])
4397 splines = calculate_splines(self.interpolation, bm_mod,
4398 tknots, loop[0][:])
4399 move.append(space_calculate_verts(bm_mod, self.interpolation,
4400 tknots, tpoints, loop[0][:-1], splines))
4401 # move vertices to new locations
4402 if self.lock_x or self.lock_y or self.lock_z:
4403 lock = [self.lock_x, self.lock_y, self.lock_z]
4404 else:
4405 lock = False
4406 move_verts(object, bm, mapping, move, lock, self.influence)
4408 # cleaning up
4409 if derived:
4410 bm_mod.free()
4411 terminate()
4413 cache_delete("Space")
4415 return{'FINISHED'}
4418 # ########################################
4419 # ##### GUI and registration #############
4420 # ########################################
4422 # menu containing all tools
4423 class VIEW3D_MT_edit_mesh_looptools(Menu):
4424 bl_label = "LoopTools"
4426 def draw(self, context):
4427 layout = self.layout
4429 layout.operator("mesh.looptools_bridge", text="Bridge").loft = False
4430 layout.operator("mesh.looptools_circle")
4431 layout.operator("mesh.looptools_curve")
4432 layout.operator("mesh.looptools_flatten")
4433 layout.operator("mesh.looptools_gstretch")
4434 layout.operator("mesh.looptools_bridge", text="Loft").loft = True
4435 layout.operator("mesh.looptools_relax")
4436 layout.operator("mesh.looptools_space")
4439 # panel containing all tools
4440 class VIEW3D_PT_tools_looptools(Panel):
4441 bl_space_type = 'VIEW_3D'
4442 bl_region_type = 'UI'
4443 bl_category = 'Edit'
4444 bl_context = "mesh_edit"
4445 bl_label = "LoopTools"
4446 bl_options = {'DEFAULT_CLOSED'}
4448 def draw(self, context):
4449 layout = self.layout
4450 col = layout.column(align=True)
4451 lt = context.window_manager.looptools
4453 # bridge - first line
4454 split = col.split(factor=0.15, align=True)
4455 if lt.display_bridge:
4456 split.prop(lt, "display_bridge", text="", icon='DOWNARROW_HLT')
4457 else:
4458 split.prop(lt, "display_bridge", text="", icon='RIGHTARROW')
4459 split.operator("mesh.looptools_bridge", text="Bridge").loft = False
4460 # bridge - settings
4461 if lt.display_bridge:
4462 box = col.column(align=True).box().column()
4463 # box.prop(self, "mode")
4465 # top row
4466 col_top = box.column(align=True)
4467 row = col_top.row(align=True)
4468 col_left = row.column(align=True)
4469 col_right = row.column(align=True)
4470 col_right.active = lt.bridge_segments != 1
4471 col_left.prop(lt, "bridge_segments")
4472 col_right.prop(lt, "bridge_min_width", text="")
4473 # bottom row
4474 bottom_left = col_left.row()
4475 bottom_left.active = lt.bridge_segments != 1
4476 bottom_left.prop(lt, "bridge_interpolation", text="")
4477 bottom_right = col_right.row()
4478 bottom_right.active = lt.bridge_interpolation == 'cubic'
4479 bottom_right.prop(lt, "bridge_cubic_strength")
4480 # boolean properties
4481 col_top.prop(lt, "bridge_remove_faces")
4483 # override properties
4484 col_top.separator()
4485 row = box.row(align=True)
4486 row.prop(lt, "bridge_twist")
4487 row.prop(lt, "bridge_reverse")
4489 # circle - first line
4490 split = col.split(factor=0.15, align=True)
4491 if lt.display_circle:
4492 split.prop(lt, "display_circle", text="", icon='DOWNARROW_HLT')
4493 else:
4494 split.prop(lt, "display_circle", text="", icon='RIGHTARROW')
4495 split.operator("mesh.looptools_circle")
4496 # circle - settings
4497 if lt.display_circle:
4498 box = col.column(align=True).box().column()
4499 box.prop(lt, "circle_fit")
4500 box.separator()
4502 box.prop(lt, "circle_flatten")
4503 row = box.row(align=True)
4504 row.prop(lt, "circle_custom_radius")
4505 row_right = row.row(align=True)
4506 row_right.active = lt.circle_custom_radius
4507 row_right.prop(lt, "circle_radius", text="")
4508 box.prop(lt, "circle_regular")
4509 box.separator()
4511 col_move = box.column(align=True)
4512 row = col_move.row(align=True)
4513 if lt.circle_lock_x:
4514 row.prop(lt, "circle_lock_x", text="X", icon='LOCKED')
4515 else:
4516 row.prop(lt, "circle_lock_x", text="X", icon='UNLOCKED')
4517 if lt.circle_lock_y:
4518 row.prop(lt, "circle_lock_y", text="Y", icon='LOCKED')
4519 else:
4520 row.prop(lt, "circle_lock_y", text="Y", icon='UNLOCKED')
4521 if lt.circle_lock_z:
4522 row.prop(lt, "circle_lock_z", text="Z", icon='LOCKED')
4523 else:
4524 row.prop(lt, "circle_lock_z", text="Z", icon='UNLOCKED')
4525 col_move.prop(lt, "circle_influence")
4527 # curve - first line
4528 split = col.split(factor=0.15, align=True)
4529 if lt.display_curve:
4530 split.prop(lt, "display_curve", text="", icon='DOWNARROW_HLT')
4531 else:
4532 split.prop(lt, "display_curve", text="", icon='RIGHTARROW')
4533 split.operator("mesh.looptools_curve")
4534 # curve - settings
4535 if lt.display_curve:
4536 box = col.column(align=True).box().column()
4537 box.prop(lt, "curve_interpolation")
4538 box.prop(lt, "curve_restriction")
4539 box.prop(lt, "curve_boundaries")
4540 box.prop(lt, "curve_regular")
4541 box.separator()
4543 col_move = box.column(align=True)
4544 row = col_move.row(align=True)
4545 if lt.curve_lock_x:
4546 row.prop(lt, "curve_lock_x", text="X", icon='LOCKED')
4547 else:
4548 row.prop(lt, "curve_lock_x", text="X", icon='UNLOCKED')
4549 if lt.curve_lock_y:
4550 row.prop(lt, "curve_lock_y", text="Y", icon='LOCKED')
4551 else:
4552 row.prop(lt, "curve_lock_y", text="Y", icon='UNLOCKED')
4553 if lt.curve_lock_z:
4554 row.prop(lt, "curve_lock_z", text="Z", icon='LOCKED')
4555 else:
4556 row.prop(lt, "curve_lock_z", text="Z", icon='UNLOCKED')
4557 col_move.prop(lt, "curve_influence")
4559 # flatten - first line
4560 split = col.split(factor=0.15, align=True)
4561 if lt.display_flatten:
4562 split.prop(lt, "display_flatten", text="", icon='DOWNARROW_HLT')
4563 else:
4564 split.prop(lt, "display_flatten", text="", icon='RIGHTARROW')
4565 split.operator("mesh.looptools_flatten")
4566 # flatten - settings
4567 if lt.display_flatten:
4568 box = col.column(align=True).box().column()
4569 box.prop(lt, "flatten_plane")
4570 # box.prop(lt, "flatten_restriction")
4571 box.separator()
4573 col_move = box.column(align=True)
4574 row = col_move.row(align=True)
4575 if lt.flatten_lock_x:
4576 row.prop(lt, "flatten_lock_x", text="X", icon='LOCKED')
4577 else:
4578 row.prop(lt, "flatten_lock_x", text="X", icon='UNLOCKED')
4579 if lt.flatten_lock_y:
4580 row.prop(lt, "flatten_lock_y", text="Y", icon='LOCKED')
4581 else:
4582 row.prop(lt, "flatten_lock_y", text="Y", icon='UNLOCKED')
4583 if lt.flatten_lock_z:
4584 row.prop(lt, "flatten_lock_z", text="Z", icon='LOCKED')
4585 else:
4586 row.prop(lt, "flatten_lock_z", text="Z", icon='UNLOCKED')
4587 col_move.prop(lt, "flatten_influence")
4589 # gstretch - first line
4590 split = col.split(factor=0.15, align=True)
4591 if lt.display_gstretch:
4592 split.prop(lt, "display_gstretch", text="", icon='DOWNARROW_HLT')
4593 else:
4594 split.prop(lt, "display_gstretch", text="", icon='RIGHTARROW')
4595 split.operator("mesh.looptools_gstretch")
4596 # gstretch settings
4597 if lt.display_gstretch:
4598 box = col.column(align=True).box().column()
4599 box.prop(lt, "gstretch_use_guide")
4600 if lt.gstretch_use_guide == "GPencil":
4601 box.prop(lt, "gstretch_guide")
4602 box.prop(lt, "gstretch_method")
4604 col_conv = box.column(align=True)
4605 col_conv.prop(lt, "gstretch_conversion", text="")
4606 if lt.gstretch_conversion == 'distance':
4607 col_conv.prop(lt, "gstretch_conversion_distance")
4608 elif lt.gstretch_conversion == 'limit_vertices':
4609 row = col_conv.row(align=True)
4610 row.prop(lt, "gstretch_conversion_min", text="Min")
4611 row.prop(lt, "gstretch_conversion_max", text="Max")
4612 elif lt.gstretch_conversion == 'vertices':
4613 col_conv.prop(lt, "gstretch_conversion_vertices")
4614 box.separator()
4616 col_move = box.column(align=True)
4617 row = col_move.row(align=True)
4618 if lt.gstretch_lock_x:
4619 row.prop(lt, "gstretch_lock_x", text="X", icon='LOCKED')
4620 else:
4621 row.prop(lt, "gstretch_lock_x", text="X", icon='UNLOCKED')
4622 if lt.gstretch_lock_y:
4623 row.prop(lt, "gstretch_lock_y", text="Y", icon='LOCKED')
4624 else:
4625 row.prop(lt, "gstretch_lock_y", text="Y", icon='UNLOCKED')
4626 if lt.gstretch_lock_z:
4627 row.prop(lt, "gstretch_lock_z", text="Z", icon='LOCKED')
4628 else:
4629 row.prop(lt, "gstretch_lock_z", text="Z", icon='UNLOCKED')
4630 col_move.prop(lt, "gstretch_influence")
4631 if lt.gstretch_use_guide == "Annotation":
4632 box.operator("remove.annotation", text="Delete Annotation Strokes")
4633 if lt.gstretch_use_guide == "GPencil":
4634 box.operator("remove.gp", text="Delete GPencil Strokes")
4636 # loft - first line
4637 split = col.split(factor=0.15, align=True)
4638 if lt.display_loft:
4639 split.prop(lt, "display_loft", text="", icon='DOWNARROW_HLT')
4640 else:
4641 split.prop(lt, "display_loft", text="", icon='RIGHTARROW')
4642 split.operator("mesh.looptools_bridge", text="Loft").loft = True
4643 # loft - settings
4644 if lt.display_loft:
4645 box = col.column(align=True).box().column()
4646 # box.prop(self, "mode")
4648 # top row
4649 col_top = box.column(align=True)
4650 row = col_top.row(align=True)
4651 col_left = row.column(align=True)
4652 col_right = row.column(align=True)
4653 col_right.active = lt.bridge_segments != 1
4654 col_left.prop(lt, "bridge_segments")
4655 col_right.prop(lt, "bridge_min_width", text="")
4656 # bottom row
4657 bottom_left = col_left.row()
4658 bottom_left.active = lt.bridge_segments != 1
4659 bottom_left.prop(lt, "bridge_interpolation", text="")
4660 bottom_right = col_right.row()
4661 bottom_right.active = lt.bridge_interpolation == 'cubic'
4662 bottom_right.prop(lt, "bridge_cubic_strength")
4663 # boolean properties
4664 col_top.prop(lt, "bridge_remove_faces")
4665 col_top.prop(lt, "bridge_loft_loop")
4667 # override properties
4668 col_top.separator()
4669 row = box.row(align=True)
4670 row.prop(lt, "bridge_twist")
4671 row.prop(lt, "bridge_reverse")
4673 # relax - first line
4674 split = col.split(factor=0.15, align=True)
4675 if lt.display_relax:
4676 split.prop(lt, "display_relax", text="", icon='DOWNARROW_HLT')
4677 else:
4678 split.prop(lt, "display_relax", text="", icon='RIGHTARROW')
4679 split.operator("mesh.looptools_relax")
4680 # relax - settings
4681 if lt.display_relax:
4682 box = col.column(align=True).box().column()
4683 box.prop(lt, "relax_interpolation")
4684 box.prop(lt, "relax_input")
4685 box.prop(lt, "relax_iterations")
4686 box.prop(lt, "relax_regular")
4688 # space - first line
4689 split = col.split(factor=0.15, align=True)
4690 if lt.display_space:
4691 split.prop(lt, "display_space", text="", icon='DOWNARROW_HLT')
4692 else:
4693 split.prop(lt, "display_space", text="", icon='RIGHTARROW')
4694 split.operator("mesh.looptools_space")
4695 # space - settings
4696 if lt.display_space:
4697 box = col.column(align=True).box().column()
4698 box.prop(lt, "space_interpolation")
4699 box.prop(lt, "space_input")
4700 box.separator()
4702 col_move = box.column(align=True)
4703 row = col_move.row(align=True)
4704 if lt.space_lock_x:
4705 row.prop(lt, "space_lock_x", text="X", icon='LOCKED')
4706 else:
4707 row.prop(lt, "space_lock_x", text="X", icon='UNLOCKED')
4708 if lt.space_lock_y:
4709 row.prop(lt, "space_lock_y", text="Y", icon='LOCKED')
4710 else:
4711 row.prop(lt, "space_lock_y", text="Y", icon='UNLOCKED')
4712 if lt.space_lock_z:
4713 row.prop(lt, "space_lock_z", text="Z", icon='LOCKED')
4714 else:
4715 row.prop(lt, "space_lock_z", text="Z", icon='UNLOCKED')
4716 col_move.prop(lt, "space_influence")
4719 # property group containing all properties for the gui in the panel
4720 class LoopToolsProps(PropertyGroup):
4722 Fake module like class
4723 bpy.context.window_manager.looptools
4725 # general display properties
4726 display_bridge: BoolProperty(
4727 name="Bridge settings",
4728 description="Display settings of the Bridge tool",
4729 default=False
4731 display_circle: BoolProperty(
4732 name="Circle settings",
4733 description="Display settings of the Circle tool",
4734 default=False
4736 display_curve: BoolProperty(
4737 name="Curve settings",
4738 description="Display settings of the Curve tool",
4739 default=False
4741 display_flatten: BoolProperty(
4742 name="Flatten settings",
4743 description="Display settings of the Flatten tool",
4744 default=False
4746 display_gstretch: BoolProperty(
4747 name="Gstretch settings",
4748 description="Display settings of the Gstretch tool",
4749 default=False
4751 display_loft: BoolProperty(
4752 name="Loft settings",
4753 description="Display settings of the Loft tool",
4754 default=False
4756 display_relax: BoolProperty(
4757 name="Relax settings",
4758 description="Display settings of the Relax tool",
4759 default=False
4761 display_space: BoolProperty(
4762 name="Space settings",
4763 description="Display settings of the Space tool",
4764 default=False
4767 # bridge properties
4768 bridge_cubic_strength: FloatProperty(
4769 name="Strength",
4770 description="Higher strength results in more fluid curves",
4771 default=1.0,
4772 soft_min=-3.0,
4773 soft_max=3.0
4775 bridge_interpolation: EnumProperty(
4776 name="Interpolation mode",
4777 items=(('cubic', "Cubic", "Gives curved results"),
4778 ('linear', "Linear", "Basic, fast, straight interpolation")),
4779 description="Interpolation mode: algorithm used when creating segments",
4780 default='cubic'
4782 bridge_loft: BoolProperty(
4783 name="Loft",
4784 description="Loft multiple loops, instead of considering them as "
4785 "a multi-input for bridging",
4786 default=False
4788 bridge_loft_loop: BoolProperty(
4789 name="Loop",
4790 description="Connect the first and the last loop with each other",
4791 default=False
4793 bridge_min_width: IntProperty(
4794 name="Minimum width",
4795 description="Segments with an edge smaller than this are merged "
4796 "(compared to base edge)",
4797 default=0,
4798 min=0,
4799 max=100,
4800 subtype='PERCENTAGE'
4802 bridge_mode: EnumProperty(
4803 name="Mode",
4804 items=(('basic', "Basic", "Fast algorithm"),
4805 ('shortest', "Shortest edge", "Slower algorithm with "
4806 "better vertex matching")),
4807 description="Algorithm used for bridging",
4808 default='shortest'
4810 bridge_remove_faces: BoolProperty(
4811 name="Remove faces",
4812 description="Remove faces that are internal after bridging",
4813 default=True
4815 bridge_reverse: BoolProperty(
4816 name="Reverse",
4817 description="Manually override the direction in which the loops "
4818 "are bridged. Only use if the tool gives the wrong result",
4819 default=False
4821 bridge_segments: IntProperty(
4822 name="Segments",
4823 description="Number of segments used to bridge the gap (0=automatic)",
4824 default=1,
4825 min=0,
4826 soft_max=20
4828 bridge_twist: IntProperty(
4829 name="Twist",
4830 description="Twist what vertices are connected to each other",
4831 default=0
4834 # circle properties
4835 circle_custom_radius: BoolProperty(
4836 name="Radius",
4837 description="Force a custom radius",
4838 default=False
4840 circle_fit: EnumProperty(
4841 name="Method",
4842 items=(("best", "Best fit", "Non-linear least squares"),
4843 ("inside", "Fit inside", "Only move vertices towards the center")),
4844 description="Method used for fitting a circle to the vertices",
4845 default='best'
4847 circle_flatten: BoolProperty(
4848 name="Flatten",
4849 description="Flatten the circle, instead of projecting it on the mesh",
4850 default=True
4852 circle_influence: FloatProperty(
4853 name="Influence",
4854 description="Force of the tool",
4855 default=100.0,
4856 min=0.0,
4857 max=100.0,
4858 precision=1,
4859 subtype='PERCENTAGE'
4861 circle_lock_x: BoolProperty(
4862 name="Lock X",
4863 description="Lock editing of the x-coordinate",
4864 default=False
4866 circle_lock_y: BoolProperty(
4867 name="Lock Y",
4868 description="Lock editing of the y-coordinate",
4869 default=False
4871 circle_lock_z: BoolProperty(
4872 name="Lock Z",
4873 description="Lock editing of the z-coordinate",
4874 default=False
4876 circle_radius: FloatProperty(
4877 name="Radius",
4878 description="Custom radius for circle",
4879 default=1.0,
4880 min=0.0,
4881 soft_max=1000.0
4883 circle_regular: BoolProperty(
4884 name="Regular",
4885 description="Distribute vertices at constant distances along the circle",
4886 default=True
4888 circle_angle: FloatProperty(
4889 name="Angle",
4890 description="Rotate a circle by an angle",
4891 unit='ROTATION',
4892 default=math.radians(0.0),
4893 soft_min=math.radians(-360.0),
4894 soft_max=math.radians(360.0)
4896 # curve properties
4897 curve_boundaries: BoolProperty(
4898 name="Boundaries",
4899 description="Limit the tool to work within the boundaries of the "
4900 "selected vertices",
4901 default=False
4903 curve_influence: FloatProperty(
4904 name="Influence",
4905 description="Force of the tool",
4906 default=100.0,
4907 min=0.0,
4908 max=100.0,
4909 precision=1,
4910 subtype='PERCENTAGE'
4912 curve_interpolation: EnumProperty(
4913 name="Interpolation",
4914 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4915 ("linear", "Linear", "Simple and fast linear algorithm")),
4916 description="Algorithm used for interpolation",
4917 default='cubic'
4919 curve_lock_x: BoolProperty(
4920 name="Lock X",
4921 description="Lock editing of the x-coordinate",
4922 default=False
4924 curve_lock_y: BoolProperty(
4925 name="Lock Y",
4926 description="Lock editing of the y-coordinate",
4927 default=False
4929 curve_lock_z: BoolProperty(
4930 name="Lock Z",
4931 description="Lock editing of the z-coordinate",
4932 default=False
4934 curve_regular: BoolProperty(
4935 name="Regular",
4936 description="Distribute vertices at constant distances along the curve",
4937 default=True
4939 curve_restriction: EnumProperty(
4940 name="Restriction",
4941 items=(("none", "None", "No restrictions on vertex movement"),
4942 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
4943 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
4944 description="Restrictions on how the vertices can be moved",
4945 default='none'
4948 # flatten properties
4949 flatten_influence: FloatProperty(
4950 name="Influence",
4951 description="Force of the tool",
4952 default=100.0,
4953 min=0.0,
4954 max=100.0,
4955 precision=1,
4956 subtype='PERCENTAGE'
4958 flatten_lock_x: BoolProperty(
4959 name="Lock X",
4960 description="Lock editing of the x-coordinate",
4961 default=False)
4962 flatten_lock_y: BoolProperty(name="Lock Y",
4963 description="Lock editing of the y-coordinate",
4964 default=False
4966 flatten_lock_z: BoolProperty(
4967 name="Lock Z",
4968 description="Lock editing of the z-coordinate",
4969 default=False
4971 flatten_plane: EnumProperty(
4972 name="Plane",
4973 items=(("best_fit", "Best fit", "Calculate a best fitting plane"),
4974 ("normal", "Normal", "Derive plane from averaging vertex "
4975 "normals"),
4976 ("view", "View", "Flatten on a plane perpendicular to the "
4977 "viewing angle")),
4978 description="Plane on which vertices are flattened",
4979 default='best_fit'
4981 flatten_restriction: EnumProperty(
4982 name="Restriction",
4983 items=(("none", "None", "No restrictions on vertex movement"),
4984 ("bounding_box", "Bounding box", "Vertices are restricted to "
4985 "movement inside the bounding box of the selection")),
4986 description="Restrictions on how the vertices can be moved",
4987 default='none'
4990 # gstretch properties
4991 gstretch_conversion: EnumProperty(
4992 name="Conversion",
4993 items=(("distance", "Distance", "Set the distance between vertices "
4994 "of the converted stroke"),
4995 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
4996 "number of vertices that converted GP strokes will have"),
4997 ("vertices", "Exact vertices", "Set the exact number of vertices "
4998 "that converted strokes will have. Short strokes "
4999 "with few points may contain less vertices than this number."),
5000 ("none", "No simplification", "Convert each point "
5001 "to a vertex")),
5002 description="If strokes are converted to geometry, "
5003 "use this simplification method",
5004 default='limit_vertices'
5006 gstretch_conversion_distance: FloatProperty(
5007 name="Distance",
5008 description="Absolute distance between vertices along the converted "
5009 "stroke",
5010 default=0.1,
5011 min=0.000001,
5012 soft_min=0.01,
5013 soft_max=100
5015 gstretch_conversion_max: IntProperty(
5016 name="Max Vertices",
5017 description="Maximum number of vertices strokes will "
5018 "have, when they are converted to geomtery",
5019 default=32,
5020 min=3,
5021 soft_max=500,
5022 update=gstretch_update_min
5024 gstretch_conversion_min: IntProperty(
5025 name="Min Vertices",
5026 description="Minimum number of vertices strokes will "
5027 "have, when they are converted to geomtery",
5028 default=8,
5029 min=3,
5030 soft_max=500,
5031 update=gstretch_update_max
5033 gstretch_conversion_vertices: IntProperty(
5034 name="Vertices",
5035 description="Number of vertices strokes will "
5036 "have, when they are converted to geometry. If strokes have less "
5037 "points than required, the 'Spread evenly' method is used",
5038 default=32,
5039 min=3,
5040 soft_max=500
5042 gstretch_delete_strokes: BoolProperty(
5043 name="Delete strokes",
5044 description="Remove Grease Pencil strokes if they have been used "
5045 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
5046 default=False
5048 gstretch_influence: FloatProperty(
5049 name="Influence",
5050 description="Force of the tool",
5051 default=100.0,
5052 min=0.0,
5053 max=100.0,
5054 precision=1,
5055 subtype='PERCENTAGE'
5057 gstretch_lock_x: BoolProperty(
5058 name="Lock X",
5059 description="Lock editing of the x-coordinate",
5060 default=False
5062 gstretch_lock_y: BoolProperty(
5063 name="Lock Y",
5064 description="Lock editing of the y-coordinate",
5065 default=False
5067 gstretch_lock_z: BoolProperty(
5068 name="Lock Z",
5069 description="Lock editing of the z-coordinate",
5070 default=False
5072 gstretch_method: EnumProperty(
5073 name="Method",
5074 items=(("project", "Project", "Project vertices onto the stroke, "
5075 "using vertex normals and connected edges"),
5076 ("irregular", "Spread", "Distribute vertices along the full "
5077 "stroke, retaining relative distances between the vertices"),
5078 ("regular", "Spread evenly", "Distribute vertices at regular "
5079 "distances along the full stroke")),
5080 description="Method of distributing the vertices over the Grease "
5081 "Pencil stroke",
5082 default='regular'
5084 gstretch_use_guide: EnumProperty(
5085 name="Use guides",
5086 items=(("None", "None", "None"),
5087 ("Annotation", "Annotation", "Annotation"),
5088 ("GPencil", "GPencil", "GPencil")),
5089 default="None"
5091 gstretch_guide: PointerProperty(
5092 name="GPencil object",
5093 description="Set GPencil object",
5094 type=bpy.types.Object
5097 # relax properties
5098 relax_input: EnumProperty(name="Input",
5099 items=(("all", "Parallel (all)", "Also use non-selected "
5100 "parallel loops as input"),
5101 ("selected", "Selection", "Only use selected vertices as input")),
5102 description="Loops that are relaxed",
5103 default='selected'
5105 relax_interpolation: EnumProperty(
5106 name="Interpolation",
5107 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5108 ("linear", "Linear", "Simple and fast linear algorithm")),
5109 description="Algorithm used for interpolation",
5110 default='cubic'
5112 relax_iterations: EnumProperty(name="Iterations",
5113 items=(("1", "1", "One"),
5114 ("3", "3", "Three"),
5115 ("5", "5", "Five"),
5116 ("10", "10", "Ten"),
5117 ("25", "25", "Twenty-five")),
5118 description="Number of times the loop is relaxed",
5119 default="1"
5121 relax_regular: BoolProperty(
5122 name="Regular",
5123 description="Distribute vertices at constant distances along the loop",
5124 default=True
5127 # space properties
5128 space_influence: FloatProperty(
5129 name="Influence",
5130 description="Force of the tool",
5131 default=100.0,
5132 min=0.0,
5133 max=100.0,
5134 precision=1,
5135 subtype='PERCENTAGE'
5137 space_input: EnumProperty(
5138 name="Input",
5139 items=(("all", "Parallel (all)", "Also use non-selected "
5140 "parallel loops as input"),
5141 ("selected", "Selection", "Only use selected vertices as input")),
5142 description="Loops that are spaced",
5143 default='selected'
5145 space_interpolation: EnumProperty(
5146 name="Interpolation",
5147 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5148 ("linear", "Linear", "Vertices are projected on existing edges")),
5149 description="Algorithm used for interpolation",
5150 default='cubic'
5152 space_lock_x: BoolProperty(
5153 name="Lock X",
5154 description="Lock editing of the x-coordinate",
5155 default=False
5157 space_lock_y: BoolProperty(
5158 name="Lock Y",
5159 description="Lock editing of the y-coordinate",
5160 default=False
5162 space_lock_z: BoolProperty(
5163 name="Lock Z",
5164 description="Lock editing of the z-coordinate",
5165 default=False
5168 # draw function for integration in menus
5169 def menu_func(self, context):
5170 self.layout.menu("VIEW3D_MT_edit_mesh_looptools")
5171 self.layout.separator()
5174 # Add-ons Preferences Update Panel
5176 # Define Panel classes for updating
5177 panels = (
5178 VIEW3D_PT_tools_looptools,
5182 def update_panel(self, context):
5183 message = "LoopTools: Updating Panel locations has failed"
5184 try:
5185 for panel in panels:
5186 if "bl_rna" in panel.__dict__:
5187 bpy.utils.unregister_class(panel)
5189 for panel in panels:
5190 panel.bl_category = context.preferences.addons[__name__].preferences.category
5191 bpy.utils.register_class(panel)
5193 except Exception as e:
5194 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
5195 pass
5198 class LoopPreferences(AddonPreferences):
5199 # this must match the addon name, use '__package__'
5200 # when defining this in a submodule of a python package.
5201 bl_idname = __name__
5203 category: StringProperty(
5204 name="Tab Category",
5205 description="Choose a name for the category of the panel",
5206 default="Edit",
5207 update=update_panel
5210 def draw(self, context):
5211 layout = self.layout
5213 row = layout.row()
5214 col = row.column()
5215 col.label(text="Tab Category:")
5216 col.prop(self, "category", text="")
5219 # define classes for registration
5220 classes = (
5221 VIEW3D_MT_edit_mesh_looptools,
5222 VIEW3D_PT_tools_looptools,
5223 LoopToolsProps,
5224 Bridge,
5225 Circle,
5226 Curve,
5227 Flatten,
5228 GStretch,
5229 Relax,
5230 Space,
5231 LoopPreferences,
5232 RemoveAnnotation,
5233 RemoveGPencil,
5237 # registering and menu integration
5238 def register():
5239 for cls in classes:
5240 bpy.utils.register_class(cls)
5241 bpy.types.VIEW3D_MT_edit_mesh_context_menu.prepend(menu_func)
5242 bpy.types.WindowManager.looptools = PointerProperty(type=LoopToolsProps)
5243 update_panel(None, bpy.context)
5246 # unregistering and removing menus
5247 def unregister():
5248 for cls in reversed(classes):
5249 bpy.utils.unregister_class(cls)
5250 bpy.types.VIEW3D_MT_edit_mesh_context_menu.remove(menu_func)
5251 try:
5252 del bpy.types.WindowManager.looptools
5253 except Exception as e:
5254 print('unregister fail:\n', e)
5255 pass
5258 if __name__ == "__main__":
5259 register()