Update for 2.8
[blender-addons.git] / mesh_looptools.py
blobe3fd302b3174bbe3d158aa11fb2a407391da1fb5
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, 72, 2),
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 and
114 mod.type == 'MIRROR']
115 # update cache
116 looptools_cache[tool] = {"input": input, "object": object.name,
117 "input_method": input_method, "boundaries": boundaries,
118 "single_loops": single_loops, "loops": loops,
119 "derived": derived, "mapping": mapping, "modifiers": modifiers}
122 # calculates natural cubic splines through all given knots
123 def calculate_cubic_splines(bm_mod, tknots, knots):
124 # hack for circular loops
125 if knots[0] == knots[-1] and len(knots) > 1:
126 circular = True
127 k_new1 = []
128 for k in range(-1, -5, -1):
129 if k - 1 < -len(knots):
130 k += len(knots)
131 k_new1.append(knots[k - 1])
132 k_new2 = []
133 for k in range(4):
134 if k + 1 > len(knots) - 1:
135 k -= len(knots)
136 k_new2.append(knots[k + 1])
137 for k in k_new1:
138 knots.insert(0, k)
139 for k in k_new2:
140 knots.append(k)
141 t_new1 = []
142 total1 = 0
143 for t in range(-1, -5, -1):
144 if t - 1 < -len(tknots):
145 t += len(tknots)
146 total1 += tknots[t] - tknots[t - 1]
147 t_new1.append(tknots[0] - total1)
148 t_new2 = []
149 total2 = 0
150 for t in range(4):
151 if t + 1 > len(tknots) - 1:
152 t -= len(tknots)
153 total2 += tknots[t + 1] - tknots[t]
154 t_new2.append(tknots[-1] + total2)
155 for t in t_new1:
156 tknots.insert(0, t)
157 for t in t_new2:
158 tknots.append(t)
159 else:
160 circular = False
161 # end of hack
163 n = len(knots)
164 if n < 2:
165 return False
166 x = tknots[:]
167 locs = [bm_mod.verts[k].co[:] for k in knots]
168 result = []
169 for j in range(3):
170 a = []
171 for i in locs:
172 a.append(i[j])
173 h = []
174 for i in range(n - 1):
175 if x[i + 1] - x[i] == 0:
176 h.append(1e-8)
177 else:
178 h.append(x[i + 1] - x[i])
179 q = [False]
180 for i in range(1, n - 1):
181 q.append(3 / h[i] * (a[i + 1] - a[i]) - 3 / h[i - 1] * (a[i] - a[i - 1]))
182 l = [1.0]
183 u = [0.0]
184 z = [0.0]
185 for i in range(1, n - 1):
186 l.append(2 * (x[i + 1] - x[i - 1]) - h[i - 1] * u[i - 1])
187 if l[i] == 0:
188 l[i] = 1e-8
189 u.append(h[i] / l[i])
190 z.append((q[i] - h[i - 1] * z[i - 1]) / l[i])
191 l.append(1.0)
192 z.append(0.0)
193 b = [False for i in range(n - 1)]
194 c = [False for i in range(n)]
195 d = [False for i in range(n - 1)]
196 c[n - 1] = 0.0
197 for i in range(n - 2, -1, -1):
198 c[i] = z[i] - u[i] * c[i + 1]
199 b[i] = (a[i + 1] - a[i]) / h[i] - h[i] * (c[i + 1] + 2 * c[i]) / 3
200 d[i] = (c[i + 1] - c[i]) / (3 * h[i])
201 for i in range(n - 1):
202 result.append([a[i], b[i], c[i], d[i], x[i]])
203 splines = []
204 for i in range(len(knots) - 1):
205 splines.append([result[i], result[i + n - 1], result[i + (n - 1) * 2]])
206 if circular: # cleaning up after hack
207 knots = knots[4:-4]
208 tknots = tknots[4:-4]
210 return(splines)
213 # calculates linear splines through all given knots
214 def calculate_linear_splines(bm_mod, tknots, knots):
215 splines = []
216 for i in range(len(knots) - 1):
217 a = bm_mod.verts[knots[i]].co
218 b = bm_mod.verts[knots[i + 1]].co
219 d = b - a
220 t = tknots[i]
221 u = tknots[i + 1] - t
222 splines.append([a, d, t, u]) # [locStart, locDif, tStart, tDif]
224 return(splines)
227 # calculate a best-fit plane to the given vertices
228 def calculate_plane(bm_mod, loop, method="best_fit", object=False):
229 # getting the vertex locations
230 locs = [bm_mod.verts[v].co.copy() for v in loop[0]]
232 # calculating the center of masss
233 com = mathutils.Vector()
234 for loc in locs:
235 com += loc
236 com /= len(locs)
237 x, y, z = com
239 if method == 'best_fit':
240 # creating the covariance matrix
241 mat = mathutils.Matrix(((0.0, 0.0, 0.0),
242 (0.0, 0.0, 0.0),
243 (0.0, 0.0, 0.0),
245 for loc in locs:
246 mat[0][0] += (loc[0] - x) ** 2
247 mat[1][0] += (loc[0] - x) * (loc[1] - y)
248 mat[2][0] += (loc[0] - x) * (loc[2] - z)
249 mat[0][1] += (loc[1] - y) * (loc[0] - x)
250 mat[1][1] += (loc[1] - y) ** 2
251 mat[2][1] += (loc[1] - y) * (loc[2] - z)
252 mat[0][2] += (loc[2] - z) * (loc[0] - x)
253 mat[1][2] += (loc[2] - z) * (loc[1] - y)
254 mat[2][2] += (loc[2] - z) ** 2
256 # calculating the normal to the plane
257 normal = False
258 try:
259 mat = matrix_invert(mat)
260 except:
261 ax = 2
262 if math.fabs(sum(mat[0])) < math.fabs(sum(mat[1])):
263 if math.fabs(sum(mat[0])) < math.fabs(sum(mat[2])):
264 ax = 0
265 elif math.fabs(sum(mat[1])) < math.fabs(sum(mat[2])):
266 ax = 1
267 if ax == 0:
268 normal = mathutils.Vector((1.0, 0.0, 0.0))
269 elif ax == 1:
270 normal = mathutils.Vector((0.0, 1.0, 0.0))
271 else:
272 normal = mathutils.Vector((0.0, 0.0, 1.0))
273 if not normal:
274 # warning! this is different from .normalize()
275 itermax = 500
276 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
277 for i in range(itermax):
278 vec = vec2
279 vec2 = mat * vec
280 if vec2.length != 0:
281 vec2 /= vec2.length
282 if vec2 == vec:
283 break
284 if vec2.length == 0:
285 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
286 normal = vec2
288 elif method == 'normal':
289 # averaging the vertex normals
290 v_normals = [bm_mod.verts[v].normal for v in loop[0]]
291 normal = mathutils.Vector()
292 for v_normal in v_normals:
293 normal += v_normal
294 normal /= len(v_normals)
295 normal.normalize()
297 elif method == 'view':
298 # calculate view normal
299 rotation = bpy.context.space_data.region_3d.view_matrix.to_3x3().\
300 inverted()
301 normal = rotation * mathutils.Vector((0.0, 0.0, 1.0))
302 if object:
303 normal = object.matrix_world.inverted().to_euler().to_matrix() * \
304 normal
306 return(com, normal)
309 # calculate splines based on given interpolation method (controller function)
310 def calculate_splines(interpolation, bm_mod, tknots, knots):
311 if interpolation == 'cubic':
312 splines = calculate_cubic_splines(bm_mod, tknots, knots[:])
313 else: # interpolations == 'linear'
314 splines = calculate_linear_splines(bm_mod, tknots, knots[:])
316 return(splines)
319 # check loops and only return valid ones
320 def check_loops(loops, mapping, bm_mod):
321 valid_loops = []
322 for loop, circular in loops:
323 # loop needs to have at least 3 vertices
324 if len(loop) < 3:
325 continue
326 # loop needs at least 1 vertex in the original, non-mirrored mesh
327 if mapping:
328 all_virtual = True
329 for vert in loop:
330 if mapping[vert] > -1:
331 all_virtual = False
332 break
333 if all_virtual:
334 continue
335 # vertices can not all be at the same location
336 stacked = True
337 for i in range(len(loop) - 1):
338 if (bm_mod.verts[loop[i]].co - bm_mod.verts[loop[i + 1]].co).length > 1e-6:
339 stacked = False
340 break
341 if stacked:
342 continue
343 # passed all tests, loop is valid
344 valid_loops.append([loop, circular])
346 return(valid_loops)
349 # input: bmesh, output: dict with the edge-key as key and face-index as value
350 def dict_edge_faces(bm):
351 edge_faces = dict([[edgekey(edge), []] for edge in bm.edges if not edge.hide])
352 for face in bm.faces:
353 if face.hide:
354 continue
355 for key in face_edgekeys(face):
356 edge_faces[key].append(face.index)
358 return(edge_faces)
361 # input: bmesh (edge-faces optional), output: dict with face-face connections
362 def dict_face_faces(bm, edge_faces=False):
363 if not edge_faces:
364 edge_faces = dict_edge_faces(bm)
366 connected_faces = dict([[face.index, []] for face in bm.faces if not face.hide])
367 for face in bm.faces:
368 if face.hide:
369 continue
370 for edge_key in face_edgekeys(face):
371 for connected_face in edge_faces[edge_key]:
372 if connected_face == face.index:
373 continue
374 connected_faces[face.index].append(connected_face)
376 return(connected_faces)
379 # input: bmesh, output: dict with the vert index as key and edge-keys as value
380 def dict_vert_edges(bm):
381 vert_edges = dict([[v.index, []] for v in bm.verts if not v.hide])
382 for edge in bm.edges:
383 if edge.hide:
384 continue
385 ek = edgekey(edge)
386 for vert in ek:
387 vert_edges[vert].append(ek)
389 return(vert_edges)
392 # input: bmesh, output: dict with the vert index as key and face index as value
393 def dict_vert_faces(bm):
394 vert_faces = dict([[v.index, []] for v in bm.verts if not v.hide])
395 for face in bm.faces:
396 if not face.hide:
397 for vert in face.verts:
398 vert_faces[vert.index].append(face.index)
400 return(vert_faces)
403 # input: list of edge-keys, output: dictionary with vertex-vertex connections
404 def dict_vert_verts(edge_keys):
405 # create connection data
406 vert_verts = {}
407 for ek in edge_keys:
408 for i in range(2):
409 if ek[i] in vert_verts:
410 vert_verts[ek[i]].append(ek[1 - i])
411 else:
412 vert_verts[ek[i]] = [ek[1 - i]]
414 return(vert_verts)
417 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
418 def edgekey(edge):
419 return(tuple(sorted([edge.verts[0].index, edge.verts[1].index])))
422 # returns the edgekeys of a bmesh face
423 def face_edgekeys(face):
424 return([tuple(sorted([edge.verts[0].index, edge.verts[1].index])) for edge in face.edges])
427 # calculate input loops
428 def get_connected_input(object, bm, scene, input):
429 # get mesh with modifiers applied
430 derived, bm_mod = get_derived_bmesh(object, bm, scene)
432 # calculate selected loops
433 edge_keys = [edgekey(edge) for edge in bm_mod.edges if edge.select and not edge.hide]
434 loops = get_connected_selections(edge_keys)
436 # if only selected loops are needed, we're done
437 if input == 'selected':
438 return(derived, bm_mod, loops)
439 # elif input == 'all':
440 loops = get_parallel_loops(bm_mod, loops)
442 return(derived, bm_mod, loops)
445 # sorts all edge-keys into a list of loops
446 def get_connected_selections(edge_keys):
447 # create connection data
448 vert_verts = dict_vert_verts(edge_keys)
450 # find loops consisting of connected selected edges
451 loops = []
452 while len(vert_verts) > 0:
453 loop = [iter(vert_verts.keys()).__next__()]
454 growing = True
455 flipped = False
457 # extend loop
458 while growing:
459 # no more connection data for current vertex
460 if loop[-1] not in vert_verts:
461 if not flipped:
462 loop.reverse()
463 flipped = True
464 else:
465 growing = False
466 else:
467 extended = False
468 for i, next_vert in enumerate(vert_verts[loop[-1]]):
469 if next_vert not in loop:
470 vert_verts[loop[-1]].pop(i)
471 if len(vert_verts[loop[-1]]) == 0:
472 del vert_verts[loop[-1]]
473 # remove connection both ways
474 if next_vert in vert_verts:
475 if len(vert_verts[next_vert]) == 1:
476 del vert_verts[next_vert]
477 else:
478 vert_verts[next_vert].remove(loop[-1])
479 loop.append(next_vert)
480 extended = True
481 break
482 if not extended:
483 # found one end of the loop, continue with next
484 if not flipped:
485 loop.reverse()
486 flipped = True
487 # found both ends of the loop, stop growing
488 else:
489 growing = False
491 # check if loop is circular
492 if loop[0] in vert_verts:
493 if loop[-1] in vert_verts[loop[0]]:
494 # is circular
495 if len(vert_verts[loop[0]]) == 1:
496 del vert_verts[loop[0]]
497 else:
498 vert_verts[loop[0]].remove(loop[-1])
499 if len(vert_verts[loop[-1]]) == 1:
500 del vert_verts[loop[-1]]
501 else:
502 vert_verts[loop[-1]].remove(loop[0])
503 loop = [loop, True]
504 else:
505 # not circular
506 loop = [loop, False]
507 else:
508 # not circular
509 loop = [loop, False]
511 loops.append(loop)
513 return(loops)
516 # get the derived mesh data, if there is a mirror modifier
517 def get_derived_bmesh(object, bm, scene):
518 # check for mirror modifiers
519 if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
520 derived = True
521 # disable other modifiers
522 show_viewport = [mod.name for mod in object.modifiers if mod.show_viewport]
523 for mod in object.modifiers:
524 if mod.type != 'MIRROR':
525 mod.show_viewport = False
526 # get derived mesh
527 bm_mod = bmesh.new()
528 mesh_mod = object.to_mesh(scene, True, 'PREVIEW')
529 bm_mod.from_mesh(mesh_mod)
530 bpy.context.blend_data.meshes.remove(mesh_mod)
531 # re-enable other modifiers
532 for mod_name in show_viewport:
533 object.modifiers[mod_name].show_viewport = True
534 # no mirror modifiers, so no derived mesh necessary
535 else:
536 derived = False
537 bm_mod = bm
539 bm_mod.verts.ensure_lookup_table()
540 bm_mod.edges.ensure_lookup_table()
541 bm_mod.faces.ensure_lookup_table()
543 return(derived, bm_mod)
546 # return a mapping of derived indices to indices
547 def get_mapping(derived, bm, bm_mod, single_vertices, full_search, loops):
548 if not derived:
549 return(False)
551 if full_search:
552 verts = [v for v in bm.verts if not v.hide]
553 else:
554 verts = [v for v in bm.verts if v.select and not v.hide]
556 # non-selected vertices around single vertices also need to be mapped
557 if single_vertices:
558 mapping = dict([[vert, -1] for vert in single_vertices])
559 verts_mod = [bm_mod.verts[vert] for vert in single_vertices]
560 for v in verts:
561 for v_mod in verts_mod:
562 if (v.co - v_mod.co).length < 1e-6:
563 mapping[v_mod.index] = v.index
564 break
565 real_singles = [v_real for v_real in mapping.values() if v_real > -1]
567 verts_indices = [vert.index for vert in verts]
568 for face in [face for face in bm.faces if not face.select and not face.hide]:
569 for vert in face.verts:
570 if vert.index in real_singles:
571 for v in face.verts:
572 if v.index not in verts_indices:
573 if v not in verts:
574 verts.append(v)
575 break
577 # create mapping of derived indices to indices
578 mapping = dict([[vert, -1] for loop in loops for vert in loop[0]])
579 if single_vertices:
580 for single in single_vertices:
581 mapping[single] = -1
582 verts_mod = [bm_mod.verts[i] for i in mapping.keys()]
583 for v in verts:
584 for v_mod in verts_mod:
585 if (v.co - v_mod.co).length < 1e-6:
586 mapping[v_mod.index] = v.index
587 verts_mod.remove(v_mod)
588 break
590 return(mapping)
593 # calculate the determinant of a matrix
594 def matrix_determinant(m):
595 determinant = m[0][0] * m[1][1] * m[2][2] + m[0][1] * m[1][2] * m[2][0] \
596 + m[0][2] * m[1][0] * m[2][1] - m[0][2] * m[1][1] * m[2][0] \
597 - m[0][1] * m[1][0] * m[2][2] - m[0][0] * m[1][2] * m[2][1]
599 return(determinant)
602 # custom matrix inversion, to provide higher precision than the built-in one
603 def matrix_invert(m):
604 r = mathutils.Matrix((
605 (m[1][1] * m[2][2] - m[1][2] * m[2][1], m[0][2] * m[2][1] - m[0][1] * m[2][2],
606 m[0][1] * m[1][2] - m[0][2] * m[1][1]),
607 (m[1][2] * m[2][0] - m[1][0] * m[2][2], m[0][0] * m[2][2] - m[0][2] * m[2][0],
608 m[0][2] * m[1][0] - m[0][0] * m[1][2]),
609 (m[1][0] * m[2][1] - m[1][1] * m[2][0], m[0][1] * m[2][0] - m[0][0] * m[2][1],
610 m[0][0] * m[1][1] - m[0][1] * m[1][0])))
612 return (r * (1 / matrix_determinant(m)))
615 # returns a list of all loops parallel to the input, input included
616 def get_parallel_loops(bm_mod, loops):
617 # get required dictionaries
618 edge_faces = dict_edge_faces(bm_mod)
619 connected_faces = dict_face_faces(bm_mod, edge_faces)
620 # turn vertex loops into edge loops
621 edgeloops = []
622 for loop in loops:
623 edgeloop = [[sorted([loop[0][i], loop[0][i + 1]]) for i in
624 range(len(loop[0]) - 1)], loop[1]]
625 if loop[1]: # circular
626 edgeloop[0].append(sorted([loop[0][-1], loop[0][0]]))
627 edgeloops.append(edgeloop[:])
628 # variables to keep track while iterating
629 all_edgeloops = []
630 has_branches = False
632 for loop in edgeloops:
633 # initialise with original loop
634 all_edgeloops.append(loop[0])
635 newloops = [loop[0]]
636 verts_used = []
637 for edge in loop[0]:
638 if edge[0] not in verts_used:
639 verts_used.append(edge[0])
640 if edge[1] not in verts_used:
641 verts_used.append(edge[1])
643 # find parallel loops
644 while len(newloops) > 0:
645 side_a = []
646 side_b = []
647 for i in newloops[-1]:
648 i = tuple(i)
649 forbidden_side = False
650 if i not in edge_faces:
651 # weird input with branches
652 has_branches = True
653 break
654 for face in edge_faces[i]:
655 if len(side_a) == 0 and forbidden_side != "a":
656 side_a.append(face)
657 if forbidden_side:
658 break
659 forbidden_side = "a"
660 continue
661 elif side_a[-1] in connected_faces[face] and \
662 forbidden_side != "a":
663 side_a.append(face)
664 if forbidden_side:
665 break
666 forbidden_side = "a"
667 continue
668 if len(side_b) == 0 and forbidden_side != "b":
669 side_b.append(face)
670 if forbidden_side:
671 break
672 forbidden_side = "b"
673 continue
674 elif side_b[-1] in connected_faces[face] and \
675 forbidden_side != "b":
676 side_b.append(face)
677 if forbidden_side:
678 break
679 forbidden_side = "b"
680 continue
682 if has_branches:
683 # weird input with branches
684 break
686 newloops.pop(-1)
687 sides = []
688 if side_a:
689 sides.append(side_a)
690 if side_b:
691 sides.append(side_b)
693 for side in sides:
694 extraloop = []
695 for fi in side:
696 for key in face_edgekeys(bm_mod.faces[fi]):
697 if key[0] not in verts_used and key[1] not in \
698 verts_used:
699 extraloop.append(key)
700 break
701 if extraloop:
702 for key in extraloop:
703 for new_vert in key:
704 if new_vert not in verts_used:
705 verts_used.append(new_vert)
706 newloops.append(extraloop)
707 all_edgeloops.append(extraloop)
709 # input contains branches, only return selected loop
710 if has_branches:
711 return(loops)
713 # change edgeloops into normal loops
714 loops = []
715 for edgeloop in all_edgeloops:
716 loop = []
717 # grow loop by comparing vertices between consecutive edge-keys
718 for i in range(len(edgeloop) - 1):
719 for vert in range(2):
720 if edgeloop[i][vert] in edgeloop[i + 1]:
721 loop.append(edgeloop[i][vert])
722 break
723 if loop:
724 # add starting vertex
725 for vert in range(2):
726 if edgeloop[0][vert] != loop[0]:
727 loop = [edgeloop[0][vert]] + loop
728 break
729 # add ending vertex
730 for vert in range(2):
731 if edgeloop[-1][vert] != loop[-1]:
732 loop.append(edgeloop[-1][vert])
733 break
734 # check if loop is circular
735 if loop[0] == loop[-1]:
736 circular = True
737 loop = loop[:-1]
738 else:
739 circular = False
740 loops.append([loop, circular])
742 return(loops)
745 # gather initial data
746 def initialise():
747 global_undo = bpy.context.user_preferences.edit.use_global_undo
748 bpy.context.user_preferences.edit.use_global_undo = False
749 object = bpy.context.active_object
750 if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
751 # ensure that selection is synced for the derived mesh
752 bpy.ops.object.mode_set(mode='OBJECT')
753 bpy.ops.object.mode_set(mode='EDIT')
754 bm = bmesh.from_edit_mesh(object.data)
756 bm.verts.ensure_lookup_table()
757 bm.edges.ensure_lookup_table()
758 bm.faces.ensure_lookup_table()
760 return(global_undo, object, bm)
763 # move the vertices to their new locations
764 def move_verts(object, bm, mapping, move, lock, influence):
765 if lock:
766 lock_x, lock_y, lock_z = lock
767 orientation = bpy.context.space_data.transform_orientation
768 custom = bpy.context.space_data.current_orientation
769 if custom:
770 mat = custom.matrix.to_4x4().inverted() * object.matrix_world.copy()
771 elif orientation == 'LOCAL':
772 mat = mathutils.Matrix.Identity(4)
773 elif orientation == 'VIEW':
774 mat = bpy.context.region_data.view_matrix.copy() * \
775 object.matrix_world.copy()
776 else: # orientation == 'GLOBAL'
777 mat = object.matrix_world.copy()
778 mat_inv = mat.inverted()
780 for loop in move:
781 for index, loc in loop:
782 if mapping:
783 if mapping[index] == -1:
784 continue
785 else:
786 index = mapping[index]
787 if lock:
788 delta = (loc - bm.verts[index].co) * mat_inv
789 if lock_x:
790 delta[0] = 0
791 if lock_y:
792 delta[1] = 0
793 if lock_z:
794 delta[2] = 0
795 delta = delta * mat
796 loc = bm.verts[index].co + delta
797 if influence < 0:
798 new_loc = loc
799 else:
800 new_loc = loc * (influence / 100) + \
801 bm.verts[index].co * ((100 - influence) / 100)
802 bm.verts[index].co = new_loc
803 bm.normal_update()
804 object.data.update()
806 bm.verts.ensure_lookup_table()
807 bm.edges.ensure_lookup_table()
808 bm.faces.ensure_lookup_table()
811 # load custom tool settings
812 def settings_load(self):
813 lt = bpy.context.window_manager.looptools
814 tool = self.name.split()[0].lower()
815 keys = self.as_keywords().keys()
816 for key in keys:
817 setattr(self, key, getattr(lt, tool + "_" + key))
820 # store custom tool settings
821 def settings_write(self):
822 lt = bpy.context.window_manager.looptools
823 tool = self.name.split()[0].lower()
824 keys = self.as_keywords().keys()
825 for key in keys:
826 setattr(lt, tool + "_" + key, getattr(self, key))
829 # clean up and set settings back to original state
830 def terminate(global_undo):
831 # update editmesh cached data
832 obj = bpy.context.active_object
833 if obj.mode == 'EDIT':
834 bmesh.update_edit_mesh(obj.data, tessface=True, destructive=True)
836 bpy.context.user_preferences.edit.use_global_undo = global_undo
839 # ########################################
840 # ##### Bridge functions #################
841 # ########################################
843 # calculate a cubic spline through the middle section of 4 given coordinates
844 def bridge_calculate_cubic_spline(bm, coordinates):
845 result = []
846 x = [0, 1, 2, 3]
848 for j in range(3):
849 a = []
850 for i in coordinates:
851 a.append(float(i[j]))
852 h = []
853 for i in range(3):
854 h.append(x[i + 1] - x[i])
855 q = [False]
856 for i in range(1, 3):
857 q.append(3.0 / h[i] * (a[i + 1] - a[i]) - 3.0 / h[i - 1] * (a[i] - a[i - 1]))
858 l = [1.0]
859 u = [0.0]
860 z = [0.0]
861 for i in range(1, 3):
862 l.append(2.0 * (x[i + 1] - x[i - 1]) - h[i - 1] * u[i - 1])
863 u.append(h[i] / l[i])
864 z.append((q[i] - h[i - 1] * z[i - 1]) / l[i])
865 l.append(1.0)
866 z.append(0.0)
867 b = [False for i in range(3)]
868 c = [False for i in range(4)]
869 d = [False for i in range(3)]
870 c[3] = 0.0
871 for i in range(2, -1, -1):
872 c[i] = z[i] - u[i] * c[i + 1]
873 b[i] = (a[i + 1] - a[i]) / h[i] - h[i] * (c[i + 1] + 2.0 * c[i]) / 3.0
874 d[i] = (c[i + 1] - c[i]) / (3.0 * h[i])
875 for i in range(3):
876 result.append([a[i], b[i], c[i], d[i], x[i]])
877 spline = [result[1], result[4], result[7]]
879 return(spline)
882 # return a list with new vertex location vectors, a list with face vertex
883 # integers, and the highest vertex integer in the virtual mesh
884 def bridge_calculate_geometry(bm, lines, vertex_normals, segments,
885 interpolation, cubic_strength, min_width, max_vert_index):
886 new_verts = []
887 faces = []
889 # calculate location based on interpolation method
890 def get_location(line, segment, splines):
891 v1 = bm.verts[lines[line][0]].co
892 v2 = bm.verts[lines[line][1]].co
893 if interpolation == 'linear':
894 return v1 + (segment / segments) * (v2 - v1)
895 else: # interpolation == 'cubic'
896 m = (segment / segments)
897 ax, bx, cx, dx, tx = splines[line][0]
898 x = ax + bx * m + cx * m ** 2 + dx * m ** 3
899 ay, by, cy, dy, ty = splines[line][1]
900 y = ay + by * m + cy * m ** 2 + dy * m ** 3
901 az, bz, cz, dz, tz = splines[line][2]
902 z = az + bz * m + cz * m ** 2 + dz * m ** 3
903 return mathutils.Vector((x, y, z))
905 # no interpolation needed
906 if segments == 1:
907 for i, line in enumerate(lines):
908 if i < len(lines) - 1:
909 faces.append([line[0], lines[i + 1][0], lines[i + 1][1], line[1]])
910 # more than 1 segment, interpolate
911 else:
912 # calculate splines (if necessary) once, so no recalculations needed
913 if interpolation == 'cubic':
914 splines = []
915 for line in lines:
916 v1 = bm.verts[line[0]].co
917 v2 = bm.verts[line[1]].co
918 size = (v2 - v1).length * cubic_strength
919 splines.append(bridge_calculate_cubic_spline(bm,
920 [v1 + size * vertex_normals[line[0]], v1, v2,
921 v2 + size * vertex_normals[line[1]]]))
922 else:
923 splines = False
925 # create starting situation
926 virtual_width = [(bm.verts[lines[i][0]].co -
927 bm.verts[lines[i + 1][0]].co).length for i
928 in range(len(lines) - 1)]
929 new_verts = [get_location(0, seg, splines) for seg in range(1,
930 segments)]
931 first_line_indices = [i for i in range(max_vert_index + 1,
932 max_vert_index + segments)]
934 prev_verts = new_verts[:] # vertex locations of verts on previous line
935 prev_vert_indices = first_line_indices[:]
936 max_vert_index += segments - 1 # highest vertex index in virtual mesh
937 next_verts = [] # vertex locations of verts on current line
938 next_vert_indices = []
940 for i, line in enumerate(lines):
941 if i < len(lines) - 1:
942 v1 = line[0]
943 v2 = lines[i + 1][0]
944 end_face = True
945 for seg in range(1, segments):
946 loc1 = prev_verts[seg - 1]
947 loc2 = get_location(i + 1, seg, splines)
948 if (loc1 - loc2).length < (min_width / 100) * virtual_width[i] \
949 and line[1] == lines[i + 1][1]:
950 # triangle, no new vertex
951 faces.append([v1, v2, prev_vert_indices[seg - 1],
952 prev_vert_indices[seg - 1]])
953 next_verts += prev_verts[seg - 1:]
954 next_vert_indices += prev_vert_indices[seg - 1:]
955 end_face = False
956 break
957 else:
958 if i == len(lines) - 2 and lines[0] == lines[-1]:
959 # quad with first line, no new vertex
960 faces.append([v1, v2, first_line_indices[seg - 1],
961 prev_vert_indices[seg - 1]])
962 v2 = first_line_indices[seg - 1]
963 v1 = prev_vert_indices[seg - 1]
964 else:
965 # quad, add new vertex
966 max_vert_index += 1
967 faces.append([v1, v2, max_vert_index,
968 prev_vert_indices[seg - 1]])
969 v2 = max_vert_index
970 v1 = prev_vert_indices[seg - 1]
971 new_verts.append(loc2)
972 next_verts.append(loc2)
973 next_vert_indices.append(max_vert_index)
974 if end_face:
975 faces.append([v1, v2, lines[i + 1][1], line[1]])
977 prev_verts = next_verts[:]
978 prev_vert_indices = next_vert_indices[:]
979 next_verts = []
980 next_vert_indices = []
982 return(new_verts, faces, max_vert_index)
985 # calculate lines (list of lists, vertex indices) that are used for bridging
986 def bridge_calculate_lines(bm, loops, mode, twist, reverse):
987 lines = []
988 loop1, loop2 = [i[0] for i in loops]
989 loop1_circular, loop2_circular = [i[1] for i in loops]
990 circular = loop1_circular or loop2_circular
991 circle_full = False
993 # calculate loop centers
994 centers = []
995 for loop in [loop1, loop2]:
996 center = mathutils.Vector()
997 for vertex in loop:
998 center += bm.verts[vertex].co
999 center /= len(loop)
1000 centers.append(center)
1001 for i, loop in enumerate([loop1, loop2]):
1002 for vertex in loop:
1003 if bm.verts[vertex].co == centers[i]:
1004 # prevent zero-length vectors in angle comparisons
1005 centers[i] += mathutils.Vector((0.01, 0, 0))
1006 break
1007 center1, center2 = centers
1009 # calculate the normals of the virtual planes that the loops are on
1010 normals = []
1011 normal_plurity = False
1012 for i, loop in enumerate([loop1, loop2]):
1013 # covariance matrix
1014 mat = mathutils.Matrix(((0.0, 0.0, 0.0),
1015 (0.0, 0.0, 0.0),
1016 (0.0, 0.0, 0.0)))
1017 x, y, z = centers[i]
1018 for loc in [bm.verts[vertex].co for vertex in loop]:
1019 mat[0][0] += (loc[0] - x) ** 2
1020 mat[1][0] += (loc[0] - x) * (loc[1] - y)
1021 mat[2][0] += (loc[0] - x) * (loc[2] - z)
1022 mat[0][1] += (loc[1] - y) * (loc[0] - x)
1023 mat[1][1] += (loc[1] - y) ** 2
1024 mat[2][1] += (loc[1] - y) * (loc[2] - z)
1025 mat[0][2] += (loc[2] - z) * (loc[0] - x)
1026 mat[1][2] += (loc[2] - z) * (loc[1] - y)
1027 mat[2][2] += (loc[2] - z) ** 2
1028 # plane normal
1029 normal = False
1030 if sum(mat[0]) < 1e-6 or sum(mat[1]) < 1e-6 or sum(mat[2]) < 1e-6:
1031 normal_plurity = True
1032 try:
1033 mat.invert()
1034 except:
1035 if sum(mat[0]) == 0:
1036 normal = mathutils.Vector((1.0, 0.0, 0.0))
1037 elif sum(mat[1]) == 0:
1038 normal = mathutils.Vector((0.0, 1.0, 0.0))
1039 elif sum(mat[2]) == 0:
1040 normal = mathutils.Vector((0.0, 0.0, 1.0))
1041 if not normal:
1042 # warning! this is different from .normalize()
1043 itermax = 500
1044 iter = 0
1045 vec = mathutils.Vector((1.0, 1.0, 1.0))
1046 vec2 = (mat * vec) / (mat * vec).length
1047 while vec != vec2 and iter < itermax:
1048 iter += 1
1049 vec = vec2
1050 vec2 = mat * vec
1051 if vec2.length != 0:
1052 vec2 /= vec2.length
1053 if vec2.length == 0:
1054 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
1055 normal = vec2
1056 normals.append(normal)
1057 # have plane normals face in the same direction (maximum angle: 90 degrees)
1058 if ((center1 + normals[0]) - center2).length < \
1059 ((center1 - normals[0]) - center2).length:
1060 normals[0].negate()
1061 if ((center2 + normals[1]) - center1).length > \
1062 ((center2 - normals[1]) - center1).length:
1063 normals[1].negate()
1065 # rotation matrix, representing the difference between the plane normals
1066 axis = normals[0].cross(normals[1])
1067 axis = mathutils.Vector([loc if abs(loc) > 1e-8 else 0 for loc in axis])
1068 if axis.angle(mathutils.Vector((0, 0, 1)), 0) > 1.5707964:
1069 axis.negate()
1070 angle = normals[0].dot(normals[1])
1071 rotation_matrix = mathutils.Matrix.Rotation(angle, 4, axis)
1073 # if circular, rotate loops so they are aligned
1074 if circular:
1075 # make sure loop1 is the circular one (or both are circular)
1076 if loop2_circular and not loop1_circular:
1077 loop1_circular, loop2_circular = True, False
1078 loop1, loop2 = loop2, loop1
1080 # match start vertex of loop1 with loop2
1081 target_vector = bm.verts[loop2[0]].co - center2
1082 dif_angles = [[(rotation_matrix * (bm.verts[vertex].co - center1)
1083 ).angle(target_vector, 0), False, i] for
1084 i, vertex in enumerate(loop1)]
1085 dif_angles.sort()
1086 if len(loop1) != len(loop2):
1087 angle_limit = dif_angles[0][0] * 1.2 # 20% margin
1088 dif_angles = [
1089 [(bm.verts[loop2[0]].co -
1090 bm.verts[loop1[index]].co).length, angle, index] for
1091 angle, distance, index in dif_angles if angle <= angle_limit
1093 dif_angles.sort()
1094 loop1 = loop1[dif_angles[0][2]:] + loop1[:dif_angles[0][2]]
1096 # have both loops face the same way
1097 if normal_plurity and not circular:
1098 second_to_first, second_to_second, second_to_last = [
1099 (bm.verts[loop1[1]].co - center1).angle(
1100 bm.verts[loop2[i]].co - center2) for i in [0, 1, -1]
1102 last_to_first, last_to_second = [
1103 (bm.verts[loop1[-1]].co -
1104 center1).angle(bm.verts[loop2[i]].co - center2) for
1105 i in [0, 1]
1107 if (min(last_to_first, last_to_second) * 1.1 < min(second_to_first,
1108 second_to_second)) or (loop2_circular and second_to_last * 1.1 <
1109 min(second_to_first, second_to_second)):
1110 loop1.reverse()
1111 if circular:
1112 loop1 = [loop1[-1]] + loop1[:-1]
1113 else:
1114 angle = (bm.verts[loop1[0]].co - center1).\
1115 cross(bm.verts[loop1[1]].co - center1).angle(normals[0], 0)
1116 target_angle = (bm.verts[loop2[0]].co - center2).\
1117 cross(bm.verts[loop2[1]].co - center2).angle(normals[1], 0)
1118 limit = 1.5707964 # 0.5*pi, 90 degrees
1119 if not ((angle > limit and target_angle > limit) or
1120 (angle < limit and target_angle < limit)):
1121 loop1.reverse()
1122 if circular:
1123 loop1 = [loop1[-1]] + loop1[:-1]
1124 elif normals[0].angle(normals[1]) > limit:
1125 loop1.reverse()
1126 if circular:
1127 loop1 = [loop1[-1]] + loop1[:-1]
1129 # both loops have the same length
1130 if len(loop1) == len(loop2):
1131 # manual override
1132 if twist:
1133 if abs(twist) < len(loop1):
1134 loop1 = loop1[twist:] + loop1[:twist]
1135 if reverse:
1136 loop1.reverse()
1138 lines.append([loop1[0], loop2[0]])
1139 for i in range(1, len(loop1)):
1140 lines.append([loop1[i], loop2[i]])
1142 # loops of different lengths
1143 else:
1144 # make loop1 longest loop
1145 if len(loop2) > len(loop1):
1146 loop1, loop2 = loop2, loop1
1147 loop1_circular, loop2_circular = loop2_circular, loop1_circular
1149 # manual override
1150 if twist:
1151 if abs(twist) < len(loop1):
1152 loop1 = loop1[twist:] + loop1[:twist]
1153 if reverse:
1154 loop1.reverse()
1156 # shortest angle difference doesn't always give correct start vertex
1157 if loop1_circular and not loop2_circular:
1158 shifting = 1
1159 while shifting:
1160 if len(loop1) - shifting < len(loop2):
1161 shifting = False
1162 break
1163 to_last, to_first = [
1164 (rotation_matrix * (bm.verts[loop1[-1]].co - center1)).angle(
1165 (bm.verts[loop2[i]].co - center2), 0) for i in [-1, 0]
1167 if to_first < to_last:
1168 loop1 = [loop1[-1]] + loop1[:-1]
1169 shifting += 1
1170 else:
1171 shifting = False
1172 break
1174 # basic shortest side first
1175 if mode == 'basic':
1176 lines.append([loop1[0], loop2[0]])
1177 for i in range(1, len(loop1)):
1178 if i >= len(loop2) - 1:
1179 # triangles
1180 lines.append([loop1[i], loop2[-1]])
1181 else:
1182 # quads
1183 lines.append([loop1[i], loop2[i]])
1185 # shortest edge algorithm
1186 else: # mode == 'shortest'
1187 lines.append([loop1[0], loop2[0]])
1188 prev_vert2 = 0
1189 for i in range(len(loop1) - 1):
1190 if prev_vert2 == len(loop2) - 1 and not loop2_circular:
1191 # force triangles, reached end of loop2
1192 tri, quad = 0, 1
1193 elif prev_vert2 == len(loop2) - 1 and loop2_circular:
1194 # at end of loop2, but circular, so check with first vert
1195 tri, quad = [(bm.verts[loop1[i + 1]].co -
1196 bm.verts[loop2[j]].co).length
1197 for j in [prev_vert2, 0]]
1198 circle_full = 2
1199 elif len(loop1) - 1 - i == len(loop2) - 1 - prev_vert2 and \
1200 not circle_full:
1201 # force quads, otherwise won't make it to end of loop2
1202 tri, quad = 1, 0
1203 else:
1204 # calculate if tri or quad gives shortest edge
1205 tri, quad = [(bm.verts[loop1[i + 1]].co -
1206 bm.verts[loop2[j]].co).length
1207 for j in range(prev_vert2, prev_vert2 + 2)]
1209 # triangle
1210 if tri < quad:
1211 lines.append([loop1[i + 1], loop2[prev_vert2]])
1212 if circle_full == 2:
1213 circle_full = False
1214 # quad
1215 elif not circle_full:
1216 lines.append([loop1[i + 1], loop2[prev_vert2 + 1]])
1217 prev_vert2 += 1
1218 # quad to first vertex of loop2
1219 else:
1220 lines.append([loop1[i + 1], loop2[0]])
1221 prev_vert2 = 0
1222 circle_full = True
1224 # final face for circular loops
1225 if loop1_circular and loop2_circular:
1226 lines.append([loop1[0], loop2[0]])
1228 return(lines)
1231 # calculate number of segments needed
1232 def bridge_calculate_segments(bm, lines, loops, segments):
1233 # return if amount of segments is set by user
1234 if segments != 0:
1235 return segments
1237 # edge lengths
1238 average_edge_length = [
1239 (bm.verts[vertex].co -
1240 bm.verts[loop[0][i + 1]].co).length for loop in loops for
1241 i, vertex in enumerate(loop[0][:-1])
1243 # closing edges of circular loops
1244 average_edge_length += [
1245 (bm.verts[loop[0][-1]].co -
1246 bm.verts[loop[0][0]].co).length for loop in loops if loop[1]
1249 # average lengths
1250 average_edge_length = sum(average_edge_length) / len(average_edge_length)
1251 average_bridge_length = sum(
1252 [(bm.verts[v1].co -
1253 bm.verts[v2].co).length for v1, v2 in lines]
1254 ) / len(lines)
1256 segments = max(1, round(average_bridge_length / average_edge_length))
1258 return(segments)
1261 # return dictionary with vertex index as key, and the normal vector as value
1262 def bridge_calculate_virtual_vertex_normals(bm, lines, loops, edge_faces,
1263 edgekey_to_edge):
1264 if not edge_faces: # interpolation isn't set to cubic
1265 return False
1267 # pity reduce() isn't one of the basic functions in python anymore
1268 def average_vector_dictionary(dic):
1269 for key, vectors in dic.items():
1270 # if type(vectors) == type([]) and len(vectors) > 1:
1271 if len(vectors) > 1:
1272 average = mathutils.Vector()
1273 for vector in vectors:
1274 average += vector
1275 average /= len(vectors)
1276 dic[key] = [average]
1277 return dic
1279 # get all edges of the loop
1280 edges = [
1281 [edgekey_to_edge[tuple(sorted([loops[j][0][i],
1282 loops[j][0][i + 1]]))] for i in range(len(loops[j][0]) - 1)] for
1283 j in [0, 1]
1285 edges = edges[0] + edges[1]
1286 for j in [0, 1]:
1287 if loops[j][1]: # circular
1288 edges.append(edgekey_to_edge[tuple(sorted([loops[j][0][0],
1289 loops[j][0][-1]]))])
1292 calculation based on face topology (assign edge-normals to vertices)
1294 edge_normal = face_normal x edge_vector
1295 vertex_normal = average(edge_normals)
1297 vertex_normals = dict([(vertex, []) for vertex in loops[0][0] + loops[1][0]])
1298 for edge in edges:
1299 faces = edge_faces[edgekey(edge)] # valid faces connected to edge
1301 if faces:
1302 # get edge coordinates
1303 v1, v2 = [bm.verts[edgekey(edge)[i]].co for i in [0, 1]]
1304 edge_vector = v1 - v2
1305 if edge_vector.length < 1e-4:
1306 # zero-length edge, vertices at same location
1307 continue
1308 edge_center = (v1 + v2) / 2
1310 # average face coordinates, if connected to more than 1 valid face
1311 if len(faces) > 1:
1312 face_normal = mathutils.Vector()
1313 face_center = mathutils.Vector()
1314 for face in faces:
1315 face_normal += face.normal
1316 face_center += face.calc_center_median()
1317 face_normal /= len(faces)
1318 face_center /= len(faces)
1319 else:
1320 face_normal = faces[0].normal
1321 face_center = faces[0].calc_center_median()
1322 if face_normal.length < 1e-4:
1323 # faces with a surface of 0 have no face normal
1324 continue
1326 # calculate virtual edge normal
1327 edge_normal = edge_vector.cross(face_normal)
1328 edge_normal.length = 0.01
1329 if (face_center - (edge_center + edge_normal)).length > \
1330 (face_center - (edge_center - edge_normal)).length:
1331 # make normal face the correct way
1332 edge_normal.negate()
1333 edge_normal.normalize()
1334 # add virtual edge normal as entry for both vertices it connects
1335 for vertex in edgekey(edge):
1336 vertex_normals[vertex].append(edge_normal)
1339 calculation based on connection with other loop (vertex focused method)
1340 - used for vertices that aren't connected to any valid faces
1342 plane_normal = edge_vector x connection_vector
1343 vertex_normal = plane_normal x edge_vector
1345 vertices = [
1346 vertex for vertex, normal in vertex_normals.items() if not normal
1349 if vertices:
1350 # edge vectors connected to vertices
1351 edge_vectors = dict([[vertex, []] for vertex in vertices])
1352 for edge in edges:
1353 for v in edgekey(edge):
1354 if v in edge_vectors:
1355 edge_vector = bm.verts[edgekey(edge)[0]].co - \
1356 bm.verts[edgekey(edge)[1]].co
1357 if edge_vector.length < 1e-4:
1358 # zero-length edge, vertices at same location
1359 continue
1360 edge_vectors[v].append(edge_vector)
1362 # connection vectors between vertices of both loops
1363 connection_vectors = dict([[vertex, []] for vertex in vertices])
1364 connections = dict([[vertex, []] for vertex in vertices])
1365 for v1, v2 in lines:
1366 if v1 in connection_vectors or v2 in connection_vectors:
1367 new_vector = bm.verts[v1].co - bm.verts[v2].co
1368 if new_vector.length < 1e-4:
1369 # zero-length connection vector,
1370 # vertices in different loops at same location
1371 continue
1372 if v1 in connection_vectors:
1373 connection_vectors[v1].append(new_vector)
1374 connections[v1].append(v2)
1375 if v2 in connection_vectors:
1376 connection_vectors[v2].append(new_vector)
1377 connections[v2].append(v1)
1378 connection_vectors = average_vector_dictionary(connection_vectors)
1379 connection_vectors = dict(
1380 [[vertex, vector[0]] if vector else
1381 [vertex, []] for vertex, vector in connection_vectors.items()]
1384 for vertex, values in edge_vectors.items():
1385 # vertex normal doesn't matter, just assign a random vector to it
1386 if not connection_vectors[vertex]:
1387 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1388 continue
1390 # calculate to what location the vertex is connected,
1391 # used to determine what way to flip the normal
1392 connected_center = mathutils.Vector()
1393 for v in connections[vertex]:
1394 connected_center += bm.verts[v].co
1395 if len(connections[vertex]) > 1:
1396 connected_center /= len(connections[vertex])
1397 if len(connections[vertex]) == 0:
1398 # shouldn't be possible, but better safe than sorry
1399 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1400 continue
1402 # can't do proper calculations, because of zero-length vector
1403 if not values:
1404 if (connected_center - (bm.verts[vertex].co +
1405 connection_vectors[vertex])).length < (connected_center -
1406 (bm.verts[vertex].co - connection_vectors[vertex])).length:
1407 connection_vectors[vertex].negate()
1408 vertex_normals[vertex] = [connection_vectors[vertex].normalized()]
1409 continue
1411 # calculate vertex normals using edge-vectors,
1412 # connection-vectors and the derived plane normal
1413 for edge_vector in values:
1414 plane_normal = edge_vector.cross(connection_vectors[vertex])
1415 vertex_normal = edge_vector.cross(plane_normal)
1416 vertex_normal.length = 0.1
1417 if (connected_center - (bm.verts[vertex].co +
1418 vertex_normal)).length < (connected_center -
1419 (bm.verts[vertex].co - vertex_normal)).length:
1420 # make normal face the correct way
1421 vertex_normal.negate()
1422 vertex_normal.normalize()
1423 vertex_normals[vertex].append(vertex_normal)
1425 # average virtual vertex normals, based on all edges it's connected to
1426 vertex_normals = average_vector_dictionary(vertex_normals)
1427 vertex_normals = dict([[vertex, vector[0]] for vertex, vector in vertex_normals.items()])
1429 return(vertex_normals)
1432 # add vertices to mesh
1433 def bridge_create_vertices(bm, vertices):
1434 for i in range(len(vertices)):
1435 bm.verts.new(vertices[i])
1436 bm.verts.ensure_lookup_table()
1439 # add faces to mesh
1440 def bridge_create_faces(object, bm, faces, twist):
1441 # have the normal point the correct way
1442 if twist < 0:
1443 [face.reverse() for face in faces]
1444 faces = [face[2:] + face[:2] if face[0] == face[1] else face for face in faces]
1446 # eekadoodle prevention
1447 for i in range(len(faces)):
1448 if not faces[i][-1]:
1449 if faces[i][0] == faces[i][-1]:
1450 faces[i] = [faces[i][1], faces[i][2], faces[i][3], faces[i][1]]
1451 else:
1452 faces[i] = [faces[i][-1]] + faces[i][:-1]
1453 # result of converting from pre-bmesh period
1454 if faces[i][-1] == faces[i][-2]:
1455 faces[i] = faces[i][:-1]
1457 new_faces = []
1458 for i in range(len(faces)):
1459 new_faces.append(bm.faces.new([bm.verts[v] for v in faces[i]]))
1460 bm.normal_update()
1461 object.data.update(calc_edges=True) # calc_edges prevents memory-corruption
1463 bm.verts.ensure_lookup_table()
1464 bm.edges.ensure_lookup_table()
1465 bm.faces.ensure_lookup_table()
1467 return(new_faces)
1470 # calculate input loops
1471 def bridge_get_input(bm):
1472 # create list of internal edges, which should be skipped
1473 eks_of_selected_faces = [
1474 item for sublist in [face_edgekeys(face) for
1475 face in bm.faces if face.select and not face.hide] for item in sublist
1477 edge_count = {}
1478 for ek in eks_of_selected_faces:
1479 if ek in edge_count:
1480 edge_count[ek] += 1
1481 else:
1482 edge_count[ek] = 1
1483 internal_edges = [ek for ek in edge_count if edge_count[ek] > 1]
1485 # sort correct edges into loops
1486 selected_edges = [
1487 edgekey(edge) for edge in bm.edges if edge.select and
1488 not edge.hide and edgekey(edge) not in internal_edges
1490 loops = get_connected_selections(selected_edges)
1492 return(loops)
1495 # return values needed by the bridge operator
1496 def bridge_initialise(bm, interpolation):
1497 if interpolation == 'cubic':
1498 # dict with edge-key as key and list of connected valid faces as value
1499 face_blacklist = [
1500 face.index for face in bm.faces if face.select or
1501 face.hide
1503 edge_faces = dict(
1504 [[edgekey(edge), []] for edge in bm.edges if not edge.hide]
1506 for face in bm.faces:
1507 if face.index in face_blacklist:
1508 continue
1509 for key in face_edgekeys(face):
1510 edge_faces[key].append(face)
1511 # dictionary with the edge-key as key and edge as value
1512 edgekey_to_edge = dict(
1513 [[edgekey(edge), edge] for edge in bm.edges if edge.select and not edge.hide]
1515 else:
1516 edge_faces = False
1517 edgekey_to_edge = False
1519 # selected faces input
1520 old_selected_faces = [
1521 face.index for face in bm.faces if face.select and not face.hide
1524 # find out if faces created by bridging should be smoothed
1525 smooth = False
1526 if bm.faces:
1527 if sum([face.smooth for face in bm.faces]) / len(bm.faces) >= 0.5:
1528 smooth = True
1530 return(edge_faces, edgekey_to_edge, old_selected_faces, smooth)
1533 # return a string with the input method
1534 def bridge_input_method(loft, loft_loop):
1535 method = ""
1536 if loft:
1537 if loft_loop:
1538 method = "Loft loop"
1539 else:
1540 method = "Loft no-loop"
1541 else:
1542 method = "Bridge"
1544 return(method)
1547 # match up loops in pairs, used for multi-input bridging
1548 def bridge_match_loops(bm, loops):
1549 # calculate average loop normals and centers
1550 normals = []
1551 centers = []
1552 for vertices, circular in loops:
1553 normal = mathutils.Vector()
1554 center = mathutils.Vector()
1555 for vertex in vertices:
1556 normal += bm.verts[vertex].normal
1557 center += bm.verts[vertex].co
1558 normals.append(normal / len(vertices) / 10)
1559 centers.append(center / len(vertices))
1561 # possible matches if loop normals are faced towards the center
1562 # of the other loop
1563 matches = dict([[i, []] for i in range(len(loops))])
1564 matches_amount = 0
1565 for i in range(len(loops) + 1):
1566 for j in range(i + 1, len(loops)):
1567 if (centers[i] - centers[j]).length > \
1568 (centers[i] - (centers[j] + normals[j])).length and \
1569 (centers[j] - centers[i]).length > \
1570 (centers[j] - (centers[i] + normals[i])).length:
1571 matches_amount += 1
1572 matches[i].append([(centers[i] - centers[j]).length, i, j])
1573 matches[j].append([(centers[i] - centers[j]).length, j, i])
1574 # if no loops face each other, just make matches between all the loops
1575 if matches_amount == 0:
1576 for i in range(len(loops) + 1):
1577 for j in range(i + 1, len(loops)):
1578 matches[i].append([(centers[i] - centers[j]).length, i, j])
1579 matches[j].append([(centers[i] - centers[j]).length, j, i])
1580 for key, value in matches.items():
1581 value.sort()
1583 # matches based on distance between centers and number of vertices in loops
1584 new_order = []
1585 for loop_index in range(len(loops)):
1586 if loop_index in new_order:
1587 continue
1588 loop_matches = matches[loop_index]
1589 if not loop_matches:
1590 continue
1591 shortest_distance = loop_matches[0][0]
1592 shortest_distance *= 1.1
1593 loop_matches = [
1594 [abs(len(loops[loop_index][0]) -
1595 len(loops[loop[2]][0])), loop[0], loop[1], loop[2]] for loop in
1596 loop_matches if loop[0] < shortest_distance
1598 loop_matches.sort()
1599 for match in loop_matches:
1600 if match[3] not in new_order:
1601 new_order += [loop_index, match[3]]
1602 break
1604 # reorder loops based on matches
1605 if len(new_order) >= 2:
1606 loops = [loops[i] for i in new_order]
1608 return(loops)
1611 # remove old_selected_faces
1612 def bridge_remove_internal_faces(bm, old_selected_faces):
1613 # collect bmesh faces and internal bmesh edges
1614 remove_faces = [bm.faces[face] for face in old_selected_faces]
1615 edges = collections.Counter(
1616 [edge.index for face in remove_faces for edge in face.edges]
1618 remove_edges = [bm.edges[edge] for edge in edges if edges[edge] > 1]
1620 # remove internal faces and edges
1621 for face in remove_faces:
1622 bm.faces.remove(face)
1623 for edge in remove_edges:
1624 bm.edges.remove(edge)
1626 bm.faces.ensure_lookup_table()
1627 bm.edges.ensure_lookup_table()
1628 bm.verts.ensure_lookup_table()
1631 # update list of internal faces that are flagged for removal
1632 def bridge_save_unused_faces(bm, old_selected_faces, loops):
1633 # key: vertex index, value: lists of selected faces using it
1635 vertex_to_face = dict([[i, []] for i in range(len(bm.verts))])
1636 [[vertex_to_face[vertex.index].append(face) for vertex in
1637 bm.faces[face].verts] for face in old_selected_faces]
1639 # group selected faces that are connected
1640 groups = []
1641 grouped_faces = []
1642 for face in old_selected_faces:
1643 if face in grouped_faces:
1644 continue
1645 grouped_faces.append(face)
1646 group = [face]
1647 new_faces = [face]
1648 while new_faces:
1649 grow_face = new_faces[0]
1650 for vertex in bm.faces[grow_face].verts:
1651 vertex_face_group = [
1652 face for face in vertex_to_face[vertex.index] if
1653 face not in grouped_faces
1655 new_faces += vertex_face_group
1656 grouped_faces += vertex_face_group
1657 group += vertex_face_group
1658 new_faces.pop(0)
1659 groups.append(group)
1661 # key: vertex index, value: True/False (is it in a loop that is used)
1662 used_vertices = dict([[i, 0] for i in range(len(bm.verts))])
1663 for loop in loops:
1664 for vertex in loop[0]:
1665 used_vertices[vertex] = True
1667 # check if group is bridged, if not remove faces from internal faces list
1668 for group in groups:
1669 used = False
1670 for face in group:
1671 if used:
1672 break
1673 for vertex in bm.faces[face].verts:
1674 if used_vertices[vertex.index]:
1675 used = True
1676 break
1677 if not used:
1678 for face in group:
1679 old_selected_faces.remove(face)
1682 # add the newly created faces to the selection
1683 def bridge_select_new_faces(new_faces, smooth):
1684 for face in new_faces:
1685 face.select_set(True)
1686 face.smooth = smooth
1689 # sort loops, so they are connected in the correct order when lofting
1690 def bridge_sort_loops(bm, loops, loft_loop):
1691 # simplify loops to single points, and prepare for pathfinding
1692 x, y, z = [
1693 [sum([bm.verts[i].co[j] for i in loop[0]]) /
1694 len(loop[0]) for loop in loops] for j in range(3)
1696 nodes = [mathutils.Vector((x[i], y[i], z[i])) for i in range(len(loops))]
1698 active_node = 0
1699 open = [i for i in range(1, len(loops))]
1700 path = [[0, 0]]
1701 # connect node to path, that is shortest to active_node
1702 while len(open) > 0:
1703 distances = [(nodes[active_node] - nodes[i]).length for i in open]
1704 active_node = open[distances.index(min(distances))]
1705 open.remove(active_node)
1706 path.append([active_node, min(distances)])
1707 # check if we didn't start in the middle of the path
1708 for i in range(2, len(path)):
1709 if (nodes[path[i][0]] - nodes[0]).length < path[i][1]:
1710 temp = path[:i]
1711 path.reverse()
1712 path = path[:-i] + temp
1713 break
1715 # reorder loops
1716 loops = [loops[i[0]] for i in path]
1717 # if requested, duplicate first loop at last position, so loft can loop
1718 if loft_loop:
1719 loops = loops + [loops[0]]
1721 return(loops)
1724 # remapping old indices to new position in list
1725 def bridge_update_old_selection(bm, old_selected_faces):
1727 old_indices = old_selected_faces[:]
1728 old_selected_faces = []
1729 for i, face in enumerate(bm.faces):
1730 if face.index in old_indices:
1731 old_selected_faces.append(i)
1733 old_selected_faces = [
1734 i for i, face in enumerate(bm.faces) if face.index in old_selected_faces
1737 return(old_selected_faces)
1740 # ########################################
1741 # ##### Circle functions #################
1742 # ########################################
1744 # convert 3d coordinates to 2d coordinates on plane
1745 def circle_3d_to_2d(bm_mod, loop, com, normal):
1746 # project vertices onto the plane
1747 verts = [bm_mod.verts[v] for v in loop[0]]
1748 verts_projected = [[v.co - (v.co - com).dot(normal) * normal, v.index]
1749 for v in verts]
1751 # calculate two vectors (p and q) along the plane
1752 m = mathutils.Vector((normal[0] + 1.0, normal[1], normal[2]))
1753 p = m - (m.dot(normal) * normal)
1754 if p.dot(p) < 1e-6:
1755 m = mathutils.Vector((normal[0], normal[1] + 1.0, normal[2]))
1756 p = m - (m.dot(normal) * normal)
1757 q = p.cross(normal)
1759 # change to 2d coordinates using perpendicular projection
1760 locs_2d = []
1761 for loc, vert in verts_projected:
1762 vloc = loc - com
1763 x = p.dot(vloc) / p.dot(p)
1764 y = q.dot(vloc) / q.dot(q)
1765 locs_2d.append([x, y, vert])
1767 return(locs_2d, p, q)
1770 # calculate a best-fit circle to the 2d locations on the plane
1771 def circle_calculate_best_fit(locs_2d):
1772 # initial guess
1773 x0 = 0.0
1774 y0 = 0.0
1775 r = 1.0
1777 # calculate center and radius (non-linear least squares solution)
1778 for iter in range(500):
1779 jmat = []
1780 k = []
1781 for v in locs_2d:
1782 d = (v[0] ** 2 - 2.0 * x0 * v[0] + v[1] ** 2 - 2.0 * y0 * v[1] + x0 ** 2 + y0 ** 2) ** 0.5
1783 jmat.append([(x0 - v[0]) / d, (y0 - v[1]) / d, -1.0])
1784 k.append(-(((v[0] - x0) ** 2 + (v[1] - y0) ** 2) ** 0.5 - r))
1785 jmat2 = mathutils.Matrix(((0.0, 0.0, 0.0),
1786 (0.0, 0.0, 0.0),
1787 (0.0, 0.0, 0.0),
1789 k2 = mathutils.Vector((0.0, 0.0, 0.0))
1790 for i in range(len(jmat)):
1791 k2 += mathutils.Vector(jmat[i]) * k[i]
1792 jmat2[0][0] += jmat[i][0] ** 2
1793 jmat2[1][0] += jmat[i][0] * jmat[i][1]
1794 jmat2[2][0] += jmat[i][0] * jmat[i][2]
1795 jmat2[1][1] += jmat[i][1] ** 2
1796 jmat2[2][1] += jmat[i][1] * jmat[i][2]
1797 jmat2[2][2] += jmat[i][2] ** 2
1798 jmat2[0][1] = jmat2[1][0]
1799 jmat2[0][2] = jmat2[2][0]
1800 jmat2[1][2] = jmat2[2][1]
1801 try:
1802 jmat2.invert()
1803 except:
1804 pass
1805 dx0, dy0, dr = jmat2 * k2
1806 x0 += dx0
1807 y0 += dy0
1808 r += dr
1809 # stop iterating if we're close enough to optimal solution
1810 if abs(dx0) < 1e-6 and abs(dy0) < 1e-6 and abs(dr) < 1e-6:
1811 break
1813 # return center of circle and radius
1814 return(x0, y0, r)
1817 # calculate circle so no vertices have to be moved away from the center
1818 def circle_calculate_min_fit(locs_2d):
1819 # center of circle
1820 x0 = (min([i[0] for i in locs_2d]) + max([i[0] for i in locs_2d])) / 2.0
1821 y0 = (min([i[1] for i in locs_2d]) + max([i[1] for i in locs_2d])) / 2.0
1822 center = mathutils.Vector([x0, y0])
1823 # radius of circle
1824 r = min([(mathutils.Vector([i[0], i[1]]) - center).length for i in locs_2d])
1826 # return center of circle and radius
1827 return(x0, y0, r)
1830 # calculate the new locations of the vertices that need to be moved
1831 def circle_calculate_verts(flatten, bm_mod, locs_2d, com, p, q, normal):
1832 # changing 2d coordinates back to 3d coordinates
1833 locs_3d = []
1834 for loc in locs_2d:
1835 locs_3d.append([loc[2], loc[0] * p + loc[1] * q + com])
1837 if flatten: # flat circle
1838 return(locs_3d)
1840 else: # project the locations on the existing mesh
1841 vert_edges = dict_vert_edges(bm_mod)
1842 vert_faces = dict_vert_faces(bm_mod)
1843 faces = [f for f in bm_mod.faces if not f.hide]
1844 rays = [normal, -normal]
1845 new_locs = []
1846 for loc in locs_3d:
1847 projection = False
1848 if bm_mod.verts[loc[0]].co == loc[1]: # vertex hasn't moved
1849 projection = loc[1]
1850 else:
1851 dif = normal.angle(loc[1] - bm_mod.verts[loc[0]].co)
1852 if -1e-6 < dif < 1e-6 or math.pi - 1e-6 < dif < math.pi + 1e-6:
1853 # original location is already along projection normal
1854 projection = bm_mod.verts[loc[0]].co
1855 else:
1856 # quick search through adjacent faces
1857 for face in vert_faces[loc[0]]:
1858 verts = [v.co for v in bm_mod.faces[face].verts]
1859 if len(verts) == 3: # triangle
1860 v1, v2, v3 = verts
1861 v4 = False
1862 else: # assume quad
1863 v1, v2, v3, v4 = verts[:4]
1864 for ray in rays:
1865 intersect = mathutils.geometry.\
1866 intersect_ray_tri(v1, v2, v3, ray, loc[1])
1867 if intersect:
1868 projection = intersect
1869 break
1870 elif v4:
1871 intersect = mathutils.geometry.\
1872 intersect_ray_tri(v1, v3, v4, ray, loc[1])
1873 if intersect:
1874 projection = intersect
1875 break
1876 if projection:
1877 break
1878 if not projection:
1879 # check if projection is on adjacent edges
1880 for edgekey in vert_edges[loc[0]]:
1881 line1 = bm_mod.verts[edgekey[0]].co
1882 line2 = bm_mod.verts[edgekey[1]].co
1883 intersect, dist = mathutils.geometry.intersect_point_line(
1884 loc[1], line1, line2
1886 if 1e-6 < dist < 1 - 1e-6:
1887 projection = intersect
1888 break
1889 if not projection:
1890 # full search through the entire mesh
1891 hits = []
1892 for face in faces:
1893 verts = [v.co for v in face.verts]
1894 if len(verts) == 3: # triangle
1895 v1, v2, v3 = verts
1896 v4 = False
1897 else: # assume quad
1898 v1, v2, v3, v4 = verts[:4]
1899 for ray in rays:
1900 intersect = mathutils.geometry.intersect_ray_tri(
1901 v1, v2, v3, ray, loc[1]
1903 if intersect:
1904 hits.append([(loc[1] - intersect).length,
1905 intersect])
1906 break
1907 elif v4:
1908 intersect = mathutils.geometry.intersect_ray_tri(
1909 v1, v3, v4, ray, loc[1]
1911 if intersect:
1912 hits.append([(loc[1] - intersect).length,
1913 intersect])
1914 break
1915 if len(hits) >= 1:
1916 # if more than 1 hit with mesh, closest hit is new loc
1917 hits.sort()
1918 projection = hits[0][1]
1919 if not projection:
1920 # nothing to project on, remain at flat location
1921 projection = loc[1]
1922 new_locs.append([loc[0], projection])
1924 # return new positions of projected circle
1925 return(new_locs)
1928 # check loops and only return valid ones
1929 def circle_check_loops(single_loops, loops, mapping, bm_mod):
1930 valid_single_loops = {}
1931 valid_loops = []
1932 for i, [loop, circular] in enumerate(loops):
1933 # loop needs to have at least 3 vertices
1934 if len(loop) < 3:
1935 continue
1936 # loop needs at least 1 vertex in the original, non-mirrored mesh
1937 if mapping:
1938 all_virtual = True
1939 for vert in loop:
1940 if mapping[vert] > -1:
1941 all_virtual = False
1942 break
1943 if all_virtual:
1944 continue
1945 # loop has to be non-collinear
1946 collinear = True
1947 loc0 = mathutils.Vector(bm_mod.verts[loop[0]].co[:])
1948 loc1 = mathutils.Vector(bm_mod.verts[loop[1]].co[:])
1949 for v in loop[2:]:
1950 locn = mathutils.Vector(bm_mod.verts[v].co[:])
1951 if loc0 == loc1 or loc1 == locn:
1952 loc0 = loc1
1953 loc1 = locn
1954 continue
1955 d1 = loc1 - loc0
1956 d2 = locn - loc1
1957 if -1e-6 < d1.angle(d2, 0) < 1e-6:
1958 loc0 = loc1
1959 loc1 = locn
1960 continue
1961 collinear = False
1962 break
1963 if collinear:
1964 continue
1965 # passed all tests, loop is valid
1966 valid_loops.append([loop, circular])
1967 valid_single_loops[len(valid_loops) - 1] = single_loops[i]
1969 return(valid_single_loops, valid_loops)
1972 # calculate the location of single input vertices that need to be flattened
1973 def circle_flatten_singles(bm_mod, com, p, q, normal, single_loop):
1974 new_locs = []
1975 for vert in single_loop:
1976 loc = mathutils.Vector(bm_mod.verts[vert].co[:])
1977 new_locs.append([vert, loc - (loc - com).dot(normal) * normal])
1979 return(new_locs)
1982 # calculate input loops
1983 def circle_get_input(object, bm, scene):
1984 # get mesh with modifiers applied
1985 derived, bm_mod = get_derived_bmesh(object, bm, scene)
1987 # create list of edge-keys based on selection state
1988 faces = False
1989 for face in bm.faces:
1990 if face.select and not face.hide:
1991 faces = True
1992 break
1993 if faces:
1994 # get selected, non-hidden , non-internal edge-keys
1995 eks_selected = [
1996 key for keys in [face_edgekeys(face) for face in
1997 bm_mod.faces if face.select and not face.hide] for key in keys
1999 edge_count = {}
2000 for ek in eks_selected:
2001 if ek in edge_count:
2002 edge_count[ek] += 1
2003 else:
2004 edge_count[ek] = 1
2005 edge_keys = [
2006 edgekey(edge) for edge in bm_mod.edges if edge.select and
2007 not edge.hide and edge_count.get(edgekey(edge), 1) == 1
2009 else:
2010 # no faces, so no internal edges either
2011 edge_keys = [
2012 edgekey(edge) for edge in bm_mod.edges if edge.select and not edge.hide
2015 # add edge-keys around single vertices
2016 verts_connected = dict(
2017 [[vert, 1] for edge in [edge for edge in
2018 bm_mod.edges if edge.select and not edge.hide] for vert in
2019 edgekey(edge)]
2021 single_vertices = [
2022 vert.index for vert in bm_mod.verts if
2023 vert.select and not vert.hide and
2024 not verts_connected.get(vert.index, False)
2027 if single_vertices and len(bm.faces) > 0:
2028 vert_to_single = dict(
2029 [[v.index, []] for v in bm_mod.verts if not v.hide]
2031 for face in [face for face in bm_mod.faces if not face.select and not face.hide]:
2032 for vert in face.verts:
2033 vert = vert.index
2034 if vert in single_vertices:
2035 for ek in face_edgekeys(face):
2036 if vert not in ek:
2037 edge_keys.append(ek)
2038 if vert not in vert_to_single[ek[0]]:
2039 vert_to_single[ek[0]].append(vert)
2040 if vert not in vert_to_single[ek[1]]:
2041 vert_to_single[ek[1]].append(vert)
2042 break
2044 # sort edge-keys into loops
2045 loops = get_connected_selections(edge_keys)
2047 # find out to which loops the single vertices belong
2048 single_loops = dict([[i, []] for i in range(len(loops))])
2049 if single_vertices and len(bm.faces) > 0:
2050 for i, [loop, circular] in enumerate(loops):
2051 for vert in loop:
2052 if vert_to_single[vert]:
2053 for single in vert_to_single[vert]:
2054 if single not in single_loops[i]:
2055 single_loops[i].append(single)
2057 return(derived, bm_mod, single_vertices, single_loops, loops)
2060 # recalculate positions based on the influence of the circle shape
2061 def circle_influence_locs(locs_2d, new_locs_2d, influence):
2062 for i in range(len(locs_2d)):
2063 oldx, oldy, j = locs_2d[i]
2064 newx, newy, k = new_locs_2d[i]
2065 altx = newx * (influence / 100) + oldx * ((100 - influence) / 100)
2066 alty = newy * (influence / 100) + oldy * ((100 - influence) / 100)
2067 locs_2d[i] = [altx, alty, j]
2069 return(locs_2d)
2072 # project 2d locations on circle, respecting distance relations between verts
2073 def circle_project_non_regular(locs_2d, x0, y0, r):
2074 for i in range(len(locs_2d)):
2075 x, y, j = locs_2d[i]
2076 loc = mathutils.Vector([x - x0, y - y0])
2077 loc.length = r
2078 locs_2d[i] = [loc[0], loc[1], j]
2080 return(locs_2d)
2083 # project 2d locations on circle, with equal distance between all vertices
2084 def circle_project_regular(locs_2d, x0, y0, r):
2085 # find offset angle and circling direction
2086 x, y, i = locs_2d[0]
2087 loc = mathutils.Vector([x - x0, y - y0])
2088 loc.length = r
2089 offset_angle = loc.angle(mathutils.Vector([1.0, 0.0]), 0.0)
2090 loca = mathutils.Vector([x - x0, y - y0, 0.0])
2091 if loc[1] < -1e-6:
2092 offset_angle *= -1
2093 x, y, j = locs_2d[1]
2094 locb = mathutils.Vector([x - x0, y - y0, 0.0])
2095 if loca.cross(locb)[2] >= 0:
2096 ccw = 1
2097 else:
2098 ccw = -1
2099 # distribute vertices along the circle
2100 for i in range(len(locs_2d)):
2101 t = offset_angle + ccw * (i / len(locs_2d) * 2 * math.pi)
2102 x = math.cos(t) * r
2103 y = math.sin(t) * r
2104 locs_2d[i] = [x, y, locs_2d[i][2]]
2106 return(locs_2d)
2109 # shift loop, so the first vertex is closest to the center
2110 def circle_shift_loop(bm_mod, loop, com):
2111 verts, circular = loop
2112 distances = [
2113 [(bm_mod.verts[vert].co - com).length, i] for i, vert in enumerate(verts)
2115 distances.sort()
2116 shift = distances[0][1]
2117 loop = [verts[shift:] + verts[:shift], circular]
2119 return(loop)
2122 # ########################################
2123 # ##### Curve functions ##################
2124 # ########################################
2126 # create lists with knots and points, all correctly sorted
2127 def curve_calculate_knots(loop, verts_selected):
2128 knots = [v for v in loop[0] if v in verts_selected]
2129 points = loop[0][:]
2130 # circular loop, potential for weird splines
2131 if loop[1]:
2132 offset = int(len(loop[0]) / 4)
2133 kpos = []
2134 for k in knots:
2135 kpos.append(loop[0].index(k))
2136 kdif = []
2137 for i in range(len(kpos) - 1):
2138 kdif.append(kpos[i + 1] - kpos[i])
2139 kdif.append(len(loop[0]) - kpos[-1] + kpos[0])
2140 kadd = []
2141 for k in kdif:
2142 if k > 2 * offset:
2143 kadd.append([kdif.index(k), True])
2144 # next 2 lines are optional, they insert
2145 # an extra control point in small gaps
2146 # elif k > offset:
2147 # kadd.append([kdif.index(k), False])
2148 kins = []
2149 krot = False
2150 for k in kadd: # extra knots to be added
2151 if k[1]: # big gap (break circular spline)
2152 kpos = loop[0].index(knots[k[0]]) + offset
2153 if kpos > len(loop[0]) - 1:
2154 kpos -= len(loop[0])
2155 kins.append([knots[k[0]], loop[0][kpos]])
2156 kpos2 = k[0] + 1
2157 if kpos2 > len(knots) - 1:
2158 kpos2 -= len(knots)
2159 kpos2 = loop[0].index(knots[kpos2]) - offset
2160 if kpos2 < 0:
2161 kpos2 += len(loop[0])
2162 kins.append([loop[0][kpos], loop[0][kpos2]])
2163 krot = loop[0][kpos2]
2164 else: # small gap (keep circular spline)
2165 k1 = loop[0].index(knots[k[0]])
2166 k2 = k[0] + 1
2167 if k2 > len(knots) - 1:
2168 k2 -= len(knots)
2169 k2 = loop[0].index(knots[k2])
2170 if k2 < k1:
2171 dif = len(loop[0]) - 1 - k1 + k2
2172 else:
2173 dif = k2 - k1
2174 kn = k1 + int(dif / 2)
2175 if kn > len(loop[0]) - 1:
2176 kn -= len(loop[0])
2177 kins.append([loop[0][k1], loop[0][kn]])
2178 for j in kins: # insert new knots
2179 knots.insert(knots.index(j[0]) + 1, j[1])
2180 if not krot: # circular loop
2181 knots.append(knots[0])
2182 points = loop[0][loop[0].index(knots[0]):]
2183 points += loop[0][0:loop[0].index(knots[0]) + 1]
2184 else: # non-circular loop (broken by script)
2185 krot = knots.index(krot)
2186 knots = knots[krot:] + knots[0:krot]
2187 if loop[0].index(knots[0]) > loop[0].index(knots[-1]):
2188 points = loop[0][loop[0].index(knots[0]):]
2189 points += loop[0][0:loop[0].index(knots[-1]) + 1]
2190 else:
2191 points = loop[0][loop[0].index(knots[0]):loop[0].index(knots[-1]) + 1]
2192 # non-circular loop, add first and last point as knots
2193 else:
2194 if loop[0][0] not in knots:
2195 knots.insert(0, loop[0][0])
2196 if loop[0][-1] not in knots:
2197 knots.append(loop[0][-1])
2199 return(knots, points)
2202 # calculate relative positions compared to first knot
2203 def curve_calculate_t(bm_mod, knots, points, pknots, regular, circular):
2204 tpoints = []
2205 loc_prev = False
2206 len_total = 0
2208 for p in points:
2209 if p in knots:
2210 loc = pknots[knots.index(p)] # use projected knot location
2211 else:
2212 loc = mathutils.Vector(bm_mod.verts[p].co[:])
2213 if not loc_prev:
2214 loc_prev = loc
2215 len_total += (loc - loc_prev).length
2216 tpoints.append(len_total)
2217 loc_prev = loc
2218 tknots = []
2219 for p in points:
2220 if p in knots:
2221 tknots.append(tpoints[points.index(p)])
2222 if circular:
2223 tknots[-1] = tpoints[-1]
2225 # regular option
2226 if regular:
2227 tpoints_average = tpoints[-1] / (len(tpoints) - 1)
2228 for i in range(1, len(tpoints) - 1):
2229 tpoints[i] = i * tpoints_average
2230 for i in range(len(knots)):
2231 tknots[i] = tpoints[points.index(knots[i])]
2232 if circular:
2233 tknots[-1] = tpoints[-1]
2235 return(tknots, tpoints)
2238 # change the location of non-selected points to their place on the spline
2239 def curve_calculate_vertices(bm_mod, knots, tknots, points, tpoints, splines,
2240 interpolation, restriction):
2241 newlocs = {}
2242 move = []
2244 for p in points:
2245 if p in knots:
2246 continue
2247 m = tpoints[points.index(p)]
2248 if m in tknots:
2249 n = tknots.index(m)
2250 else:
2251 t = tknots[:]
2252 t.append(m)
2253 t.sort()
2254 n = t.index(m) - 1
2255 if n > len(splines) - 1:
2256 n = len(splines) - 1
2257 elif n < 0:
2258 n = 0
2260 if interpolation == 'cubic':
2261 ax, bx, cx, dx, tx = splines[n][0]
2262 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
2263 ay, by, cy, dy, ty = splines[n][1]
2264 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
2265 az, bz, cz, dz, tz = splines[n][2]
2266 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
2267 newloc = mathutils.Vector([x, y, z])
2268 else: # interpolation == 'linear'
2269 a, d, t, u = splines[n]
2270 newloc = ((m - t) / u) * d + a
2272 if restriction != 'none': # vertex movement is restricted
2273 newlocs[p] = newloc
2274 else: # set the vertex to its new location
2275 move.append([p, newloc])
2277 if restriction != 'none': # vertex movement is restricted
2278 for p in points:
2279 if p in newlocs:
2280 newloc = newlocs[p]
2281 else:
2282 move.append([p, bm_mod.verts[p].co])
2283 continue
2284 oldloc = bm_mod.verts[p].co
2285 normal = bm_mod.verts[p].normal
2286 dloc = newloc - oldloc
2287 if dloc.length < 1e-6:
2288 move.append([p, newloc])
2289 elif restriction == 'extrude': # only extrusions
2290 if dloc.angle(normal, 0) < 0.5 * math.pi + 1e-6:
2291 move.append([p, newloc])
2292 else: # restriction == 'indent' only indentations
2293 if dloc.angle(normal) > 0.5 * math.pi - 1e-6:
2294 move.append([p, newloc])
2296 return(move)
2299 # trim loops to part between first and last selected vertices (including)
2300 def curve_cut_boundaries(bm_mod, loops):
2301 cut_loops = []
2302 for loop, circular in loops:
2303 if circular:
2304 # don't cut
2305 cut_loops.append([loop, circular])
2306 continue
2307 selected = [bm_mod.verts[v].select for v in loop]
2308 first = selected.index(True)
2309 selected.reverse()
2310 last = -selected.index(True)
2311 if last == 0:
2312 cut_loops.append([loop[first:], circular])
2313 else:
2314 cut_loops.append([loop[first:last], circular])
2316 return(cut_loops)
2319 # calculate input loops
2320 def curve_get_input(object, bm, boundaries, scene):
2321 # get mesh with modifiers applied
2322 derived, bm_mod = get_derived_bmesh(object, bm, scene)
2324 # vertices that still need a loop to run through it
2325 verts_unsorted = [
2326 v.index for v in bm_mod.verts if v.select and not v.hide
2328 # necessary dictionaries
2329 vert_edges = dict_vert_edges(bm_mod)
2330 edge_faces = dict_edge_faces(bm_mod)
2331 correct_loops = []
2332 # find loops through each selected vertex
2333 while len(verts_unsorted) > 0:
2334 loops = curve_vertex_loops(bm_mod, verts_unsorted[0], vert_edges,
2335 edge_faces)
2336 verts_unsorted.pop(0)
2338 # check if loop is fully selected
2339 search_perpendicular = False
2340 i = -1
2341 for loop, circular in loops:
2342 i += 1
2343 selected = [v for v in loop if bm_mod.verts[v].select]
2344 if len(selected) < 2:
2345 # only one selected vertex on loop, don't use
2346 loops.pop(i)
2347 continue
2348 elif len(selected) == len(loop):
2349 search_perpendicular = loop
2350 break
2351 # entire loop is selected, find perpendicular loops
2352 if search_perpendicular:
2353 for vert in loop:
2354 if vert in verts_unsorted:
2355 verts_unsorted.remove(vert)
2356 perp_loops = curve_perpendicular_loops(bm_mod, loop,
2357 vert_edges, edge_faces)
2358 for perp_loop in perp_loops:
2359 correct_loops.append(perp_loop)
2360 # normal input
2361 else:
2362 for loop, circular in loops:
2363 correct_loops.append([loop, circular])
2365 # boundaries option
2366 if boundaries:
2367 correct_loops = curve_cut_boundaries(bm_mod, correct_loops)
2369 return(derived, bm_mod, correct_loops)
2372 # return all loops that are perpendicular to the given one
2373 def curve_perpendicular_loops(bm_mod, start_loop, vert_edges, edge_faces):
2374 # find perpendicular loops
2375 perp_loops = []
2376 for start_vert in start_loop:
2377 loops = curve_vertex_loops(bm_mod, start_vert, vert_edges,
2378 edge_faces)
2379 for loop, circular in loops:
2380 selected = [v for v in loop if bm_mod.verts[v].select]
2381 if len(selected) == len(loop):
2382 continue
2383 else:
2384 perp_loops.append([loop, circular, loop.index(start_vert)])
2386 # trim loops to same lengths
2387 shortest = [
2388 [len(loop[0]), i] for i, loop in enumerate(perp_loops) if not loop[1]
2390 if not shortest:
2391 # all loops are circular, not trimming
2392 return([[loop[0], loop[1]] for loop in perp_loops])
2393 else:
2394 shortest = min(shortest)
2395 shortest_start = perp_loops[shortest[1]][2]
2396 before_start = shortest_start
2397 after_start = shortest[0] - shortest_start - 1
2398 bigger_before = before_start > after_start
2399 trimmed_loops = []
2400 for loop in perp_loops:
2401 # have the loop face the same direction as the shortest one
2402 if bigger_before:
2403 if loop[2] < len(loop[0]) / 2:
2404 loop[0].reverse()
2405 loop[2] = len(loop[0]) - loop[2] - 1
2406 else:
2407 if loop[2] > len(loop[0]) / 2:
2408 loop[0].reverse()
2409 loop[2] = len(loop[0]) - loop[2] - 1
2410 # circular loops can shift, to prevent wrong trimming
2411 if loop[1]:
2412 shift = shortest_start - loop[2]
2413 if loop[2] + shift > 0 and loop[2] + shift < len(loop[0]):
2414 loop[0] = loop[0][-shift:] + loop[0][:-shift]
2415 loop[2] += shift
2416 if loop[2] < 0:
2417 loop[2] += len(loop[0])
2418 elif loop[2] > len(loop[0]) - 1:
2419 loop[2] -= len(loop[0])
2420 # trim
2421 start = max(0, loop[2] - before_start)
2422 end = min(len(loop[0]), loop[2] + after_start + 1)
2423 trimmed_loops.append([loop[0][start:end], False])
2425 return(trimmed_loops)
2428 # project knots on non-selected geometry
2429 def curve_project_knots(bm_mod, verts_selected, knots, points, circular):
2430 # function to project vertex on edge
2431 def project(v1, v2, v3):
2432 # v1 and v2 are part of a line
2433 # v3 is projected onto it
2434 v2 -= v1
2435 v3 -= v1
2436 p = v3.project(v2)
2437 return(p + v1)
2439 if circular: # project all knots
2440 start = 0
2441 end = len(knots)
2442 pknots = []
2443 else: # first and last knot shouldn't be projected
2444 start = 1
2445 end = -1
2446 pknots = [mathutils.Vector(bm_mod.verts[knots[0]].co[:])]
2447 for knot in knots[start:end]:
2448 if knot in verts_selected:
2449 knot_left = knot_right = False
2450 for i in range(points.index(knot) - 1, -1 * len(points), -1):
2451 if points[i] not in knots:
2452 knot_left = points[i]
2453 break
2454 for i in range(points.index(knot) + 1, 2 * len(points)):
2455 if i > len(points) - 1:
2456 i -= len(points)
2457 if points[i] not in knots:
2458 knot_right = points[i]
2459 break
2460 if knot_left and knot_right and knot_left != knot_right:
2461 knot_left = mathutils.Vector(bm_mod.verts[knot_left].co[:])
2462 knot_right = mathutils.Vector(bm_mod.verts[knot_right].co[:])
2463 knot = mathutils.Vector(bm_mod.verts[knot].co[:])
2464 pknots.append(project(knot_left, knot_right, knot))
2465 else:
2466 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2467 else: # knot isn't selected, so shouldn't be changed
2468 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2469 if not circular:
2470 pknots.append(mathutils.Vector(bm_mod.verts[knots[-1]].co[:]))
2472 return(pknots)
2475 # find all loops through a given vertex
2476 def curve_vertex_loops(bm_mod, start_vert, vert_edges, edge_faces):
2477 edges_used = []
2478 loops = []
2480 for edge in vert_edges[start_vert]:
2481 if edge in edges_used:
2482 continue
2483 loop = []
2484 circular = False
2485 for vert in edge:
2486 active_faces = edge_faces[edge]
2487 new_vert = vert
2488 growing = True
2489 while growing:
2490 growing = False
2491 new_edges = vert_edges[new_vert]
2492 loop.append(new_vert)
2493 if len(loop) > 1:
2494 edges_used.append(tuple(sorted([loop[-1], loop[-2]])))
2495 if len(new_edges) < 3 or len(new_edges) > 4:
2496 # pole
2497 break
2498 else:
2499 # find next edge
2500 for new_edge in new_edges:
2501 if new_edge in edges_used:
2502 continue
2503 eliminate = False
2504 for new_face in edge_faces[new_edge]:
2505 if new_face in active_faces:
2506 eliminate = True
2507 break
2508 if eliminate:
2509 continue
2510 # found correct new edge
2511 active_faces = edge_faces[new_edge]
2512 v1, v2 = new_edge
2513 if v1 != new_vert:
2514 new_vert = v1
2515 else:
2516 new_vert = v2
2517 if new_vert == loop[0]:
2518 circular = True
2519 else:
2520 growing = True
2521 break
2522 if circular:
2523 break
2524 loop.reverse()
2525 loops.append([loop, circular])
2527 return(loops)
2530 # ########################################
2531 # ##### Flatten functions ################
2532 # ########################################
2534 # sort input into loops
2535 def flatten_get_input(bm):
2536 vert_verts = dict_vert_verts(
2537 [edgekey(edge) for edge in bm.edges if edge.select and not edge.hide]
2539 verts = [v.index for v in bm.verts if v.select and not v.hide]
2541 # no connected verts, consider all selected verts as a single input
2542 if not vert_verts:
2543 return([[verts, False]])
2545 loops = []
2546 while len(verts) > 0:
2547 # start of loop
2548 loop = [verts[0]]
2549 verts.pop(0)
2550 if loop[-1] in vert_verts:
2551 to_grow = vert_verts[loop[-1]]
2552 else:
2553 to_grow = []
2554 # grow loop
2555 while len(to_grow) > 0:
2556 new_vert = to_grow[0]
2557 to_grow.pop(0)
2558 if new_vert in loop:
2559 continue
2560 loop.append(new_vert)
2561 verts.remove(new_vert)
2562 to_grow += vert_verts[new_vert]
2563 # add loop to loops
2564 loops.append([loop, False])
2566 return(loops)
2569 # calculate position of vertex projections on plane
2570 def flatten_project(bm, loop, com, normal):
2571 verts = [bm.verts[v] for v in loop[0]]
2572 verts_projected = [
2573 [v.index, mathutils.Vector(v.co[:]) -
2574 (mathutils.Vector(v.co[:]) - com).dot(normal) * normal] for v in verts
2577 return(verts_projected)
2580 # ########################################
2581 # ##### Gstretch functions ###############
2582 # ########################################
2584 # fake stroke class, used to create custom strokes if no GP data is found
2585 class gstretch_fake_stroke():
2586 def __init__(self, points):
2587 self.points = [gstretch_fake_stroke_point(p) for p in points]
2590 # fake stroke point class, used in fake strokes
2591 class gstretch_fake_stroke_point():
2592 def __init__(self, loc):
2593 self.co = loc
2596 # flips loops, if necessary, to obtain maximum alignment to stroke
2597 def gstretch_align_pairs(ls_pairs, object, bm_mod, method):
2598 # returns total distance between all verts in loop and corresponding stroke
2599 def distance_loop_stroke(loop, stroke, object, bm_mod, method):
2600 stroke_lengths_cache = False
2601 loop_length = len(loop[0])
2602 total_distance = 0
2604 if method != 'regular':
2605 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2607 for i, v_index in enumerate(loop[0]):
2608 if method == 'regular':
2609 relative_distance = i / (loop_length - 1)
2610 else:
2611 relative_distance = relative_lengths[i]
2613 loc1 = object.matrix_world * bm_mod.verts[v_index].co
2614 loc2, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2615 relative_distance, stroke_lengths_cache)
2616 total_distance += (loc2 - loc1).length
2618 return(total_distance)
2620 if ls_pairs:
2621 for (loop, stroke) in ls_pairs:
2622 total_dist = distance_loop_stroke(loop, stroke, object, bm_mod,
2623 method)
2624 loop[0].reverse()
2625 total_dist_rev = distance_loop_stroke(loop, stroke, object, bm_mod,
2626 method)
2627 if total_dist_rev > total_dist:
2628 loop[0].reverse()
2630 return(ls_pairs)
2633 # calculate vertex positions on stroke
2634 def gstretch_calculate_verts(loop, stroke, object, bm_mod, method):
2635 move = []
2636 stroke_lengths_cache = False
2637 loop_length = len(loop[0])
2638 matrix_inverse = object.matrix_world.inverted()
2640 # return intersection of line with stroke, or None
2641 def intersect_line_stroke(vec1, vec2, stroke):
2642 for i, p in enumerate(stroke.points[1:]):
2643 intersections = mathutils.geometry.intersect_line_line(vec1, vec2,
2644 p.co, stroke.points[i].co)
2645 if intersections and \
2646 (intersections[0] - intersections[1]).length < 1e-2:
2647 x, dist = mathutils.geometry.intersect_point_line(
2648 intersections[0], p.co, stroke.points[i].co)
2649 if -1 < dist < 1:
2650 return(intersections[0])
2651 return(None)
2653 if method == 'project':
2654 vert_edges = dict_vert_edges(bm_mod)
2656 for v_index in loop[0]:
2657 intersection = None
2658 for ek in vert_edges[v_index]:
2659 v1, v2 = ek
2660 v1 = bm_mod.verts[v1]
2661 v2 = bm_mod.verts[v2]
2662 if v1.select + v2.select == 1 and not v1.hide and not v2.hide:
2663 vec1 = object.matrix_world * v1.co
2664 vec2 = object.matrix_world * v2.co
2665 intersection = intersect_line_stroke(vec1, vec2, stroke)
2666 if intersection:
2667 break
2668 if not intersection:
2669 v = bm_mod.verts[v_index]
2670 intersection = intersect_line_stroke(v.co, v.co + v.normal,
2671 stroke)
2672 if intersection:
2673 move.append([v_index, matrix_inverse * intersection])
2675 else:
2676 if method == 'irregular':
2677 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2679 for i, v_index in enumerate(loop[0]):
2680 if method == 'regular':
2681 relative_distance = i / (loop_length - 1)
2682 else: # method == 'irregular'
2683 relative_distance = relative_lengths[i]
2684 loc, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2685 relative_distance, stroke_lengths_cache)
2686 loc = matrix_inverse * loc
2687 move.append([v_index, loc])
2689 return(move)
2692 # create new vertices, based on GP strokes
2693 def gstretch_create_verts(object, bm_mod, strokes, method, conversion,
2694 conversion_distance, conversion_max, conversion_min, conversion_vertices):
2695 move = []
2696 stroke_verts = []
2697 mat_world = object.matrix_world.inverted()
2698 singles = gstretch_match_single_verts(bm_mod, strokes, mat_world)
2700 for stroke in strokes:
2701 stroke_verts.append([stroke, []])
2702 min_end_point = 0
2703 if conversion == 'vertices':
2704 min_end_point = conversion_vertices
2705 end_point = conversion_vertices
2706 elif conversion == 'limit_vertices':
2707 min_end_point = conversion_min
2708 end_point = conversion_max
2709 else:
2710 end_point = len(stroke.points)
2711 # creation of new vertices at fixed user-defined distances
2712 if conversion == 'distance':
2713 method = 'project'
2714 prev_point = stroke.points[0]
2715 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * prev_point.co))
2716 distance = 0
2717 limit = conversion_distance
2718 for point in stroke.points:
2719 new_distance = distance + (point.co - prev_point.co).length
2720 iteration = 0
2721 while new_distance > limit:
2722 to_cover = limit - distance + (limit * iteration)
2723 new_loc = prev_point.co + to_cover * \
2724 (point.co - prev_point.co).normalized()
2725 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * new_loc))
2726 new_distance -= limit
2727 iteration += 1
2728 distance = new_distance
2729 prev_point = point
2730 # creation of new vertices for other methods
2731 else:
2732 # add vertices at stroke points
2733 for point in stroke.points[:end_point]:
2734 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * point.co))
2735 # add more vertices, beyond the points that are available
2736 if min_end_point > min(len(stroke.points), end_point):
2737 for i in range(min_end_point -
2738 (min(len(stroke.points), end_point))):
2739 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * point.co))
2740 # force even spreading of points, so they are placed on stroke
2741 method = 'regular'
2742 bm_mod.verts.ensure_lookup_table()
2743 bm_mod.verts.index_update()
2744 for stroke, verts_seq in stroke_verts:
2745 if len(verts_seq) < 2:
2746 continue
2747 # spread vertices evenly over the stroke
2748 if method == 'regular':
2749 loop = [[vert.index for vert in verts_seq], False]
2750 move += gstretch_calculate_verts(loop, stroke, object, bm_mod,
2751 method)
2752 # create edges
2753 for i, vert in enumerate(verts_seq):
2754 if i > 0:
2755 bm_mod.edges.new((verts_seq[i - 1], verts_seq[i]))
2756 vert.select = True
2757 # connect single vertices to the closest stroke
2758 if singles:
2759 for vert, m_stroke, point in singles:
2760 if m_stroke != stroke:
2761 continue
2762 bm_mod.edges.new((vert, verts_seq[point]))
2763 bm_mod.edges.ensure_lookup_table()
2764 bmesh.update_edit_mesh(object.data)
2766 return(move)
2769 # erases the grease pencil stroke
2770 def gstretch_erase_stroke(stroke, context):
2771 # change 3d coordinate into a stroke-point
2772 def sp(loc, context):
2773 lib = {'name': "",
2774 'pen_flip': False,
2775 'is_start': False,
2776 'location': (0, 0, 0),
2777 'mouse': (
2778 view3d_utils.location_3d_to_region_2d(
2779 context.region, context.space_data.region_3d, loc)
2781 'pressure': 1,
2782 'size': 0,
2783 'time': 0}
2784 return(lib)
2786 if type(stroke) != bpy.types.GPencilStroke:
2787 # fake stroke, there is nothing to delete
2788 return
2790 erase_stroke = [sp(p.co, context) for p in stroke.points]
2791 if erase_stroke:
2792 erase_stroke[0]['is_start'] = True
2793 bpy.ops.gpencil.draw(mode='ERASER', stroke=erase_stroke)
2796 # get point on stroke, given by relative distance (0.0 - 1.0)
2797 def gstretch_eval_stroke(stroke, distance, stroke_lengths_cache=False):
2798 # use cache if available
2799 if not stroke_lengths_cache:
2800 lengths = [0]
2801 for i, p in enumerate(stroke.points[1:]):
2802 lengths.append((p.co - stroke.points[i].co).length + lengths[-1])
2803 total_length = max(lengths[-1], 1e-7)
2804 stroke_lengths_cache = [length / total_length for length in
2805 lengths]
2806 stroke_lengths = stroke_lengths_cache[:]
2808 if distance in stroke_lengths:
2809 loc = stroke.points[stroke_lengths.index(distance)].co
2810 elif distance > stroke_lengths[-1]:
2811 # should be impossible, but better safe than sorry
2812 loc = stroke.points[-1].co
2813 else:
2814 stroke_lengths.append(distance)
2815 stroke_lengths.sort()
2816 stroke_index = stroke_lengths.index(distance)
2817 interval_length = stroke_lengths[
2818 stroke_index + 1] - stroke_lengths[stroke_index - 1
2820 distance_relative = (distance - stroke_lengths[stroke_index - 1]) / interval_length
2821 interval_vector = stroke.points[stroke_index].co - stroke.points[stroke_index - 1].co
2822 loc = stroke.points[stroke_index - 1].co + distance_relative * interval_vector
2824 return(loc, stroke_lengths_cache)
2827 # create fake grease pencil strokes for the active object
2828 def gstretch_get_fake_strokes(object, bm_mod, loops):
2829 strokes = []
2830 for loop in loops:
2831 p1 = object.matrix_world * bm_mod.verts[loop[0][0]].co
2832 p2 = object.matrix_world * bm_mod.verts[loop[0][-1]].co
2833 strokes.append(gstretch_fake_stroke([p1, p2]))
2835 return(strokes)
2838 # get grease pencil strokes for the active object
2839 def gstretch_get_strokes(object, context):
2840 gp = get_grease_pencil(object, context)
2841 if not gp:
2842 return(None)
2843 layer = gp.layers.active
2844 if not layer:
2845 return(None)
2846 frame = layer.active_frame
2847 if not frame:
2848 return(None)
2849 strokes = frame.strokes
2850 if len(strokes) < 1:
2851 return(None)
2853 return(strokes)
2856 # returns a list with loop-stroke pairs
2857 def gstretch_match_loops_strokes(loops, strokes, object, bm_mod):
2858 if not loops or not strokes:
2859 return(None)
2861 # calculate loop centers
2862 loop_centers = []
2863 bm_mod.verts.ensure_lookup_table()
2864 for loop in loops:
2865 center = mathutils.Vector()
2866 for v_index in loop[0]:
2867 center += bm_mod.verts[v_index].co
2868 center /= len(loop[0])
2869 center = object.matrix_world * center
2870 loop_centers.append([center, loop])
2872 # calculate stroke centers
2873 stroke_centers = []
2874 for stroke in strokes:
2875 center = mathutils.Vector()
2876 for p in stroke.points:
2877 center += p.co
2878 center /= len(stroke.points)
2879 stroke_centers.append([center, stroke, 0])
2881 # match, first by stroke use count, then by distance
2882 ls_pairs = []
2883 for lc in loop_centers:
2884 distances = []
2885 for i, sc in enumerate(stroke_centers):
2886 distances.append([sc[2], (lc[0] - sc[0]).length, i])
2887 distances.sort()
2888 best_stroke = distances[0][2]
2889 ls_pairs.append([lc[1], stroke_centers[best_stroke][1]])
2890 stroke_centers[best_stroke][2] += 1 # increase stroke use count
2892 return(ls_pairs)
2895 # match single selected vertices to the closest stroke endpoint
2896 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2897 def gstretch_match_single_verts(bm_mod, strokes, mat_world):
2898 # calculate stroke endpoints in object space
2899 endpoints = []
2900 for stroke in strokes:
2901 endpoints.append((mat_world * stroke.points[0].co, stroke, 0))
2902 endpoints.append((mat_world * stroke.points[-1].co, stroke, -1))
2904 distances = []
2905 # find single vertices (not connected to other selected verts)
2906 for vert in bm_mod.verts:
2907 if not vert.select:
2908 continue
2909 single = True
2910 for edge in vert.link_edges:
2911 if edge.other_vert(vert).select:
2912 single = False
2913 break
2914 if not single:
2915 continue
2916 # calculate distances from vertex to endpoints
2917 distance = [((vert.co - loc).length, vert, stroke, stroke_point,
2918 endpoint_index) for endpoint_index, (loc, stroke, stroke_point) in
2919 enumerate(endpoints)]
2920 distance.sort()
2921 distances.append(distance[0])
2923 # create matches, based on shortest distance first
2924 singles = []
2925 while distances:
2926 distances.sort()
2927 singles.append((distances[0][1], distances[0][2], distances[0][3]))
2928 endpoints.pop(distances[0][4])
2929 distances.pop(0)
2930 distances_new = []
2931 for (i, vert, j, k, l) in distances:
2932 distance_new = [((vert.co - loc).length, vert, stroke, stroke_point,
2933 endpoint_index) for endpoint_index, (loc, stroke,
2934 stroke_point) in enumerate(endpoints)]
2935 distance_new.sort()
2936 distances_new.append(distance_new[0])
2937 distances = distances_new
2939 return(singles)
2942 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2943 def gstretch_relative_lengths(loop, bm_mod):
2944 lengths = [0]
2945 for i, v_index in enumerate(loop[0][1:]):
2946 lengths.append(
2947 (bm_mod.verts[v_index].co -
2948 bm_mod.verts[loop[0][i]].co).length + lengths[-1]
2950 total_length = max(lengths[-1], 1e-7)
2951 relative_lengths = [length / total_length for length in
2952 lengths]
2954 return(relative_lengths)
2957 # convert cache-stored strokes into usable (fake) GP strokes
2958 def gstretch_safe_to_true_strokes(safe_strokes):
2959 strokes = []
2960 for safe_stroke in safe_strokes:
2961 strokes.append(gstretch_fake_stroke(safe_stroke))
2963 return(strokes)
2966 # convert a GP stroke into a list of points which can be stored in cache
2967 def gstretch_true_to_safe_strokes(strokes):
2968 safe_strokes = []
2969 for stroke in strokes:
2970 safe_strokes.append([p.co.copy() for p in stroke.points])
2972 return(safe_strokes)
2975 # force consistency in GUI, max value can never be lower than min value
2976 def gstretch_update_max(self, context):
2977 # called from operator settings (after execution)
2978 if 'conversion_min' in self.keys():
2979 if self.conversion_min > self.conversion_max:
2980 self.conversion_max = self.conversion_min
2981 # called from toolbar
2982 else:
2983 lt = context.window_manager.looptools
2984 if lt.gstretch_conversion_min > lt.gstretch_conversion_max:
2985 lt.gstretch_conversion_max = lt.gstretch_conversion_min
2988 # force consistency in GUI, min value can never be higher than max value
2989 def gstretch_update_min(self, context):
2990 # called from operator settings (after execution)
2991 if 'conversion_max' in self.keys():
2992 if self.conversion_max < self.conversion_min:
2993 self.conversion_min = self.conversion_max
2994 # called from toolbar
2995 else:
2996 lt = context.window_manager.looptools
2997 if lt.gstretch_conversion_max < lt.gstretch_conversion_min:
2998 lt.gstretch_conversion_min = lt.gstretch_conversion_max
3001 # ########################################
3002 # ##### Relax functions ##################
3003 # ########################################
3005 # create lists with knots and points, all correctly sorted
3006 def relax_calculate_knots(loops):
3007 all_knots = []
3008 all_points = []
3009 for loop, circular in loops:
3010 knots = [[], []]
3011 points = [[], []]
3012 if circular:
3013 if len(loop) % 2 == 1: # odd
3014 extend = [False, True, 0, 1, 0, 1]
3015 else: # even
3016 extend = [True, False, 0, 1, 1, 2]
3017 else:
3018 if len(loop) % 2 == 1: # odd
3019 extend = [False, False, 0, 1, 1, 2]
3020 else: # even
3021 extend = [False, False, 0, 1, 1, 2]
3022 for j in range(2):
3023 if extend[j]:
3024 loop = [loop[-1]] + loop + [loop[0]]
3025 for i in range(extend[2 + 2 * j], len(loop), 2):
3026 knots[j].append(loop[i])
3027 for i in range(extend[3 + 2 * j], len(loop), 2):
3028 if loop[i] == loop[-1] and not circular:
3029 continue
3030 if len(points[j]) == 0:
3031 points[j].append(loop[i])
3032 elif loop[i] != points[j][0]:
3033 points[j].append(loop[i])
3034 if circular:
3035 if knots[j][0] != knots[j][-1]:
3036 knots[j].append(knots[j][0])
3037 if len(points[1]) == 0:
3038 knots.pop(1)
3039 points.pop(1)
3040 for k in knots:
3041 all_knots.append(k)
3042 for p in points:
3043 all_points.append(p)
3045 return(all_knots, all_points)
3048 # calculate relative positions compared to first knot
3049 def relax_calculate_t(bm_mod, knots, points, regular):
3050 all_tknots = []
3051 all_tpoints = []
3052 for i in range(len(knots)):
3053 amount = len(knots[i]) + len(points[i])
3054 mix = []
3055 for j in range(amount):
3056 if j % 2 == 0:
3057 mix.append([True, knots[i][round(j / 2)]])
3058 elif j == amount - 1:
3059 mix.append([True, knots[i][-1]])
3060 else:
3061 mix.append([False, points[i][int(j / 2)]])
3062 len_total = 0
3063 loc_prev = False
3064 tknots = []
3065 tpoints = []
3066 for m in mix:
3067 loc = mathutils.Vector(bm_mod.verts[m[1]].co[:])
3068 if not loc_prev:
3069 loc_prev = loc
3070 len_total += (loc - loc_prev).length
3071 if m[0]:
3072 tknots.append(len_total)
3073 else:
3074 tpoints.append(len_total)
3075 loc_prev = loc
3076 if regular:
3077 tpoints = []
3078 for p in range(len(points[i])):
3079 tpoints.append((tknots[p] + tknots[p + 1]) / 2)
3080 all_tknots.append(tknots)
3081 all_tpoints.append(tpoints)
3083 return(all_tknots, all_tpoints)
3086 # change the location of the points to their place on the spline
3087 def relax_calculate_verts(bm_mod, interpolation, tknots, knots, tpoints,
3088 points, splines):
3089 change = []
3090 move = []
3091 for i in range(len(knots)):
3092 for p in points[i]:
3093 m = tpoints[i][points[i].index(p)]
3094 if m in tknots[i]:
3095 n = tknots[i].index(m)
3096 else:
3097 t = tknots[i][:]
3098 t.append(m)
3099 t.sort()
3100 n = t.index(m) - 1
3101 if n > len(splines[i]) - 1:
3102 n = len(splines[i]) - 1
3103 elif n < 0:
3104 n = 0
3106 if interpolation == 'cubic':
3107 ax, bx, cx, dx, tx = splines[i][n][0]
3108 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
3109 ay, by, cy, dy, ty = splines[i][n][1]
3110 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
3111 az, bz, cz, dz, tz = splines[i][n][2]
3112 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
3113 change.append([p, mathutils.Vector([x, y, z])])
3114 else: # interpolation == 'linear'
3115 a, d, t, u = splines[i][n]
3116 if u == 0:
3117 u = 1e-8
3118 change.append([p, ((m - t) / u) * d + a])
3119 for c in change:
3120 move.append([c[0], (bm_mod.verts[c[0]].co + c[1]) / 2])
3122 return(move)
3125 # ########################################
3126 # ##### Space functions ##################
3127 # ########################################
3129 # calculate relative positions compared to first knot
3130 def space_calculate_t(bm_mod, knots):
3131 tknots = []
3132 loc_prev = False
3133 len_total = 0
3134 for k in knots:
3135 loc = mathutils.Vector(bm_mod.verts[k].co[:])
3136 if not loc_prev:
3137 loc_prev = loc
3138 len_total += (loc - loc_prev).length
3139 tknots.append(len_total)
3140 loc_prev = loc
3141 amount = len(knots)
3142 t_per_segment = len_total / (amount - 1)
3143 tpoints = [i * t_per_segment for i in range(amount)]
3145 return(tknots, tpoints)
3148 # change the location of the points to their place on the spline
3149 def space_calculate_verts(bm_mod, interpolation, tknots, tpoints, points,
3150 splines):
3151 move = []
3152 for p in points:
3153 m = tpoints[points.index(p)]
3154 if m in tknots:
3155 n = tknots.index(m)
3156 else:
3157 t = tknots[:]
3158 t.append(m)
3159 t.sort()
3160 n = t.index(m) - 1
3161 if n > len(splines) - 1:
3162 n = len(splines) - 1
3163 elif n < 0:
3164 n = 0
3166 if interpolation == 'cubic':
3167 ax, bx, cx, dx, tx = splines[n][0]
3168 x = ax + bx * (m - tx) + cx * (m - tx) ** 2 + dx * (m - tx) ** 3
3169 ay, by, cy, dy, ty = splines[n][1]
3170 y = ay + by * (m - ty) + cy * (m - ty) ** 2 + dy * (m - ty) ** 3
3171 az, bz, cz, dz, tz = splines[n][2]
3172 z = az + bz * (m - tz) + cz * (m - tz) ** 2 + dz * (m - tz) ** 3
3173 move.append([p, mathutils.Vector([x, y, z])])
3174 else: # interpolation == 'linear'
3175 a, d, t, u = splines[n]
3176 move.append([p, ((m - t) / u) * d + a])
3178 return(move)
3181 # ########################################
3182 # ##### Operators ########################
3183 # ########################################
3185 # bridge operator
3186 class Bridge(Operator):
3187 bl_idname = 'mesh.looptools_bridge'
3188 bl_label = "Bridge / Loft"
3189 bl_description = "Bridge two, or loft several, loops of vertices"
3190 bl_options = {'REGISTER', 'UNDO'}
3192 cubic_strength = FloatProperty(
3193 name="Strength",
3194 description="Higher strength results in more fluid curves",
3195 default=1.0,
3196 soft_min=-3.0,
3197 soft_max=3.0
3199 interpolation = EnumProperty(
3200 name="Interpolation mode",
3201 items=(('cubic', "Cubic", "Gives curved results"),
3202 ('linear', "Linear", "Basic, fast, straight interpolation")),
3203 description="Interpolation mode: algorithm used when creating "
3204 "segments",
3205 default='cubic'
3207 loft = BoolProperty(
3208 name="Loft",
3209 description="Loft multiple loops, instead of considering them as "
3210 "a multi-input for bridging",
3211 default=False
3213 loft_loop = BoolProperty(
3214 name="Loop",
3215 description="Connect the first and the last loop with each other",
3216 default=False
3218 min_width = IntProperty(
3219 name="Minimum width",
3220 description="Segments with an edge smaller than this are merged "
3221 "(compared to base edge)",
3222 default=0,
3223 min=0,
3224 max=100,
3225 subtype='PERCENTAGE'
3227 mode = EnumProperty(
3228 name="Mode",
3229 items=(('basic', "Basic", "Fast algorithm"),
3230 ('shortest', "Shortest edge", "Slower algorithm with better vertex matching")),
3231 description="Algorithm used for bridging",
3232 default='shortest'
3234 remove_faces = BoolProperty(
3235 name="Remove faces",
3236 description="Remove faces that are internal after bridging",
3237 default=True
3239 reverse = BoolProperty(
3240 name="Reverse",
3241 description="Manually override the direction in which the loops "
3242 "are bridged. Only use if the tool gives the wrong result",
3243 default=False
3245 segments = IntProperty(
3246 name="Segments",
3247 description="Number of segments used to bridge the gap (0=automatic)",
3248 default=1,
3249 min=0,
3250 soft_max=20
3252 twist = IntProperty(
3253 name="Twist",
3254 description="Twist what vertices are connected to each other",
3255 default=0
3258 @classmethod
3259 def poll(cls, context):
3260 ob = context.active_object
3261 return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3263 def draw(self, context):
3264 layout = self.layout
3265 # layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3267 # top row
3268 col_top = layout.column(align=True)
3269 row = col_top.row(align=True)
3270 col_left = row.column(align=True)
3271 col_right = row.column(align=True)
3272 col_right.active = self.segments != 1
3273 col_left.prop(self, "segments")
3274 col_right.prop(self, "min_width", text="")
3275 # bottom row
3276 bottom_left = col_left.row()
3277 bottom_left.active = self.segments != 1
3278 bottom_left.prop(self, "interpolation", text="")
3279 bottom_right = col_right.row()
3280 bottom_right.active = self.interpolation == 'cubic'
3281 bottom_right.prop(self, "cubic_strength")
3282 # boolean properties
3283 col_top.prop(self, "remove_faces")
3284 if self.loft:
3285 col_top.prop(self, "loft_loop")
3287 # override properties
3288 col_top.separator()
3289 row = layout.row(align=True)
3290 row.prop(self, "twist")
3291 row.prop(self, "reverse")
3293 def invoke(self, context, event):
3294 # load custom settings
3295 context.window_manager.looptools.bridge_loft = self.loft
3296 settings_load(self)
3297 return self.execute(context)
3299 def execute(self, context):
3300 # initialise
3301 global_undo, object, bm = initialise()
3302 edge_faces, edgekey_to_edge, old_selected_faces, smooth = \
3303 bridge_initialise(bm, self.interpolation)
3304 settings_write(self)
3306 # check cache to see if we can save time
3307 input_method = bridge_input_method(self.loft, self.loft_loop)
3308 cached, single_loops, loops, derived, mapping = cache_read("Bridge",
3309 object, bm, input_method, False)
3310 if not cached:
3311 # get loops
3312 loops = bridge_get_input(bm)
3313 if loops:
3314 # reorder loops if there are more than 2
3315 if len(loops) > 2:
3316 if self.loft:
3317 loops = bridge_sort_loops(bm, loops, self.loft_loop)
3318 else:
3319 loops = bridge_match_loops(bm, loops)
3321 # saving cache for faster execution next time
3322 if not cached:
3323 cache_write("Bridge", object, bm, input_method, False, False,
3324 loops, False, False)
3326 if loops:
3327 # calculate new geometry
3328 vertices = []
3329 faces = []
3330 max_vert_index = len(bm.verts) - 1
3331 for i in range(1, len(loops)):
3332 if not self.loft and i % 2 == 0:
3333 continue
3334 lines = bridge_calculate_lines(bm, loops[i - 1:i + 1],
3335 self.mode, self.twist, self.reverse)
3336 vertex_normals = bridge_calculate_virtual_vertex_normals(bm,
3337 lines, loops[i - 1:i + 1], edge_faces, edgekey_to_edge)
3338 segments = bridge_calculate_segments(bm, lines,
3339 loops[i - 1:i + 1], self.segments)
3340 new_verts, new_faces, max_vert_index = \
3341 bridge_calculate_geometry(
3342 bm, lines, vertex_normals,
3343 segments, self.interpolation, self.cubic_strength,
3344 self.min_width, max_vert_index
3346 if new_verts:
3347 vertices += new_verts
3348 if new_faces:
3349 faces += new_faces
3350 # make sure faces in loops that aren't used, aren't removed
3351 if self.remove_faces and old_selected_faces:
3352 bridge_save_unused_faces(bm, old_selected_faces, loops)
3353 # create vertices
3354 if vertices:
3355 bridge_create_vertices(bm, vertices)
3356 # create faces
3357 if faces:
3358 new_faces = bridge_create_faces(object, bm, faces, self.twist)
3359 old_selected_faces = [
3360 i for i, face in enumerate(bm.faces) if face.index in old_selected_faces
3361 ] # updating list
3362 bridge_select_new_faces(new_faces, smooth)
3363 # edge-data could have changed, can't use cache next run
3364 if faces and not vertices:
3365 cache_delete("Bridge")
3366 # delete internal faces
3367 if self.remove_faces and old_selected_faces:
3368 bridge_remove_internal_faces(bm, old_selected_faces)
3369 # make sure normals are facing outside
3370 bmesh.update_edit_mesh(object.data, tessface=False,
3371 destructive=True)
3372 bpy.ops.mesh.normals_make_consistent()
3374 # cleaning up
3375 terminate(global_undo)
3377 return{'FINISHED'}
3380 # circle operator
3381 class Circle(Operator):
3382 bl_idname = "mesh.looptools_circle"
3383 bl_label = "Circle"
3384 bl_description = "Move selected vertices into a circle shape"
3385 bl_options = {'REGISTER', 'UNDO'}
3387 custom_radius = BoolProperty(
3388 name="Radius",
3389 description="Force a custom radius",
3390 default=False
3392 fit = EnumProperty(
3393 name="Method",
3394 items=(("best", "Best fit", "Non-linear least squares"),
3395 ("inside", "Fit inside", "Only move vertices towards the center")),
3396 description="Method used for fitting a circle to the vertices",
3397 default='best'
3399 flatten = BoolProperty(
3400 name="Flatten",
3401 description="Flatten the circle, instead of projecting it on the mesh",
3402 default=True
3404 influence = FloatProperty(
3405 name="Influence",
3406 description="Force of the tool",
3407 default=100.0,
3408 min=0.0,
3409 max=100.0,
3410 precision=1,
3411 subtype='PERCENTAGE'
3413 lock_x = BoolProperty(
3414 name="Lock X",
3415 description="Lock editing of the x-coordinate",
3416 default=False
3418 lock_y = BoolProperty(
3419 name="Lock Y",
3420 description="Lock editing of the y-coordinate",
3421 default=False
3423 lock_z = BoolProperty(name="Lock Z",
3424 description="Lock editing of the z-coordinate",
3425 default=False
3427 radius = FloatProperty(
3428 name="Radius",
3429 description="Custom radius for circle",
3430 default=1.0,
3431 min=0.0,
3432 soft_max=1000.0
3434 regular = BoolProperty(
3435 name="Regular",
3436 description="Distribute vertices at constant distances along the circle",
3437 default=True
3440 @classmethod
3441 def poll(cls, context):
3442 ob = context.active_object
3443 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3445 def draw(self, context):
3446 layout = self.layout
3447 col = layout.column()
3449 col.prop(self, "fit")
3450 col.separator()
3452 col.prop(self, "flatten")
3453 row = col.row(align=True)
3454 row.prop(self, "custom_radius")
3455 row_right = row.row(align=True)
3456 row_right.active = self.custom_radius
3457 row_right.prop(self, "radius", text="")
3458 col.prop(self, "regular")
3459 col.separator()
3461 col_move = col.column(align=True)
3462 row = col_move.row(align=True)
3463 if self.lock_x:
3464 row.prop(self, "lock_x", text="X", icon='LOCKED')
3465 else:
3466 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3467 if self.lock_y:
3468 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3469 else:
3470 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3471 if self.lock_z:
3472 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3473 else:
3474 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3475 col_move.prop(self, "influence")
3477 def invoke(self, context, event):
3478 # load custom settings
3479 settings_load(self)
3480 return self.execute(context)
3482 def execute(self, context):
3483 # initialise
3484 global_undo, object, bm = initialise()
3485 settings_write(self)
3486 # check cache to see if we can save time
3487 cached, single_loops, loops, derived, mapping = cache_read("Circle",
3488 object, bm, False, False)
3489 if cached:
3490 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
3491 else:
3492 # find loops
3493 derived, bm_mod, single_vertices, single_loops, loops = \
3494 circle_get_input(object, bm, context.scene)
3495 mapping = get_mapping(derived, bm, bm_mod, single_vertices,
3496 False, loops)
3497 single_loops, loops = circle_check_loops(single_loops, loops,
3498 mapping, bm_mod)
3500 # saving cache for faster execution next time
3501 if not cached:
3502 cache_write("Circle", object, bm, False, False, single_loops,
3503 loops, derived, mapping)
3505 move = []
3506 for i, loop in enumerate(loops):
3507 # best fitting flat plane
3508 com, normal = calculate_plane(bm_mod, loop)
3509 # if circular, shift loop so we get a good starting vertex
3510 if loop[1]:
3511 loop = circle_shift_loop(bm_mod, loop, com)
3512 # flatten vertices on plane
3513 locs_2d, p, q = circle_3d_to_2d(bm_mod, loop, com, normal)
3514 # calculate circle
3515 if self.fit == 'best':
3516 x0, y0, r = circle_calculate_best_fit(locs_2d)
3517 else: # self.fit == 'inside'
3518 x0, y0, r = circle_calculate_min_fit(locs_2d)
3519 # radius override
3520 if self.custom_radius:
3521 r = self.radius / p.length
3522 # calculate positions on circle
3523 if self.regular:
3524 new_locs_2d = circle_project_regular(locs_2d[:], x0, y0, r)
3525 else:
3526 new_locs_2d = circle_project_non_regular(locs_2d[:], x0, y0, r)
3527 # take influence into account
3528 locs_2d = circle_influence_locs(locs_2d, new_locs_2d,
3529 self.influence)
3530 # calculate 3d positions of the created 2d input
3531 move.append(circle_calculate_verts(self.flatten, bm_mod,
3532 locs_2d, com, p, q, normal))
3533 # flatten single input vertices on plane defined by loop
3534 if self.flatten and single_loops:
3535 move.append(circle_flatten_singles(bm_mod, com, p, q,
3536 normal, single_loops[i]))
3538 # move vertices to new locations
3539 if self.lock_x or self.lock_y or self.lock_z:
3540 lock = [self.lock_x, self.lock_y, self.lock_z]
3541 else:
3542 lock = False
3543 move_verts(object, bm, mapping, move, lock, -1)
3545 # cleaning up
3546 if derived:
3547 bm_mod.free()
3548 terminate(global_undo)
3550 return{'FINISHED'}
3553 # curve operator
3554 class Curve(Operator):
3555 bl_idname = "mesh.looptools_curve"
3556 bl_label = "Curve"
3557 bl_description = "Turn a loop into a smooth curve"
3558 bl_options = {'REGISTER', 'UNDO'}
3560 boundaries = BoolProperty(
3561 name="Boundaries",
3562 description="Limit the tool to work within the boundaries of the selected vertices",
3563 default=False
3565 influence = FloatProperty(
3566 name="Influence",
3567 description="Force of the tool",
3568 default=100.0,
3569 min=0.0,
3570 max=100.0,
3571 precision=1,
3572 subtype='PERCENTAGE'
3574 interpolation = EnumProperty(
3575 name="Interpolation",
3576 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
3577 ("linear", "Linear", "Simple and fast linear algorithm")),
3578 description="Algorithm used for interpolation",
3579 default='cubic'
3581 lock_x = BoolProperty(
3582 name="Lock X",
3583 description="Lock editing of the x-coordinate",
3584 default=False
3586 lock_y = BoolProperty(
3587 name="Lock Y",
3588 description="Lock editing of the y-coordinate",
3589 default=False
3591 lock_z = BoolProperty(
3592 name="Lock Z",
3593 description="Lock editing of the z-coordinate",
3594 default=False
3596 regular = BoolProperty(
3597 name="Regular",
3598 description="Distribute vertices at constant distances along the curve",
3599 default=True
3601 restriction = EnumProperty(
3602 name="Restriction",
3603 items=(("none", "None", "No restrictions on vertex movement"),
3604 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
3605 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
3606 description="Restrictions on how the vertices can be moved",
3607 default='none'
3610 @classmethod
3611 def poll(cls, context):
3612 ob = context.active_object
3613 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3615 def draw(self, context):
3616 layout = self.layout
3617 col = layout.column()
3619 col.prop(self, "interpolation")
3620 col.prop(self, "restriction")
3621 col.prop(self, "boundaries")
3622 col.prop(self, "regular")
3623 col.separator()
3625 col_move = col.column(align=True)
3626 row = col_move.row(align=True)
3627 if self.lock_x:
3628 row.prop(self, "lock_x", text="X", icon='LOCKED')
3629 else:
3630 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
3631 if self.lock_y:
3632 row.prop(self, "lock_y", text="Y", icon='LOCKED')
3633 else:
3634 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
3635 if self.lock_z:
3636 row.prop(self, "lock_z", text="Z", icon='LOCKED')
3637 else:
3638 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
3639 col_move.prop(self, "influence")
3641 def invoke(self, context, event):
3642 # load custom settings
3643 settings_load(self)
3644 return self.execute(context)
3646 def execute(self, context):
3647 # initialise
3648 global_undo, object, bm = initialise()
3649 settings_write(self)
3650 # check cache to see if we can save time
3651 cached, single_loops, loops, derived, mapping = cache_read("Curve",
3652 object, bm, False, self.boundaries)
3653 if cached:
3654 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
3655 else:
3656 # find loops
3657 derived, bm_mod, loops = curve_get_input(object, bm,
3658 self.boundaries, context.scene)
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, context.scene)
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,
4019 context.scene, input='selected')
4020 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4021 loops = check_loops(loops, mapping, bm_mod)
4022 # get strokes
4023 strokes = gstretch_get_strokes(object, context)
4024 else:
4025 # straightening function (no GP) -> loops ignore modifiers
4026 derived = False
4027 mapping = False
4028 bm_mod = bm.copy()
4029 bm_mod.verts.ensure_lookup_table()
4030 bm_mod.edges.ensure_lookup_table()
4031 bm_mod.faces.ensure_lookup_table()
4032 edge_keys = [
4033 edgekey(edge) for edge in bm_mod.edges if
4034 edge.select and not edge.hide
4036 loops = get_connected_selections(edge_keys)
4037 loops = check_loops(loops, mapping, bm_mod)
4038 # create fake strokes
4039 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
4041 # saving cache for faster execution next time
4042 if not cached:
4043 if strokes:
4044 safe_strokes = gstretch_true_to_safe_strokes(strokes)
4045 else:
4046 safe_strokes = []
4047 cache_write("Gstretch", object, bm, False, False,
4048 safe_strokes, loops, derived, mapping)
4050 # pair loops and strokes
4051 ls_pairs = gstretch_match_loops_strokes(loops, strokes, object, bm_mod)
4052 ls_pairs = gstretch_align_pairs(ls_pairs, object, bm_mod, self.method)
4054 move = []
4055 if not loops:
4056 # no selected geometry, convert GP to verts
4057 if strokes:
4058 move.append(gstretch_create_verts(object, bm, strokes,
4059 self.method, self.conversion, self.conversion_distance,
4060 self.conversion_max, self.conversion_min,
4061 self.conversion_vertices))
4062 for stroke in strokes:
4063 gstretch_erase_stroke(stroke, context)
4064 elif ls_pairs:
4065 for (loop, stroke) in ls_pairs:
4066 move.append(gstretch_calculate_verts(loop, stroke, object,
4067 bm_mod, self.method))
4068 if self.delete_strokes:
4069 if type(stroke) != bpy.types.GPencilStroke:
4070 # in case of cached fake stroke, get the real one
4071 if get_grease_pencil(object, context):
4072 strokes = gstretch_get_strokes(object, context)
4073 if loops and strokes:
4074 ls_pairs = gstretch_match_loops_strokes(loops,
4075 strokes, object, bm_mod)
4076 ls_pairs = gstretch_align_pairs(ls_pairs,
4077 object, bm_mod, self.method)
4078 for (l, s) in ls_pairs:
4079 if l == loop:
4080 stroke = s
4081 break
4082 gstretch_erase_stroke(stroke, context)
4084 # move vertices to new locations
4085 if self.lock_x or self.lock_y or self.lock_z:
4086 lock = [self.lock_x, self.lock_y, self.lock_z]
4087 else:
4088 lock = False
4089 bmesh.update_edit_mesh(object.data, tessface=True, destructive=True)
4090 move_verts(object, bm, mapping, move, lock, self.influence)
4092 # cleaning up
4093 if derived:
4094 bm_mod.free()
4095 terminate(global_undo)
4097 return{'FINISHED'}
4100 # relax operator
4101 class Relax(Operator):
4102 bl_idname = "mesh.looptools_relax"
4103 bl_label = "Relax"
4104 bl_description = "Relax the loop, so it is smoother"
4105 bl_options = {'REGISTER', 'UNDO'}
4107 input = EnumProperty(
4108 name="Input",
4109 items=(("all", "Parallel (all)", "Also use non-selected "
4110 "parallel loops as input"),
4111 ("selected", "Selection", "Only use selected vertices as input")),
4112 description="Loops that are relaxed",
4113 default='selected'
4115 interpolation = EnumProperty(
4116 name="Interpolation",
4117 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4118 ("linear", "Linear", "Simple and fast linear algorithm")),
4119 description="Algorithm used for interpolation",
4120 default='cubic'
4122 iterations = EnumProperty(
4123 name="Iterations",
4124 items=(("1", "1", "One"),
4125 ("3", "3", "Three"),
4126 ("5", "5", "Five"),
4127 ("10", "10", "Ten"),
4128 ("25", "25", "Twenty-five")),
4129 description="Number of times the loop is relaxed",
4130 default="1"
4132 regular = BoolProperty(
4133 name="Regular",
4134 description="Distribute vertices at constant distances along the loop",
4135 default=True
4138 @classmethod
4139 def poll(cls, context):
4140 ob = context.active_object
4141 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4143 def draw(self, context):
4144 layout = self.layout
4145 col = layout.column()
4147 col.prop(self, "interpolation")
4148 col.prop(self, "input")
4149 col.prop(self, "iterations")
4150 col.prop(self, "regular")
4152 def invoke(self, context, event):
4153 # load custom settings
4154 settings_load(self)
4155 return self.execute(context)
4157 def execute(self, context):
4158 # initialise
4159 global_undo, object, bm = initialise()
4160 settings_write(self)
4161 # check cache to see if we can save time
4162 cached, single_loops, loops, derived, mapping = cache_read("Relax",
4163 object, bm, self.input, False)
4164 if cached:
4165 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
4166 else:
4167 # find loops
4168 derived, bm_mod, loops = get_connected_input(object, bm,
4169 context.scene, self.input)
4170 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4171 loops = check_loops(loops, mapping, bm_mod)
4172 knots, points = relax_calculate_knots(loops)
4174 # saving cache for faster execution next time
4175 if not cached:
4176 cache_write("Relax", object, bm, self.input, False, False, loops,
4177 derived, mapping)
4179 for iteration in range(int(self.iterations)):
4180 # calculate splines and new positions
4181 tknots, tpoints = relax_calculate_t(bm_mod, knots, points,
4182 self.regular)
4183 splines = []
4184 for i in range(len(knots)):
4185 splines.append(calculate_splines(self.interpolation, bm_mod,
4186 tknots[i], knots[i]))
4187 move = [relax_calculate_verts(bm_mod, self.interpolation,
4188 tknots, knots, tpoints, points, splines)]
4189 move_verts(object, bm, mapping, move, False, -1)
4191 # cleaning up
4192 if derived:
4193 bm_mod.free()
4194 terminate(global_undo)
4196 return{'FINISHED'}
4199 # space operator
4200 class Space(Operator):
4201 bl_idname = "mesh.looptools_space"
4202 bl_label = "Space"
4203 bl_description = "Space the vertices in a regular distrubtion on the loop"
4204 bl_options = {'REGISTER', 'UNDO'}
4206 influence = FloatProperty(
4207 name="Influence",
4208 description="Force of the tool",
4209 default=100.0,
4210 min=0.0,
4211 max=100.0,
4212 precision=1,
4213 subtype='PERCENTAGE'
4215 input = EnumProperty(
4216 name="Input",
4217 items=(("all", "Parallel (all)", "Also use non-selected "
4218 "parallel loops as input"),
4219 ("selected", "Selection", "Only use selected vertices as input")),
4220 description="Loops that are spaced",
4221 default='selected'
4223 interpolation = EnumProperty(
4224 name="Interpolation",
4225 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4226 ("linear", "Linear", "Vertices are projected on existing edges")),
4227 description="Algorithm used for interpolation",
4228 default='cubic'
4230 lock_x = BoolProperty(
4231 name="Lock X",
4232 description="Lock editing of the x-coordinate",
4233 default=False
4235 lock_y = BoolProperty(
4236 name="Lock Y",
4237 description="Lock editing of the y-coordinate",
4238 default=False
4240 lock_z = BoolProperty(
4241 name="Lock Z",
4242 description="Lock editing of the z-coordinate",
4243 default=False
4246 @classmethod
4247 def poll(cls, context):
4248 ob = context.active_object
4249 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4251 def draw(self, context):
4252 layout = self.layout
4253 col = layout.column()
4255 col.prop(self, "interpolation")
4256 col.prop(self, "input")
4257 col.separator()
4259 col_move = col.column(align=True)
4260 row = col_move.row(align=True)
4261 if self.lock_x:
4262 row.prop(self, "lock_x", text="X", icon='LOCKED')
4263 else:
4264 row.prop(self, "lock_x", text="X", icon='UNLOCKED')
4265 if self.lock_y:
4266 row.prop(self, "lock_y", text="Y", icon='LOCKED')
4267 else:
4268 row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
4269 if self.lock_z:
4270 row.prop(self, "lock_z", text="Z", icon='LOCKED')
4271 else:
4272 row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
4273 col_move.prop(self, "influence")
4275 def invoke(self, context, event):
4276 # load custom settings
4277 settings_load(self)
4278 return self.execute(context)
4280 def execute(self, context):
4281 # initialise
4282 global_undo, object, bm = initialise()
4283 settings_write(self)
4284 # check cache to see if we can save time
4285 cached, single_loops, loops, derived, mapping = cache_read("Space",
4286 object, bm, self.input, False)
4287 if cached:
4288 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
4289 else:
4290 # find loops
4291 derived, bm_mod, loops = get_connected_input(object, bm,
4292 context.scene, self.input)
4293 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4294 loops = check_loops(loops, mapping, bm_mod)
4296 # saving cache for faster execution next time
4297 if not cached:
4298 cache_write("Space", object, bm, self.input, False, False, loops,
4299 derived, mapping)
4301 move = []
4302 for loop in loops:
4303 # calculate splines and new positions
4304 if loop[1]: # circular
4305 loop[0].append(loop[0][0])
4306 tknots, tpoints = space_calculate_t(bm_mod, loop[0][:])
4307 splines = calculate_splines(self.interpolation, bm_mod,
4308 tknots, loop[0][:])
4309 move.append(space_calculate_verts(bm_mod, self.interpolation,
4310 tknots, tpoints, loop[0][:-1], splines))
4311 # move vertices to new locations
4312 if self.lock_x or self.lock_y or self.lock_z:
4313 lock = [self.lock_x, self.lock_y, self.lock_z]
4314 else:
4315 lock = False
4316 move_verts(object, bm, mapping, move, lock, self.influence)
4318 # cleaning up
4319 if derived:
4320 bm_mod.free()
4321 terminate(global_undo)
4323 return{'FINISHED'}
4326 # ########################################
4327 # ##### GUI and registration #############
4328 # ########################################
4330 # menu containing all tools
4331 class VIEW3D_MT_edit_mesh_looptools(Menu):
4332 bl_label = "LoopTools"
4334 def draw(self, context):
4335 layout = self.layout
4337 layout.operator("mesh.looptools_bridge", text="Bridge").loft = False
4338 layout.operator("mesh.looptools_circle")
4339 layout.operator("mesh.looptools_curve")
4340 layout.operator("mesh.looptools_flatten")
4341 layout.operator("mesh.looptools_gstretch")
4342 layout.operator("mesh.looptools_bridge", text="Loft").loft = True
4343 layout.operator("mesh.looptools_relax")
4344 layout.operator("mesh.looptools_space")
4347 # panel containing all tools
4348 class VIEW3D_PT_tools_looptools(Panel):
4349 bl_space_type = 'VIEW_3D'
4350 bl_region_type = 'TOOLS'
4351 bl_category = 'Tools'
4352 bl_context = "mesh_edit"
4353 bl_label = "LoopTools"
4354 bl_options = {'DEFAULT_CLOSED'}
4356 def draw(self, context):
4357 layout = self.layout
4358 col = layout.column(align=True)
4359 lt = context.window_manager.looptools
4361 # bridge - first line
4362 split = col.split(percentage=0.15, align=True)
4363 if lt.display_bridge:
4364 split.prop(lt, "display_bridge", text="", icon='DOWNARROW_HLT')
4365 else:
4366 split.prop(lt, "display_bridge", text="", icon='RIGHTARROW')
4367 split.operator("mesh.looptools_bridge", text="Bridge").loft = False
4368 # bridge - settings
4369 if lt.display_bridge:
4370 box = col.column(align=True).box().column()
4371 # box.prop(self, "mode")
4373 # top row
4374 col_top = box.column(align=True)
4375 row = col_top.row(align=True)
4376 col_left = row.column(align=True)
4377 col_right = row.column(align=True)
4378 col_right.active = lt.bridge_segments != 1
4379 col_left.prop(lt, "bridge_segments")
4380 col_right.prop(lt, "bridge_min_width", text="")
4381 # bottom row
4382 bottom_left = col_left.row()
4383 bottom_left.active = lt.bridge_segments != 1
4384 bottom_left.prop(lt, "bridge_interpolation", text="")
4385 bottom_right = col_right.row()
4386 bottom_right.active = lt.bridge_interpolation == 'cubic'
4387 bottom_right.prop(lt, "bridge_cubic_strength")
4388 # boolean properties
4389 col_top.prop(lt, "bridge_remove_faces")
4391 # override properties
4392 col_top.separator()
4393 row = box.row(align=True)
4394 row.prop(lt, "bridge_twist")
4395 row.prop(lt, "bridge_reverse")
4397 # circle - first line
4398 split = col.split(percentage=0.15, align=True)
4399 if lt.display_circle:
4400 split.prop(lt, "display_circle", text="", icon='DOWNARROW_HLT')
4401 else:
4402 split.prop(lt, "display_circle", text="", icon='RIGHTARROW')
4403 split.operator("mesh.looptools_circle")
4404 # circle - settings
4405 if lt.display_circle:
4406 box = col.column(align=True).box().column()
4407 box.prop(lt, "circle_fit")
4408 box.separator()
4410 box.prop(lt, "circle_flatten")
4411 row = box.row(align=True)
4412 row.prop(lt, "circle_custom_radius")
4413 row_right = row.row(align=True)
4414 row_right.active = lt.circle_custom_radius
4415 row_right.prop(lt, "circle_radius", text="")
4416 box.prop(lt, "circle_regular")
4417 box.separator()
4419 col_move = box.column(align=True)
4420 row = col_move.row(align=True)
4421 if lt.circle_lock_x:
4422 row.prop(lt, "circle_lock_x", text="X", icon='LOCKED')
4423 else:
4424 row.prop(lt, "circle_lock_x", text="X", icon='UNLOCKED')
4425 if lt.circle_lock_y:
4426 row.prop(lt, "circle_lock_y", text="Y", icon='LOCKED')
4427 else:
4428 row.prop(lt, "circle_lock_y", text="Y", icon='UNLOCKED')
4429 if lt.circle_lock_z:
4430 row.prop(lt, "circle_lock_z", text="Z", icon='LOCKED')
4431 else:
4432 row.prop(lt, "circle_lock_z", text="Z", icon='UNLOCKED')
4433 col_move.prop(lt, "circle_influence")
4435 # curve - first line
4436 split = col.split(percentage=0.15, align=True)
4437 if lt.display_curve:
4438 split.prop(lt, "display_curve", text="", icon='DOWNARROW_HLT')
4439 else:
4440 split.prop(lt, "display_curve", text="", icon='RIGHTARROW')
4441 split.operator("mesh.looptools_curve")
4442 # curve - settings
4443 if lt.display_curve:
4444 box = col.column(align=True).box().column()
4445 box.prop(lt, "curve_interpolation")
4446 box.prop(lt, "curve_restriction")
4447 box.prop(lt, "curve_boundaries")
4448 box.prop(lt, "curve_regular")
4449 box.separator()
4451 col_move = box.column(align=True)
4452 row = col_move.row(align=True)
4453 if lt.curve_lock_x:
4454 row.prop(lt, "curve_lock_x", text="X", icon='LOCKED')
4455 else:
4456 row.prop(lt, "curve_lock_x", text="X", icon='UNLOCKED')
4457 if lt.curve_lock_y:
4458 row.prop(lt, "curve_lock_y", text="Y", icon='LOCKED')
4459 else:
4460 row.prop(lt, "curve_lock_y", text="Y", icon='UNLOCKED')
4461 if lt.curve_lock_z:
4462 row.prop(lt, "curve_lock_z", text="Z", icon='LOCKED')
4463 else:
4464 row.prop(lt, "curve_lock_z", text="Z", icon='UNLOCKED')
4465 col_move.prop(lt, "curve_influence")
4467 # flatten - first line
4468 split = col.split(percentage=0.15, align=True)
4469 if lt.display_flatten:
4470 split.prop(lt, "display_flatten", text="", icon='DOWNARROW_HLT')
4471 else:
4472 split.prop(lt, "display_flatten", text="", icon='RIGHTARROW')
4473 split.operator("mesh.looptools_flatten")
4474 # flatten - settings
4475 if lt.display_flatten:
4476 box = col.column(align=True).box().column()
4477 box.prop(lt, "flatten_plane")
4478 # box.prop(lt, "flatten_restriction")
4479 box.separator()
4481 col_move = box.column(align=True)
4482 row = col_move.row(align=True)
4483 if lt.flatten_lock_x:
4484 row.prop(lt, "flatten_lock_x", text="X", icon='LOCKED')
4485 else:
4486 row.prop(lt, "flatten_lock_x", text="X", icon='UNLOCKED')
4487 if lt.flatten_lock_y:
4488 row.prop(lt, "flatten_lock_y", text="Y", icon='LOCKED')
4489 else:
4490 row.prop(lt, "flatten_lock_y", text="Y", icon='UNLOCKED')
4491 if lt.flatten_lock_z:
4492 row.prop(lt, "flatten_lock_z", text="Z", icon='LOCKED')
4493 else:
4494 row.prop(lt, "flatten_lock_z", text="Z", icon='UNLOCKED')
4495 col_move.prop(lt, "flatten_influence")
4497 # gstretch - first line
4498 split = col.split(percentage=0.15, align=True)
4499 if lt.display_gstretch:
4500 split.prop(lt, "display_gstretch", text="", icon='DOWNARROW_HLT')
4501 else:
4502 split.prop(lt, "display_gstretch", text="", icon='RIGHTARROW')
4503 split.operator("mesh.looptools_gstretch")
4504 # gstretch settings
4505 if lt.display_gstretch:
4506 box = col.column(align=True).box().column()
4507 box.prop(lt, "gstretch_method")
4509 col_conv = box.column(align=True)
4510 col_conv.prop(lt, "gstretch_conversion", text="")
4511 if lt.gstretch_conversion == 'distance':
4512 col_conv.prop(lt, "gstretch_conversion_distance")
4513 elif lt.gstretch_conversion == 'limit_vertices':
4514 row = col_conv.row(align=True)
4515 row.prop(lt, "gstretch_conversion_min", text="Min")
4516 row.prop(lt, "gstretch_conversion_max", text="Max")
4517 elif lt.gstretch_conversion == 'vertices':
4518 col_conv.prop(lt, "gstretch_conversion_vertices")
4519 box.separator()
4521 col_move = box.column(align=True)
4522 row = col_move.row(align=True)
4523 if lt.gstretch_lock_x:
4524 row.prop(lt, "gstretch_lock_x", text="X", icon='LOCKED')
4525 else:
4526 row.prop(lt, "gstretch_lock_x", text="X", icon='UNLOCKED')
4527 if lt.gstretch_lock_y:
4528 row.prop(lt, "gstretch_lock_y", text="Y", icon='LOCKED')
4529 else:
4530 row.prop(lt, "gstretch_lock_y", text="Y", icon='UNLOCKED')
4531 if lt.gstretch_lock_z:
4532 row.prop(lt, "gstretch_lock_z", text="Z", icon='LOCKED')
4533 else:
4534 row.prop(lt, "gstretch_lock_z", text="Z", icon='UNLOCKED')
4535 col_move.prop(lt, "gstretch_influence")
4536 box.operator("remove.gp", text="Delete GP Strokes")
4538 # loft - first line
4539 split = col.split(percentage=0.15, align=True)
4540 if lt.display_loft:
4541 split.prop(lt, "display_loft", text="", icon='DOWNARROW_HLT')
4542 else:
4543 split.prop(lt, "display_loft", text="", icon='RIGHTARROW')
4544 split.operator("mesh.looptools_bridge", text="Loft").loft = True
4545 # loft - settings
4546 if lt.display_loft:
4547 box = col.column(align=True).box().column()
4548 # box.prop(self, "mode")
4550 # top row
4551 col_top = box.column(align=True)
4552 row = col_top.row(align=True)
4553 col_left = row.column(align=True)
4554 col_right = row.column(align=True)
4555 col_right.active = lt.bridge_segments != 1
4556 col_left.prop(lt, "bridge_segments")
4557 col_right.prop(lt, "bridge_min_width", text="")
4558 # bottom row
4559 bottom_left = col_left.row()
4560 bottom_left.active = lt.bridge_segments != 1
4561 bottom_left.prop(lt, "bridge_interpolation", text="")
4562 bottom_right = col_right.row()
4563 bottom_right.active = lt.bridge_interpolation == 'cubic'
4564 bottom_right.prop(lt, "bridge_cubic_strength")
4565 # boolean properties
4566 col_top.prop(lt, "bridge_remove_faces")
4567 col_top.prop(lt, "bridge_loft_loop")
4569 # override properties
4570 col_top.separator()
4571 row = box.row(align=True)
4572 row.prop(lt, "bridge_twist")
4573 row.prop(lt, "bridge_reverse")
4575 # relax - first line
4576 split = col.split(percentage=0.15, align=True)
4577 if lt.display_relax:
4578 split.prop(lt, "display_relax", text="", icon='DOWNARROW_HLT')
4579 else:
4580 split.prop(lt, "display_relax", text="", icon='RIGHTARROW')
4581 split.operator("mesh.looptools_relax")
4582 # relax - settings
4583 if lt.display_relax:
4584 box = col.column(align=True).box().column()
4585 box.prop(lt, "relax_interpolation")
4586 box.prop(lt, "relax_input")
4587 box.prop(lt, "relax_iterations")
4588 box.prop(lt, "relax_regular")
4590 # space - first line
4591 split = col.split(percentage=0.15, align=True)
4592 if lt.display_space:
4593 split.prop(lt, "display_space", text="", icon='DOWNARROW_HLT')
4594 else:
4595 split.prop(lt, "display_space", text="", icon='RIGHTARROW')
4596 split.operator("mesh.looptools_space")
4597 # space - settings
4598 if lt.display_space:
4599 box = col.column(align=True).box().column()
4600 box.prop(lt, "space_interpolation")
4601 box.prop(lt, "space_input")
4602 box.separator()
4604 col_move = box.column(align=True)
4605 row = col_move.row(align=True)
4606 if lt.space_lock_x:
4607 row.prop(lt, "space_lock_x", text="X", icon='LOCKED')
4608 else:
4609 row.prop(lt, "space_lock_x", text="X", icon='UNLOCKED')
4610 if lt.space_lock_y:
4611 row.prop(lt, "space_lock_y", text="Y", icon='LOCKED')
4612 else:
4613 row.prop(lt, "space_lock_y", text="Y", icon='UNLOCKED')
4614 if lt.space_lock_z:
4615 row.prop(lt, "space_lock_z", text="Z", icon='LOCKED')
4616 else:
4617 row.prop(lt, "space_lock_z", text="Z", icon='UNLOCKED')
4618 col_move.prop(lt, "space_influence")
4621 # property group containing all properties for the gui in the panel
4622 class LoopToolsProps(PropertyGroup):
4624 Fake module like class
4625 bpy.context.window_manager.looptools
4627 # general display properties
4628 display_bridge = BoolProperty(
4629 name="Bridge settings",
4630 description="Display settings of the Bridge tool",
4631 default=False
4633 display_circle = BoolProperty(
4634 name="Circle settings",
4635 description="Display settings of the Circle tool",
4636 default=False
4638 display_curve = BoolProperty(
4639 name="Curve settings",
4640 description="Display settings of the Curve tool",
4641 default=False
4643 display_flatten = BoolProperty(
4644 name="Flatten settings",
4645 description="Display settings of the Flatten tool",
4646 default=False
4648 display_gstretch = BoolProperty(
4649 name="Gstretch settings",
4650 description="Display settings of the Gstretch tool",
4651 default=False
4653 display_loft = BoolProperty(
4654 name="Loft settings",
4655 description="Display settings of the Loft tool",
4656 default=False
4658 display_relax = BoolProperty(
4659 name="Relax settings",
4660 description="Display settings of the Relax tool",
4661 default=False
4663 display_space = BoolProperty(
4664 name="Space settings",
4665 description="Display settings of the Space tool",
4666 default=False
4669 # bridge properties
4670 bridge_cubic_strength = FloatProperty(
4671 name="Strength",
4672 description="Higher strength results in more fluid curves",
4673 default=1.0,
4674 soft_min=-3.0,
4675 soft_max=3.0
4677 bridge_interpolation = EnumProperty(
4678 name="Interpolation mode",
4679 items=(('cubic', "Cubic", "Gives curved results"),
4680 ('linear', "Linear", "Basic, fast, straight interpolation")),
4681 description="Interpolation mode: algorithm used when creating segments",
4682 default='cubic'
4684 bridge_loft = BoolProperty(
4685 name="Loft",
4686 description="Loft multiple loops, instead of considering them as "
4687 "a multi-input for bridging",
4688 default=False
4690 bridge_loft_loop = BoolProperty(
4691 name="Loop",
4692 description="Connect the first and the last loop with each other",
4693 default=False
4695 bridge_min_width = IntProperty(
4696 name="Minimum width",
4697 description="Segments with an edge smaller than this are merged "
4698 "(compared to base edge)",
4699 default=0,
4700 min=0,
4701 max=100,
4702 subtype='PERCENTAGE'
4704 bridge_mode = EnumProperty(
4705 name="Mode",
4706 items=(('basic', "Basic", "Fast algorithm"),
4707 ('shortest', "Shortest edge", "Slower algorithm with "
4708 "better vertex matching")),
4709 description="Algorithm used for bridging",
4710 default='shortest'
4712 bridge_remove_faces = BoolProperty(
4713 name="Remove faces",
4714 description="Remove faces that are internal after bridging",
4715 default=True
4717 bridge_reverse = BoolProperty(
4718 name="Reverse",
4719 description="Manually override the direction in which the loops "
4720 "are bridged. Only use if the tool gives the wrong result",
4721 default=False
4723 bridge_segments = IntProperty(
4724 name="Segments",
4725 description="Number of segments used to bridge the gap (0=automatic)",
4726 default=1,
4727 min=0,
4728 soft_max=20
4730 bridge_twist = IntProperty(
4731 name="Twist",
4732 description="Twist what vertices are connected to each other",
4733 default=0
4736 # circle properties
4737 circle_custom_radius = BoolProperty(
4738 name="Radius",
4739 description="Force a custom radius",
4740 default=False
4742 circle_fit = EnumProperty(
4743 name="Method",
4744 items=(("best", "Best fit", "Non-linear least squares"),
4745 ("inside", "Fit inside", "Only move vertices towards the center")),
4746 description="Method used for fitting a circle to the vertices",
4747 default='best'
4749 circle_flatten = BoolProperty(
4750 name="Flatten",
4751 description="Flatten the circle, instead of projecting it on the mesh",
4752 default=True
4754 circle_influence = FloatProperty(
4755 name="Influence",
4756 description="Force of the tool",
4757 default=100.0,
4758 min=0.0,
4759 max=100.0,
4760 precision=1,
4761 subtype='PERCENTAGE'
4763 circle_lock_x = BoolProperty(
4764 name="Lock X",
4765 description="Lock editing of the x-coordinate",
4766 default=False
4768 circle_lock_y = BoolProperty(
4769 name="Lock Y",
4770 description="Lock editing of the y-coordinate",
4771 default=False
4773 circle_lock_z = BoolProperty(
4774 name="Lock Z",
4775 description="Lock editing of the z-coordinate",
4776 default=False
4778 circle_radius = FloatProperty(
4779 name="Radius",
4780 description="Custom radius for circle",
4781 default=1.0,
4782 min=0.0,
4783 soft_max=1000.0
4785 circle_regular = BoolProperty(
4786 name="Regular",
4787 description="Distribute vertices at constant distances along the circle",
4788 default=True
4790 # curve properties
4791 curve_boundaries = BoolProperty(
4792 name="Boundaries",
4793 description="Limit the tool to work within the boundaries of the "
4794 "selected vertices",
4795 default=False
4797 curve_influence = FloatProperty(
4798 name="Influence",
4799 description="Force of the tool",
4800 default=100.0,
4801 min=0.0,
4802 max=100.0,
4803 precision=1,
4804 subtype='PERCENTAGE'
4806 curve_interpolation = EnumProperty(
4807 name="Interpolation",
4808 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4809 ("linear", "Linear", "Simple and fast linear algorithm")),
4810 description="Algorithm used for interpolation",
4811 default='cubic'
4813 curve_lock_x = BoolProperty(
4814 name="Lock X",
4815 description="Lock editing of the x-coordinate",
4816 default=False
4818 curve_lock_y = BoolProperty(
4819 name="Lock Y",
4820 description="Lock editing of the y-coordinate",
4821 default=False
4823 curve_lock_z = BoolProperty(
4824 name="Lock Z",
4825 description="Lock editing of the z-coordinate",
4826 default=False
4828 curve_regular = BoolProperty(
4829 name="Regular",
4830 description="Distribute vertices at constant distances along the curve",
4831 default=True
4833 curve_restriction = EnumProperty(
4834 name="Restriction",
4835 items=(("none", "None", "No restrictions on vertex movement"),
4836 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
4837 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
4838 description="Restrictions on how the vertices can be moved",
4839 default='none'
4842 # flatten properties
4843 flatten_influence = FloatProperty(
4844 name="Influence",
4845 description="Force of the tool",
4846 default=100.0,
4847 min=0.0,
4848 max=100.0,
4849 precision=1,
4850 subtype='PERCENTAGE'
4852 flatten_lock_x = BoolProperty(
4853 name="Lock X",
4854 description="Lock editing of the x-coordinate",
4855 default=False)
4856 flatten_lock_y = BoolProperty(name="Lock Y",
4857 description="Lock editing of the y-coordinate",
4858 default=False
4860 flatten_lock_z = BoolProperty(
4861 name="Lock Z",
4862 description="Lock editing of the z-coordinate",
4863 default=False
4865 flatten_plane = EnumProperty(
4866 name="Plane",
4867 items=(("best_fit", "Best fit", "Calculate a best fitting plane"),
4868 ("normal", "Normal", "Derive plane from averaging vertex "
4869 "normals"),
4870 ("view", "View", "Flatten on a plane perpendicular to the "
4871 "viewing angle")),
4872 description="Plane on which vertices are flattened",
4873 default='best_fit'
4875 flatten_restriction = EnumProperty(
4876 name="Restriction",
4877 items=(("none", "None", "No restrictions on vertex movement"),
4878 ("bounding_box", "Bounding box", "Vertices are restricted to "
4879 "movement inside the bounding box of the selection")),
4880 description="Restrictions on how the vertices can be moved",
4881 default='none'
4884 # gstretch properties
4885 gstretch_conversion = EnumProperty(
4886 name="Conversion",
4887 items=(("distance", "Distance", "Set the distance between vertices "
4888 "of the converted grease pencil stroke"),
4889 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
4890 "number of vertices that converted GP strokes will have"),
4891 ("vertices", "Exact vertices", "Set the exact number of vertices "
4892 "that converted grease pencil strokes will have. Short strokes "
4893 "with few points may contain less vertices than this number."),
4894 ("none", "No simplification", "Convert each grease pencil point "
4895 "to a vertex")),
4896 description="If grease pencil strokes are converted to geometry, "
4897 "use this simplification method",
4898 default='limit_vertices'
4900 gstretch_conversion_distance = FloatProperty(
4901 name="Distance",
4902 description="Absolute distance between vertices along the converted "
4903 "grease pencil stroke",
4904 default=0.1,
4905 min=0.000001,
4906 soft_min=0.01,
4907 soft_max=100
4909 gstretch_conversion_max = IntProperty(
4910 name="Max Vertices",
4911 description="Maximum number of vertices grease pencil strokes will "
4912 "have, when they are converted to geomtery",
4913 default=32,
4914 min=3,
4915 soft_max=500,
4916 update=gstretch_update_min
4918 gstretch_conversion_min = IntProperty(
4919 name="Min Vertices",
4920 description="Minimum number of vertices grease pencil strokes will "
4921 "have, when they are converted to geomtery",
4922 default=8,
4923 min=3,
4924 soft_max=500,
4925 update=gstretch_update_max
4927 gstretch_conversion_vertices = IntProperty(
4928 name="Vertices",
4929 description="Number of vertices grease pencil strokes will "
4930 "have, when they are converted to geometry. If strokes have less "
4931 "points than required, the 'Spread evenly' method is used",
4932 default=32,
4933 min=3,
4934 soft_max=500
4936 gstretch_delete_strokes = BoolProperty(
4937 name="Delete strokes",
4938 description="Remove Grease Pencil strokes if they have been used "
4939 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
4940 default=False
4942 gstretch_influence = FloatProperty(
4943 name="Influence",
4944 description="Force of the tool",
4945 default=100.0,
4946 min=0.0,
4947 max=100.0,
4948 precision=1,
4949 subtype='PERCENTAGE'
4951 gstretch_lock_x = BoolProperty(
4952 name="Lock X",
4953 description="Lock editing of the x-coordinate",
4954 default=False
4956 gstretch_lock_y = BoolProperty(
4957 name="Lock Y",
4958 description="Lock editing of the y-coordinate",
4959 default=False
4961 gstretch_lock_z = BoolProperty(
4962 name="Lock Z",
4963 description="Lock editing of the z-coordinate",
4964 default=False
4966 gstretch_method = EnumProperty(
4967 name="Method",
4968 items=(("project", "Project", "Project vertices onto the stroke, "
4969 "using vertex normals and connected edges"),
4970 ("irregular", "Spread", "Distribute vertices along the full "
4971 "stroke, retaining relative distances between the vertices"),
4972 ("regular", "Spread evenly", "Distribute vertices at regular "
4973 "distances along the full stroke")),
4974 description="Method of distributing the vertices over the Grease "
4975 "Pencil stroke",
4976 default='regular'
4979 # relax properties
4980 relax_input = EnumProperty(name="Input",
4981 items=(("all", "Parallel (all)", "Also use non-selected "
4982 "parallel loops as input"),
4983 ("selected", "Selection", "Only use selected vertices as input")),
4984 description="Loops that are relaxed",
4985 default='selected'
4987 relax_interpolation = EnumProperty(
4988 name="Interpolation",
4989 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4990 ("linear", "Linear", "Simple and fast linear algorithm")),
4991 description="Algorithm used for interpolation",
4992 default='cubic'
4994 relax_iterations = EnumProperty(name="Iterations",
4995 items=(("1", "1", "One"),
4996 ("3", "3", "Three"),
4997 ("5", "5", "Five"),
4998 ("10", "10", "Ten"),
4999 ("25", "25", "Twenty-five")),
5000 description="Number of times the loop is relaxed",
5001 default="1"
5003 relax_regular = BoolProperty(
5004 name="Regular",
5005 description="Distribute vertices at constant distances along the loop",
5006 default=True
5009 # space properties
5010 space_influence = FloatProperty(
5011 name="Influence",
5012 description="Force of the tool",
5013 default=100.0,
5014 min=0.0,
5015 max=100.0,
5016 precision=1,
5017 subtype='PERCENTAGE'
5019 space_input = EnumProperty(
5020 name="Input",
5021 items=(("all", "Parallel (all)", "Also use non-selected "
5022 "parallel loops as input"),
5023 ("selected", "Selection", "Only use selected vertices as input")),
5024 description="Loops that are spaced",
5025 default='selected'
5027 space_interpolation = EnumProperty(
5028 name="Interpolation",
5029 items=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5030 ("linear", "Linear", "Vertices are projected on existing edges")),
5031 description="Algorithm used for interpolation",
5032 default='cubic'
5034 space_lock_x = BoolProperty(
5035 name="Lock X",
5036 description="Lock editing of the x-coordinate",
5037 default=False
5039 space_lock_y = BoolProperty(
5040 name="Lock Y",
5041 description="Lock editing of the y-coordinate",
5042 default=False
5044 space_lock_z = BoolProperty(
5045 name="Lock Z",
5046 description="Lock editing of the z-coordinate",
5047 default=False
5051 # draw function for integration in menus
5052 def menu_func(self, context):
5053 self.layout.menu("VIEW3D_MT_edit_mesh_looptools")
5054 self.layout.separator()
5057 # Add-ons Preferences Update Panel
5059 # Define Panel classes for updating
5060 panels = (
5061 VIEW3D_PT_tools_looptools,
5065 def update_panel(self, context):
5066 message = "LoopTools: Updating Panel locations has failed"
5067 try:
5068 for panel in panels:
5069 if "bl_rna" in panel.__dict__:
5070 bpy.utils.unregister_class(panel)
5072 for panel in panels:
5073 panel.bl_category = context.user_preferences.addons[__name__].preferences.category
5074 bpy.utils.register_class(panel)
5076 except Exception as e:
5077 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
5078 pass
5081 class LoopPreferences(AddonPreferences):
5082 # this must match the addon name, use '__package__'
5083 # when defining this in a submodule of a python package.
5084 bl_idname = __name__
5086 category = StringProperty(
5087 name="Tab Category",
5088 description="Choose a name for the category of the panel",
5089 default="Tools",
5090 update=update_panel
5093 def draw(self, context):
5094 layout = self.layout
5096 row = layout.row()
5097 col = row.column()
5098 col.label(text="Tab Category:")
5099 col.prop(self, "category", text="")
5102 # define classes for registration
5103 classes = [
5104 VIEW3D_MT_edit_mesh_looptools,
5105 VIEW3D_PT_tools_looptools,
5106 LoopToolsProps,
5107 Bridge,
5108 Circle,
5109 Curve,
5110 Flatten,
5111 GStretch,
5112 Relax,
5113 Space,
5114 LoopPreferences,
5115 RemoveGP,
5119 # registering and menu integration
5120 def register():
5121 for c in classes:
5122 bpy.utils.register_class(c)
5123 bpy.types.VIEW3D_MT_edit_mesh_specials.prepend(menu_func)
5124 bpy.types.WindowManager.looptools = PointerProperty(type=LoopToolsProps)
5125 update_panel(None, bpy.context)
5128 # unregistering and removing menus
5129 def unregister():
5130 for c in classes:
5131 bpy.utils.unregister_class(c)
5132 bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func)
5133 try:
5134 del bpy.types.WindowManager.looptools
5135 except:
5136 pass
5139 if __name__ == "__main__":
5140 register()