Merge branch 'master' into blender2.8
[blender-addons.git] / mesh_looptools.py
blob25c63f9e412a90aee6eba7e8910b68077041119e
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 bl_info = {
20 "name": "LoopTools",
21 "author": "Bart Crouch",
22 "version": (4, 6, 7),
23 "blender": (2, 80, 0),
24 "location": "View3D > Toolbar and View3D > Specials (W-key)",
25 "warning": "",
26 "description": "Mesh modelling toolkit. Several tools to aid modelling",
27 "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
28 "Scripts/Modeling/LoopTools",
29 "category": "Mesh",
33 import bmesh
34 import bpy
35 import collections
36 import mathutils
37 import math
38 from bpy_extras import view3d_utils
39 from bpy.types import (
40 Operator,
41 Menu,
42 Panel,
43 PropertyGroup,
44 AddonPreferences,
46 from bpy.props import (
47 BoolProperty,
48 EnumProperty,
49 FloatProperty,
50 IntProperty,
51 PointerProperty,
52 StringProperty,
55 # ########################################
56 # ##### General functions ################
57 # ########################################
59 # used by all tools to improve speed on reruns Unlink
60 looptools_cache = {}
63 def get_grease_pencil(object, context):
64 gp = object.grease_pencil
65 if not gp:
66 gp = context.scene.grease_pencil
67 return gp
70 # force a full recalculation next time
71 def cache_delete(tool):
72 if tool in looptools_cache:
73 del looptools_cache[tool]
76 # check cache for stored information
77 def cache_read(tool, object, bm, input_method, boundaries):
78 # current tool not cached yet
79 if tool not in looptools_cache:
80 return(False, False, False, False, False)
81 # check if selected object didn't change
82 if object.name != looptools_cache[tool]["object"]:
83 return(False, False, False, False, False)
84 # check if input didn't change
85 if input_method != looptools_cache[tool]["input_method"]:
86 return(False, False, False, False, False)
87 if boundaries != looptools_cache[tool]["boundaries"]:
88 return(False, False, False, False, False)
89 modifiers = [mod.name for mod in object.modifiers if mod.show_viewport and
90 mod.type == 'MIRROR']
91 if modifiers != looptools_cache[tool]["modifiers"]:
92 return(False, False, False, False, False)
93 input = [v.index for v in bm.verts if v.select and not v.hide]
94 if input != looptools_cache[tool]["input"]:
95 return(False, False, False, False, False)
96 # reading values
97 single_loops = looptools_cache[tool]["single_loops"]
98 loops = looptools_cache[tool]["loops"]
99 derived = looptools_cache[tool]["derived"]
100 mapping = looptools_cache[tool]["mapping"]
102 return(True, single_loops, loops, derived, mapping)
105 # store information in the cache
106 def cache_write(tool, object, bm, input_method, boundaries, single_loops,
107 loops, derived, mapping):
108 # clear cache of current tool
109 if tool in looptools_cache:
110 del looptools_cache[tool]
111 # prepare values to be saved to cache
112 input = [v.index for v in bm.verts if v.select and not v.hide]
113 modifiers = [mod.name for mod in object.modifiers if mod.show_viewport
114 and mod.type == 'MIRROR']
115 # update cache
116 looptools_cache[tool] = {
117 "input": input, "object": object.name,
118 "input_method": input_method, "boundaries": boundaries,
119 "single_loops": single_loops, "loops": loops,
120 "derived": derived, "mapping": mapping, "modifiers": modifiers}
123 # calculates natural cubic splines through all given knots
124 def calculate_cubic_splines(bm_mod, tknots, knots):
125 # hack for circular loops
126 if knots[0] == knots[-1] and len(knots) > 1:
127 circular = True
128 k_new1 = []
129 for k in range(-1, -5, -1):
130 if k - 1 < -len(knots):
131 k += len(knots)
132 k_new1.append(knots[k - 1])
133 k_new2 = []
134 for k in range(4):
135 if k + 1 > len(knots) - 1:
136 k -= len(knots)
137 k_new2.append(knots[k + 1])
138 for k in k_new1:
139 knots.insert(0, k)
140 for k in k_new2:
141 knots.append(k)
142 t_new1 = []
143 total1 = 0
144 for t in range(-1, -5, -1):
145 if t - 1 < -len(tknots):
146 t += len(tknots)
147 total1 += tknots[t] - tknots[t - 1]
148 t_new1.append(tknots[0] - total1)
149 t_new2 = []
150 total2 = 0
151 for t in range(4):
152 if t + 1 > len(tknots) - 1:
153 t -= len(tknots)
154 total2 += tknots[t + 1] - tknots[t]
155 t_new2.append(tknots[-1] + total2)
156 for t in t_new1:
157 tknots.insert(0, t)
158 for t in t_new2:
159 tknots.append(t)
160 else:
161 circular = False
162 # end of hack
164 n = len(knots)
165 if n < 2:
166 return False
167 x = tknots[:]
168 locs = [bm_mod.verts[k].co[:] for k in knots]
169 result = []
170 for j in range(3):
171 a = []
172 for i in locs:
173 a.append(i[j])
174 h = []
175 for i in range(n - 1):
176 if x[i + 1] - x[i] == 0:
177 h.append(1e-8)
178 else:
179 h.append(x[i + 1] - x[i])
180 q = [False]
181 for i in range(1, n - 1):
182 q.append(3 / h[i] * (a[i + 1] - a[i]) - 3 / h[i - 1] * (a[i] - a[i - 1]))
183 l = [1.0]
184 u = [0.0]
185 z = [0.0]
186 for i in range(1, n - 1):
187 l.append(2 * (x[i + 1] - x[i - 1]) - h[i - 1] * u[i - 1])
188 if l[i] == 0:
189 l[i] = 1e-8
190 u.append(h[i] / l[i])
191 z.append((q[i] - h[i - 1] * z[i - 1]) / l[i])
192 l.append(1.0)
193 z.append(0.0)
194 b = [False for i in range(n - 1)]
195 c = [False for i in range(n)]
196 d = [False for i in range(n - 1)]
197 c[n - 1] = 0.0
198 for i in range(n - 2, -1, -1):
199 c[i] = z[i] - u[i] * c[i + 1]
200 b[i] = (a[i + 1] - a[i]) / h[i] - h[i] * (c[i + 1] + 2 * c[i]) / 3
201 d[i] = (c[i + 1] - c[i]) / (3 * h[i])
202 for i in range(n - 1):
203 result.append([a[i], b[i], c[i], d[i], x[i]])
204 splines = []
205 for i in range(len(knots) - 1):
206 splines.append([result[i], result[i + n - 1], result[i + (n - 1) * 2]])
207 if circular: # cleaning up after hack
208 knots = knots[4:-4]
209 tknots = tknots[4:-4]
211 return(splines)
214 # calculates linear splines through all given knots
215 def calculate_linear_splines(bm_mod, tknots, knots):
216 splines = []
217 for i in range(len(knots) - 1):
218 a = bm_mod.verts[knots[i]].co
219 b = bm_mod.verts[knots[i + 1]].co
220 d = b - a
221 t = tknots[i]
222 u = tknots[i + 1] - t
223 splines.append([a, d, t, u]) # [locStart, locDif, tStart, tDif]
225 return(splines)
228 # calculate a best-fit plane to the given vertices
229 def calculate_plane(bm_mod, loop, method="best_fit", object=False):
230 # getting the vertex locations
231 locs = [bm_mod.verts[v].co.copy() for v in loop[0]]
233 # calculating the center of masss
234 com = mathutils.Vector()
235 for loc in locs:
236 com += loc
237 com /= len(locs)
238 x, y, z = com
240 if method == 'best_fit':
241 # creating the covariance matrix
242 mat = mathutils.Matrix(((0.0, 0.0, 0.0),
243 (0.0, 0.0, 0.0),
244 (0.0, 0.0, 0.0),
246 for loc in locs:
247 mat[0][0] += (loc[0] - x) ** 2
248 mat[1][0] += (loc[0] - x) * (loc[1] - y)
249 mat[2][0] += (loc[0] - x) * (loc[2] - z)
250 mat[0][1] += (loc[1] - y) * (loc[0] - x)
251 mat[1][1] += (loc[1] - y) ** 2
252 mat[2][1] += (loc[1] - y) * (loc[2] - z)
253 mat[0][2] += (loc[2] - z) * (loc[0] - x)
254 mat[1][2] += (loc[2] - z) * (loc[1] - y)
255 mat[2][2] += (loc[2] - z) ** 2
257 # calculating the normal to the plane
258 normal = False
259 try:
260 mat = matrix_invert(mat)
261 except:
262 ax = 2
263 if math.fabs(sum(mat[0])) < math.fabs(sum(mat[1])):
264 if math.fabs(sum(mat[0])) < math.fabs(sum(mat[2])):
265 ax = 0
266 elif math.fabs(sum(mat[1])) < math.fabs(sum(mat[2])):
267 ax = 1
268 if ax == 0:
269 normal = mathutils.Vector((1.0, 0.0, 0.0))
270 elif ax == 1:
271 normal = mathutils.Vector((0.0, 1.0, 0.0))
272 else:
273 normal = mathutils.Vector((0.0, 0.0, 1.0))
274 if not normal:
275 # warning! this is different from .normalize()
276 itermax = 500
277 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
278 for i in range(itermax):
279 vec = vec2
280 vec2 = mat @ vec
281 if vec2.length != 0:
282 vec2 /= vec2.length
283 if vec2 == vec:
284 break
285 if vec2.length == 0:
286 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
287 normal = vec2
289 elif method == 'normal':
290 # averaging the vertex normals
291 v_normals = [bm_mod.verts[v].normal for v in loop[0]]
292 normal = mathutils.Vector()
293 for v_normal in v_normals:
294 normal += v_normal
295 normal /= len(v_normals)
296 normal.normalize()
298 elif method == 'view':
299 # calculate view normal
300 rotation = bpy.context.space_data.region_3d.view_matrix.to_3x3().\
301 inverted()
302 normal = rotation * mathutils.Vector((0.0, 0.0, 1.0))
303 if object:
304 normal = object.matrix_world.inverted().to_euler().to_matrix() * \
305 normal
307 return(com, normal)
310 # calculate splines based on given interpolation method (controller function)
311 def calculate_splines(interpolation, bm_mod, tknots, knots):
312 if interpolation == 'cubic':
313 splines = calculate_cubic_splines(bm_mod, tknots, knots[:])
314 else: # interpolations == 'linear'
315 splines = calculate_linear_splines(bm_mod, tknots, knots[:])
317 return(splines)
320 # check loops and only return valid ones
321 def check_loops(loops, mapping, bm_mod):
322 valid_loops = []
323 for loop, circular in loops:
324 # loop needs to have at least 3 vertices
325 if len(loop) < 3:
326 continue
327 # loop needs at least 1 vertex in the original, non-mirrored mesh
328 if mapping:
329 all_virtual = True
330 for vert in loop:
331 if mapping[vert] > -1:
332 all_virtual = False
333 break
334 if all_virtual:
335 continue
336 # vertices can not all be at the same location
337 stacked = True
338 for i in range(len(loop) - 1):
339 if (bm_mod.verts[loop[i]].co - bm_mod.verts[loop[i + 1]].co).length > 1e-6:
340 stacked = False
341 break
342 if stacked:
343 continue
344 # passed all tests, loop is valid
345 valid_loops.append([loop, circular])
347 return(valid_loops)
350 # input: bmesh, output: dict with the edge-key as key and face-index as value
351 def dict_edge_faces(bm):
352 edge_faces = dict([[edgekey(edge), []] for edge in bm.edges if not edge.hide])
353 for face in bm.faces:
354 if face.hide:
355 continue
356 for key in face_edgekeys(face):
357 edge_faces[key].append(face.index)
359 return(edge_faces)
362 # input: bmesh (edge-faces optional), output: dict with face-face connections
363 def dict_face_faces(bm, edge_faces=False):
364 if not edge_faces:
365 edge_faces = dict_edge_faces(bm)
367 connected_faces = dict([[face.index, []] for face in bm.faces if not face.hide])
368 for face in bm.faces:
369 if face.hide:
370 continue
371 for edge_key in face_edgekeys(face):
372 for connected_face in edge_faces[edge_key]:
373 if connected_face == face.index:
374 continue
375 connected_faces[face.index].append(connected_face)
377 return(connected_faces)
380 # input: bmesh, output: dict with the vert index as key and edge-keys as value
381 def dict_vert_edges(bm):
382 vert_edges = dict([[v.index, []] for v in bm.verts if not v.hide])
383 for edge in bm.edges:
384 if edge.hide:
385 continue
386 ek = edgekey(edge)
387 for vert in ek:
388 vert_edges[vert].append(ek)
390 return(vert_edges)
393 # input: bmesh, output: dict with the vert index as key and face index as value
394 def dict_vert_faces(bm):
395 vert_faces = dict([[v.index, []] for v in bm.verts if not v.hide])
396 for face in bm.faces:
397 if not face.hide:
398 for vert in face.verts:
399 vert_faces[vert.index].append(face.index)
401 return(vert_faces)
404 # input: list of edge-keys, output: dictionary with vertex-vertex connections
405 def dict_vert_verts(edge_keys):
406 # create connection data
407 vert_verts = {}
408 for ek in edge_keys:
409 for i in range(2):
410 if ek[i] in vert_verts:
411 vert_verts[ek[i]].append(ek[1 - i])
412 else:
413 vert_verts[ek[i]] = [ek[1 - i]]
415 return(vert_verts)
418 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
419 def edgekey(edge):
420 return(tuple(sorted([edge.verts[0].index, edge.verts[1].index])))
423 # returns the edgekeys of a bmesh face
424 def face_edgekeys(face):
425 return([tuple(sorted([edge.verts[0].index, edge.verts[1].index])) for edge in face.edges])
428 # calculate input loops
429 def get_connected_input(object, bm, input):
430 # get mesh with modifiers applied
431 derived, bm_mod = get_derived_bmesh(object, bm)
433 # calculate selected loops
434 edge_keys = [edgekey(edge) for edge in bm_mod.edges if edge.select and not edge.hide]
435 loops = get_connected_selections(edge_keys)
437 # if only selected loops are needed, we're done
438 if input == 'selected':
439 return(derived, bm_mod, loops)
440 # elif input == 'all':
441 loops = get_parallel_loops(bm_mod, loops)
443 return(derived, bm_mod, loops)
446 # sorts all edge-keys into a list of loops
447 def get_connected_selections(edge_keys):
448 # create connection data
449 vert_verts = dict_vert_verts(edge_keys)
451 # find loops consisting of connected selected edges
452 loops = []
453 while len(vert_verts) > 0:
454 loop = [iter(vert_verts.keys()).__next__()]
455 growing = True
456 flipped = False
458 # extend loop
459 while growing:
460 # no more connection data for current vertex
461 if loop[-1] not in vert_verts:
462 if not flipped:
463 loop.reverse()
464 flipped = True
465 else:
466 growing = False
467 else:
468 extended = False
469 for i, next_vert in enumerate(vert_verts[loop[-1]]):
470 if next_vert not in loop:
471 vert_verts[loop[-1]].pop(i)
472 if len(vert_verts[loop[-1]]) == 0:
473 del vert_verts[loop[-1]]
474 # remove connection both ways
475 if next_vert in vert_verts:
476 if len(vert_verts[next_vert]) == 1:
477 del vert_verts[next_vert]
478 else:
479 vert_verts[next_vert].remove(loop[-1])
480 loop.append(next_vert)
481 extended = True
482 break
483 if not extended:
484 # found one end of the loop, continue with next
485 if not flipped:
486 loop.reverse()
487 flipped = True
488 # found both ends of the loop, stop growing
489 else:
490 growing = False
492 # check if loop is circular
493 if loop[0] in vert_verts:
494 if loop[-1] in vert_verts[loop[0]]:
495 # is circular
496 if len(vert_verts[loop[0]]) == 1:
497 del vert_verts[loop[0]]
498 else:
499 vert_verts[loop[0]].remove(loop[-1])
500 if len(vert_verts[loop[-1]]) == 1:
501 del vert_verts[loop[-1]]
502 else:
503 vert_verts[loop[-1]].remove(loop[0])
504 loop = [loop, True]
505 else:
506 # not circular
507 loop = [loop, False]
508 else:
509 # not circular
510 loop = [loop, False]
512 loops.append(loop)
514 return(loops)
517 # get the derived mesh data, if there is a mirror modifier
518 def get_derived_bmesh(object, bm):
519 # check for mirror modifiers
520 if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
521 derived = True
522 # disable other modifiers
523 show_viewport = [mod.name for mod in object.modifiers if mod.show_viewport]
524 for mod in object.modifiers:
525 if mod.type != 'MIRROR':
526 mod.show_viewport = False
527 # get derived mesh
528 bm_mod = bmesh.new()
529 mesh_mod = object.to_mesh(bpy.context.depsgraph, True)
530 bm_mod.from_mesh(mesh_mod)
531 bpy.context.blend_data.meshes.remove(mesh_mod)
532 # re-enable other modifiers
533 for mod_name in show_viewport:
534 object.modifiers[mod_name].show_viewport = True
535 # no mirror modifiers, so no derived mesh necessary
536 else:
537 derived = False
538 bm_mod = bm
540 bm_mod.verts.ensure_lookup_table()
541 bm_mod.edges.ensure_lookup_table()
542 bm_mod.faces.ensure_lookup_table()
544 return(derived, bm_mod)
547 # return a mapping of derived indices to indices
548 def get_mapping(derived, bm, bm_mod, single_vertices, full_search, loops):
549 if not derived:
550 return(False)
552 if full_search:
553 verts = [v for v in bm.verts if not v.hide]
554 else:
555 verts = [v for v in bm.verts if v.select and not v.hide]
557 # non-selected vertices around single vertices also need to be mapped
558 if single_vertices:
559 mapping = dict([[vert, -1] for vert in single_vertices])
560 verts_mod = [bm_mod.verts[vert] for vert in single_vertices]
561 for v in verts:
562 for v_mod in verts_mod:
563 if (v.co - v_mod.co).length < 1e-6:
564 mapping[v_mod.index] = v.index
565 break
566 real_singles = [v_real for v_real in mapping.values() if v_real > -1]
568 verts_indices = [vert.index for vert in verts]
569 for face in [face for face in bm.faces if not face.select and not face.hide]:
570 for vert in face.verts:
571 if vert.index in real_singles:
572 for v in face.verts:
573 if v.index not in verts_indices:
574 if v not in verts:
575 verts.append(v)
576 break
578 # create mapping of derived indices to indices
579 mapping = dict([[vert, -1] for loop in loops for vert in loop[0]])
580 if single_vertices:
581 for single in single_vertices:
582 mapping[single] = -1
583 verts_mod = [bm_mod.verts[i] for i in mapping.keys()]
584 for v in verts:
585 for v_mod in verts_mod:
586 if (v.co - v_mod.co).length < 1e-6:
587 mapping[v_mod.index] = v.index
588 verts_mod.remove(v_mod)
589 break
591 return(mapping)
594 # calculate the determinant of a matrix
595 def matrix_determinant(m):
596 determinant = m[0][0] * m[1][1] * m[2][2] + m[0][1] * m[1][2] * m[2][0] \
597 + m[0][2] * m[1][0] * m[2][1] - m[0][2] * m[1][1] * m[2][0] \
598 - m[0][1] * m[1][0] * m[2][2] - m[0][0] * m[1][2] * m[2][1]
600 return(determinant)
603 # custom matrix inversion, to provide higher precision than the built-in one
604 def matrix_invert(m):
605 r = mathutils.Matrix((
606 (m[1][1] * m[2][2] - m[1][2] * m[2][1], m[0][2] * m[2][1] - m[0][1] * m[2][2],
607 m[0][1] * m[1][2] - m[0][2] * m[1][1]),
608 (m[1][2] * m[2][0] - m[1][0] * m[2][2], m[0][0] * m[2][2] - m[0][2] * m[2][0],
609 m[0][2] * m[1][0] - m[0][0] * m[1][2]),
610 (m[1][0] * m[2][1] - m[1][1] * m[2][0], m[0][1] * m[2][0] - m[0][0] * m[2][1],
611 m[0][0] * m[1][1] - m[0][1] * m[1][0])))
613 return (r * (1 / matrix_determinant(m)))
616 # returns a list of all loops parallel to the input, input included
617 def get_parallel_loops(bm_mod, loops):
618 # get required dictionaries
619 edge_faces = dict_edge_faces(bm_mod)
620 connected_faces = dict_face_faces(bm_mod, edge_faces)
621 # turn vertex loops into edge loops
622 edgeloops = []
623 for loop in loops:
624 edgeloop = [[sorted([loop[0][i], loop[0][i + 1]]) for i in
625 range(len(loop[0]) - 1)], loop[1]]
626 if loop[1]: # circular
627 edgeloop[0].append(sorted([loop[0][-1], loop[0][0]]))
628 edgeloops.append(edgeloop[:])
629 # variables to keep track while iterating
630 all_edgeloops = []
631 has_branches = False
633 for loop in edgeloops:
634 # initialise with original loop
635 all_edgeloops.append(loop[0])
636 newloops = [loop[0]]
637 verts_used = []
638 for edge in loop[0]:
639 if edge[0] not in verts_used:
640 verts_used.append(edge[0])
641 if edge[1] not in verts_used:
642 verts_used.append(edge[1])
644 # find parallel loops
645 while len(newloops) > 0:
646 side_a = []
647 side_b = []
648 for i in newloops[-1]:
649 i = tuple(i)
650 forbidden_side = False
651 if i not in edge_faces:
652 # weird input with branches
653 has_branches = True
654 break
655 for face in edge_faces[i]:
656 if len(side_a) == 0 and forbidden_side != "a":
657 side_a.append(face)
658 if forbidden_side:
659 break
660 forbidden_side = "a"
661 continue
662 elif side_a[-1] in connected_faces[face] and \
663 forbidden_side != "a":
664 side_a.append(face)
665 if forbidden_side:
666 break
667 forbidden_side = "a"
668 continue
669 if len(side_b) == 0 and forbidden_side != "b":
670 side_b.append(face)
671 if forbidden_side:
672 break
673 forbidden_side = "b"
674 continue
675 elif side_b[-1] in connected_faces[face] and \
676 forbidden_side != "b":
677 side_b.append(face)
678 if forbidden_side:
679 break
680 forbidden_side = "b"
681 continue
683 if has_branches:
684 # weird input with branches
685 break
687 newloops.pop(-1)
688 sides = []
689 if side_a:
690 sides.append(side_a)
691 if side_b:
692 sides.append(side_b)
694 for side in sides:
695 extraloop = []
696 for fi in side:
697 for key in face_edgekeys(bm_mod.faces[fi]):
698 if key[0] not in verts_used and key[1] not in \
699 verts_used:
700 extraloop.append(key)
701 break
702 if extraloop:
703 for key in extraloop:
704 for new_vert in key:
705 if new_vert not in verts_used:
706 verts_used.append(new_vert)
707 newloops.append(extraloop)
708 all_edgeloops.append(extraloop)
710 # input contains branches, only return selected loop
711 if has_branches:
712 return(loops)
714 # change edgeloops into normal loops
715 loops = []
716 for edgeloop in all_edgeloops:
717 loop = []
718 # grow loop by comparing vertices between consecutive edge-keys
719 for i in range(len(edgeloop) - 1):
720 for vert in range(2):
721 if edgeloop[i][vert] in edgeloop[i + 1]:
722 loop.append(edgeloop[i][vert])
723 break
724 if loop:
725 # add starting vertex
726 for vert in range(2):
727 if edgeloop[0][vert] != loop[0]:
728 loop = [edgeloop[0][vert]] + loop
729 break
730 # add ending vertex
731 for vert in range(2):
732 if edgeloop[-1][vert] != loop[-1]:
733 loop.append(edgeloop[-1][vert])
734 break
735 # check if loop is circular
736 if loop[0] == loop[-1]:
737 circular = True
738 loop = loop[:-1]
739 else:
740 circular = False
741 loops.append([loop, circular])
743 return(loops)
746 # gather initial data
747 def initialise():
748 global_undo = bpy.context.user_preferences.edit.use_global_undo
749 bpy.context.user_preferences.edit.use_global_undo = False
750 object = bpy.context.active_object
751 if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
752 # ensure that selection is synced for the derived mesh
753 bpy.ops.object.mode_set(mode='OBJECT')
754 bpy.ops.object.mode_set(mode='EDIT')
755 bm = bmesh.from_edit_mesh(object.data)
757 bm.verts.ensure_lookup_table()
758 bm.edges.ensure_lookup_table()
759 bm.faces.ensure_lookup_table()
761 return(global_undo, object, bm)
764 # move the vertices to their new locations
765 def move_verts(object, bm, mapping, move, lock, influence):
766 if lock:
767 lock_x, lock_y, lock_z = lock
768 orientation = bpy.context.space_data.transform_orientation
769 custom = bpy.context.space_data.current_orientation
770 if custom:
771 mat = custom.matrix.to_4x4().inverted() * object.matrix_world.copy()
772 elif orientation == 'LOCAL':
773 mat = mathutils.Matrix.Identity(4)
774 elif orientation == 'VIEW':
775 mat = bpy.context.region_data.view_matrix.copy() * \
776 object.matrix_world.copy()
777 else: # orientation == 'GLOBAL'
778 mat = object.matrix_world.copy()
779 mat_inv = mat.inverted()
781 for loop in move:
782 for index, loc in loop:
783 if mapping:
784 if mapping[index] == -1:
785 continue
786 else:
787 index = mapping[index]
788 if lock:
789 delta = (loc - bm.verts[index].co) * mat_inv
790 if lock_x:
791 delta[0] = 0
792 if lock_y:
793 delta[1] = 0
794 if lock_z:
795 delta[2] = 0
796 delta = delta * mat
797 loc = bm.verts[index].co + delta
798 if influence < 0:
799 new_loc = loc
800 else:
801 new_loc = loc * (influence / 100) + \
802 bm.verts[index].co * ((100 - influence) / 100)
803 bm.verts[index].co = new_loc
804 bm.normal_update()
805 object.data.update()
807 bm.verts.ensure_lookup_table()
808 bm.edges.ensure_lookup_table()
809 bm.faces.ensure_lookup_table()
812 # load custom tool settings
813 def settings_load(self):
814 lt = bpy.context.window_manager.looptools
815 tool = self.name.split()[0].lower()
816 keys = self.as_keywords().keys()
817 for key in keys:
818 setattr(self, key, getattr(lt, tool + "_" + key))
821 # store custom tool settings
822 def settings_write(self):
823 lt = bpy.context.window_manager.looptools
824 tool = self.name.split()[0].lower()
825 keys = self.as_keywords().keys()
826 for key in keys:
827 setattr(lt, tool + "_" + key, getattr(self, key))
830 # clean up and set settings back to original state
831 def terminate(global_undo):
832 # update editmesh cached data
833 obj = bpy.context.active_object
834 if obj.mode == 'EDIT':
835 bmesh.update_edit_mesh(obj.data, loop_triangles=True, destructive=True)
837 bpy.context.user_preferences.edit.use_global_undo = global_undo
840 # ########################################
841 # ##### Bridge functions #################
842 # ########################################
844 # calculate a cubic spline through the middle section of 4 given coordinates
845 def bridge_calculate_cubic_spline(bm, coordinates):
846 result = []
847 x = [0, 1, 2, 3]
849 for j in range(3):
850 a = []
851 for i in coordinates:
852 a.append(float(i[j]))
853 h = []
854 for i in range(3):
855 h.append(x[i + 1] - x[i])
856 q = [False]
857 for i in range(1, 3):
858 q.append(3.0 / h[i] * (a[i + 1] - a[i]) - 3.0 / h[i - 1] * (a[i] - a[i - 1]))
859 l = [1.0]
860 u = [0.0]
861 z = [0.0]
862 for i in range(1, 3):
863 l.append(2.0 * (x[i + 1] - x[i - 1]) - h[i - 1] * u[i - 1])
864 u.append(h[i] / l[i])
865 z.append((q[i] - h[i - 1] * z[i - 1]) / l[i])
866 l.append(1.0)
867 z.append(0.0)
868 b = [False for i in range(3)]
869 c = [False for i in range(4)]
870 d = [False for i in range(3)]
871 c[3] = 0.0
872 for i in range(2, -1, -1):
873 c[i] = z[i] - u[i] * c[i + 1]
874 b[i] = (a[i + 1] - a[i]) / h[i] - h[i] * (c[i + 1] + 2.0 * c[i]) / 3.0
875 d[i] = (c[i + 1] - c[i]) / (3.0 * h[i])
876 for i in range(3):
877 result.append([a[i], b[i], c[i], d[i], x[i]])
878 spline = [result[1], result[4], result[7]]
880 return(spline)
883 # return a list with new vertex location vectors, a list with face vertex
884 # integers, and the highest vertex integer in the virtual mesh
885 def bridge_calculate_geometry(bm, lines, vertex_normals, segments,
886 interpolation, cubic_strength, min_width, max_vert_index):
887 new_verts = []
888 faces = []
890 # calculate location based on interpolation method
891 def get_location(line, segment, splines):
892 v1 = bm.verts[lines[line][0]].co
893 v2 = bm.verts[lines[line][1]].co
894 if interpolation == 'linear':
895 return v1 + (segment / segments) * (v2 - v1)
896 else: # interpolation == 'cubic'
897 m = (segment / segments)
898 ax, bx, cx, dx, tx = splines[line][0]
899 x = ax + bx * m + cx * m ** 2 + dx * m ** 3
900 ay, by, cy, dy, ty = splines[line][1]
901 y = ay + by * m + cy * m ** 2 + dy * m ** 3
902 az, bz, cz, dz, tz = splines[line][2]
903 z = az + bz * m + cz * m ** 2 + dz * m ** 3
904 return mathutils.Vector((x, y, z))
906 # no interpolation needed
907 if segments == 1:
908 for i, line in enumerate(lines):
909 if i < len(lines) - 1:
910 faces.append([line[0], lines[i + 1][0], lines[i + 1][1], line[1]])
911 # more than 1 segment, interpolate
912 else:
913 # calculate splines (if necessary) once, so no recalculations needed
914 if interpolation == 'cubic':
915 splines = []
916 for line in lines:
917 v1 = bm.verts[line[0]].co
918 v2 = bm.verts[line[1]].co
919 size = (v2 - v1).length * cubic_strength
920 splines.append(bridge_calculate_cubic_spline(bm,
921 [v1 + size * vertex_normals[line[0]], v1, v2,
922 v2 + size * vertex_normals[line[1]]]))
923 else:
924 splines = False
926 # create starting situation
927 virtual_width = [(bm.verts[lines[i][0]].co -
928 bm.verts[lines[i + 1][0]].co).length for i
929 in range(len(lines) - 1)]
930 new_verts = [get_location(0, seg, splines) for seg in range(1,
931 segments)]
932 first_line_indices = [i for i in range(max_vert_index + 1,
933 max_vert_index + segments)]
935 prev_verts = new_verts[:] # vertex locations of verts on previous line
936 prev_vert_indices = first_line_indices[:]
937 max_vert_index += segments - 1 # highest vertex index in virtual mesh
938 next_verts = [] # vertex locations of verts on current line
939 next_vert_indices = []
941 for i, line in enumerate(lines):
942 if i < len(lines) - 1:
943 v1 = line[0]
944 v2 = lines[i + 1][0]
945 end_face = True
946 for seg in range(1, segments):
947 loc1 = prev_verts[seg - 1]
948 loc2 = get_location(i + 1, seg, splines)
949 if (loc1 - loc2).length < (min_width / 100) * virtual_width[i] \
950 and line[1] == lines[i + 1][1]:
951 # triangle, no new vertex
952 faces.append([v1, v2, prev_vert_indices[seg - 1],
953 prev_vert_indices[seg - 1]])
954 next_verts += prev_verts[seg - 1:]
955 next_vert_indices += prev_vert_indices[seg - 1:]
956 end_face = False
957 break
958 else:
959 if i == len(lines) - 2 and lines[0] == lines[-1]:
960 # quad with first line, no new vertex
961 faces.append([v1, v2, first_line_indices[seg - 1],
962 prev_vert_indices[seg - 1]])
963 v2 = first_line_indices[seg - 1]
964 v1 = prev_vert_indices[seg - 1]
965 else:
966 # quad, add new vertex
967 max_vert_index += 1
968 faces.append([v1, v2, max_vert_index,
969 prev_vert_indices[seg - 1]])
970 v2 = max_vert_index
971 v1 = prev_vert_indices[seg - 1]
972 new_verts.append(loc2)
973 next_verts.append(loc2)
974 next_vert_indices.append(max_vert_index)
975 if end_face:
976 faces.append([v1, v2, lines[i + 1][1], line[1]])
978 prev_verts = next_verts[:]
979 prev_vert_indices = next_vert_indices[:]
980 next_verts = []
981 next_vert_indices = []
983 return(new_verts, faces, max_vert_index)
986 # calculate lines (list of lists, vertex indices) that are used for bridging
987 def bridge_calculate_lines(bm, loops, mode, twist, reverse):
988 lines = []
989 loop1, loop2 = [i[0] for i in loops]
990 loop1_circular, loop2_circular = [i[1] for i in loops]
991 circular = loop1_circular or loop2_circular
992 circle_full = False
994 # calculate loop centers
995 centers = []
996 for loop in [loop1, loop2]:
997 center = mathutils.Vector()
998 for vertex in loop:
999 center += bm.verts[vertex].co
1000 center /= len(loop)
1001 centers.append(center)
1002 for i, loop in enumerate([loop1, loop2]):
1003 for vertex in loop:
1004 if bm.verts[vertex].co == centers[i]:
1005 # prevent zero-length vectors in angle comparisons
1006 centers[i] += mathutils.Vector((0.01, 0, 0))
1007 break
1008 center1, center2 = centers
1010 # calculate the normals of the virtual planes that the loops are on
1011 normals = []
1012 normal_plurity = False
1013 for i, loop in enumerate([loop1, loop2]):
1014 # covariance matrix
1015 mat = mathutils.Matrix(((0.0, 0.0, 0.0),
1016 (0.0, 0.0, 0.0),
1017 (0.0, 0.0, 0.0)))
1018 x, y, z = centers[i]
1019 for loc in [bm.verts[vertex].co for vertex in loop]:
1020 mat[0][0] += (loc[0] - x) ** 2
1021 mat[1][0] += (loc[0] - x) * (loc[1] - y)
1022 mat[2][0] += (loc[0] - x) * (loc[2] - z)
1023 mat[0][1] += (loc[1] - y) * (loc[0] - x)
1024 mat[1][1] += (loc[1] - y) ** 2
1025 mat[2][1] += (loc[1] - y) * (loc[2] - z)
1026 mat[0][2] += (loc[2] - z) * (loc[0] - x)
1027 mat[1][2] += (loc[2] - z) * (loc[1] - y)
1028 mat[2][2] += (loc[2] - z) ** 2
1029 # plane normal
1030 normal = False
1031 if sum(mat[0]) < 1e-6 or sum(mat[1]) < 1e-6 or sum(mat[2]) < 1e-6:
1032 normal_plurity = True
1033 try:
1034 mat.invert()
1035 except:
1036 if sum(mat[0]) == 0:
1037 normal = mathutils.Vector((1.0, 0.0, 0.0))
1038 elif sum(mat[1]) == 0:
1039 normal = mathutils.Vector((0.0, 1.0, 0.0))
1040 elif sum(mat[2]) == 0:
1041 normal = mathutils.Vector((0.0, 0.0, 1.0))
1042 if not normal:
1043 # warning! this is different from .normalize()
1044 itermax = 500
1045 iter = 0
1046 vec = mathutils.Vector((1.0, 1.0, 1.0))
1047 vec2 = (mat @ vec) / (mat @ vec).length
1048 while vec != vec2 and iter < itermax:
1049 iter += 1
1050 vec = vec2
1051 vec2 = mat @ vec
1052 if vec2.length != 0:
1053 vec2 /= vec2.length
1054 if vec2.length == 0:
1055 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
1056 normal = vec2
1057 normals.append(normal)
1058 # have plane normals face in the same direction (maximum angle: 90 degrees)
1059 if ((center1 + normals[0]) - center2).length < \
1060 ((center1 - normals[0]) - center2).length:
1061 normals[0].negate()
1062 if ((center2 + normals[1]) - center1).length > \
1063 ((center2 - normals[1]) - center1).length:
1064 normals[1].negate()
1066 # rotation matrix, representing the difference between the plane normals
1067 axis = normals[0].cross(normals[1])
1068 axis = mathutils.Vector([loc if abs(loc) > 1e-8 else 0 for loc in axis])
1069 if axis.angle(mathutils.Vector((0, 0, 1)), 0) > 1.5707964:
1070 axis.negate()
1071 angle = normals[0].dot(normals[1])
1072 rotation_matrix = mathutils.Matrix.Rotation(angle, 4, axis)
1074 # if circular, rotate loops so they are aligned
1075 if circular:
1076 # make sure loop1 is the circular one (or both are circular)
1077 if loop2_circular and not loop1_circular:
1078 loop1_circular, loop2_circular = True, False
1079 loop1, loop2 = loop2, loop1
1081 # match start vertex of loop1 with loop2
1082 target_vector = bm.verts[loop2[0]].co - center2
1083 dif_angles = [[(rotation_matrix @ (bm.verts[vertex].co - center1)
1084 ).angle(target_vector, 0), False, i] for
1085 i, vertex in enumerate(loop1)]
1086 dif_angles.sort()
1087 if len(loop1) != len(loop2):
1088 angle_limit = dif_angles[0][0] * 1.2 # 20% margin
1089 dif_angles = [
1090 [(bm.verts[loop2[0]].co -
1091 bm.verts[loop1[index]].co).length, angle, index] for
1092 angle, distance, index in dif_angles if angle <= angle_limit
1094 dif_angles.sort()
1095 loop1 = loop1[dif_angles[0][2]:] + loop1[:dif_angles[0][2]]
1097 # have both loops face the same way
1098 if normal_plurity and not circular:
1099 second_to_first, second_to_second, second_to_last = [
1100 (bm.verts[loop1[1]].co - center1).angle(
1101 bm.verts[loop2[i]].co - center2) for i in [0, 1, -1]
1103 last_to_first, last_to_second = [
1104 (bm.verts[loop1[-1]].co -
1105 center1).angle(bm.verts[loop2[i]].co - center2) for
1106 i in [0, 1]
1108 if (min(last_to_first, last_to_second) * 1.1 < min(second_to_first,
1109 second_to_second)) or (loop2_circular and second_to_last * 1.1 <
1110 min(second_to_first, second_to_second)):
1111 loop1.reverse()
1112 if circular:
1113 loop1 = [loop1[-1]] + loop1[:-1]
1114 else:
1115 angle = (bm.verts[loop1[0]].co - center1).\
1116 cross(bm.verts[loop1[1]].co - center1).angle(normals[0], 0)
1117 target_angle = (bm.verts[loop2[0]].co - center2).\
1118 cross(bm.verts[loop2[1]].co - center2).angle(normals[1], 0)
1119 limit = 1.5707964 # 0.5*pi, 90 degrees
1120 if not ((angle > limit and target_angle > limit) or
1121 (angle < limit and target_angle < limit)):
1122 loop1.reverse()
1123 if circular:
1124 loop1 = [loop1[-1]] + loop1[:-1]
1125 elif normals[0].angle(normals[1]) > limit:
1126 loop1.reverse()
1127 if circular:
1128 loop1 = [loop1[-1]] + loop1[:-1]
1130 # both loops have the same length
1131 if len(loop1) == len(loop2):
1132 # manual override
1133 if twist:
1134 if abs(twist) < len(loop1):
1135 loop1 = loop1[twist:] + loop1[:twist]
1136 if reverse:
1137 loop1.reverse()
1139 lines.append([loop1[0], loop2[0]])
1140 for i in range(1, len(loop1)):
1141 lines.append([loop1[i], loop2[i]])
1143 # loops of different lengths
1144 else:
1145 # make loop1 longest loop
1146 if len(loop2) > len(loop1):
1147 loop1, loop2 = loop2, loop1
1148 loop1_circular, loop2_circular = loop2_circular, loop1_circular
1150 # manual override
1151 if twist:
1152 if abs(twist) < len(loop1):
1153 loop1 = loop1[twist:] + loop1[:twist]
1154 if reverse:
1155 loop1.reverse()
1157 # shortest angle difference doesn't always give correct start vertex
1158 if loop1_circular and not loop2_circular:
1159 shifting = 1
1160 while shifting:
1161 if len(loop1) - shifting < len(loop2):
1162 shifting = False
1163 break
1164 to_last, to_first = [
1165 (rotation_matrix @ (bm.verts[loop1[-1]].co - center1)).angle(
1166 (bm.verts[loop2[i]].co - center2), 0) for i in [-1, 0]
1168 if to_first < to_last:
1169 loop1 = [loop1[-1]] + loop1[:-1]
1170 shifting += 1
1171 else:
1172 shifting = False
1173 break
1175 # basic shortest side first
1176 if mode == 'basic':
1177 lines.append([loop1[0], loop2[0]])
1178 for i in range(1, len(loop1)):
1179 if i >= len(loop2) - 1:
1180 # triangles
1181 lines.append([loop1[i], loop2[-1]])
1182 else:
1183 # quads
1184 lines.append([loop1[i], loop2[i]])
1186 # shortest edge algorithm
1187 else: # mode == 'shortest'
1188 lines.append([loop1[0], loop2[0]])
1189 prev_vert2 = 0
1190 for i in range(len(loop1) - 1):
1191 if prev_vert2 == len(loop2) - 1 and not loop2_circular:
1192 # force triangles, reached end of loop2
1193 tri, quad = 0, 1
1194 elif prev_vert2 == len(loop2) - 1 and loop2_circular:
1195 # at end of loop2, but circular, so check with first vert
1196 tri, quad = [(bm.verts[loop1[i + 1]].co -
1197 bm.verts[loop2[j]].co).length
1198 for j in [prev_vert2, 0]]
1199 circle_full = 2
1200 elif len(loop1) - 1 - i == len(loop2) - 1 - prev_vert2 and \
1201 not circle_full:
1202 # force quads, otherwise won't make it to end of loop2
1203 tri, quad = 1, 0
1204 else:
1205 # calculate if tri or quad gives shortest edge
1206 tri, quad = [(bm.verts[loop1[i + 1]].co -
1207 bm.verts[loop2[j]].co).length
1208 for j in range(prev_vert2, prev_vert2 + 2)]
1210 # triangle
1211 if tri < quad:
1212 lines.append([loop1[i + 1], loop2[prev_vert2]])
1213 if circle_full == 2:
1214 circle_full = False
1215 # quad
1216 elif not circle_full:
1217 lines.append([loop1[i + 1], loop2[prev_vert2 + 1]])
1218 prev_vert2 += 1
1219 # quad to first vertex of loop2
1220 else:
1221 lines.append([loop1[i + 1], loop2[0]])
1222 prev_vert2 = 0
1223 circle_full = True
1225 # final face for circular loops
1226 if loop1_circular and loop2_circular:
1227 lines.append([loop1[0], loop2[0]])
1229 return(lines)
1232 # calculate number of segments needed
1233 def bridge_calculate_segments(bm, lines, loops, segments):
1234 # return if amount of segments is set by user
1235 if segments != 0:
1236 return segments
1238 # edge lengths
1239 average_edge_length = [
1240 (bm.verts[vertex].co -
1241 bm.verts[loop[0][i + 1]].co).length for loop in loops for
1242 i, vertex in enumerate(loop[0][:-1])
1244 # closing edges of circular loops
1245 average_edge_length += [
1246 (bm.verts[loop[0][-1]].co -
1247 bm.verts[loop[0][0]].co).length for loop in loops if loop[1]
1250 # average lengths
1251 average_edge_length = sum(average_edge_length) / len(average_edge_length)
1252 average_bridge_length = sum(
1253 [(bm.verts[v1].co -
1254 bm.verts[v2].co).length for v1, v2 in lines]
1255 ) / len(lines)
1257 segments = max(1, round(average_bridge_length / average_edge_length))
1259 return(segments)
1262 # return dictionary with vertex index as key, and the normal vector as value
1263 def bridge_calculate_virtual_vertex_normals(bm, lines, loops, edge_faces,
1264 edgekey_to_edge):
1265 if not edge_faces: # interpolation isn't set to cubic
1266 return False
1268 # pity reduce() isn't one of the basic functions in python anymore
1269 def average_vector_dictionary(dic):
1270 for key, vectors in dic.items():
1271 # if type(vectors) == type([]) and len(vectors) > 1:
1272 if len(vectors) > 1:
1273 average = mathutils.Vector()
1274 for vector in vectors:
1275 average += vector
1276 average /= len(vectors)
1277 dic[key] = [average]
1278 return dic
1280 # get all edges of the loop
1281 edges = [
1282 [edgekey_to_edge[tuple(sorted([loops[j][0][i],
1283 loops[j][0][i + 1]]))] for i in range(len(loops[j][0]) - 1)] for
1284 j in [0, 1]
1286 edges = edges[0] + edges[1]
1287 for j in [0, 1]:
1288 if loops[j][1]: # circular
1289 edges.append(edgekey_to_edge[tuple(sorted([loops[j][0][0],
1290 loops[j][0][-1]]))])
1293 calculation based on face topology (assign edge-normals to vertices)
1295 edge_normal = face_normal x edge_vector
1296 vertex_normal = average(edge_normals)
1298 vertex_normals = dict([(vertex, []) for vertex in loops[0][0] + loops[1][0]])
1299 for edge in edges:
1300 faces = edge_faces[edgekey(edge)] # valid faces connected to edge
1302 if faces:
1303 # get edge coordinates
1304 v1, v2 = [bm.verts[edgekey(edge)[i]].co for i in [0, 1]]
1305 edge_vector = v1 - v2
1306 if edge_vector.length < 1e-4:
1307 # zero-length edge, vertices at same location
1308 continue
1309 edge_center = (v1 + v2) / 2
1311 # average face coordinates, if connected to more than 1 valid face
1312 if len(faces) > 1:
1313 face_normal = mathutils.Vector()
1314 face_center = mathutils.Vector()
1315 for face in faces:
1316 face_normal += face.normal
1317 face_center += face.calc_center_median()
1318 face_normal /= len(faces)
1319 face_center /= len(faces)
1320 else:
1321 face_normal = faces[0].normal
1322 face_center = faces[0].calc_center_median()
1323 if face_normal.length < 1e-4:
1324 # faces with a surface of 0 have no face normal
1325 continue
1327 # calculate virtual edge normal
1328 edge_normal = edge_vector.cross(face_normal)
1329 edge_normal.length = 0.01
1330 if (face_center - (edge_center + edge_normal)).length > \
1331 (face_center - (edge_center - edge_normal)).length:
1332 # make normal face the correct way
1333 edge_normal.negate()
1334 edge_normal.normalize()
1335 # add virtual edge normal as entry for both vertices it connects
1336 for vertex in edgekey(edge):
1337 vertex_normals[vertex].append(edge_normal)
1340 calculation based on connection with other loop (vertex focused method)
1341 - used for vertices that aren't connected to any valid faces
1343 plane_normal = edge_vector x connection_vector
1344 vertex_normal = plane_normal x edge_vector
1346 vertices = [
1347 vertex for vertex, normal in vertex_normals.items() if not normal
1350 if vertices:
1351 # edge vectors connected to vertices
1352 edge_vectors = dict([[vertex, []] for vertex in vertices])
1353 for edge in edges:
1354 for v in edgekey(edge):
1355 if v in edge_vectors:
1356 edge_vector = bm.verts[edgekey(edge)[0]].co - \
1357 bm.verts[edgekey(edge)[1]].co
1358 if edge_vector.length < 1e-4:
1359 # zero-length edge, vertices at same location
1360 continue
1361 edge_vectors[v].append(edge_vector)
1363 # connection vectors between vertices of both loops
1364 connection_vectors = dict([[vertex, []] for vertex in vertices])
1365 connections = dict([[vertex, []] for vertex in vertices])
1366 for v1, v2 in lines:
1367 if v1 in connection_vectors or v2 in connection_vectors:
1368 new_vector = bm.verts[v1].co - bm.verts[v2].co
1369 if new_vector.length < 1e-4:
1370 # zero-length connection vector,
1371 # vertices in different loops at same location
1372 continue
1373 if v1 in connection_vectors:
1374 connection_vectors[v1].append(new_vector)
1375 connections[v1].append(v2)
1376 if v2 in connection_vectors:
1377 connection_vectors[v2].append(new_vector)
1378 connections[v2].append(v1)
1379 connection_vectors = average_vector_dictionary(connection_vectors)
1380 connection_vectors = dict(
1381 [[vertex, vector[0]] if vector else
1382 [vertex, []] for vertex, vector in connection_vectors.items()]
1385 for vertex, values in edge_vectors.items():
1386 # vertex normal doesn't matter, just assign a random vector to it
1387 if not connection_vectors[vertex]:
1388 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1389 continue
1391 # calculate to what location the vertex is connected,
1392 # used to determine what way to flip the normal
1393 connected_center = mathutils.Vector()
1394 for v in connections[vertex]:
1395 connected_center += bm.verts[v].co
1396 if len(connections[vertex]) > 1:
1397 connected_center /= len(connections[vertex])
1398 if len(connections[vertex]) == 0:
1399 # shouldn't be possible, but better safe than sorry
1400 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1401 continue
1403 # can't do proper calculations, because of zero-length vector
1404 if not values:
1405 if (connected_center - (bm.verts[vertex].co +
1406 connection_vectors[vertex])).length < (connected_center -
1407 (bm.verts[vertex].co - connection_vectors[vertex])).length:
1408 connection_vectors[vertex].negate()
1409 vertex_normals[vertex] = [connection_vectors[vertex].normalized()]
1410 continue
1412 # calculate vertex normals using edge-vectors,
1413 # connection-vectors and the derived plane normal
1414 for edge_vector in values:
1415 plane_normal = edge_vector.cross(connection_vectors[vertex])
1416 vertex_normal = edge_vector.cross(plane_normal)
1417 vertex_normal.length = 0.1
1418 if (connected_center - (bm.verts[vertex].co +
1419 vertex_normal)).length < (connected_center -
1420 (bm.verts[vertex].co - vertex_normal)).length:
1421 # make normal face the correct way
1422 vertex_normal.negate()
1423 vertex_normal.normalize()
1424 vertex_normals[vertex].append(vertex_normal)
1426 # average virtual vertex normals, based on all edges it's connected to
1427 vertex_normals = average_vector_dictionary(vertex_normals)
1428 vertex_normals = dict([[vertex, vector[0]] for vertex, vector in vertex_normals.items()])
1430 return(vertex_normals)
1433 # add vertices to mesh
1434 def bridge_create_vertices(bm, vertices):
1435 for i in range(len(vertices)):
1436 bm.verts.new(vertices[i])
1437 bm.verts.ensure_lookup_table()
1440 # add faces to mesh
1441 def bridge_create_faces(object, bm, faces, twist):
1442 # have the normal point the correct way
1443 if twist < 0:
1444 [face.reverse() for face in faces]
1445 faces = [face[2:] + face[:2] if face[0] == face[1] else face for face in faces]
1447 # eekadoodle prevention
1448 for i in range(len(faces)):
1449 if not faces[i][-1]:
1450 if faces[i][0] == faces[i][-1]:
1451 faces[i] = [faces[i][1], faces[i][2], faces[i][3], faces[i][1]]
1452 else:
1453 faces[i] = [faces[i][-1]] + faces[i][:-1]
1454 # result of converting from pre-bmesh period
1455 if faces[i][-1] == faces[i][-2]:
1456 faces[i] = faces[i][:-1]
1458 new_faces = []
1459 for i in range(len(faces)):
1460 new_faces.append(bm.faces.new([bm.verts[v] for v in faces[i]]))
1461 bm.normal_update()
1462 object.data.update(calc_edges=True) # calc_edges prevents memory-corruption
1464 bm.verts.ensure_lookup_table()
1465 bm.edges.ensure_lookup_table()
1466 bm.faces.ensure_lookup_table()
1468 return(new_faces)
1471 # calculate input loops
1472 def bridge_get_input(bm):
1473 # create list of internal edges, which should be skipped
1474 eks_of_selected_faces = [
1475 item for sublist in [face_edgekeys(face) for
1476 face in bm.faces if face.select and not face.hide] for item in sublist
1478 edge_count = {}
1479 for ek in eks_of_selected_faces:
1480 if ek in edge_count:
1481 edge_count[ek] += 1
1482 else:
1483 edge_count[ek] = 1
1484 internal_edges = [ek for ek in edge_count if edge_count[ek] > 1]
1486 # sort correct edges into loops
1487 selected_edges = [
1488 edgekey(edge) for edge in bm.edges if edge.select and
1489 not edge.hide and edgekey(edge) not in internal_edges
1491 loops = get_connected_selections(selected_edges)
1493 return(loops)
1496 # return values needed by the bridge operator
1497 def bridge_initialise(bm, interpolation):
1498 if interpolation == 'cubic':
1499 # dict with edge-key as key and list of connected valid faces as value
1500 face_blacklist = [
1501 face.index for face in bm.faces if face.select or
1502 face.hide
1504 edge_faces = dict(
1505 [[edgekey(edge), []] for edge in bm.edges if not edge.hide]
1507 for face in bm.faces:
1508 if face.index in face_blacklist:
1509 continue
1510 for key in face_edgekeys(face):
1511 edge_faces[key].append(face)
1512 # dictionary with the edge-key as key and edge as value
1513 edgekey_to_edge = dict(
1514 [[edgekey(edge), edge] for edge in bm.edges if edge.select and not edge.hide]
1516 else:
1517 edge_faces = False
1518 edgekey_to_edge = False
1520 # selected faces input
1521 old_selected_faces = [
1522 face.index for face in bm.faces if face.select and not face.hide
1525 # find out if faces created by bridging should be smoothed
1526 smooth = False
1527 if bm.faces:
1528 if sum([face.smooth for face in bm.faces]) / len(bm.faces) >= 0.5:
1529 smooth = True
1531 return(edge_faces, edgekey_to_edge, old_selected_faces, smooth)
1534 # return a string with the input method
1535 def bridge_input_method(loft, loft_loop):
1536 method = ""
1537 if loft:
1538 if loft_loop:
1539 method = "Loft loop"
1540 else:
1541 method = "Loft no-loop"
1542 else:
1543 method = "Bridge"
1545 return(method)
1548 # match up loops in pairs, used for multi-input bridging
1549 def bridge_match_loops(bm, loops):
1550 # calculate average loop normals and centers
1551 normals = []
1552 centers = []
1553 for vertices, circular in loops:
1554 normal = mathutils.Vector()
1555 center = mathutils.Vector()
1556 for vertex in vertices:
1557 normal += bm.verts[vertex].normal
1558 center += bm.verts[vertex].co
1559 normals.append(normal / len(vertices) / 10)
1560 centers.append(center / len(vertices))
1562 # possible matches if loop normals are faced towards the center
1563 # of the other loop
1564 matches = dict([[i, []] for i in range(len(loops))])
1565 matches_amount = 0
1566 for i in range(len(loops) + 1):
1567 for j in range(i + 1, len(loops)):
1568 if (centers[i] - centers[j]).length > \
1569 (centers[i] - (centers[j] + normals[j])).length and \
1570 (centers[j] - centers[i]).length > \
1571 (centers[j] - (centers[i] + normals[i])).length:
1572 matches_amount += 1
1573 matches[i].append([(centers[i] - centers[j]).length, i, j])
1574 matches[j].append([(centers[i] - centers[j]).length, j, i])
1575 # if no loops face each other, just make matches between all the loops
1576 if matches_amount == 0:
1577 for i in range(len(loops) + 1):
1578 for j in range(i + 1, len(loops)):
1579 matches[i].append([(centers[i] - centers[j]).length, i, j])
1580 matches[j].append([(centers[i] - centers[j]).length, j, i])
1581 for key, value in matches.items():
1582 value.sort()
1584 # matches based on distance between centers and number of vertices in loops
1585 new_order = []
1586 for loop_index in range(len(loops)):
1587 if loop_index in new_order:
1588 continue
1589 loop_matches = matches[loop_index]
1590 if not loop_matches:
1591 continue
1592 shortest_distance = loop_matches[0][0]
1593 shortest_distance *= 1.1
1594 loop_matches = [
1595 [abs(len(loops[loop_index][0]) -
1596 len(loops[loop[2]][0])), loop[0], loop[1], loop[2]] for loop in
1597 loop_matches if loop[0] < shortest_distance
1599 loop_matches.sort()
1600 for match in loop_matches:
1601 if match[3] not in new_order:
1602 new_order += [loop_index, match[3]]
1603 break
1605 # reorder loops based on matches
1606 if len(new_order) >= 2:
1607 loops = [loops[i] for i in new_order]
1609 return(loops)
1612 # remove old_selected_faces
1613 def bridge_remove_internal_faces(bm, old_selected_faces):
1614 # collect bmesh faces and internal bmesh edges
1615 remove_faces = [bm.faces[face] for face in old_selected_faces]
1616 edges = collections.Counter(
1617 [edge.index for face in remove_faces for edge in face.edges]
1619 remove_edges = [bm.edges[edge] for edge in edges if edges[edge] > 1]
1621 # remove internal faces and edges
1622 for face in remove_faces:
1623 bm.faces.remove(face)
1624 for edge in remove_edges:
1625 bm.edges.remove(edge)
1627 bm.faces.ensure_lookup_table()
1628 bm.edges.ensure_lookup_table()
1629 bm.verts.ensure_lookup_table()
1632 # update list of internal faces that are flagged for removal
1633 def bridge_save_unused_faces(bm, old_selected_faces, loops):
1634 # key: vertex index, value: lists of selected faces using it
1636 vertex_to_face = dict([[i, []] for i in range(len(bm.verts))])
1637 [[vertex_to_face[vertex.index].append(face) for vertex in
1638 bm.faces[face].verts] for face in old_selected_faces]
1640 # group selected faces that are connected
1641 groups = []
1642 grouped_faces = []
1643 for face in old_selected_faces:
1644 if face in grouped_faces:
1645 continue
1646 grouped_faces.append(face)
1647 group = [face]
1648 new_faces = [face]
1649 while new_faces:
1650 grow_face = new_faces[0]
1651 for vertex in bm.faces[grow_face].verts:
1652 vertex_face_group = [
1653 face for face in vertex_to_face[vertex.index] if
1654 face not in grouped_faces
1656 new_faces += vertex_face_group
1657 grouped_faces += vertex_face_group
1658 group += vertex_face_group
1659 new_faces.pop(0)
1660 groups.append(group)
1662 # key: vertex index, value: True/False (is it in a loop that is used)
1663 used_vertices = dict([[i, 0] for i in range(len(bm.verts))])
1664 for loop in loops:
1665 for vertex in loop[0]:
1666 used_vertices[vertex] = True
1668 # check if group is bridged, if not remove faces from internal faces list
1669 for group in groups:
1670 used = False
1671 for face in group:
1672 if used:
1673 break
1674 for vertex in bm.faces[face].verts:
1675 if used_vertices[vertex.index]:
1676 used = True
1677 break
1678 if not used:
1679 for face in group:
1680 old_selected_faces.remove(face)
1683 # add the newly created faces to the selection
1684 def bridge_select_new_faces(new_faces, smooth):
1685 for face in new_faces:
1686 face.select_set(True)
1687 face.smooth = smooth
1690 # sort loops, so they are connected in the correct order when lofting
1691 def bridge_sort_loops(bm, loops, loft_loop):
1692 # simplify loops to single points, and prepare for pathfinding
1693 x, y, z = [
1694 [sum([bm.verts[i].co[j] for i in loop[0]]) /
1695 len(loop[0]) for loop in loops] for j in range(3)
1697 nodes = [mathutils.Vector((x[i], y[i], z[i])) for i in range(len(loops))]
1699 active_node = 0
1700 open = [i for i in range(1, len(loops))]
1701 path = [[0, 0]]
1702 # connect node to path, that is shortest to active_node
1703 while len(open) > 0:
1704 distances = [(nodes[active_node] - nodes[i]).length for i in open]
1705 active_node = open[distances.index(min(distances))]
1706 open.remove(active_node)
1707 path.append([active_node, min(distances)])
1708 # check if we didn't start in the middle of the path
1709 for i in range(2, len(path)):
1710 if (nodes[path[i][0]] - nodes[0]).length < path[i][1]:
1711 temp = path[:i]
1712 path.reverse()
1713 path = path[:-i] + temp
1714 break
1716 # reorder loops
1717 loops = [loops[i[0]] for i in path]
1718 # if requested, duplicate first loop at last position, so loft can loop
1719 if loft_loop:
1720 loops = loops + [loops[0]]
1722 return(loops)
1725 # remapping old indices to new position in list
1726 def bridge_update_old_selection(bm, old_selected_faces):
1728 old_indices = old_selected_faces[:]
1729 old_selected_faces = []
1730 for i, face in enumerate(bm.faces):
1731 if face.index in old_indices:
1732 old_selected_faces.append(i)
1734 old_selected_faces = [
1735 i for i, face in enumerate(bm.faces) if face.index in old_selected_faces
1738 return(old_selected_faces)
1741 # ########################################
1742 # ##### Circle functions #################
1743 # ########################################
1745 # convert 3d coordinates to 2d coordinates on plane
1746 def circle_3d_to_2d(bm_mod, loop, com, normal):
1747 # project vertices onto the plane
1748 verts = [bm_mod.verts[v] for v in loop[0]]
1749 verts_projected = [[v.co - (v.co - com).dot(normal) * normal, v.index]
1750 for v in verts]
1752 # calculate two vectors (p and q) along the plane
1753 m = mathutils.Vector((normal[0] + 1.0, normal[1], normal[2]))
1754 p = m - (m.dot(normal) * normal)
1755 if p.dot(p) < 1e-6:
1756 m = mathutils.Vector((normal[0], normal[1] + 1.0, normal[2]))
1757 p = m - (m.dot(normal) * normal)
1758 q = p.cross(normal)
1760 # change to 2d coordinates using perpendicular projection
1761 locs_2d = []
1762 for loc, vert in verts_projected:
1763 vloc = loc - com
1764 x = p.dot(vloc) / p.dot(p)
1765 y = q.dot(vloc) / q.dot(q)
1766 locs_2d.append([x, y, vert])
1768 return(locs_2d, p, q)
1771 # calculate a best-fit circle to the 2d locations on the plane
1772 def circle_calculate_best_fit(locs_2d):
1773 # initial guess
1774 x0 = 0.0
1775 y0 = 0.0
1776 r = 1.0
1778 # calculate center and radius (non-linear least squares solution)
1779 for iter in range(500):
1780 jmat = []
1781 k = []
1782 for v in locs_2d:
1783 d = (v[0] ** 2 - 2.0 * x0 * v[0] + v[1] ** 2 - 2.0 * y0 * v[1] + x0 ** 2 + y0 ** 2) ** 0.5
1784 jmat.append([(x0 - v[0]) / d, (y0 - v[1]) / d, -1.0])
1785 k.append(-(((v[0] - x0) ** 2 + (v[1] - y0) ** 2) ** 0.5 - r))
1786 jmat2 = mathutils.Matrix(((0.0, 0.0, 0.0),
1787 (0.0, 0.0, 0.0),
1788 (0.0, 0.0, 0.0),
1790 k2 = mathutils.Vector((0.0, 0.0, 0.0))
1791 for i in range(len(jmat)):
1792 k2 += mathutils.Vector(jmat[i]) * k[i]
1793 jmat2[0][0] += jmat[i][0] ** 2
1794 jmat2[1][0] += jmat[i][0] * jmat[i][1]
1795 jmat2[2][0] += jmat[i][0] * jmat[i][2]
1796 jmat2[1][1] += jmat[i][1] ** 2
1797 jmat2[2][1] += jmat[i][1] * jmat[i][2]
1798 jmat2[2][2] += jmat[i][2] ** 2
1799 jmat2[0][1] = jmat2[1][0]
1800 jmat2[0][2] = jmat2[2][0]
1801 jmat2[1][2] = jmat2[2][1]
1802 try:
1803 jmat2.invert()
1804 except:
1805 pass
1806 dx0, dy0, dr = jmat2 @ k2
1807 x0 += dx0
1808 y0 += dy0
1809 r += dr
1810 # stop iterating if we're close enough to optimal solution
1811 if abs(dx0) < 1e-6 and abs(dy0) < 1e-6 and abs(dr) < 1e-6:
1812 break
1814 # return center of circle and radius
1815 return(x0, y0, r)
1818 # calculate circle so no vertices have to be moved away from the center
1819 def circle_calculate_min_fit(locs_2d):
1820 # center of circle
1821 x0 = (min([i[0] for i in locs_2d]) + max([i[0] for i in locs_2d])) / 2.0
1822 y0 = (min([i[1] for i in locs_2d]) + max([i[1] for i in locs_2d])) / 2.0
1823 center = mathutils.Vector([x0, y0])
1824 # radius of circle
1825 r = min([(mathutils.Vector([i[0], i[1]]) - center).length for i in locs_2d])
1827 # return center of circle and radius
1828 return(x0, y0, r)
1831 # calculate the new locations of the vertices that need to be moved
1832 def circle_calculate_verts(flatten, bm_mod, locs_2d, com, p, q, normal):
1833 # changing 2d coordinates back to 3d coordinates
1834 locs_3d = []
1835 for loc in locs_2d:
1836 locs_3d.append([loc[2], loc[0] * p + loc[1] * q + com])
1838 if flatten: # flat circle
1839 return(locs_3d)
1841 else: # project the locations on the existing mesh
1842 vert_edges = dict_vert_edges(bm_mod)
1843 vert_faces = dict_vert_faces(bm_mod)
1844 faces = [f for f in bm_mod.faces if not f.hide]
1845 rays = [normal, -normal]
1846 new_locs = []
1847 for loc in locs_3d:
1848 projection = False
1849 if bm_mod.verts[loc[0]].co == loc[1]: # vertex hasn't moved
1850 projection = loc[1]
1851 else:
1852 dif = normal.angle(loc[1] - bm_mod.verts[loc[0]].co)
1853 if -1e-6 < dif < 1e-6 or math.pi - 1e-6 < dif < math.pi + 1e-6:
1854 # original location is already along projection normal
1855 projection = bm_mod.verts[loc[0]].co
1856 else:
1857 # quick search through adjacent faces
1858 for face in vert_faces[loc[0]]:
1859 verts = [v.co for v in bm_mod.faces[face].verts]
1860 if len(verts) == 3: # triangle
1861 v1, v2, v3 = verts
1862 v4 = False
1863 else: # assume quad
1864 v1, v2, v3, v4 = verts[:4]
1865 for ray in rays:
1866 intersect = mathutils.geometry.\
1867 intersect_ray_tri(v1, v2, v3, ray, loc[1])
1868 if intersect:
1869 projection = intersect
1870 break
1871 elif v4:
1872 intersect = mathutils.geometry.\
1873 intersect_ray_tri(v1, v3, v4, ray, loc[1])
1874 if intersect:
1875 projection = intersect
1876 break
1877 if projection:
1878 break
1879 if not projection:
1880 # check if projection is on adjacent edges
1881 for edgekey in vert_edges[loc[0]]:
1882 line1 = bm_mod.verts[edgekey[0]].co
1883 line2 = bm_mod.verts[edgekey[1]].co
1884 intersect, dist = mathutils.geometry.intersect_point_line(
1885 loc[1], line1, line2
1887 if 1e-6 < dist < 1 - 1e-6:
1888 projection = intersect
1889 break
1890 if not projection:
1891 # full search through the entire mesh
1892 hits = []
1893 for face in faces:
1894 verts = [v.co for v in face.verts]
1895 if len(verts) == 3: # triangle
1896 v1, v2, v3 = verts
1897 v4 = False
1898 else: # assume quad
1899 v1, v2, v3, v4 = verts[:4]
1900 for ray in rays:
1901 intersect = mathutils.geometry.intersect_ray_tri(
1902 v1, v2, v3, ray, loc[1]
1904 if intersect:
1905 hits.append([(loc[1] - intersect).length,
1906 intersect])
1907 break
1908 elif v4:
1909 intersect = mathutils.geometry.intersect_ray_tri(
1910 v1, v3, v4, ray, loc[1]
1912 if intersect:
1913 hits.append([(loc[1] - intersect).length,
1914 intersect])
1915 break
1916 if len(hits) >= 1:
1917 # if more than 1 hit with mesh, closest hit is new loc
1918 hits.sort()
1919 projection = hits[0][1]
1920 if not projection:
1921 # nothing to project on, remain at flat location
1922 projection = loc[1]
1923 new_locs.append([loc[0], projection])
1925 # return new positions of projected circle
1926 return(new_locs)
1929 # check loops and only return valid ones
1930 def circle_check_loops(single_loops, loops, mapping, bm_mod):
1931 valid_single_loops = {}
1932 valid_loops = []
1933 for i, [loop, circular] in enumerate(loops):
1934 # loop needs to have at least 3 vertices
1935 if len(loop) < 3:
1936 continue
1937 # loop needs at least 1 vertex in the original, non-mirrored mesh
1938 if mapping:
1939 all_virtual = True
1940 for vert in loop:
1941 if mapping[vert] > -1:
1942 all_virtual = False
1943 break
1944 if all_virtual:
1945 continue
1946 # loop has to be non-collinear
1947 collinear = True
1948 loc0 = mathutils.Vector(bm_mod.verts[loop[0]].co[:])
1949 loc1 = mathutils.Vector(bm_mod.verts[loop[1]].co[:])
1950 for v in loop[2:]:
1951 locn = mathutils.Vector(bm_mod.verts[v].co[:])
1952 if loc0 == loc1 or loc1 == locn:
1953 loc0 = loc1
1954 loc1 = locn
1955 continue
1956 d1 = loc1 - loc0
1957 d2 = locn - loc1
1958 if -1e-6 < d1.angle(d2, 0) < 1e-6:
1959 loc0 = loc1
1960 loc1 = locn
1961 continue
1962 collinear = False
1963 break
1964 if collinear:
1965 continue
1966 # passed all tests, loop is valid
1967 valid_loops.append([loop, circular])
1968 valid_single_loops[len(valid_loops) - 1] = single_loops[i]
1970 return(valid_single_loops, valid_loops)
1973 # calculate the location of single input vertices that need to be flattened
1974 def circle_flatten_singles(bm_mod, com, p, q, normal, single_loop):
1975 new_locs = []
1976 for vert in single_loop:
1977 loc = mathutils.Vector(bm_mod.verts[vert].co[:])
1978 new_locs.append([vert, loc - (loc - com).dot(normal) * normal])
1980 return(new_locs)
1983 # calculate input loops
1984 def circle_get_input(object, bm):
1985 # get mesh with modifiers applied
1986 derived, bm_mod = get_derived_bmesh(object, bm)
1988 # create list of edge-keys based on selection state
1989 faces = False
1990 for face in bm.faces:
1991 if face.select and not face.hide:
1992 faces = True
1993 break
1994 if faces:
1995 # get selected, non-hidden , non-internal edge-keys
1996 eks_selected = [
1997 key for keys in [face_edgekeys(face) for face in
1998 bm_mod.faces if face.select and not face.hide] for key in keys
2000 edge_count = {}
2001 for ek in eks_selected:
2002 if ek in edge_count:
2003 edge_count[ek] += 1
2004 else:
2005 edge_count[ek] = 1
2006 edge_keys = [
2007 edgekey(edge) for edge in bm_mod.edges if edge.select and
2008 not edge.hide and edge_count.get(edgekey(edge), 1) == 1
2010 else:
2011 # no faces, so no internal edges either
2012 edge_keys = [
2013 edgekey(edge) for edge in bm_mod.edges if edge.select and not edge.hide
2016 # add edge-keys around single vertices
2017 verts_connected = dict(
2018 [[vert, 1] for edge in [edge for edge in
2019 bm_mod.edges if edge.select and not edge.hide] for vert in
2020 edgekey(edge)]
2022 single_vertices = [
2023 vert.index for vert in bm_mod.verts if
2024 vert.select and not vert.hide and
2025 not verts_connected.get(vert.index, False)
2028 if single_vertices and len(bm.faces) > 0:
2029 vert_to_single = dict(
2030 [[v.index, []] for v in bm_mod.verts if not v.hide]
2032 for face in [face for face in bm_mod.faces if not face.select and not face.hide]:
2033 for vert in face.verts:
2034 vert = vert.index
2035 if vert in single_vertices:
2036 for ek in face_edgekeys(face):
2037 if vert not in ek:
2038 edge_keys.append(ek)
2039 if vert not in vert_to_single[ek[0]]:
2040 vert_to_single[ek[0]].append(vert)
2041 if vert not in vert_to_single[ek[1]]:
2042 vert_to_single[ek[1]].append(vert)
2043 break
2045 # sort edge-keys into loops
2046 loops = get_connected_selections(edge_keys)
2048 # find out to which loops the single vertices belong
2049 single_loops = dict([[i, []] for i in range(len(loops))])
2050 if single_vertices and len(bm.faces) > 0:
2051 for i, [loop, circular] in enumerate(loops):
2052 for vert in loop:
2053 if vert_to_single[vert]:
2054 for single in vert_to_single[vert]:
2055 if single not in single_loops[i]:
2056 single_loops[i].append(single)
2058 return(derived, bm_mod, single_vertices, single_loops, loops)
2061 # recalculate positions based on the influence of the circle shape
2062 def circle_influence_locs(locs_2d, new_locs_2d, influence):
2063 for i in range(len(locs_2d)):
2064 oldx, oldy, j = locs_2d[i]
2065 newx, newy, k = new_locs_2d[i]
2066 altx = newx * (influence / 100) + oldx * ((100 - influence) / 100)
2067 alty = newy * (influence / 100) + oldy * ((100 - influence) / 100)
2068 locs_2d[i] = [altx, alty, j]
2070 return(locs_2d)
2073 # project 2d locations on circle, respecting distance relations between verts
2074 def circle_project_non_regular(locs_2d, x0, y0, r):
2075 for i in range(len(locs_2d)):
2076 x, y, j = locs_2d[i]
2077 loc = mathutils.Vector([x - x0, y - y0])
2078 loc.length = r
2079 locs_2d[i] = [loc[0], loc[1], j]
2081 return(locs_2d)
2084 # project 2d locations on circle, with equal distance between all vertices
2085 def circle_project_regular(locs_2d, x0, y0, r):
2086 # find offset angle and circling direction
2087 x, y, i = locs_2d[0]
2088 loc = mathutils.Vector([x - x0, y - y0])
2089 loc.length = r
2090 offset_angle = loc.angle(mathutils.Vector([1.0, 0.0]), 0.0)
2091 loca = mathutils.Vector([x - x0, y - y0, 0.0])
2092 if loc[1] < -1e-6:
2093 offset_angle *= -1
2094 x, y, j = locs_2d[1]
2095 locb = mathutils.Vector([x - x0, y - y0, 0.0])
2096 if loca.cross(locb)[2] >= 0:
2097 ccw = 1
2098 else:
2099 ccw = -1
2100 # distribute vertices along the circle
2101 for i in range(len(locs_2d)):
2102 t = offset_angle + ccw * (i / len(locs_2d) * 2 * math.pi)
2103 x = math.cos(t) * r
2104 y = math.sin(t) * r
2105 locs_2d[i] = [x, y, locs_2d[i][2]]
2107 return(locs_2d)
2110 # shift loop, so the first vertex is closest to the center
2111 def circle_shift_loop(bm_mod, loop, com):
2112 verts, circular = loop
2113 distances = [
2114 [(bm_mod.verts[vert].co - com).length, i] for i, vert in enumerate(verts)
2116 distances.sort()
2117 shift = distances[0][1]
2118 loop = [verts[shift:] + verts[:shift], circular]
2120 return(loop)
2123 # ########################################
2124 # ##### Curve functions ##################
2125 # ########################################
2127 # create lists with knots and points, all correctly sorted
2128 def curve_calculate_knots(loop, verts_selected):
2129 knots = [v for v in loop[0] if v in verts_selected]
2130 points = loop[0][:]
2131 # circular loop, potential for weird splines
2132 if loop[1]:
2133 offset = int(len(loop[0]) / 4)
2134 kpos = []
2135 for k in knots:
2136 kpos.append(loop[0].index(k))
2137 kdif = []
2138 for i in range(len(kpos) - 1):
2139 kdif.append(kpos[i + 1] - kpos[i])
2140 kdif.append(len(loop[0]) - kpos[-1] + kpos[0])
2141 kadd = []
2142 for k in kdif:
2143 if k > 2 * offset:
2144 kadd.append([kdif.index(k), True])
2145 # next 2 lines are optional, they insert
2146 # an extra control point in small gaps
2147 # elif k > offset:
2148 # kadd.append([kdif.index(k), False])
2149 kins = []
2150 krot = False
2151 for k in kadd: # extra knots to be added
2152 if k[1]: # big gap (break circular spline)
2153 kpos = loop[0].index(knots[k[0]]) + offset
2154 if kpos > len(loop[0]) - 1:
2155 kpos -= len(loop[0])
2156 kins.append([knots[k[0]], loop[0][kpos]])
2157 kpos2 = k[0] + 1
2158 if kpos2 > len(knots) - 1:
2159 kpos2 -= len(knots)
2160 kpos2 = loop[0].index(knots[kpos2]) - offset
2161 if kpos2 < 0:
2162 kpos2 += len(loop[0])
2163 kins.append([loop[0][kpos], loop[0][kpos2]])
2164 krot = loop[0][kpos2]
2165 else: # small gap (keep circular spline)
2166 k1 = loop[0].index(knots[k[0]])
2167 k2 = k[0] + 1
2168 if k2 > len(knots) - 1:
2169 k2 -= len(knots)
2170 k2 = loop[0].index(knots[k2])
2171 if k2 < k1:
2172 dif = len(loop[0]) - 1 - k1 + k2
2173 else:
2174 dif = k2 - k1
2175 kn = k1 + int(dif / 2)
2176 if kn > len(loop[0]) - 1:
2177 kn -= len(loop[0])
2178 kins.append([loop[0][k1], loop[0][kn]])
2179 for j in kins: # insert new knots
2180 knots.insert(knots.index(j[0]) + 1, j[1])
2181 if not krot: # circular loop
2182 knots.append(knots[0])
2183 points = loop[0][loop[0].index(knots[0]):]
2184 points += loop[0][0:loop[0].index(knots[0]) + 1]
2185 else: # non-circular loop (broken by script)
2186 krot = knots.index(krot)
2187 knots = knots[krot:] + knots[0:krot]
2188 if loop[0].index(knots[0]) > loop[0].index(knots[-1]):
2189 points = loop[0][loop[0].index(knots[0]):]
2190 points += loop[0][0:loop[0].index(knots[-1]) + 1]
2191 else:
2192 points = loop[0][loop[0].index(knots[0]):loop[0].index(knots[-1]) + 1]
2193 # non-circular loop, add first and last point as knots
2194 else:
2195 if loop[0][0] not in knots:
2196 knots.insert(0, loop[0][0])
2197 if loop[0][-1] not in knots:
2198 knots.append(loop[0][-1])
2200 return(knots, points)
2203 # calculate relative positions compared to first knot
2204 def curve_calculate_t(bm_mod, knots, points, pknots, regular, circular):
2205 tpoints = []
2206 loc_prev = False
2207 len_total = 0
2209 for p in points:
2210 if p in knots:
2211 loc = pknots[knots.index(p)] # use projected knot location
2212 else:
2213 loc = mathutils.Vector(bm_mod.verts[p].co[:])
2214 if not loc_prev:
2215 loc_prev = loc
2216 len_total += (loc - loc_prev).length
2217 tpoints.append(len_total)
2218 loc_prev = loc
2219 tknots = []
2220 for p in points:
2221 if p in knots:
2222 tknots.append(tpoints[points.index(p)])
2223 if circular:
2224 tknots[-1] = tpoints[-1]
2226 # regular option
2227 if regular:
2228 tpoints_average = tpoints[-1] / (len(tpoints) - 1)
2229 for i in range(1, len(tpoints) - 1):
2230 tpoints[i] = i * tpoints_average
2231 for i in range(len(knots)):
2232 tknots[i] = tpoints[points.index(knots[i])]
2233 if circular:
2234 tknots[-1] = tpoints[-1]
2236 return(tknots, tpoints)
2239 # change the location of non-selected points to their place on the spline
2240 def curve_calculate_vertices(bm_mod, knots, tknots, points, tpoints, splines,
2241 interpolation, restriction):
2242 newlocs = {}
2243 move = []
2245 for p in points:
2246 if p in knots:
2247 continue
2248 m = tpoints[points.index(p)]
2249 if m in tknots:
2250 n = tknots.index(m)
2251 else:
2252 t = tknots[:]
2253 t.append(m)
2254 t.sort()
2255 n = t.index(m) - 1
2256 if n > len(splines) - 1:
2257 n = len(splines) - 1
2258 elif n < 0:
2259 n = 0
2261 if interpolation == 'cubic':
2262 ax, bx, cx, dx, tx = splines[n][0]
2263 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
2264 ay, by, cy, dy, ty = splines[n][1]
2265 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
2266 az, bz, cz, dz, tz = splines[n][2]
2267 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
2268 newloc = mathutils.Vector([x, y, z])
2269 else: # interpolation == 'linear'
2270 a, d, t, u = splines[n]
2271 newloc = ((m - t) / u) * d + a
2273 if restriction != 'none': # vertex movement is restricted
2274 newlocs[p] = newloc
2275 else: # set the vertex to its new location
2276 move.append([p, newloc])
2278 if restriction != 'none': # vertex movement is restricted
2279 for p in points:
2280 if p in newlocs:
2281 newloc = newlocs[p]
2282 else:
2283 move.append([p, bm_mod.verts[p].co])
2284 continue
2285 oldloc = bm_mod.verts[p].co
2286 normal = bm_mod.verts[p].normal
2287 dloc = newloc - oldloc
2288 if dloc.length < 1e-6:
2289 move.append([p, newloc])
2290 elif restriction == 'extrude': # only extrusions
2291 if dloc.angle(normal, 0) < 0.5 * math.pi + 1e-6:
2292 move.append([p, newloc])
2293 else: # restriction == 'indent' only indentations
2294 if dloc.angle(normal) > 0.5 * math.pi - 1e-6:
2295 move.append([p, newloc])
2297 return(move)
2300 # trim loops to part between first and last selected vertices (including)
2301 def curve_cut_boundaries(bm_mod, loops):
2302 cut_loops = []
2303 for loop, circular in loops:
2304 if circular:
2305 # don't cut
2306 cut_loops.append([loop, circular])
2307 continue
2308 selected = [bm_mod.verts[v].select for v in loop]
2309 first = selected.index(True)
2310 selected.reverse()
2311 last = -selected.index(True)
2312 if last == 0:
2313 cut_loops.append([loop[first:], circular])
2314 else:
2315 cut_loops.append([loop[first:last], circular])
2317 return(cut_loops)
2320 # calculate input loops
2321 def curve_get_input(object, bm, boundaries):
2322 # get mesh with modifiers applied
2323 derived, bm_mod = get_derived_bmesh(object, bm)
2325 # vertices that still need a loop to run through it
2326 verts_unsorted = [
2327 v.index for v in bm_mod.verts if v.select and not v.hide
2329 # necessary dictionaries
2330 vert_edges = dict_vert_edges(bm_mod)
2331 edge_faces = dict_edge_faces(bm_mod)
2332 correct_loops = []
2333 # find loops through each selected vertex
2334 while len(verts_unsorted) > 0:
2335 loops = curve_vertex_loops(bm_mod, verts_unsorted[0], vert_edges,
2336 edge_faces)
2337 verts_unsorted.pop(0)
2339 # check if loop is fully selected
2340 search_perpendicular = False
2341 i = -1
2342 for loop, circular in loops:
2343 i += 1
2344 selected = [v for v in loop if bm_mod.verts[v].select]
2345 if len(selected) < 2:
2346 # only one selected vertex on loop, don't use
2347 loops.pop(i)
2348 continue
2349 elif len(selected) == len(loop):
2350 search_perpendicular = loop
2351 break
2352 # entire loop is selected, find perpendicular loops
2353 if search_perpendicular:
2354 for vert in loop:
2355 if vert in verts_unsorted:
2356 verts_unsorted.remove(vert)
2357 perp_loops = curve_perpendicular_loops(bm_mod, loop,
2358 vert_edges, edge_faces)
2359 for perp_loop in perp_loops:
2360 correct_loops.append(perp_loop)
2361 # normal input
2362 else:
2363 for loop, circular in loops:
2364 correct_loops.append([loop, circular])
2366 # boundaries option
2367 if boundaries:
2368 correct_loops = curve_cut_boundaries(bm_mod, correct_loops)
2370 return(derived, bm_mod, correct_loops)
2373 # return all loops that are perpendicular to the given one
2374 def curve_perpendicular_loops(bm_mod, start_loop, vert_edges, edge_faces):
2375 # find perpendicular loops
2376 perp_loops = []
2377 for start_vert in start_loop:
2378 loops = curve_vertex_loops(bm_mod, start_vert, vert_edges,
2379 edge_faces)
2380 for loop, circular in loops:
2381 selected = [v for v in loop if bm_mod.verts[v].select]
2382 if len(selected) == len(loop):
2383 continue
2384 else:
2385 perp_loops.append([loop, circular, loop.index(start_vert)])
2387 # trim loops to same lengths
2388 shortest = [
2389 [len(loop[0]), i] for i, loop in enumerate(perp_loops) if not loop[1]
2391 if not shortest:
2392 # all loops are circular, not trimming
2393 return([[loop[0], loop[1]] for loop in perp_loops])
2394 else:
2395 shortest = min(shortest)
2396 shortest_start = perp_loops[shortest[1]][2]
2397 before_start = shortest_start
2398 after_start = shortest[0] - shortest_start - 1
2399 bigger_before = before_start > after_start
2400 trimmed_loops = []
2401 for loop in perp_loops:
2402 # have the loop face the same direction as the shortest one
2403 if bigger_before:
2404 if loop[2] < len(loop[0]) / 2:
2405 loop[0].reverse()
2406 loop[2] = len(loop[0]) - loop[2] - 1
2407 else:
2408 if loop[2] > len(loop[0]) / 2:
2409 loop[0].reverse()
2410 loop[2] = len(loop[0]) - loop[2] - 1
2411 # circular loops can shift, to prevent wrong trimming
2412 if loop[1]:
2413 shift = shortest_start - loop[2]
2414 if loop[2] + shift > 0 and loop[2] + shift < len(loop[0]):
2415 loop[0] = loop[0][-shift:] + loop[0][:-shift]
2416 loop[2] += shift
2417 if loop[2] < 0:
2418 loop[2] += len(loop[0])
2419 elif loop[2] > len(loop[0]) - 1:
2420 loop[2] -= len(loop[0])
2421 # trim
2422 start = max(0, loop[2] - before_start)
2423 end = min(len(loop[0]), loop[2] + after_start + 1)
2424 trimmed_loops.append([loop[0][start:end], False])
2426 return(trimmed_loops)
2429 # project knots on non-selected geometry
2430 def curve_project_knots(bm_mod, verts_selected, knots, points, circular):
2431 # function to project vertex on edge
2432 def project(v1, v2, v3):
2433 # v1 and v2 are part of a line
2434 # v3 is projected onto it
2435 v2 -= v1
2436 v3 -= v1
2437 p = v3.project(v2)
2438 return(p + v1)
2440 if circular: # project all knots
2441 start = 0
2442 end = len(knots)
2443 pknots = []
2444 else: # first and last knot shouldn't be projected
2445 start = 1
2446 end = -1
2447 pknots = [mathutils.Vector(bm_mod.verts[knots[0]].co[:])]
2448 for knot in knots[start:end]:
2449 if knot in verts_selected:
2450 knot_left = knot_right = False
2451 for i in range(points.index(knot) - 1, -1 * len(points), -1):
2452 if points[i] not in knots:
2453 knot_left = points[i]
2454 break
2455 for i in range(points.index(knot) + 1, 2 * len(points)):
2456 if i > len(points) - 1:
2457 i -= len(points)
2458 if points[i] not in knots:
2459 knot_right = points[i]
2460 break
2461 if knot_left and knot_right and knot_left != knot_right:
2462 knot_left = mathutils.Vector(bm_mod.verts[knot_left].co[:])
2463 knot_right = mathutils.Vector(bm_mod.verts[knot_right].co[:])
2464 knot = mathutils.Vector(bm_mod.verts[knot].co[:])
2465 pknots.append(project(knot_left, knot_right, knot))
2466 else:
2467 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2468 else: # knot isn't selected, so shouldn't be changed
2469 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2470 if not circular:
2471 pknots.append(mathutils.Vector(bm_mod.verts[knots[-1]].co[:]))
2473 return(pknots)
2476 # find all loops through a given vertex
2477 def curve_vertex_loops(bm_mod, start_vert, vert_edges, edge_faces):
2478 edges_used = []
2479 loops = []
2481 for edge in vert_edges[start_vert]:
2482 if edge in edges_used:
2483 continue
2484 loop = []
2485 circular = False
2486 for vert in edge:
2487 active_faces = edge_faces[edge]
2488 new_vert = vert
2489 growing = True
2490 while growing:
2491 growing = False
2492 new_edges = vert_edges[new_vert]
2493 loop.append(new_vert)
2494 if len(loop) > 1:
2495 edges_used.append(tuple(sorted([loop[-1], loop[-2]])))
2496 if len(new_edges) < 3 or len(new_edges) > 4:
2497 # pole
2498 break
2499 else:
2500 # find next edge
2501 for new_edge in new_edges:
2502 if new_edge in edges_used:
2503 continue
2504 eliminate = False
2505 for new_face in edge_faces[new_edge]:
2506 if new_face in active_faces:
2507 eliminate = True
2508 break
2509 if eliminate:
2510 continue
2511 # found correct new edge
2512 active_faces = edge_faces[new_edge]
2513 v1, v2 = new_edge
2514 if v1 != new_vert:
2515 new_vert = v1
2516 else:
2517 new_vert = v2
2518 if new_vert == loop[0]:
2519 circular = True
2520 else:
2521 growing = True
2522 break
2523 if circular:
2524 break
2525 loop.reverse()
2526 loops.append([loop, circular])
2528 return(loops)
2531 # ########################################
2532 # ##### Flatten functions ################
2533 # ########################################
2535 # sort input into loops
2536 def flatten_get_input(bm):
2537 vert_verts = dict_vert_verts(
2538 [edgekey(edge) for edge in bm.edges if edge.select and not edge.hide]
2540 verts = [v.index for v in bm.verts if v.select and not v.hide]
2542 # no connected verts, consider all selected verts as a single input
2543 if not vert_verts:
2544 return([[verts, False]])
2546 loops = []
2547 while len(verts) > 0:
2548 # start of loop
2549 loop = [verts[0]]
2550 verts.pop(0)
2551 if loop[-1] in vert_verts:
2552 to_grow = vert_verts[loop[-1]]
2553 else:
2554 to_grow = []
2555 # grow loop
2556 while len(to_grow) > 0:
2557 new_vert = to_grow[0]
2558 to_grow.pop(0)
2559 if new_vert in loop:
2560 continue
2561 loop.append(new_vert)
2562 verts.remove(new_vert)
2563 to_grow += vert_verts[new_vert]
2564 # add loop to loops
2565 loops.append([loop, False])
2567 return(loops)
2570 # calculate position of vertex projections on plane
2571 def flatten_project(bm, loop, com, normal):
2572 verts = [bm.verts[v] for v in loop[0]]
2573 verts_projected = [
2574 [v.index, mathutils.Vector(v.co[:]) -
2575 (mathutils.Vector(v.co[:]) - com).dot(normal) * normal] for v in verts
2578 return(verts_projected)
2581 # ########################################
2582 # ##### Gstretch functions ###############
2583 # ########################################
2585 # fake stroke class, used to create custom strokes if no GP data is found
2586 class gstretch_fake_stroke():
2587 def __init__(self, points):
2588 self.points = [gstretch_fake_stroke_point(p) for p in points]
2591 # fake stroke point class, used in fake strokes
2592 class gstretch_fake_stroke_point():
2593 def __init__(self, loc):
2594 self.co = loc
2597 # flips loops, if necessary, to obtain maximum alignment to stroke
2598 def gstretch_align_pairs(ls_pairs, object, bm_mod, method):
2599 # returns total distance between all verts in loop and corresponding stroke
2600 def distance_loop_stroke(loop, stroke, object, bm_mod, method):
2601 stroke_lengths_cache = False
2602 loop_length = len(loop[0])
2603 total_distance = 0
2605 if method != 'regular':
2606 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2608 for i, v_index in enumerate(loop[0]):
2609 if method == 'regular':
2610 relative_distance = i / (loop_length - 1)
2611 else:
2612 relative_distance = relative_lengths[i]
2614 loc1 = object.matrix_world @ bm_mod.verts[v_index].co
2615 loc2, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2616 relative_distance, stroke_lengths_cache)
2617 total_distance += (loc2 - loc1).length
2619 return(total_distance)
2621 if ls_pairs:
2622 for (loop, stroke) in ls_pairs:
2623 total_dist = distance_loop_stroke(loop, stroke, object, bm_mod,
2624 method)
2625 loop[0].reverse()
2626 total_dist_rev = distance_loop_stroke(loop, stroke, object, bm_mod,
2627 method)
2628 if total_dist_rev > total_dist:
2629 loop[0].reverse()
2631 return(ls_pairs)
2634 # calculate vertex positions on stroke
2635 def gstretch_calculate_verts(loop, stroke, object, bm_mod, method):
2636 move = []
2637 stroke_lengths_cache = False
2638 loop_length = len(loop[0])
2639 matrix_inverse = object.matrix_world.inverted()
2641 # return intersection of line with stroke, or None
2642 def intersect_line_stroke(vec1, vec2, stroke):
2643 for i, p in enumerate(stroke.points[1:]):
2644 intersections = mathutils.geometry.intersect_line_line(vec1, vec2,
2645 p.co, stroke.points[i].co)
2646 if intersections and \
2647 (intersections[0] - intersections[1]).length < 1e-2:
2648 x, dist = mathutils.geometry.intersect_point_line(
2649 intersections[0], p.co, stroke.points[i].co)
2650 if -1 < dist < 1:
2651 return(intersections[0])
2652 return(None)
2654 if method == 'project':
2655 vert_edges = dict_vert_edges(bm_mod)
2657 for v_index in loop[0]:
2658 intersection = None
2659 for ek in vert_edges[v_index]:
2660 v1, v2 = ek
2661 v1 = bm_mod.verts[v1]
2662 v2 = bm_mod.verts[v2]
2663 if v1.select + v2.select == 1 and not v1.hide and not v2.hide:
2664 vec1 = object.matrix_world * v1.co
2665 vec2 = object.matrix_world * v2.co
2666 intersection = intersect_line_stroke(vec1, vec2, stroke)
2667 if intersection:
2668 break
2669 if not intersection:
2670 v = bm_mod.verts[v_index]
2671 intersection = intersect_line_stroke(v.co, v.co + v.normal,
2672 stroke)
2673 if intersection:
2674 move.append([v_index, matrix_inverse * intersection])
2676 else:
2677 if method == 'irregular':
2678 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2680 for i, v_index in enumerate(loop[0]):
2681 if method == 'regular':
2682 relative_distance = i / (loop_length - 1)
2683 else: # method == 'irregular'
2684 relative_distance = relative_lengths[i]
2685 loc, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2686 relative_distance, stroke_lengths_cache)
2687 loc = matrix_inverse @ loc
2688 move.append([v_index, loc])
2690 return(move)
2693 # create new vertices, based on GP strokes
2694 def gstretch_create_verts(object, bm_mod, strokes, method, conversion,
2695 conversion_distance, conversion_max, conversion_min, conversion_vertices):
2696 move = []
2697 stroke_verts = []
2698 mat_world = object.matrix_world.inverted()
2699 singles = gstretch_match_single_verts(bm_mod, strokes, mat_world)
2701 for stroke in strokes:
2702 stroke_verts.append([stroke, []])
2703 min_end_point = 0
2704 if conversion == 'vertices':
2705 min_end_point = conversion_vertices
2706 end_point = conversion_vertices
2707 elif conversion == 'limit_vertices':
2708 min_end_point = conversion_min
2709 end_point = conversion_max
2710 else:
2711 end_point = len(stroke.points)
2712 # creation of new vertices at fixed user-defined distances
2713 if conversion == 'distance':
2714 method = 'project'
2715 prev_point = stroke.points[0]
2716 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * prev_point.co))
2717 distance = 0
2718 limit = conversion_distance
2719 for point in stroke.points:
2720 new_distance = distance + (point.co - prev_point.co).length
2721 iteration = 0
2722 while new_distance > limit:
2723 to_cover = limit - distance + (limit * iteration)
2724 new_loc = prev_point.co + to_cover * \
2725 (point.co - prev_point.co).normalized()
2726 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * new_loc))
2727 new_distance -= limit
2728 iteration += 1
2729 distance = new_distance
2730 prev_point = point
2731 # creation of new vertices for other methods
2732 else:
2733 # add vertices at stroke points
2734 for point in stroke.points[:end_point]:
2735 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * point.co))
2736 # add more vertices, beyond the points that are available
2737 if min_end_point > min(len(stroke.points), end_point):
2738 for i in range(min_end_point -
2739 (min(len(stroke.points), end_point))):
2740 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * point.co))
2741 # force even spreading of points, so they are placed on stroke
2742 method = 'regular'
2743 bm_mod.verts.ensure_lookup_table()
2744 bm_mod.verts.index_update()
2745 for stroke, verts_seq in stroke_verts:
2746 if len(verts_seq) < 2:
2747 continue
2748 # spread vertices evenly over the stroke
2749 if method == 'regular':
2750 loop = [[vert.index for vert in verts_seq], False]
2751 move += gstretch_calculate_verts(loop, stroke, object, bm_mod,
2752 method)
2753 # create edges
2754 for i, vert in enumerate(verts_seq):
2755 if i > 0:
2756 bm_mod.edges.new((verts_seq[i - 1], verts_seq[i]))
2757 vert.select = True
2758 # connect single vertices to the closest stroke
2759 if singles:
2760 for vert, m_stroke, point in singles:
2761 if m_stroke != stroke:
2762 continue
2763 bm_mod.edges.new((vert, verts_seq[point]))
2764 bm_mod.edges.ensure_lookup_table()
2765 bmesh.update_edit_mesh(object.data)
2767 return(move)
2770 # erases the grease pencil stroke
2771 def gstretch_erase_stroke(stroke, context):
2772 # change 3d coordinate into a stroke-point
2773 def sp(loc, context):
2774 lib = {'name': "",
2775 'pen_flip': False,
2776 'is_start': False,
2777 'location': (0, 0, 0),
2778 'mouse': (
2779 view3d_utils.location_3d_to_region_2d(
2780 context.region, context.space_data.region_3d, loc)
2782 'pressure': 1,
2783 'size': 0,
2784 'time': 0}
2785 return(lib)
2787 if type(stroke) != bpy.types.GPencilStroke:
2788 # fake stroke, there is nothing to delete
2789 return
2791 erase_stroke = [sp(p.co, context) for p in stroke.points]
2792 if erase_stroke:
2793 erase_stroke[0]['is_start'] = True
2794 bpy.ops.gpencil.draw(mode='ERASER', stroke=erase_stroke)
2797 # get point on stroke, given by relative distance (0.0 - 1.0)
2798 def gstretch_eval_stroke(stroke, distance, stroke_lengths_cache=False):
2799 # use cache if available
2800 if not stroke_lengths_cache:
2801 lengths = [0]
2802 for i, p in enumerate(stroke.points[1:]):
2803 lengths.append((p.co - stroke.points[i].co).length + lengths[-1])
2804 total_length = max(lengths[-1], 1e-7)
2805 stroke_lengths_cache = [length / total_length for length in
2806 lengths]
2807 stroke_lengths = stroke_lengths_cache[:]
2809 if distance in stroke_lengths:
2810 loc = stroke.points[stroke_lengths.index(distance)].co
2811 elif distance > stroke_lengths[-1]:
2812 # should be impossible, but better safe than sorry
2813 loc = stroke.points[-1].co
2814 else:
2815 stroke_lengths.append(distance)
2816 stroke_lengths.sort()
2817 stroke_index = stroke_lengths.index(distance)
2818 interval_length = stroke_lengths[
2819 stroke_index + 1] - stroke_lengths[stroke_index - 1
2821 distance_relative = (distance - stroke_lengths[stroke_index - 1]) / interval_length
2822 interval_vector = stroke.points[stroke_index].co - stroke.points[stroke_index - 1].co
2823 loc = stroke.points[stroke_index - 1].co + distance_relative * interval_vector
2825 return(loc, stroke_lengths_cache)
2828 # create fake grease pencil strokes for the active object
2829 def gstretch_get_fake_strokes(object, bm_mod, loops):
2830 strokes = []
2831 for loop in loops:
2832 p1 = object.matrix_world @ bm_mod.verts[loop[0][0]].co
2833 p2 = object.matrix_world @ bm_mod.verts[loop[0][-1]].co
2834 strokes.append(gstretch_fake_stroke([p1, p2]))
2836 return(strokes)
2839 # get grease pencil strokes for the active object
2840 def gstretch_get_strokes(object, context):
2841 gp = get_grease_pencil(object, context)
2842 if not gp:
2843 return(None)
2844 layer = gp.layers.active
2845 if not layer:
2846 return(None)
2847 frame = layer.active_frame
2848 if not frame:
2849 return(None)
2850 strokes = frame.strokes
2851 if len(strokes) < 1:
2852 return(None)
2854 return(strokes)
2857 # returns a list with loop-stroke pairs
2858 def gstretch_match_loops_strokes(loops, strokes, object, bm_mod):
2859 if not loops or not strokes:
2860 return(None)
2862 # calculate loop centers
2863 loop_centers = []
2864 bm_mod.verts.ensure_lookup_table()
2865 for loop in loops:
2866 center = mathutils.Vector()
2867 for v_index in loop[0]:
2868 center += bm_mod.verts[v_index].co
2869 center /= len(loop[0])
2870 center = object.matrix_world @ center
2871 loop_centers.append([center, loop])
2873 # calculate stroke centers
2874 stroke_centers = []
2875 for stroke in strokes:
2876 center = mathutils.Vector()
2877 for p in stroke.points:
2878 center += p.co
2879 center /= len(stroke.points)
2880 stroke_centers.append([center, stroke, 0])
2882 # match, first by stroke use count, then by distance
2883 ls_pairs = []
2884 for lc in loop_centers:
2885 distances = []
2886 for i, sc in enumerate(stroke_centers):
2887 distances.append([sc[2], (lc[0] - sc[0]).length, i])
2888 distances.sort()
2889 best_stroke = distances[0][2]
2890 ls_pairs.append([lc[1], stroke_centers[best_stroke][1]])
2891 stroke_centers[best_stroke][2] += 1 # increase stroke use count
2893 return(ls_pairs)
2896 # match single selected vertices to the closest stroke endpoint
2897 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2898 def gstretch_match_single_verts(bm_mod, strokes, mat_world):
2899 # calculate stroke endpoints in object space
2900 endpoints = []
2901 for stroke in strokes:
2902 endpoints.append((mat_world * stroke.points[0].co, stroke, 0))
2903 endpoints.append((mat_world * stroke.points[-1].co, stroke, -1))
2905 distances = []
2906 # find single vertices (not connected to other selected verts)
2907 for vert in bm_mod.verts:
2908 if not vert.select:
2909 continue
2910 single = True
2911 for edge in vert.link_edges:
2912 if edge.other_vert(vert).select:
2913 single = False
2914 break
2915 if not single:
2916 continue
2917 # calculate distances from vertex to endpoints
2918 distance = [((vert.co - loc).length, vert, stroke, stroke_point,
2919 endpoint_index) for endpoint_index, (loc, stroke, stroke_point) in
2920 enumerate(endpoints)]
2921 distance.sort()
2922 distances.append(distance[0])
2924 # create matches, based on shortest distance first
2925 singles = []
2926 while distances:
2927 distances.sort()
2928 singles.append((distances[0][1], distances[0][2], distances[0][3]))
2929 endpoints.pop(distances[0][4])
2930 distances.pop(0)
2931 distances_new = []
2932 for (i, vert, j, k, l) in distances:
2933 distance_new = [((vert.co - loc).length, vert, stroke, stroke_point,
2934 endpoint_index) for endpoint_index, (loc, stroke,
2935 stroke_point) in enumerate(endpoints)]
2936 distance_new.sort()
2937 distances_new.append(distance_new[0])
2938 distances = distances_new
2940 return(singles)
2943 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2944 def gstretch_relative_lengths(loop, bm_mod):
2945 lengths = [0]
2946 for i, v_index in enumerate(loop[0][1:]):
2947 lengths.append(
2948 (bm_mod.verts[v_index].co -
2949 bm_mod.verts[loop[0][i]].co).length + lengths[-1]
2951 total_length = max(lengths[-1], 1e-7)
2952 relative_lengths = [length / total_length for length in
2953 lengths]
2955 return(relative_lengths)
2958 # convert cache-stored strokes into usable (fake) GP strokes
2959 def gstretch_safe_to_true_strokes(safe_strokes):
2960 strokes = []
2961 for safe_stroke in safe_strokes:
2962 strokes.append(gstretch_fake_stroke(safe_stroke))
2964 return(strokes)
2967 # convert a GP stroke into a list of points which can be stored in cache
2968 def gstretch_true_to_safe_strokes(strokes):
2969 safe_strokes = []
2970 for stroke in strokes:
2971 safe_strokes.append([p.co.copy() for p in stroke.points])
2973 return(safe_strokes)
2976 # force consistency in GUI, max value can never be lower than min value
2977 def gstretch_update_max(self, context):
2978 # called from operator settings (after execution)
2979 if 'conversion_min' in self.keys():
2980 if self.conversion_min > self.conversion_max:
2981 self.conversion_max = self.conversion_min
2982 # called from toolbar
2983 else:
2984 lt = context.window_manager.looptools
2985 if lt.gstretch_conversion_min > lt.gstretch_conversion_max:
2986 lt.gstretch_conversion_max = lt.gstretch_conversion_min
2989 # force consistency in GUI, min value can never be higher than max value
2990 def gstretch_update_min(self, context):
2991 # called from operator settings (after execution)
2992 if 'conversion_max' in self.keys():
2993 if self.conversion_max < self.conversion_min:
2994 self.conversion_min = self.conversion_max
2995 # called from toolbar
2996 else:
2997 lt = context.window_manager.looptools
2998 if lt.gstretch_conversion_max < lt.gstretch_conversion_min:
2999 lt.gstretch_conversion_min = lt.gstretch_conversion_max
3002 # ########################################
3003 # ##### Relax functions ##################
3004 # ########################################
3006 # create lists with knots and points, all correctly sorted
3007 def relax_calculate_knots(loops):
3008 all_knots = []
3009 all_points = []
3010 for loop, circular in loops:
3011 knots = [[], []]
3012 points = [[], []]
3013 if circular:
3014 if len(loop) % 2 == 1: # odd
3015 extend = [False, True, 0, 1, 0, 1]
3016 else: # even
3017 extend = [True, False, 0, 1, 1, 2]
3018 else:
3019 if len(loop) % 2 == 1: # odd
3020 extend = [False, False, 0, 1, 1, 2]
3021 else: # even
3022 extend = [False, False, 0, 1, 1, 2]
3023 for j in range(2):
3024 if extend[j]:
3025 loop = [loop[-1]] + loop + [loop[0]]
3026 for i in range(extend[2 + 2 * j], len(loop), 2):
3027 knots[j].append(loop[i])
3028 for i in range(extend[3 + 2 * j], len(loop), 2):
3029 if loop[i] == loop[-1] and not circular:
3030 continue
3031 if len(points[j]) == 0:
3032 points[j].append(loop[i])
3033 elif loop[i] != points[j][0]:
3034 points[j].append(loop[i])
3035 if circular:
3036 if knots[j][0] != knots[j][-1]:
3037 knots[j].append(knots[j][0])
3038 if len(points[1]) == 0:
3039 knots.pop(1)
3040 points.pop(1)
3041 for k in knots:
3042 all_knots.append(k)
3043 for p in points:
3044 all_points.append(p)
3046 return(all_knots, all_points)
3049 # calculate relative positions compared to first knot
3050 def relax_calculate_t(bm_mod, knots, points, regular):
3051 all_tknots = []
3052 all_tpoints = []
3053 for i in range(len(knots)):
3054 amount = len(knots[i]) + len(points[i])
3055 mix = []
3056 for j in range(amount):
3057 if j % 2 == 0:
3058 mix.append([True, knots[i][round(j / 2)]])
3059 elif j == amount - 1:
3060 mix.append([True, knots[i][-1]])
3061 else:
3062 mix.append([False, points[i][int(j / 2)]])
3063 len_total = 0
3064 loc_prev = False
3065 tknots = []
3066 tpoints = []
3067 for m in mix:
3068 loc = mathutils.Vector(bm_mod.verts[m[1]].co[:])
3069 if not loc_prev:
3070 loc_prev = loc
3071 len_total += (loc - loc_prev).length
3072 if m[0]:
3073 tknots.append(len_total)
3074 else:
3075 tpoints.append(len_total)
3076 loc_prev = loc
3077 if regular:
3078 tpoints = []
3079 for p in range(len(points[i])):
3080 tpoints.append((tknots[p] + tknots[p + 1]) / 2)
3081 all_tknots.append(tknots)
3082 all_tpoints.append(tpoints)
3084 return(all_tknots, all_tpoints)
3087 # change the location of the points to their place on the spline
3088 def relax_calculate_verts(bm_mod, interpolation, tknots, knots, tpoints,
3089 points, splines):
3090 change = []
3091 move = []
3092 for i in range(len(knots)):
3093 for p in points[i]:
3094 m = tpoints[i][points[i].index(p)]
3095 if m in tknots[i]:
3096 n = tknots[i].index(m)
3097 else:
3098 t = tknots[i][:]
3099 t.append(m)
3100 t.sort()
3101 n = t.index(m) - 1
3102 if n > len(splines[i]) - 1:
3103 n = len(splines[i]) - 1
3104 elif n < 0:
3105 n = 0
3107 if interpolation == 'cubic':
3108 ax, bx, cx, dx, tx = splines[i][n][0]
3109 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
3110 ay, by, cy, dy, ty = splines[i][n][1]
3111 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
3112 az, bz, cz, dz, tz = splines[i][n][2]
3113 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
3114 change.append([p, mathutils.Vector([x, y, z])])
3115 else: # interpolation == 'linear'
3116 a, d, t, u = splines[i][n]
3117 if u == 0:
3118 u = 1e-8
3119 change.append([p, ((m - t) / u) * d + a])
3120 for c in change:
3121 move.append([c[0], (bm_mod.verts[c[0]].co + c[1]) / 2])
3123 return(move)
3126 # ########################################
3127 # ##### Space functions ##################
3128 # ########################################
3130 # calculate relative positions compared to first knot
3131 def space_calculate_t(bm_mod, knots):
3132 tknots = []
3133 loc_prev = False
3134 len_total = 0
3135 for k in knots:
3136 loc = mathutils.Vector(bm_mod.verts[k].co[:])
3137 if not loc_prev:
3138 loc_prev = loc
3139 len_total += (loc - loc_prev).length
3140 tknots.append(len_total)
3141 loc_prev = loc
3142 amount = len(knots)
3143 t_per_segment = len_total / (amount - 1)
3144 tpoints = [i * t_per_segment for i in range(amount)]
3146 return(tknots, tpoints)
3149 # change the location of the points to their place on the spline
3150 def space_calculate_verts(bm_mod, interpolation, tknots, tpoints, points,
3151 splines):
3152 move = []
3153 for p in points:
3154 m = tpoints[points.index(p)]
3155 if m in tknots:
3156 n = tknots.index(m)
3157 else:
3158 t = tknots[:]
3159 t.append(m)
3160 t.sort()
3161 n = t.index(m) - 1
3162 if n > len(splines) - 1:
3163 n = len(splines) - 1
3164 elif n < 0:
3165 n = 0
3167 if interpolation == 'cubic':
3168 ax, bx, cx, dx, tx = splines[n][0]
3169 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
3170 ay, by, cy, dy, ty = splines[n][1]
3171 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
3172 az, bz, cz, dz, tz = splines[n][2]
3173 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
3174 move.append([p, mathutils.Vector([x, y, z])])
3175 else: # interpolation == 'linear'
3176 a, d, t, u = splines[n]
3177 move.append([p, ((m - t) / u) * d + a])
3179 return(move)
3182 # ########################################
3183 # ##### Operators ########################
3184 # ########################################
3186 # bridge operator
3187 class Bridge(Operator):
3188 bl_idname = 'mesh.looptools_bridge'
3189 bl_label = "Bridge / Loft"
3190 bl_description = "Bridge two, or loft several, loops of vertices"
3191 bl_options = {'REGISTER', 'UNDO'}
3193 cubic_strength: FloatProperty(
3194 name="Strength",
3195 description="Higher strength results in more fluid curves",
3196 default=1.0,
3197 soft_min=-3.0,
3198 soft_max=3.0
3200 interpolation: EnumProperty(
3201 name="Interpolation mode",
3202 items=(('cubic', "Cubic", "Gives curved results"),
3203 ('linear', "Linear", "Basic, fast, straight interpolation")),
3204 description="Interpolation mode: algorithm used when creating "
3205 "segments",
3206 default='cubic'
3208 loft: BoolProperty(
3209 name="Loft",
3210 description="Loft multiple loops, instead of considering them as "
3211 "a multi-input for bridging",
3212 default=False
3214 loft_loop: BoolProperty(
3215 name="Loop",
3216 description="Connect the first and the last loop with each other",
3217 default=False
3219 min_width: IntProperty(
3220 name="Minimum width",
3221 description="Segments with an edge smaller than this are merged "
3222 "(compared to base edge)",
3223 default=0,
3224 min=0,
3225 max=100,
3226 subtype='PERCENTAGE'
3228 mode: EnumProperty(
3229 name="Mode",
3230 items=(('basic', "Basic", "Fast algorithm"),
3231 ('shortest', "Shortest edge", "Slower algorithm with better vertex matching")),
3232 description="Algorithm used for bridging",
3233 default='shortest'
3235 remove_faces: BoolProperty(
3236 name="Remove faces",
3237 description="Remove faces that are internal after bridging",
3238 default=True
3240 reverse: BoolProperty(
3241 name="Reverse",
3242 description="Manually override the direction in which the loops "
3243 "are bridged. Only use if the tool gives the wrong result",
3244 default=False
3246 segments: IntProperty(
3247 name="Segments",
3248 description="Number of segments used to bridge the gap (0=automatic)",
3249 default=1,
3250 min=0,
3251 soft_max=20
3253 twist: IntProperty(
3254 name="Twist",
3255 description="Twist what vertices are connected to each other",
3256 default=0
3259 @classmethod
3260 def poll(cls, context):
3261 ob = context.active_object
3262 return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3264 def draw(self, context):
3265 layout = self.layout
3266 # layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3268 # top row
3269 col_top = layout.column(align=True)
3270 row = col_top.row(align=True)
3271 col_left = row.column(align=True)
3272 col_right = row.column(align=True)
3273 col_right.active = self.segments != 1
3274 col_left.prop(self, "segments")
3275 col_right.prop(self, "min_width", text="")
3276 # bottom row
3277 bottom_left = col_left.row()
3278 bottom_left.active = self.segments != 1
3279 bottom_left.prop(self, "interpolation", text="")
3280 bottom_right = col_right.row()
3281 bottom_right.active = self.interpolation == 'cubic'
3282 bottom_right.prop(self, "cubic_strength")
3283 # boolean properties
3284 col_top.prop(self, "remove_faces")
3285 if self.loft:
3286 col_top.prop(self, "loft_loop")
3288 # override properties
3289 col_top.separator()
3290 row = layout.row(align=True)
3291 row.prop(self, "twist")
3292 row.prop(self, "reverse")
3294 def invoke(self, context, event):
3295 # load custom settings
3296 context.window_manager.looptools.bridge_loft = self.loft
3297 settings_load(self)
3298 return self.execute(context)
3300 def execute(self, context):
3301 # initialise
3302 global_undo, object, bm = initialise()
3303 edge_faces, edgekey_to_edge, old_selected_faces, smooth = \
3304 bridge_initialise(bm, self.interpolation)
3305 settings_write(self)
3307 # check cache to see if we can save time
3308 input_method = bridge_input_method(self.loft, self.loft_loop)
3309 cached, single_loops, loops, derived, mapping = cache_read("Bridge",
3310 object, bm, input_method, False)
3311 if not cached:
3312 # get loops
3313 loops = bridge_get_input(bm)
3314 if loops:
3315 # reorder loops if there are more than 2
3316 if len(loops) > 2:
3317 if self.loft:
3318 loops = bridge_sort_loops(bm, loops, self.loft_loop)
3319 else:
3320 loops = bridge_match_loops(bm, loops)
3322 # saving cache for faster execution next time
3323 if not cached:
3324 cache_write("Bridge", object, bm, input_method, False, False,
3325 loops, False, False)
3327 if loops:
3328 # calculate new geometry
3329 vertices = []
3330 faces = []
3331 max_vert_index = len(bm.verts) - 1
3332 for i in range(1, len(loops)):
3333 if not self.loft and i % 2 == 0:
3334 continue
3335 lines = bridge_calculate_lines(bm, loops[i - 1:i + 1],
3336 self.mode, self.twist, self.reverse)
3337 vertex_normals = bridge_calculate_virtual_vertex_normals(bm,
3338 lines, loops[i - 1:i + 1], edge_faces, edgekey_to_edge)
3339 segments = bridge_calculate_segments(bm, lines,
3340 loops[i - 1:i + 1], self.segments)
3341 new_verts, new_faces, max_vert_index = \
3342 bridge_calculate_geometry(
3343 bm, lines, vertex_normals,
3344 segments, self.interpolation, self.cubic_strength,
3345 self.min_width, max_vert_index
3347 if new_verts:
3348 vertices += new_verts
3349 if new_faces:
3350 faces += new_faces
3351 # make sure faces in loops that aren't used, aren't removed
3352 if self.remove_faces and old_selected_faces:
3353 bridge_save_unused_faces(bm, old_selected_faces, loops)
3354 # create vertices
3355 if vertices:
3356 bridge_create_vertices(bm, vertices)
3357 # create faces
3358 if faces:
3359 new_faces = bridge_create_faces(object, bm, faces, self.twist)
3360 old_selected_faces = [
3361 i for i, face in enumerate(bm.faces) if face.index in old_selected_faces
3362 ] # updating list
3363 bridge_select_new_faces(new_faces, smooth)
3364 # edge-data could have changed, can't use cache next run
3365 if faces and not vertices:
3366 cache_delete("Bridge")
3367 # delete internal faces
3368 if self.remove_faces and old_selected_faces:
3369 bridge_remove_internal_faces(bm, old_selected_faces)
3370 # make sure normals are facing outside
3371 bmesh.update_edit_mesh(object.data, loop_triangles=False,
3372 destructive=True)
3373 bpy.ops.mesh.normals_make_consistent()
3375 # cleaning up
3376 terminate(global_undo)
3378 return{'FINISHED'}
3381 # circle operator
3382 class Circle(Operator):
3383 bl_idname = "mesh.looptools_circle"
3384 bl_label = "Circle"
3385 bl_description = "Move selected vertices into a circle shape"
3386 bl_options = {'REGISTER', 'UNDO'}
3388 custom_radius: BoolProperty(
3389 name="Radius",
3390 description="Force a custom radius",
3391 default=False
3393 fit: EnumProperty(
3394 name="Method",
3395 items=(("best", "Best fit", "Non-linear least squares"),
3396 ("inside", "Fit inside", "Only move vertices towards the center")),
3397 description="Method used for fitting a circle to the vertices",
3398 default='best'
3400 flatten: BoolProperty(
3401 name="Flatten",
3402 description="Flatten the circle, instead of projecting it on the mesh",
3403 default=True
3405 influence: FloatProperty(
3406 name="Influence",
3407 description="Force of the tool",
3408 default=100.0,
3409 min=0.0,
3410 max=100.0,
3411 precision=1,
3412 subtype='PERCENTAGE'
3414 lock_x: BoolProperty(
3415 name="Lock X",
3416 description="Lock editing of the x-coordinate",
3417 default=False
3419 lock_y: BoolProperty(
3420 name="Lock Y",
3421 description="Lock editing of the y-coordinate",
3422 default=False
3424 lock_z: BoolProperty(name="Lock Z",
3425 description="Lock editing of the z-coordinate",
3426 default=False
3428 radius: FloatProperty(
3429 name="Radius",
3430 description="Custom radius for circle",
3431 default=1.0,
3432 min=0.0,
3433 soft_max=1000.0
3435 regular: BoolProperty(
3436 name="Regular",
3437 description="Distribute vertices at constant distances along the circle",
3438 default=True
3441 @classmethod
3442 def poll(cls, context):
3443 ob = context.active_object
3444 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3446 def draw(self, context):
3447 layout = self.layout
3448 col = layout.column()
3450 col.prop(self, "fit")
3451 col.separator()
3453 col.prop(self, "flatten")
3454 row = col.row(align=True)
3455 row.prop(self, "custom_radius")
3456 row_right = row.row(align=True)
3457 row_right.active = self.custom_radius
3458 row_right.prop(self, "radius", text="")
3459 col.prop(self, "regular")
3460 col.separator()
3462 col_move = col.column(align=True)
3463 row = col_move.row(align=True)
3464 if self.lock_x:
3465 row.prop(self, "lock_x", text="X", icon='LOCKED')
3466 else:
3467 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3468 if self.lock_y:
3469 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3470 else:
3471 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3472 if self.lock_z:
3473 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3474 else:
3475 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3476 col_move.prop(self, "influence")
3478 def invoke(self, context, event):
3479 # load custom settings
3480 settings_load(self)
3481 return self.execute(context)
3483 def execute(self, context):
3484 # initialise
3485 global_undo, object, bm = initialise()
3486 settings_write(self)
3487 # check cache to see if we can save time
3488 cached, single_loops, loops, derived, mapping = cache_read("Circle",
3489 object, bm, False, False)
3490 if cached:
3491 derived, bm_mod = get_derived_bmesh(object, bm)
3492 else:
3493 # find loops
3494 derived, bm_mod, single_vertices, single_loops, loops = \
3495 circle_get_input(object, bm)
3496 mapping = get_mapping(derived, bm, bm_mod, single_vertices,
3497 False, loops)
3498 single_loops, loops = circle_check_loops(single_loops, loops,
3499 mapping, bm_mod)
3501 # saving cache for faster execution next time
3502 if not cached:
3503 cache_write("Circle", object, bm, False, False, single_loops,
3504 loops, derived, mapping)
3506 move = []
3507 for i, loop in enumerate(loops):
3508 # best fitting flat plane
3509 com, normal = calculate_plane(bm_mod, loop)
3510 # if circular, shift loop so we get a good starting vertex
3511 if loop[1]:
3512 loop = circle_shift_loop(bm_mod, loop, com)
3513 # flatten vertices on plane
3514 locs_2d, p, q = circle_3d_to_2d(bm_mod, loop, com, normal)
3515 # calculate circle
3516 if self.fit == 'best':
3517 x0, y0, r = circle_calculate_best_fit(locs_2d)
3518 else: # self.fit == 'inside'
3519 x0, y0, r = circle_calculate_min_fit(locs_2d)
3520 # radius override
3521 if self.custom_radius:
3522 r = self.radius / p.length
3523 # calculate positions on circle
3524 if self.regular:
3525 new_locs_2d = circle_project_regular(locs_2d[:], x0, y0, r)
3526 else:
3527 new_locs_2d = circle_project_non_regular(locs_2d[:], x0, y0, r)
3528 # take influence into account
3529 locs_2d = circle_influence_locs(locs_2d, new_locs_2d,
3530 self.influence)
3531 # calculate 3d positions of the created 2d input
3532 move.append(circle_calculate_verts(self.flatten, bm_mod,
3533 locs_2d, com, p, q, normal))
3534 # flatten single input vertices on plane defined by loop
3535 if self.flatten and single_loops:
3536 move.append(circle_flatten_singles(bm_mod, com, p, q,
3537 normal, single_loops[i]))
3539 # move vertices to new locations
3540 if self.lock_x or self.lock_y or self.lock_z:
3541 lock = [self.lock_x, self.lock_y, self.lock_z]
3542 else:
3543 lock = False
3544 move_verts(object, bm, mapping, move, lock, -1)
3546 # cleaning up
3547 if derived:
3548 bm_mod.free()
3549 terminate(global_undo)
3551 return{'FINISHED'}
3554 # curve operator
3555 class Curve(Operator):
3556 bl_idname = "mesh.looptools_curve"
3557 bl_label = "Curve"
3558 bl_description = "Turn a loop into a smooth curve"
3559 bl_options = {'REGISTER', 'UNDO'}
3561 boundaries: BoolProperty(
3562 name="Boundaries",
3563 description="Limit the tool to work within the boundaries of the selected vertices",
3564 default=False
3566 influence: FloatProperty(
3567 name="Influence",
3568 description="Force of the tool",
3569 default=100.0,
3570 min=0.0,
3571 max=100.0,
3572 precision=1,
3573 subtype='PERCENTAGE'
3575 interpolation: EnumProperty(
3576 name="Interpolation",
3577 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
3578 ("linear", "Linear", "Simple and fast linear algorithm")),
3579 description="Algorithm used for interpolation",
3580 default='cubic'
3582 lock_x: BoolProperty(
3583 name="Lock X",
3584 description="Lock editing of the x-coordinate",
3585 default=False
3587 lock_y: BoolProperty(
3588 name="Lock Y",
3589 description="Lock editing of the y-coordinate",
3590 default=False
3592 lock_z: BoolProperty(
3593 name="Lock Z",
3594 description="Lock editing of the z-coordinate",
3595 default=False
3597 regular: BoolProperty(
3598 name="Regular",
3599 description="Distribute vertices at constant distances along the curve",
3600 default=True
3602 restriction: EnumProperty(
3603 name="Restriction",
3604 items=(("none", "None", "No restrictions on vertex movement"),
3605 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
3606 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
3607 description="Restrictions on how the vertices can be moved",
3608 default='none'
3611 @classmethod
3612 def poll(cls, context):
3613 ob = context.active_object
3614 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3616 def draw(self, context):
3617 layout = self.layout
3618 col = layout.column()
3620 col.prop(self, "interpolation")
3621 col.prop(self, "restriction")
3622 col.prop(self, "boundaries")
3623 col.prop(self, "regular")
3624 col.separator()
3626 col_move = col.column(align=True)
3627 row = col_move.row(align=True)
3628 if self.lock_x:
3629 row.prop(self, "lock_x", text="X", icon='LOCKED')
3630 else:
3631 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3632 if self.lock_y:
3633 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3634 else:
3635 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3636 if self.lock_z:
3637 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3638 else:
3639 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3640 col_move.prop(self, "influence")
3642 def invoke(self, context, event):
3643 # load custom settings
3644 settings_load(self)
3645 return self.execute(context)
3647 def execute(self, context):
3648 # initialise
3649 global_undo, object, bm = initialise()
3650 settings_write(self)
3651 # check cache to see if we can save time
3652 cached, single_loops, loops, derived, mapping = cache_read("Curve",
3653 object, bm, False, self.boundaries)
3654 if cached:
3655 derived, bm_mod = get_derived_bmesh(object, bm)
3656 else:
3657 # find loops
3658 derived, bm_mod, loops = curve_get_input(object, bm, self.boundaries)
3659 mapping = get_mapping(derived, bm, bm_mod, False, True, loops)
3660 loops = check_loops(loops, mapping, bm_mod)
3661 verts_selected = [
3662 v.index for v in bm_mod.verts if v.select and not v.hide
3665 # saving cache for faster execution next time
3666 if not cached:
3667 cache_write("Curve", object, bm, False, self.boundaries, False,
3668 loops, derived, mapping)
3670 move = []
3671 for loop in loops:
3672 knots, points = curve_calculate_knots(loop, verts_selected)
3673 pknots = curve_project_knots(bm_mod, verts_selected, knots,
3674 points, loop[1])
3675 tknots, tpoints = curve_calculate_t(bm_mod, knots, points,
3676 pknots, self.regular, loop[1])
3677 splines = calculate_splines(self.interpolation, bm_mod,
3678 tknots, knots)
3679 move.append(curve_calculate_vertices(bm_mod, knots, tknots,
3680 points, tpoints, splines, self.interpolation,
3681 self.restriction))
3683 # move vertices to new locations
3684 if self.lock_x or self.lock_y or self.lock_z:
3685 lock = [self.lock_x, self.lock_y, self.lock_z]
3686 else:
3687 lock = False
3688 move_verts(object, bm, mapping, move, lock, self.influence)
3690 # cleaning up
3691 if derived:
3692 bm_mod.free()
3693 terminate(global_undo)
3695 return{'FINISHED'}
3698 # flatten operator
3699 class Flatten(Operator):
3700 bl_idname = "mesh.looptools_flatten"
3701 bl_label = "Flatten"
3702 bl_description = "Flatten vertices on a best-fitting plane"
3703 bl_options = {'REGISTER', 'UNDO'}
3705 influence: FloatProperty(
3706 name="Influence",
3707 description="Force of the tool",
3708 default=100.0,
3709 min=0.0,
3710 max=100.0,
3711 precision=1,
3712 subtype='PERCENTAGE'
3714 lock_x: BoolProperty(
3715 name="Lock X",
3716 description="Lock editing of the x-coordinate",
3717 default=False
3719 lock_y: BoolProperty(
3720 name="Lock Y",
3721 description="Lock editing of the y-coordinate",
3722 default=False
3724 lock_z: BoolProperty(name="Lock Z",
3725 description="Lock editing of the z-coordinate",
3726 default=False
3728 plane: EnumProperty(
3729 name="Plane",
3730 items=(("best_fit", "Best fit", "Calculate a best fitting plane"),
3731 ("normal", "Normal", "Derive plane from averaging vertex normals"),
3732 ("view", "View", "Flatten on a plane perpendicular to the viewing angle")),
3733 description="Plane on which vertices are flattened",
3734 default='best_fit'
3736 restriction: EnumProperty(
3737 name="Restriction",
3738 items=(("none", "None", "No restrictions on vertex movement"),
3739 ("bounding_box", "Bounding box", "Vertices are restricted to "
3740 "movement inside the bounding box of the selection")),
3741 description="Restrictions on how the vertices can be moved",
3742 default='none'
3745 @classmethod
3746 def poll(cls, context):
3747 ob = context.active_object
3748 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3750 def draw(self, context):
3751 layout = self.layout
3752 col = layout.column()
3754 col.prop(self, "plane")
3755 # col.prop(self, "restriction")
3756 col.separator()
3758 col_move = col.column(align=True)
3759 row = col_move.row(align=True)
3760 if self.lock_x:
3761 row.prop(self, "lock_x", text="X", icon='LOCKED')
3762 else:
3763 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3764 if self.lock_y:
3765 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3766 else:
3767 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3768 if self.lock_z:
3769 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3770 else:
3771 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3772 col_move.prop(self, "influence")
3774 def invoke(self, context, event):
3775 # load custom settings
3776 settings_load(self)
3777 return self.execute(context)
3779 def execute(self, context):
3780 # initialise
3781 global_undo, object, bm = initialise()
3782 settings_write(self)
3783 # check cache to see if we can save time
3784 cached, single_loops, loops, derived, mapping = cache_read("Flatten",
3785 object, bm, False, False)
3786 if not cached:
3787 # order input into virtual loops
3788 loops = flatten_get_input(bm)
3789 loops = check_loops(loops, mapping, bm)
3791 # saving cache for faster execution next time
3792 if not cached:
3793 cache_write("Flatten", object, bm, False, False, False, loops,
3794 False, False)
3796 move = []
3797 for loop in loops:
3798 # calculate plane and position of vertices on them
3799 com, normal = calculate_plane(bm, loop, method=self.plane,
3800 object=object)
3801 to_move = flatten_project(bm, loop, com, normal)
3802 if self.restriction == 'none':
3803 move.append(to_move)
3804 else:
3805 move.append(to_move)
3807 # move vertices to new locations
3808 if self.lock_x or self.lock_y or self.lock_z:
3809 lock = [self.lock_x, self.lock_y, self.lock_z]
3810 else:
3811 lock = False
3812 move_verts(object, bm, False, move, lock, self.influence)
3814 # cleaning up
3815 terminate(global_undo)
3817 return{'FINISHED'}
3820 # gstretch operator
3821 class RemoveGP(Operator):
3822 bl_idname = "remove.gp"
3823 bl_label = "Remove GP"
3824 bl_description = "Remove all Grease Pencil Strokes"
3825 bl_options = {'REGISTER', 'UNDO'}
3827 def execute(self, context):
3829 if context.gpencil_data is not None:
3830 bpy.ops.gpencil.data_unlink()
3831 else:
3832 self.report({'INFO'}, "No Grease Pencil data to Unlink")
3833 return {'CANCELLED'}
3835 return{'FINISHED'}
3838 class GStretch(Operator):
3839 bl_idname = "mesh.looptools_gstretch"
3840 bl_label = "Gstretch"
3841 bl_description = "Stretch selected vertices to Grease Pencil stroke"
3842 bl_options = {'REGISTER', 'UNDO'}
3844 conversion: EnumProperty(
3845 name="Conversion",
3846 items=(("distance", "Distance", "Set the distance between vertices "
3847 "of the converted grease pencil stroke"),
3848 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
3849 "number of vertices that converted GP strokes will have"),
3850 ("vertices", "Exact vertices", "Set the exact number of vertices "
3851 "that converted grease pencil strokes will have. Short strokes "
3852 "with few points may contain less vertices than this number."),
3853 ("none", "No simplification", "Convert each grease pencil point "
3854 "to a vertex")),
3855 description="If grease pencil strokes are converted to geometry, "
3856 "use this simplification method",
3857 default='limit_vertices'
3859 conversion_distance: FloatProperty(
3860 name="Distance",
3861 description="Absolute distance between vertices along the converted "
3862 "grease pencil stroke",
3863 default=0.1,
3864 min=0.000001,
3865 soft_min=0.01,
3866 soft_max=100
3868 conversion_max: IntProperty(
3869 name="Max Vertices",
3870 description="Maximum number of vertices grease pencil strokes will "
3871 "have, when they are converted to geomtery",
3872 default=32,
3873 min=3,
3874 soft_max=500,
3875 update=gstretch_update_min
3877 conversion_min: IntProperty(
3878 name="Min Vertices",
3879 description="Minimum number of vertices grease pencil strokes will "
3880 "have, when they are converted to geomtery",
3881 default=8,
3882 min=3,
3883 soft_max=500,
3884 update=gstretch_update_max
3886 conversion_vertices: IntProperty(
3887 name="Vertices",
3888 description="Number of vertices grease pencil strokes will "
3889 "have, when they are converted to geometry. If strokes have less "
3890 "points than required, the 'Spread evenly' method is used",
3891 default=32,
3892 min=3,
3893 soft_max=500
3895 delete_strokes: BoolProperty(
3896 name="Delete strokes",
3897 description="Remove Grease Pencil strokes if they have been used "
3898 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
3899 default=False
3901 influence: FloatProperty(
3902 name="Influence",
3903 description="Force of the tool",
3904 default=100.0,
3905 min=0.0,
3906 max=100.0,
3907 precision=1,
3908 subtype='PERCENTAGE'
3910 lock_x: BoolProperty(
3911 name="Lock X",
3912 description="Lock editing of the x-coordinate",
3913 default=False
3915 lock_y: BoolProperty(
3916 name="Lock Y",
3917 description="Lock editing of the y-coordinate",
3918 default=False
3920 lock_z: BoolProperty(
3921 name="Lock Z",
3922 description="Lock editing of the z-coordinate",
3923 default=False
3925 method: EnumProperty(
3926 name="Method",
3927 items=(("project", "Project", "Project vertices onto the stroke, "
3928 "using vertex normals and connected edges"),
3929 ("irregular", "Spread", "Distribute vertices along the full "
3930 "stroke, retaining relative distances between the vertices"),
3931 ("regular", "Spread evenly", "Distribute vertices at regular "
3932 "distances along the full stroke")),
3933 description="Method of distributing the vertices over the Grease "
3934 "Pencil stroke",
3935 default='regular'
3938 @classmethod
3939 def poll(cls, context):
3940 ob = context.active_object
3941 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3943 def draw(self, context):
3944 layout = self.layout
3945 col = layout.column()
3947 col.prop(self, "method")
3948 col.separator()
3950 col_conv = col.column(align=True)
3951 col_conv.prop(self, "conversion", text="")
3952 if self.conversion == 'distance':
3953 col_conv.prop(self, "conversion_distance")
3954 elif self.conversion == 'limit_vertices':
3955 row = col_conv.row(align=True)
3956 row.prop(self, "conversion_min", text="Min")
3957 row.prop(self, "conversion_max", text="Max")
3958 elif self.conversion == 'vertices':
3959 col_conv.prop(self, "conversion_vertices")
3960 col.separator()
3962 col_move = col.column(align=True)
3963 row = col_move.row(align=True)
3964 if self.lock_x:
3965 row.prop(self, "lock_x", text="X", icon='LOCKED')
3966 else:
3967 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3968 if self.lock_y:
3969 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3970 else:
3971 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3972 if self.lock_z:
3973 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3974 else:
3975 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3976 col_move.prop(self, "influence")
3977 col.separator()
3978 col.operator("remove.gp", text="Delete GP Strokes")
3980 def invoke(self, context, event):
3981 # flush cached strokes
3982 if 'Gstretch' in looptools_cache:
3983 looptools_cache['Gstretch']['single_loops'] = []
3984 # load custom settings
3985 settings_load(self)
3986 return self.execute(context)
3988 def execute(self, context):
3989 # initialise
3990 global_undo, object, bm = initialise()
3991 settings_write(self)
3993 # check cache to see if we can save time
3994 cached, safe_strokes, loops, derived, mapping = cache_read("Gstretch",
3995 object, bm, False, False)
3996 if cached:
3997 straightening = False
3998 if safe_strokes:
3999 strokes = gstretch_safe_to_true_strokes(safe_strokes)
4000 # cached strokes were flushed (see operator's invoke function)
4001 elif get_grease_pencil(object, context):
4002 strokes = gstretch_get_strokes(object, context)
4003 else:
4004 # straightening function (no GP) -> loops ignore modifiers
4005 straightening = True
4006 derived = False
4007 bm_mod = bm.copy()
4008 bm_mod.verts.ensure_lookup_table()
4009 bm_mod.edges.ensure_lookup_table()
4010 bm_mod.faces.ensure_lookup_table()
4011 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
4012 if not straightening:
4013 derived, bm_mod = get_derived_bmesh(object, bm)
4014 else:
4015 # get loops and strokes
4016 if get_grease_pencil(object, context):
4017 # find loops
4018 derived, bm_mod, loops = get_connected_input(object, bm, input='selected')
4019 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4020 loops = check_loops(loops, mapping, bm_mod)
4021 # get strokes
4022 strokes = gstretch_get_strokes(object, context)
4023 else:
4024 # straightening function (no GP) -> loops ignore modifiers
4025 derived = False
4026 mapping = False
4027 bm_mod = bm.copy()
4028 bm_mod.verts.ensure_lookup_table()
4029 bm_mod.edges.ensure_lookup_table()
4030 bm_mod.faces.ensure_lookup_table()
4031 edge_keys = [
4032 edgekey(edge) for edge in bm_mod.edges if
4033 edge.select and not edge.hide
4035 loops = get_connected_selections(edge_keys)
4036 loops = check_loops(loops, mapping, bm_mod)
4037 # create fake strokes
4038 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
4040 # saving cache for faster execution next time
4041 if not cached:
4042 if strokes:
4043 safe_strokes = gstretch_true_to_safe_strokes(strokes)
4044 else:
4045 safe_strokes = []
4046 cache_write("Gstretch", object, bm, False, False,
4047 safe_strokes, loops, derived, mapping)
4049 # pair loops and strokes
4050 ls_pairs = gstretch_match_loops_strokes(loops, strokes, object, bm_mod)
4051 ls_pairs = gstretch_align_pairs(ls_pairs, object, bm_mod, self.method)
4053 move = []
4054 if not loops:
4055 # no selected geometry, convert GP to verts
4056 if strokes:
4057 move.append(gstretch_create_verts(object, bm, strokes,
4058 self.method, self.conversion, self.conversion_distance,
4059 self.conversion_max, self.conversion_min,
4060 self.conversion_vertices))
4061 for stroke in strokes:
4062 gstretch_erase_stroke(stroke, context)
4063 elif ls_pairs:
4064 for (loop, stroke) in ls_pairs:
4065 move.append(gstretch_calculate_verts(loop, stroke, object,
4066 bm_mod, self.method))
4067 if self.delete_strokes:
4068 if type(stroke) != bpy.types.GPencilStroke:
4069 # in case of cached fake stroke, get the real one
4070 if get_grease_pencil(object, context):
4071 strokes = gstretch_get_strokes(object, context)
4072 if loops and strokes:
4073 ls_pairs = gstretch_match_loops_strokes(loops,
4074 strokes, object, bm_mod)
4075 ls_pairs = gstretch_align_pairs(ls_pairs,
4076 object, bm_mod, self.method)
4077 for (l, s) in ls_pairs:
4078 if l == loop:
4079 stroke = s
4080 break
4081 gstretch_erase_stroke(stroke, context)
4083 # move vertices to new locations
4084 if self.lock_x or self.lock_y or self.lock_z:
4085 lock = [self.lock_x, self.lock_y, self.lock_z]
4086 else:
4087 lock = False
4088 bmesh.update_edit_mesh(object.data, loop_triangles=True, destructive=True)
4089 move_verts(object, bm, mapping, move, lock, self.influence)
4091 # cleaning up
4092 if derived:
4093 bm_mod.free()
4094 terminate(global_undo)
4096 return{'FINISHED'}
4099 # relax operator
4100 class Relax(Operator):
4101 bl_idname = "mesh.looptools_relax"
4102 bl_label = "Relax"
4103 bl_description = "Relax the loop, so it is smoother"
4104 bl_options = {'REGISTER', 'UNDO'}
4106 input: EnumProperty(
4107 name="Input",
4108 items=(("all", "Parallel (all)", "Also use non-selected "
4109 "parallel loops as input"),
4110 ("selected", "Selection", "Only use selected vertices as input")),
4111 description="Loops that are relaxed",
4112 default='selected'
4114 interpolation: EnumProperty(
4115 name="Interpolation",
4116 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4117 ("linear", "Linear", "Simple and fast linear algorithm")),
4118 description="Algorithm used for interpolation",
4119 default='cubic'
4121 iterations: EnumProperty(
4122 name="Iterations",
4123 items=(("1", "1", "One"),
4124 ("3", "3", "Three"),
4125 ("5", "5", "Five"),
4126 ("10", "10", "Ten"),
4127 ("25", "25", "Twenty-five")),
4128 description="Number of times the loop is relaxed",
4129 default="1"
4131 regular: BoolProperty(
4132 name="Regular",
4133 description="Distribute vertices at constant distances along the loop",
4134 default=True
4137 @classmethod
4138 def poll(cls, context):
4139 ob = context.active_object
4140 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4142 def draw(self, context):
4143 layout = self.layout
4144 col = layout.column()
4146 col.prop(self, "interpolation")
4147 col.prop(self, "input")
4148 col.prop(self, "iterations")
4149 col.prop(self, "regular")
4151 def invoke(self, context, event):
4152 # load custom settings
4153 settings_load(self)
4154 return self.execute(context)
4156 def execute(self, context):
4157 # initialise
4158 global_undo, object, bm = initialise()
4159 settings_write(self)
4160 # check cache to see if we can save time
4161 cached, single_loops, loops, derived, mapping = cache_read("Relax",
4162 object, bm, self.input, False)
4163 if cached:
4164 derived, bm_mod = get_derived_bmesh(object, bm)
4165 else:
4166 # find loops
4167 derived, bm_mod, loops = get_connected_input(object, bm, self.input)
4168 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4169 loops = check_loops(loops, mapping, bm_mod)
4170 knots, points = relax_calculate_knots(loops)
4172 # saving cache for faster execution next time
4173 if not cached:
4174 cache_write("Relax", object, bm, self.input, False, False, loops,
4175 derived, mapping)
4177 for iteration in range(int(self.iterations)):
4178 # calculate splines and new positions
4179 tknots, tpoints = relax_calculate_t(bm_mod, knots, points,
4180 self.regular)
4181 splines = []
4182 for i in range(len(knots)):
4183 splines.append(calculate_splines(self.interpolation, bm_mod,
4184 tknots[i], knots[i]))
4185 move = [relax_calculate_verts(bm_mod, self.interpolation,
4186 tknots, knots, tpoints, points, splines)]
4187 move_verts(object, bm, mapping, move, False, -1)
4189 # cleaning up
4190 if derived:
4191 bm_mod.free()
4192 terminate(global_undo)
4194 return{'FINISHED'}
4197 # space operator
4198 class Space(Operator):
4199 bl_idname = "mesh.looptools_space"
4200 bl_label = "Space"
4201 bl_description = "Space the vertices in a regular distrubtion on the loop"
4202 bl_options = {'REGISTER', 'UNDO'}
4204 influence: FloatProperty(
4205 name="Influence",
4206 description="Force of the tool",
4207 default=100.0,
4208 min=0.0,
4209 max=100.0,
4210 precision=1,
4211 subtype='PERCENTAGE'
4213 input: EnumProperty(
4214 name="Input",
4215 items=(("all", "Parallel (all)", "Also use non-selected "
4216 "parallel loops as input"),
4217 ("selected", "Selection", "Only use selected vertices as input")),
4218 description="Loops that are spaced",
4219 default='selected'
4221 interpolation: EnumProperty(
4222 name="Interpolation",
4223 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4224 ("linear", "Linear", "Vertices are projected on existing edges")),
4225 description="Algorithm used for interpolation",
4226 default='cubic'
4228 lock_x: BoolProperty(
4229 name="Lock X",
4230 description="Lock editing of the x-coordinate",
4231 default=False
4233 lock_y: BoolProperty(
4234 name="Lock Y",
4235 description="Lock editing of the y-coordinate",
4236 default=False
4238 lock_z: BoolProperty(
4239 name="Lock Z",
4240 description="Lock editing of the z-coordinate",
4241 default=False
4244 @classmethod
4245 def poll(cls, context):
4246 ob = context.active_object
4247 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4249 def draw(self, context):
4250 layout = self.layout
4251 col = layout.column()
4253 col.prop(self, "interpolation")
4254 col.prop(self, "input")
4255 col.separator()
4257 col_move = col.column(align=True)
4258 row = col_move.row(align=True)
4259 if self.lock_x:
4260 row.prop(self, "lock_x", text="X", icon='LOCKED')
4261 else:
4262 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
4263 if self.lock_y:
4264 row.prop(self, "lock_y", text="Y", icon='LOCKED')
4265 else:
4266 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
4267 if self.lock_z:
4268 row.prop(self, "lock_z", text="Z", icon='LOCKED')
4269 else:
4270 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
4271 col_move.prop(self, "influence")
4273 def invoke(self, context, event):
4274 # load custom settings
4275 settings_load(self)
4276 return self.execute(context)
4278 def execute(self, context):
4279 # initialise
4280 global_undo, object, bm = initialise()
4281 settings_write(self)
4282 # check cache to see if we can save time
4283 cached, single_loops, loops, derived, mapping = cache_read("Space",
4284 object, bm, self.input, False)
4285 if cached:
4286 derived, bm_mod = get_derived_bmesh(object, bm)
4287 else:
4288 # find loops
4289 derived, bm_mod, loops = get_connected_input(object, bm, self.input)
4290 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4291 loops = check_loops(loops, mapping, bm_mod)
4293 # saving cache for faster execution next time
4294 if not cached:
4295 cache_write("Space", object, bm, self.input, False, False, loops,
4296 derived, mapping)
4298 move = []
4299 for loop in loops:
4300 # calculate splines and new positions
4301 if loop[1]: # circular
4302 loop[0].append(loop[0][0])
4303 tknots, tpoints = space_calculate_t(bm_mod, loop[0][:])
4304 splines = calculate_splines(self.interpolation, bm_mod,
4305 tknots, loop[0][:])
4306 move.append(space_calculate_verts(bm_mod, self.interpolation,
4307 tknots, tpoints, loop[0][:-1], splines))
4308 # move vertices to new locations
4309 if self.lock_x or self.lock_y or self.lock_z:
4310 lock = [self.lock_x, self.lock_y, self.lock_z]
4311 else:
4312 lock = False
4313 move_verts(object, bm, mapping, move, lock, self.influence)
4315 # cleaning up
4316 if derived:
4317 bm_mod.free()
4318 terminate(global_undo)
4320 return{'FINISHED'}
4323 # ########################################
4324 # ##### GUI and registration #############
4325 # ########################################
4327 # menu containing all tools
4328 class VIEW3D_MT_edit_mesh_looptools(Menu):
4329 bl_label = "LoopTools"
4331 def draw(self, context):
4332 layout = self.layout
4334 layout.operator("mesh.looptools_bridge", text="Bridge").loft = False
4335 layout.operator("mesh.looptools_circle")
4336 layout.operator("mesh.looptools_curve")
4337 layout.operator("mesh.looptools_flatten")
4338 layout.operator("mesh.looptools_gstretch")
4339 layout.operator("mesh.looptools_bridge", text="Loft").loft = True
4340 layout.operator("mesh.looptools_relax")
4341 layout.operator("mesh.looptools_space")
4344 # panel containing all tools
4345 class VIEW3D_PT_tools_looptools(Panel):
4346 bl_space_type = 'VIEW_3D'
4347 bl_region_type = 'UI'
4348 bl_category = 'View'
4349 bl_context = "mesh_edit"
4350 bl_label = "LoopTools"
4351 bl_options = {'DEFAULT_CLOSED'}
4353 def draw(self, context):
4354 layout = self.layout
4355 col = layout.column(align=True)
4356 lt = context.window_manager.looptools
4358 # bridge - first line
4359 split = col.split(factor=0.15, align=True)
4360 if lt.display_bridge:
4361 split.prop(lt, "display_bridge", text="", icon='DOWNARROW_HLT')
4362 else:
4363 split.prop(lt, "display_bridge", text="", icon='RIGHTARROW')
4364 split.operator("mesh.looptools_bridge", text="Bridge").loft = False
4365 # bridge - settings
4366 if lt.display_bridge:
4367 box = col.column(align=True).box().column()
4368 # box.prop(self, "mode")
4370 # top row
4371 col_top = box.column(align=True)
4372 row = col_top.row(align=True)
4373 col_left = row.column(align=True)
4374 col_right = row.column(align=True)
4375 col_right.active = lt.bridge_segments != 1
4376 col_left.prop(lt, "bridge_segments")
4377 col_right.prop(lt, "bridge_min_width", text="")
4378 # bottom row
4379 bottom_left = col_left.row()
4380 bottom_left.active = lt.bridge_segments != 1
4381 bottom_left.prop(lt, "bridge_interpolation", text="")
4382 bottom_right = col_right.row()
4383 bottom_right.active = lt.bridge_interpolation == 'cubic'
4384 bottom_right.prop(lt, "bridge_cubic_strength")
4385 # boolean properties
4386 col_top.prop(lt, "bridge_remove_faces")
4388 # override properties
4389 col_top.separator()
4390 row = box.row(align=True)
4391 row.prop(lt, "bridge_twist")
4392 row.prop(lt, "bridge_reverse")
4394 # circle - first line
4395 split = col.split(factor=0.15, align=True)
4396 if lt.display_circle:
4397 split.prop(lt, "display_circle", text="", icon='DOWNARROW_HLT')
4398 else:
4399 split.prop(lt, "display_circle", text="", icon='RIGHTARROW')
4400 split.operator("mesh.looptools_circle")
4401 # circle - settings
4402 if lt.display_circle:
4403 box = col.column(align=True).box().column()
4404 box.prop(lt, "circle_fit")
4405 box.separator()
4407 box.prop(lt, "circle_flatten")
4408 row = box.row(align=True)
4409 row.prop(lt, "circle_custom_radius")
4410 row_right = row.row(align=True)
4411 row_right.active = lt.circle_custom_radius
4412 row_right.prop(lt, "circle_radius", text="")
4413 box.prop(lt, "circle_regular")
4414 box.separator()
4416 col_move = box.column(align=True)
4417 row = col_move.row(align=True)
4418 if lt.circle_lock_x:
4419 row.prop(lt, "circle_lock_x", text="X", icon='LOCKED')
4420 else:
4421 row.prop(lt, "circle_lock_x", text="X", icon='UNLOCKED')
4422 if lt.circle_lock_y:
4423 row.prop(lt, "circle_lock_y", text="Y", icon='LOCKED')
4424 else:
4425 row.prop(lt, "circle_lock_y", text="Y", icon='UNLOCKED')
4426 if lt.circle_lock_z:
4427 row.prop(lt, "circle_lock_z", text="Z", icon='LOCKED')
4428 else:
4429 row.prop(lt, "circle_lock_z", text="Z", icon='UNLOCKED')
4430 col_move.prop(lt, "circle_influence")
4432 # curve - first line
4433 split = col.split(factor=0.15, align=True)
4434 if lt.display_curve:
4435 split.prop(lt, "display_curve", text="", icon='DOWNARROW_HLT')
4436 else:
4437 split.prop(lt, "display_curve", text="", icon='RIGHTARROW')
4438 split.operator("mesh.looptools_curve")
4439 # curve - settings
4440 if lt.display_curve:
4441 box = col.column(align=True).box().column()
4442 box.prop(lt, "curve_interpolation")
4443 box.prop(lt, "curve_restriction")
4444 box.prop(lt, "curve_boundaries")
4445 box.prop(lt, "curve_regular")
4446 box.separator()
4448 col_move = box.column(align=True)
4449 row = col_move.row(align=True)
4450 if lt.curve_lock_x:
4451 row.prop(lt, "curve_lock_x", text="X", icon='LOCKED')
4452 else:
4453 row.prop(lt, "curve_lock_x", text="X", icon='UNLOCKED')
4454 if lt.curve_lock_y:
4455 row.prop(lt, "curve_lock_y", text="Y", icon='LOCKED')
4456 else:
4457 row.prop(lt, "curve_lock_y", text="Y", icon='UNLOCKED')
4458 if lt.curve_lock_z:
4459 row.prop(lt, "curve_lock_z", text="Z", icon='LOCKED')
4460 else:
4461 row.prop(lt, "curve_lock_z", text="Z", icon='UNLOCKED')
4462 col_move.prop(lt, "curve_influence")
4464 # flatten - first line
4465 split = col.split(factor=0.15, align=True)
4466 if lt.display_flatten:
4467 split.prop(lt, "display_flatten", text="", icon='DOWNARROW_HLT')
4468 else:
4469 split.prop(lt, "display_flatten", text="", icon='RIGHTARROW')
4470 split.operator("mesh.looptools_flatten")
4471 # flatten - settings
4472 if lt.display_flatten:
4473 box = col.column(align=True).box().column()
4474 box.prop(lt, "flatten_plane")
4475 # box.prop(lt, "flatten_restriction")
4476 box.separator()
4478 col_move = box.column(align=True)
4479 row = col_move.row(align=True)
4480 if lt.flatten_lock_x:
4481 row.prop(lt, "flatten_lock_x", text="X", icon='LOCKED')
4482 else:
4483 row.prop(lt, "flatten_lock_x", text="X", icon='UNLOCKED')
4484 if lt.flatten_lock_y:
4485 row.prop(lt, "flatten_lock_y", text="Y", icon='LOCKED')
4486 else:
4487 row.prop(lt, "flatten_lock_y", text="Y", icon='UNLOCKED')
4488 if lt.flatten_lock_z:
4489 row.prop(lt, "flatten_lock_z", text="Z", icon='LOCKED')
4490 else:
4491 row.prop(lt, "flatten_lock_z", text="Z", icon='UNLOCKED')
4492 col_move.prop(lt, "flatten_influence")
4494 # gstretch - first line
4495 split = col.split(factor=0.15, align=True)
4496 if lt.display_gstretch:
4497 split.prop(lt, "display_gstretch", text="", icon='DOWNARROW_HLT')
4498 else:
4499 split.prop(lt, "display_gstretch", text="", icon='RIGHTARROW')
4500 split.operator("mesh.looptools_gstretch")
4501 # gstretch settings
4502 if lt.display_gstretch:
4503 box = col.column(align=True).box().column()
4504 box.prop(lt, "gstretch_method")
4506 col_conv = box.column(align=True)
4507 col_conv.prop(lt, "gstretch_conversion", text="")
4508 if lt.gstretch_conversion == 'distance':
4509 col_conv.prop(lt, "gstretch_conversion_distance")
4510 elif lt.gstretch_conversion == 'limit_vertices':
4511 row = col_conv.row(align=True)
4512 row.prop(lt, "gstretch_conversion_min", text="Min")
4513 row.prop(lt, "gstretch_conversion_max", text="Max")
4514 elif lt.gstretch_conversion == 'vertices':
4515 col_conv.prop(lt, "gstretch_conversion_vertices")
4516 box.separator()
4518 col_move = box.column(align=True)
4519 row = col_move.row(align=True)
4520 if lt.gstretch_lock_x:
4521 row.prop(lt, "gstretch_lock_x", text="X", icon='LOCKED')
4522 else:
4523 row.prop(lt, "gstretch_lock_x", text="X", icon='UNLOCKED')
4524 if lt.gstretch_lock_y:
4525 row.prop(lt, "gstretch_lock_y", text="Y", icon='LOCKED')
4526 else:
4527 row.prop(lt, "gstretch_lock_y", text="Y", icon='UNLOCKED')
4528 if lt.gstretch_lock_z:
4529 row.prop(lt, "gstretch_lock_z", text="Z", icon='LOCKED')
4530 else:
4531 row.prop(lt, "gstretch_lock_z", text="Z", icon='UNLOCKED')
4532 col_move.prop(lt, "gstretch_influence")
4533 box.operator("remove.gp", text="Delete GP Strokes")
4535 # loft - first line
4536 split = col.split(factor=0.15, align=True)
4537 if lt.display_loft:
4538 split.prop(lt, "display_loft", text="", icon='DOWNARROW_HLT')
4539 else:
4540 split.prop(lt, "display_loft", text="", icon='RIGHTARROW')
4541 split.operator("mesh.looptools_bridge", text="Loft").loft = True
4542 # loft - settings
4543 if lt.display_loft:
4544 box = col.column(align=True).box().column()
4545 # box.prop(self, "mode")
4547 # top row
4548 col_top = box.column(align=True)
4549 row = col_top.row(align=True)
4550 col_left = row.column(align=True)
4551 col_right = row.column(align=True)
4552 col_right.active = lt.bridge_segments != 1
4553 col_left.prop(lt, "bridge_segments")
4554 col_right.prop(lt, "bridge_min_width", text="")
4555 # bottom row
4556 bottom_left = col_left.row()
4557 bottom_left.active = lt.bridge_segments != 1
4558 bottom_left.prop(lt, "bridge_interpolation", text="")
4559 bottom_right = col_right.row()
4560 bottom_right.active = lt.bridge_interpolation == 'cubic'
4561 bottom_right.prop(lt, "bridge_cubic_strength")
4562 # boolean properties
4563 col_top.prop(lt, "bridge_remove_faces")
4564 col_top.prop(lt, "bridge_loft_loop")
4566 # override properties
4567 col_top.separator()
4568 row = box.row(align=True)
4569 row.prop(lt, "bridge_twist")
4570 row.prop(lt, "bridge_reverse")
4572 # relax - first line
4573 split = col.split(factor=0.15, align=True)
4574 if lt.display_relax:
4575 split.prop(lt, "display_relax", text="", icon='DOWNARROW_HLT')
4576 else:
4577 split.prop(lt, "display_relax", text="", icon='RIGHTARROW')
4578 split.operator("mesh.looptools_relax")
4579 # relax - settings
4580 if lt.display_relax:
4581 box = col.column(align=True).box().column()
4582 box.prop(lt, "relax_interpolation")
4583 box.prop(lt, "relax_input")
4584 box.prop(lt, "relax_iterations")
4585 box.prop(lt, "relax_regular")
4587 # space - first line
4588 split = col.split(factor=0.15, align=True)
4589 if lt.display_space:
4590 split.prop(lt, "display_space", text="", icon='DOWNARROW_HLT')
4591 else:
4592 split.prop(lt, "display_space", text="", icon='RIGHTARROW')
4593 split.operator("mesh.looptools_space")
4594 # space - settings
4595 if lt.display_space:
4596 box = col.column(align=True).box().column()
4597 box.prop(lt, "space_interpolation")
4598 box.prop(lt, "space_input")
4599 box.separator()
4601 col_move = box.column(align=True)
4602 row = col_move.row(align=True)
4603 if lt.space_lock_x:
4604 row.prop(lt, "space_lock_x", text="X", icon='LOCKED')
4605 else:
4606 row.prop(lt, "space_lock_x", text="X", icon='UNLOCKED')
4607 if lt.space_lock_y:
4608 row.prop(lt, "space_lock_y", text="Y", icon='LOCKED')
4609 else:
4610 row.prop(lt, "space_lock_y", text="Y", icon='UNLOCKED')
4611 if lt.space_lock_z:
4612 row.prop(lt, "space_lock_z", text="Z", icon='LOCKED')
4613 else:
4614 row.prop(lt, "space_lock_z", text="Z", icon='UNLOCKED')
4615 col_move.prop(lt, "space_influence")
4618 # property group containing all properties for the gui in the panel
4619 class LoopToolsProps(PropertyGroup):
4621 Fake module like class
4622 bpy.context.window_manager.looptools
4624 # general display properties
4625 display_bridge: BoolProperty(
4626 name="Bridge settings",
4627 description="Display settings of the Bridge tool",
4628 default=False
4630 display_circle: BoolProperty(
4631 name="Circle settings",
4632 description="Display settings of the Circle tool",
4633 default=False
4635 display_curve: BoolProperty(
4636 name="Curve settings",
4637 description="Display settings of the Curve tool",
4638 default=False
4640 display_flatten: BoolProperty(
4641 name="Flatten settings",
4642 description="Display settings of the Flatten tool",
4643 default=False
4645 display_gstretch: BoolProperty(
4646 name="Gstretch settings",
4647 description="Display settings of the Gstretch tool",
4648 default=False
4650 display_loft: BoolProperty(
4651 name="Loft settings",
4652 description="Display settings of the Loft tool",
4653 default=False
4655 display_relax: BoolProperty(
4656 name="Relax settings",
4657 description="Display settings of the Relax tool",
4658 default=False
4660 display_space: BoolProperty(
4661 name="Space settings",
4662 description="Display settings of the Space tool",
4663 default=False
4666 # bridge properties
4667 bridge_cubic_strength: FloatProperty(
4668 name="Strength",
4669 description="Higher strength results in more fluid curves",
4670 default=1.0,
4671 soft_min=-3.0,
4672 soft_max=3.0
4674 bridge_interpolation: EnumProperty(
4675 name="Interpolation mode",
4676 items=(('cubic', "Cubic", "Gives curved results"),
4677 ('linear', "Linear", "Basic, fast, straight interpolation")),
4678 description="Interpolation mode: algorithm used when creating segments",
4679 default='cubic'
4681 bridge_loft: BoolProperty(
4682 name="Loft",
4683 description="Loft multiple loops, instead of considering them as "
4684 "a multi-input for bridging",
4685 default=False
4687 bridge_loft_loop: BoolProperty(
4688 name="Loop",
4689 description="Connect the first and the last loop with each other",
4690 default=False
4692 bridge_min_width: IntProperty(
4693 name="Minimum width",
4694 description="Segments with an edge smaller than this are merged "
4695 "(compared to base edge)",
4696 default=0,
4697 min=0,
4698 max=100,
4699 subtype='PERCENTAGE'
4701 bridge_mode: EnumProperty(
4702 name="Mode",
4703 items=(('basic', "Basic", "Fast algorithm"),
4704 ('shortest', "Shortest edge", "Slower algorithm with "
4705 "better vertex matching")),
4706 description="Algorithm used for bridging",
4707 default='shortest'
4709 bridge_remove_faces: BoolProperty(
4710 name="Remove faces",
4711 description="Remove faces that are internal after bridging",
4712 default=True
4714 bridge_reverse: BoolProperty(
4715 name="Reverse",
4716 description="Manually override the direction in which the loops "
4717 "are bridged. Only use if the tool gives the wrong result",
4718 default=False
4720 bridge_segments: IntProperty(
4721 name="Segments",
4722 description="Number of segments used to bridge the gap (0=automatic)",
4723 default=1,
4724 min=0,
4725 soft_max=20
4727 bridge_twist: IntProperty(
4728 name="Twist",
4729 description="Twist what vertices are connected to each other",
4730 default=0
4733 # circle properties
4734 circle_custom_radius: BoolProperty(
4735 name="Radius",
4736 description="Force a custom radius",
4737 default=False
4739 circle_fit: EnumProperty(
4740 name="Method",
4741 items=(("best", "Best fit", "Non-linear least squares"),
4742 ("inside", "Fit inside", "Only move vertices towards the center")),
4743 description="Method used for fitting a circle to the vertices",
4744 default='best'
4746 circle_flatten: BoolProperty(
4747 name="Flatten",
4748 description="Flatten the circle, instead of projecting it on the mesh",
4749 default=True
4751 circle_influence: FloatProperty(
4752 name="Influence",
4753 description="Force of the tool",
4754 default=100.0,
4755 min=0.0,
4756 max=100.0,
4757 precision=1,
4758 subtype='PERCENTAGE'
4760 circle_lock_x: BoolProperty(
4761 name="Lock X",
4762 description="Lock editing of the x-coordinate",
4763 default=False
4765 circle_lock_y: BoolProperty(
4766 name="Lock Y",
4767 description="Lock editing of the y-coordinate",
4768 default=False
4770 circle_lock_z: BoolProperty(
4771 name="Lock Z",
4772 description="Lock editing of the z-coordinate",
4773 default=False
4775 circle_radius: FloatProperty(
4776 name="Radius",
4777 description="Custom radius for circle",
4778 default=1.0,
4779 min=0.0,
4780 soft_max=1000.0
4782 circle_regular: BoolProperty(
4783 name="Regular",
4784 description="Distribute vertices at constant distances along the circle",
4785 default=True
4787 # curve properties
4788 curve_boundaries: BoolProperty(
4789 name="Boundaries",
4790 description="Limit the tool to work within the boundaries of the "
4791 "selected vertices",
4792 default=False
4794 curve_influence: FloatProperty(
4795 name="Influence",
4796 description="Force of the tool",
4797 default=100.0,
4798 min=0.0,
4799 max=100.0,
4800 precision=1,
4801 subtype='PERCENTAGE'
4803 curve_interpolation: EnumProperty(
4804 name="Interpolation",
4805 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4806 ("linear", "Linear", "Simple and fast linear algorithm")),
4807 description="Algorithm used for interpolation",
4808 default='cubic'
4810 curve_lock_x: BoolProperty(
4811 name="Lock X",
4812 description="Lock editing of the x-coordinate",
4813 default=False
4815 curve_lock_y: BoolProperty(
4816 name="Lock Y",
4817 description="Lock editing of the y-coordinate",
4818 default=False
4820 curve_lock_z: BoolProperty(
4821 name="Lock Z",
4822 description="Lock editing of the z-coordinate",
4823 default=False
4825 curve_regular: BoolProperty(
4826 name="Regular",
4827 description="Distribute vertices at constant distances along the curve",
4828 default=True
4830 curve_restriction: EnumProperty(
4831 name="Restriction",
4832 items=(("none", "None", "No restrictions on vertex movement"),
4833 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
4834 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
4835 description="Restrictions on how the vertices can be moved",
4836 default='none'
4839 # flatten properties
4840 flatten_influence: FloatProperty(
4841 name="Influence",
4842 description="Force of the tool",
4843 default=100.0,
4844 min=0.0,
4845 max=100.0,
4846 precision=1,
4847 subtype='PERCENTAGE'
4849 flatten_lock_x: BoolProperty(
4850 name="Lock X",
4851 description="Lock editing of the x-coordinate",
4852 default=False)
4853 flatten_lock_y: BoolProperty(name="Lock Y",
4854 description="Lock editing of the y-coordinate",
4855 default=False
4857 flatten_lock_z: BoolProperty(
4858 name="Lock Z",
4859 description="Lock editing of the z-coordinate",
4860 default=False
4862 flatten_plane: EnumProperty(
4863 name="Plane",
4864 items=(("best_fit", "Best fit", "Calculate a best fitting plane"),
4865 ("normal", "Normal", "Derive plane from averaging vertex "
4866 "normals"),
4867 ("view", "View", "Flatten on a plane perpendicular to the "
4868 "viewing angle")),
4869 description="Plane on which vertices are flattened",
4870 default='best_fit'
4872 flatten_restriction: EnumProperty(
4873 name="Restriction",
4874 items=(("none", "None", "No restrictions on vertex movement"),
4875 ("bounding_box", "Bounding box", "Vertices are restricted to "
4876 "movement inside the bounding box of the selection")),
4877 description="Restrictions on how the vertices can be moved",
4878 default='none'
4881 # gstretch properties
4882 gstretch_conversion: EnumProperty(
4883 name="Conversion",
4884 items=(("distance", "Distance", "Set the distance between vertices "
4885 "of the converted grease pencil stroke"),
4886 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
4887 "number of vertices that converted GP strokes will have"),
4888 ("vertices", "Exact vertices", "Set the exact number of vertices "
4889 "that converted grease pencil strokes will have. Short strokes "
4890 "with few points may contain less vertices than this number."),
4891 ("none", "No simplification", "Convert each grease pencil point "
4892 "to a vertex")),
4893 description="If grease pencil strokes are converted to geometry, "
4894 "use this simplification method",
4895 default='limit_vertices'
4897 gstretch_conversion_distance: FloatProperty(
4898 name="Distance",
4899 description="Absolute distance between vertices along the converted "
4900 "grease pencil stroke",
4901 default=0.1,
4902 min=0.000001,
4903 soft_min=0.01,
4904 soft_max=100
4906 gstretch_conversion_max: IntProperty(
4907 name="Max Vertices",
4908 description="Maximum number of vertices grease pencil strokes will "
4909 "have, when they are converted to geomtery",
4910 default=32,
4911 min=3,
4912 soft_max=500,
4913 update=gstretch_update_min
4915 gstretch_conversion_min: IntProperty(
4916 name="Min Vertices",
4917 description="Minimum number of vertices grease pencil strokes will "
4918 "have, when they are converted to geomtery",
4919 default=8,
4920 min=3,
4921 soft_max=500,
4922 update=gstretch_update_max
4924 gstretch_conversion_vertices: IntProperty(
4925 name="Vertices",
4926 description="Number of vertices grease pencil strokes will "
4927 "have, when they are converted to geometry. If strokes have less "
4928 "points than required, the 'Spread evenly' method is used",
4929 default=32,
4930 min=3,
4931 soft_max=500
4933 gstretch_delete_strokes: BoolProperty(
4934 name="Delete strokes",
4935 description="Remove Grease Pencil strokes if they have been used "
4936 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
4937 default=False
4939 gstretch_influence: FloatProperty(
4940 name="Influence",
4941 description="Force of the tool",
4942 default=100.0,
4943 min=0.0,
4944 max=100.0,
4945 precision=1,
4946 subtype='PERCENTAGE'
4948 gstretch_lock_x: BoolProperty(
4949 name="Lock X",
4950 description="Lock editing of the x-coordinate",
4951 default=False
4953 gstretch_lock_y: BoolProperty(
4954 name="Lock Y",
4955 description="Lock editing of the y-coordinate",
4956 default=False
4958 gstretch_lock_z: BoolProperty(
4959 name="Lock Z",
4960 description="Lock editing of the z-coordinate",
4961 default=False
4963 gstretch_method: EnumProperty(
4964 name="Method",
4965 items=(("project", "Project", "Project vertices onto the stroke, "
4966 "using vertex normals and connected edges"),
4967 ("irregular", "Spread", "Distribute vertices along the full "
4968 "stroke, retaining relative distances between the vertices"),
4969 ("regular", "Spread evenly", "Distribute vertices at regular "
4970 "distances along the full stroke")),
4971 description="Method of distributing the vertices over the Grease "
4972 "Pencil stroke",
4973 default='regular'
4976 # relax properties
4977 relax_input: EnumProperty(name="Input",
4978 items=(("all", "Parallel (all)", "Also use non-selected "
4979 "parallel loops as input"),
4980 ("selected", "Selection", "Only use selected vertices as input")),
4981 description="Loops that are relaxed",
4982 default='selected'
4984 relax_interpolation: EnumProperty(
4985 name="Interpolation",
4986 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4987 ("linear", "Linear", "Simple and fast linear algorithm")),
4988 description="Algorithm used for interpolation",
4989 default='cubic'
4991 relax_iterations: EnumProperty(name="Iterations",
4992 items=(("1", "1", "One"),
4993 ("3", "3", "Three"),
4994 ("5", "5", "Five"),
4995 ("10", "10", "Ten"),
4996 ("25", "25", "Twenty-five")),
4997 description="Number of times the loop is relaxed",
4998 default="1"
5000 relax_regular: BoolProperty(
5001 name="Regular",
5002 description="Distribute vertices at constant distances along the loop",
5003 default=True
5006 # space properties
5007 space_influence: FloatProperty(
5008 name="Influence",
5009 description="Force of the tool",
5010 default=100.0,
5011 min=0.0,
5012 max=100.0,
5013 precision=1,
5014 subtype='PERCENTAGE'
5016 space_input: EnumProperty(
5017 name="Input",
5018 items=(("all", "Parallel (all)", "Also use non-selected "
5019 "parallel loops as input"),
5020 ("selected", "Selection", "Only use selected vertices as input")),
5021 description="Loops that are spaced",
5022 default='selected'
5024 space_interpolation: EnumProperty(
5025 name="Interpolation",
5026 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5027 ("linear", "Linear", "Vertices are projected on existing edges")),
5028 description="Algorithm used for interpolation",
5029 default='cubic'
5031 space_lock_x: BoolProperty(
5032 name="Lock X",
5033 description="Lock editing of the x-coordinate",
5034 default=False
5036 space_lock_y: BoolProperty(
5037 name="Lock Y",
5038 description="Lock editing of the y-coordinate",
5039 default=False
5041 space_lock_z: BoolProperty(
5042 name="Lock Z",
5043 description="Lock editing of the z-coordinate",
5044 default=False
5048 # draw function for integration in menus
5049 def menu_func(self, context):
5050 self.layout.menu("VIEW3D_MT_edit_mesh_looptools")
5051 self.layout.separator()
5054 # Add-ons Preferences Update Panel
5056 # Define Panel classes for updating
5057 panels = (
5058 VIEW3D_PT_tools_looptools,
5062 def update_panel(self, context):
5063 message = "LoopTools: Updating Panel locations has failed"
5064 try:
5065 for panel in panels:
5066 if "bl_rna" in panel.__dict__:
5067 bpy.utils.unregister_class(panel)
5069 for panel in panels:
5070 panel.bl_category = context.user_preferences.addons[__name__].preferences.category
5071 bpy.utils.register_class(panel)
5073 except Exception as e:
5074 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
5075 pass
5078 class LoopPreferences(AddonPreferences):
5079 # this must match the addon name, use '__package__'
5080 # when defining this in a submodule of a python package.
5081 bl_idname = __name__
5083 category: StringProperty(
5084 name="Tab Category",
5085 description="Choose a name for the category of the panel",
5086 default="Tools",
5087 update=update_panel
5090 def draw(self, context):
5091 layout = self.layout
5093 row = layout.row()
5094 col = row.column()
5095 col.label(text="Tab Category:")
5096 col.prop(self, "category", text="")
5099 # define classes for registration
5100 classes = (
5101 VIEW3D_MT_edit_mesh_looptools,
5102 VIEW3D_PT_tools_looptools,
5103 LoopToolsProps,
5104 Bridge,
5105 Circle,
5106 Curve,
5107 Flatten,
5108 GStretch,
5109 Relax,
5110 Space,
5111 LoopPreferences,
5112 RemoveGP,
5116 # registering and menu integration
5117 def register():
5118 for cls in classes:
5119 bpy.utils.register_class(cls)
5120 bpy.types.VIEW3D_MT_edit_mesh_specials.prepend(menu_func)
5121 bpy.types.WindowManager.looptools = PointerProperty(type=LoopToolsProps)
5122 update_panel(None, bpy.context)
5125 # unregistering and removing menus
5126 def unregister():
5127 for cls in reversed(classes):
5128 bpy.utils.unregister_class(cls)
5129 bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func)
5130 try:
5131 del bpy.types.WindowManager.looptools
5132 except Exception as e:
5133 print('unregister fail:\n', e)
5134 pass
5137 if __name__ == "__main__":
5138 register()