Edit recent commit, no need to assign dummy vars
[blender-addons.git] / mesh_looptools.py
blob49a26c48080c79a5f1adb766680338143c5d2def
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, 5),
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": "http://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
41 ##########################################
42 ####### General functions ################
43 ##########################################
46 # used by all tools to improve speed on reruns
47 looptools_cache = {}
50 def get_grease_pencil(object, context):
51 gp = object.grease_pencil
52 if not gp:
53 gp = context.scene.grease_pencil
54 return gp
57 # force a full recalculation next time
58 def cache_delete(tool):
59 if tool in looptools_cache:
60 del looptools_cache[tool]
63 # check cache for stored information
64 def cache_read(tool, object, bm, input_method, boundaries):
65 # current tool not cached yet
66 if tool not in looptools_cache:
67 return(False, False, False, False, False)
68 # check if selected object didn't change
69 if object.name != looptools_cache[tool]["object"]:
70 return(False, False, False, False, False)
71 # check if input didn't change
72 if input_method != looptools_cache[tool]["input_method"]:
73 return(False, False, False, False, False)
74 if boundaries != looptools_cache[tool]["boundaries"]:
75 return(False, False, False, False, False)
76 modifiers = [mod.name for mod in object.modifiers if mod.show_viewport \
77 and mod.type == 'MIRROR']
78 if modifiers != looptools_cache[tool]["modifiers"]:
79 return(False, False, False, False, False)
80 input = [v.index for v in bm.verts if v.select and not v.hide]
81 if input != looptools_cache[tool]["input"]:
82 return(False, False, False, False, False)
83 # reading values
84 single_loops = looptools_cache[tool]["single_loops"]
85 loops = looptools_cache[tool]["loops"]
86 derived = looptools_cache[tool]["derived"]
87 mapping = looptools_cache[tool]["mapping"]
89 return(True, single_loops, loops, derived, mapping)
92 # store information in the cache
93 def cache_write(tool, object, bm, input_method, boundaries, single_loops,
94 loops, derived, mapping):
95 # clear cache of current tool
96 if tool in looptools_cache:
97 del looptools_cache[tool]
98 # prepare values to be saved to cache
99 input = [v.index for v in bm.verts if v.select and not v.hide]
100 modifiers = [mod.name for mod in object.modifiers if mod.show_viewport \
101 and mod.type == 'MIRROR']
102 # update cache
103 looptools_cache[tool] = {"input": input, "object": object.name,
104 "input_method": input_method, "boundaries": boundaries,
105 "single_loops": single_loops, "loops": loops,
106 "derived": derived, "mapping": mapping, "modifiers": modifiers}
109 # calculates natural cubic splines through all given knots
110 def calculate_cubic_splines(bm_mod, tknots, knots):
111 # hack for circular loops
112 if knots[0] == knots[-1] and len(knots) > 1:
113 circular = True
114 k_new1 = []
115 for k in range(-1, -5, -1):
116 if k - 1 < -len(knots):
117 k += len(knots)
118 k_new1.append(knots[k-1])
119 k_new2 = []
120 for k in range(4):
121 if k + 1 > len(knots) - 1:
122 k -= len(knots)
123 k_new2.append(knots[k+1])
124 for k in k_new1:
125 knots.insert(0, k)
126 for k in k_new2:
127 knots.append(k)
128 t_new1 = []
129 total1 = 0
130 for t in range(-1, -5, -1):
131 if t - 1 < -len(tknots):
132 t += len(tknots)
133 total1 += tknots[t] - tknots[t-1]
134 t_new1.append(tknots[0] - total1)
135 t_new2 = []
136 total2 = 0
137 for t in range(4):
138 if t + 1 > len(tknots) - 1:
139 t -= len(tknots)
140 total2 += tknots[t+1] - tknots[t]
141 t_new2.append(tknots[-1] + total2)
142 for t in t_new1:
143 tknots.insert(0, t)
144 for t in t_new2:
145 tknots.append(t)
146 else:
147 circular = False
148 # end of hack
150 n = len(knots)
151 if n < 2:
152 return False
153 x = tknots[:]
154 locs = [bm_mod.verts[k].co[:] for k in knots]
155 result = []
156 for j in range(3):
157 a = []
158 for i in locs:
159 a.append(i[j])
160 h = []
161 for i in range(n-1):
162 if x[i+1] - x[i] == 0:
163 h.append(1e-8)
164 else:
165 h.append(x[i+1] - x[i])
166 q = [False]
167 for i in range(1, n-1):
168 q.append(3/h[i]*(a[i+1]-a[i]) - 3/h[i-1]*(a[i]-a[i-1]))
169 l = [1.0]
170 u = [0.0]
171 z = [0.0]
172 for i in range(1, n-1):
173 l.append(2*(x[i+1]-x[i-1]) - h[i-1]*u[i-1])
174 if l[i] == 0:
175 l[i] = 1e-8
176 u.append(h[i] / l[i])
177 z.append((q[i] - h[i-1] * z[i-1]) / l[i])
178 l.append(1.0)
179 z.append(0.0)
180 b = [False for i in range(n-1)]
181 c = [False for i in range(n)]
182 d = [False for i in range(n-1)]
183 c[n-1] = 0.0
184 for i in range(n-2, -1, -1):
185 c[i] = z[i] - u[i]*c[i+1]
186 b[i] = (a[i+1]-a[i])/h[i] - h[i]*(c[i+1]+2*c[i])/3
187 d[i] = (c[i+1]-c[i]) / (3*h[i])
188 for i in range(n-1):
189 result.append([a[i], b[i], c[i], d[i], x[i]])
190 splines = []
191 for i in range(len(knots)-1):
192 splines.append([result[i], result[i+n-1], result[i+(n-1)*2]])
193 if circular: # cleaning up after hack
194 knots = knots[4:-4]
195 tknots = tknots[4:-4]
197 return(splines)
200 # calculates linear splines through all given knots
201 def calculate_linear_splines(bm_mod, tknots, knots):
202 splines = []
203 for i in range(len(knots)-1):
204 a = bm_mod.verts[knots[i]].co
205 b = bm_mod.verts[knots[i+1]].co
206 d = b-a
207 t = tknots[i]
208 u = tknots[i+1]-t
209 splines.append([a, d, t, u]) # [locStart, locDif, tStart, tDif]
211 return(splines)
214 # calculate a best-fit plane to the given vertices
215 def calculate_plane(bm_mod, loop, method="best_fit", object=False):
216 # getting the vertex locations
217 locs = [bm_mod.verts[v].co.copy() for v in loop[0]]
219 # calculating the center of masss
220 com = mathutils.Vector()
221 for loc in locs:
222 com += loc
223 com /= len(locs)
224 x, y, z = com
226 if method == 'best_fit':
227 # creating the covariance matrix
228 mat = mathutils.Matrix(((0.0, 0.0, 0.0),
229 (0.0, 0.0, 0.0),
230 (0.0, 0.0, 0.0),
232 for loc in locs:
233 mat[0][0] += (loc[0]-x)**2
234 mat[1][0] += (loc[0]-x)*(loc[1]-y)
235 mat[2][0] += (loc[0]-x)*(loc[2]-z)
236 mat[0][1] += (loc[1]-y)*(loc[0]-x)
237 mat[1][1] += (loc[1]-y)**2
238 mat[2][1] += (loc[1]-y)*(loc[2]-z)
239 mat[0][2] += (loc[2]-z)*(loc[0]-x)
240 mat[1][2] += (loc[2]-z)*(loc[1]-y)
241 mat[2][2] += (loc[2]-z)**2
243 # calculating the normal to the plane
244 normal = False
245 try:
246 mat = matrix_invert(mat)
247 except:
248 ax = 2
249 if math.fabs(sum(mat[0])) < math.fabs(sum(mat[1])):
250 if math.fabs(sum(mat[0])) < math.fabs(sum(mat[2])):
251 ax = 0
252 elif math.fabs(sum(mat[1])) < math.fabs(sum(mat[2])):
253 ax = 1
254 if ax == 0:
255 normal = mathutils.Vector((1.0, 0.0, 0.0))
256 elif ax == 1:
257 normal = mathutils.Vector((0.0, 1.0, 0.0))
258 else:
259 normal = mathutils.Vector((0.0, 0.0, 1.0))
260 if not normal:
261 # warning! this is different from .normalize()
262 itermax = 500
263 iter = 0
264 vec = mathutils.Vector((1.0, 1.0, 1.0))
265 vec2 = (mat * vec)/(mat * vec).length
266 while vec != vec2 and iter<itermax:
267 iter+=1
268 vec = vec2
269 vec2 = mat * vec
270 if vec2.length != 0:
271 vec2 /= vec2.length
272 if vec2.length == 0:
273 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
274 normal = vec2
276 elif method == 'normal':
277 # averaging the vertex normals
278 v_normals = [bm_mod.verts[v].normal for v in loop[0]]
279 normal = mathutils.Vector()
280 for v_normal in v_normals:
281 normal += v_normal
282 normal /= len(v_normals)
283 normal.normalize()
285 elif method == 'view':
286 # calculate view normal
287 rotation = bpy.context.space_data.region_3d.view_matrix.to_3x3().\
288 inverted()
289 normal = rotation * mathutils.Vector((0.0, 0.0, 1.0))
290 if object:
291 normal = object.matrix_world.inverted().to_euler().to_matrix() * \
292 normal
294 return(com, normal)
297 # calculate splines based on given interpolation method (controller function)
298 def calculate_splines(interpolation, bm_mod, tknots, knots):
299 if interpolation == 'cubic':
300 splines = calculate_cubic_splines(bm_mod, tknots, knots[:])
301 else: # interpolations == 'linear'
302 splines = calculate_linear_splines(bm_mod, tknots, knots[:])
304 return(splines)
307 # check loops and only return valid ones
308 def check_loops(loops, mapping, bm_mod):
309 valid_loops = []
310 for loop, circular in loops:
311 # loop needs to have at least 3 vertices
312 if len(loop) < 3:
313 continue
314 # loop needs at least 1 vertex in the original, non-mirrored mesh
315 if mapping:
316 all_virtual = True
317 for vert in loop:
318 if mapping[vert] > -1:
319 all_virtual = False
320 break
321 if all_virtual:
322 continue
323 # vertices can not all be at the same location
324 stacked = True
325 for i in range(len(loop) - 1):
326 if (bm_mod.verts[loop[i]].co - \
327 bm_mod.verts[loop[i+1]].co).length > 1e-6:
328 stacked = False
329 break
330 if stacked:
331 continue
332 # passed all tests, loop is valid
333 valid_loops.append([loop, circular])
335 return(valid_loops)
338 # input: bmesh, output: dict with the edge-key as key and face-index as value
339 def dict_edge_faces(bm):
340 edge_faces = dict([[edgekey(edge), []] for edge in bm.edges if \
341 not edge.hide])
342 for face in bm.faces:
343 if face.hide:
344 continue
345 for key in face_edgekeys(face):
346 edge_faces[key].append(face.index)
348 return(edge_faces)
351 # input: bmesh (edge-faces optional), output: dict with face-face connections
352 def dict_face_faces(bm, edge_faces=False):
353 if not edge_faces:
354 edge_faces = dict_edge_faces(bm)
356 connected_faces = dict([[face.index, []] for face in bm.faces if \
357 not face.hide])
358 for face in bm.faces:
359 if face.hide:
360 continue
361 for edge_key in face_edgekeys(face):
362 for connected_face in edge_faces[edge_key]:
363 if connected_face == face.index:
364 continue
365 connected_faces[face.index].append(connected_face)
367 return(connected_faces)
370 # input: bmesh, output: dict with the vert index as key and edge-keys as value
371 def dict_vert_edges(bm):
372 vert_edges = dict([[v.index, []] for v in bm.verts if not v.hide])
373 for edge in bm.edges:
374 if edge.hide:
375 continue
376 ek = edgekey(edge)
377 for vert in ek:
378 vert_edges[vert].append(ek)
380 return(vert_edges)
383 # input: bmesh, output: dict with the vert index as key and face index as value
384 def dict_vert_faces(bm):
385 vert_faces = dict([[v.index, []] for v in bm.verts if not v.hide])
386 for face in bm.faces:
387 if not face.hide:
388 for vert in face.verts:
389 vert_faces[vert.index].append(face.index)
391 return(vert_faces)
394 # input: list of edge-keys, output: dictionary with vertex-vertex connections
395 def dict_vert_verts(edge_keys):
396 # create connection data
397 vert_verts = {}
398 for ek in edge_keys:
399 for i in range(2):
400 if ek[i] in vert_verts:
401 vert_verts[ek[i]].append(ek[1-i])
402 else:
403 vert_verts[ek[i]] = [ek[1-i]]
405 return(vert_verts)
408 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
409 def edgekey(edge):
410 return(tuple(sorted([edge.verts[0].index, edge.verts[1].index])))
413 # returns the edgekeys of a bmesh face
414 def face_edgekeys(face):
415 return([tuple(sorted([edge.verts[0].index, edge.verts[1].index])) for \
416 edge in face.edges])
419 # calculate input loops
420 def get_connected_input(object, bm, scene, input):
421 # get mesh with modifiers applied
422 derived, bm_mod = get_derived_bmesh(object, bm, scene)
424 # calculate selected loops
425 edge_keys = [edgekey(edge) for edge in bm_mod.edges if \
426 edge.select and not edge.hide]
427 loops = get_connected_selections(edge_keys)
429 # if only selected loops are needed, we're done
430 if input == 'selected':
431 return(derived, bm_mod, loops)
432 # elif input == 'all':
433 loops = get_parallel_loops(bm_mod, loops)
435 return(derived, bm_mod, loops)
438 # sorts all edge-keys into a list of loops
439 def get_connected_selections(edge_keys):
440 # create connection data
441 vert_verts = dict_vert_verts(edge_keys)
443 # find loops consisting of connected selected edges
444 loops = []
445 while len(vert_verts) > 0:
446 loop = [iter(vert_verts.keys()).__next__()]
447 growing = True
448 flipped = False
450 # extend loop
451 while growing:
452 # no more connection data for current vertex
453 if loop[-1] not in vert_verts:
454 if not flipped:
455 loop.reverse()
456 flipped = True
457 else:
458 growing = False
459 else:
460 extended = False
461 for i, next_vert in enumerate(vert_verts[loop[-1]]):
462 if next_vert not in loop:
463 vert_verts[loop[-1]].pop(i)
464 if len(vert_verts[loop[-1]]) == 0:
465 del vert_verts[loop[-1]]
466 # remove connection both ways
467 if next_vert in vert_verts:
468 if len(vert_verts[next_vert]) == 1:
469 del vert_verts[next_vert]
470 else:
471 vert_verts[next_vert].remove(loop[-1])
472 loop.append(next_vert)
473 extended = True
474 break
475 if not extended:
476 # found one end of the loop, continue with next
477 if not flipped:
478 loop.reverse()
479 flipped = True
480 # found both ends of the loop, stop growing
481 else:
482 growing = False
484 # check if loop is circular
485 if loop[0] in vert_verts:
486 if loop[-1] in vert_verts[loop[0]]:
487 # is circular
488 if len(vert_verts[loop[0]]) == 1:
489 del vert_verts[loop[0]]
490 else:
491 vert_verts[loop[0]].remove(loop[-1])
492 if len(vert_verts[loop[-1]]) == 1:
493 del vert_verts[loop[-1]]
494 else:
495 vert_verts[loop[-1]].remove(loop[0])
496 loop = [loop, True]
497 else:
498 # not circular
499 loop = [loop, False]
500 else:
501 # not circular
502 loop = [loop, False]
504 loops.append(loop)
506 return(loops)
509 # get the derived mesh data, if there is a mirror modifier
510 def get_derived_bmesh(object, bm, scene):
511 # check for mirror modifiers
512 if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
513 derived = True
514 # disable other modifiers
515 show_viewport = [mod.name for mod in object.modifiers if \
516 mod.show_viewport]
517 for mod in object.modifiers:
518 if mod.type != 'MIRROR':
519 mod.show_viewport = False
520 # get derived mesh
521 bm_mod = bmesh.new()
522 mesh_mod = object.to_mesh(scene, True, 'PREVIEW')
523 bm_mod.from_mesh(mesh_mod)
524 bpy.context.blend_data.meshes.remove(mesh_mod)
525 # re-enable other modifiers
526 for mod_name in show_viewport:
527 object.modifiers[mod_name].show_viewport = True
528 # no mirror modifiers, so no derived mesh necessary
529 else:
530 derived = False
531 bm_mod = bm
533 bm_mod.verts.ensure_lookup_table()
534 bm_mod.edges.ensure_lookup_table()
535 bm_mod.faces.ensure_lookup_table()
537 return(derived, bm_mod)
540 # return a mapping of derived indices to indices
541 def get_mapping(derived, bm, bm_mod, single_vertices, full_search, loops):
542 if not derived:
543 return(False)
545 if full_search:
546 verts = [v for v in bm.verts if not v.hide]
547 else:
548 verts = [v for v in bm.verts if v.select and not v.hide]
550 # non-selected vertices around single vertices also need to be mapped
551 if single_vertices:
552 mapping = dict([[vert, -1] for vert in single_vertices])
553 verts_mod = [bm_mod.verts[vert] for vert in single_vertices]
554 for v in verts:
555 for v_mod in verts_mod:
556 if (v.co - v_mod.co).length < 1e-6:
557 mapping[v_mod.index] = v.index
558 break
559 real_singles = [v_real for v_real in mapping.values() if v_real>-1]
561 verts_indices = [vert.index for vert in verts]
562 for face in [face for face in bm.faces if not face.select \
563 and not face.hide]:
564 for vert in face.verts:
565 if vert.index in real_singles:
566 for v in face.verts:
567 if not v.index in verts_indices:
568 if v not in verts:
569 verts.append(v)
570 break
572 # create mapping of derived indices to indices
573 mapping = dict([[vert, -1] for loop in loops for vert in loop[0]])
574 if single_vertices:
575 for single in single_vertices:
576 mapping[single] = -1
577 verts_mod = [bm_mod.verts[i] for i in mapping.keys()]
578 for v in verts:
579 for v_mod in verts_mod:
580 if (v.co - v_mod.co).length < 1e-6:
581 mapping[v_mod.index] = v.index
582 verts_mod.remove(v_mod)
583 break
585 return(mapping)
588 # calculate the determinant of a matrix
589 def matrix_determinant(m):
590 determinant = m[0][0] * m[1][1] * m[2][2] + m[0][1] * m[1][2] * m[2][0] \
591 + m[0][2] * m[1][0] * m[2][1] - m[0][2] * m[1][1] * m[2][0] \
592 - m[0][1] * m[1][0] * m[2][2] - m[0][0] * m[1][2] * m[2][1]
594 return(determinant)
597 # custom matrix inversion, to provide higher precision than the built-in one
598 def matrix_invert(m):
599 r = mathutils.Matrix((
600 (m[1][1]*m[2][2] - m[1][2]*m[2][1], m[0][2]*m[2][1] - m[0][1]*m[2][2],
601 m[0][1]*m[1][2] - m[0][2]*m[1][1]),
602 (m[1][2]*m[2][0] - m[1][0]*m[2][2], m[0][0]*m[2][2] - m[0][2]*m[2][0],
603 m[0][2]*m[1][0] - m[0][0]*m[1][2]),
604 (m[1][0]*m[2][1] - m[1][1]*m[2][0], m[0][1]*m[2][0] - m[0][0]*m[2][1],
605 m[0][0]*m[1][1] - m[0][1]*m[1][0])))
607 return (r * (1 / matrix_determinant(m)))
610 # returns a list of all loops parallel to the input, input included
611 def get_parallel_loops(bm_mod, loops):
612 # get required dictionaries
613 edge_faces = dict_edge_faces(bm_mod)
614 connected_faces = dict_face_faces(bm_mod, edge_faces)
615 # turn vertex loops into edge loops
616 edgeloops = []
617 for loop in loops:
618 edgeloop = [[sorted([loop[0][i], loop[0][i+1]]) for i in \
619 range(len(loop[0])-1)], loop[1]]
620 if loop[1]: # circular
621 edgeloop[0].append(sorted([loop[0][-1], loop[0][0]]))
622 edgeloops.append(edgeloop[:])
623 # variables to keep track while iterating
624 all_edgeloops = []
625 has_branches = False
627 for loop in edgeloops:
628 # initialise with original loop
629 all_edgeloops.append(loop[0])
630 newloops = [loop[0]]
631 verts_used = []
632 for edge in loop[0]:
633 if edge[0] not in verts_used:
634 verts_used.append(edge[0])
635 if edge[1] not in verts_used:
636 verts_used.append(edge[1])
638 # find parallel loops
639 while len(newloops) > 0:
640 side_a = []
641 side_b = []
642 for i in newloops[-1]:
643 i = tuple(i)
644 forbidden_side = False
645 if not i in edge_faces:
646 # weird input with branches
647 has_branches = True
648 break
649 for face in edge_faces[i]:
650 if len(side_a) == 0 and forbidden_side != "a":
651 side_a.append(face)
652 if forbidden_side:
653 break
654 forbidden_side = "a"
655 continue
656 elif side_a[-1] in connected_faces[face] and \
657 forbidden_side != "a":
658 side_a.append(face)
659 if forbidden_side:
660 break
661 forbidden_side = "a"
662 continue
663 if len(side_b) == 0 and forbidden_side != "b":
664 side_b.append(face)
665 if forbidden_side:
666 break
667 forbidden_side = "b"
668 continue
669 elif side_b[-1] in connected_faces[face] and \
670 forbidden_side != "b":
671 side_b.append(face)
672 if forbidden_side:
673 break
674 forbidden_side = "b"
675 continue
677 if has_branches:
678 # weird input with branches
679 break
681 newloops.pop(-1)
682 sides = []
683 if side_a:
684 sides.append(side_a)
685 if side_b:
686 sides.append(side_b)
688 for side in sides:
689 extraloop = []
690 for fi in side:
691 for key in face_edgekeys(bm_mod.faces[fi]):
692 if key[0] not in verts_used and key[1] not in \
693 verts_used:
694 extraloop.append(key)
695 break
696 if extraloop:
697 for key in extraloop:
698 for new_vert in key:
699 if new_vert not in verts_used:
700 verts_used.append(new_vert)
701 newloops.append(extraloop)
702 all_edgeloops.append(extraloop)
704 # input contains branches, only return selected loop
705 if has_branches:
706 return(loops)
708 # change edgeloops into normal loops
709 loops = []
710 for edgeloop in all_edgeloops:
711 loop = []
712 # grow loop by comparing vertices between consecutive edge-keys
713 for i in range(len(edgeloop)-1):
714 for vert in range(2):
715 if edgeloop[i][vert] in edgeloop[i+1]:
716 loop.append(edgeloop[i][vert])
717 break
718 if loop:
719 # add starting vertex
720 for vert in range(2):
721 if edgeloop[0][vert] != loop[0]:
722 loop = [edgeloop[0][vert]] + loop
723 break
724 # add ending vertex
725 for vert in range(2):
726 if edgeloop[-1][vert] != loop[-1]:
727 loop.append(edgeloop[-1][vert])
728 break
729 # check if loop is circular
730 if loop[0] == loop[-1]:
731 circular = True
732 loop = loop[:-1]
733 else:
734 circular = False
735 loops.append([loop, circular])
737 return(loops)
740 # gather initial data
741 def initialise():
742 global_undo = bpy.context.user_preferences.edit.use_global_undo
743 bpy.context.user_preferences.edit.use_global_undo = False
744 object = bpy.context.active_object
745 if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
746 # ensure that selection is synced for the derived mesh
747 bpy.ops.object.mode_set(mode='OBJECT')
748 bpy.ops.object.mode_set(mode='EDIT')
749 bm = bmesh.from_edit_mesh(object.data)
751 bm.verts.ensure_lookup_table()
752 bm.edges.ensure_lookup_table()
753 bm.faces.ensure_lookup_table()
755 return(global_undo, object, bm)
758 # move the vertices to their new locations
759 def move_verts(object, bm, mapping, move, lock, influence):
760 if lock:
761 lock_x, lock_y, lock_z = lock
762 orientation = bpy.context.space_data.transform_orientation
763 custom = bpy.context.space_data.current_orientation
764 if custom:
765 mat = custom.matrix.to_4x4().inverted() * object.matrix_world.copy()
766 elif orientation == 'LOCAL':
767 mat = mathutils.Matrix.Identity(4)
768 elif orientation == 'VIEW':
769 mat = bpy.context.region_data.view_matrix.copy() * \
770 object.matrix_world.copy()
771 else: # orientation == 'GLOBAL'
772 mat = object.matrix_world.copy()
773 mat_inv = mat.inverted()
775 for loop in move:
776 for index, loc in loop:
777 if mapping:
778 if mapping[index] == -1:
779 continue
780 else:
781 index = mapping[index]
782 if lock:
783 delta = (loc - bm.verts[index].co) * mat_inv
784 if lock_x:
785 delta[0] = 0
786 if lock_y:
787 delta[1] = 0
788 if lock_z:
789 delta[2] = 0
790 delta = delta * mat
791 loc = bm.verts[index].co + delta
792 if influence < 0:
793 new_loc = loc
794 else:
795 new_loc = loc*(influence/100) + \
796 bm.verts[index].co*((100-influence)/100)
797 bm.verts[index].co = new_loc
798 bm.normal_update()
799 object.data.update()
801 bm.verts.ensure_lookup_table()
802 bm.edges.ensure_lookup_table()
803 bm.faces.ensure_lookup_table()
806 # load custom tool settings
807 def settings_load(self):
808 lt = bpy.context.window_manager.looptools
809 tool = self.name.split()[0].lower()
810 keys = self.as_keywords().keys()
811 for key in keys:
812 setattr(self, key, getattr(lt, tool + "_" + key))
815 # store custom tool settings
816 def settings_write(self):
817 lt = bpy.context.window_manager.looptools
818 tool = self.name.split()[0].lower()
819 keys = self.as_keywords().keys()
820 for key in keys:
821 setattr(lt, tool + "_" + key, getattr(self, key))
824 # clean up and set settings back to original state
825 def terminate(global_undo):
826 # update editmesh cached data
827 obj = bpy.context.active_object
828 if obj.mode == 'EDIT':
829 bmesh.update_edit_mesh(obj.data, tessface=True, destructive=True)
831 bpy.context.user_preferences.edit.use_global_undo = global_undo
834 ##########################################
835 ####### Bridge functions #################
836 ##########################################
838 # calculate a cubic spline through the middle section of 4 given coordinates
839 def bridge_calculate_cubic_spline(bm, coordinates):
840 result = []
841 x = [0, 1, 2, 3]
843 for j in range(3):
844 a = []
845 for i in coordinates:
846 a.append(float(i[j]))
847 h = []
848 for i in range(3):
849 h.append(x[i+1]-x[i])
850 q = [False]
851 for i in range(1,3):
852 q.append(3.0/h[i]*(a[i+1]-a[i])-3.0/h[i-1]*(a[i]-a[i-1]))
853 l = [1.0]
854 u = [0.0]
855 z = [0.0]
856 for i in range(1,3):
857 l.append(2.0*(x[i+1]-x[i-1])-h[i-1]*u[i-1])
858 u.append(h[i]/l[i])
859 z.append((q[i]-h[i-1]*z[i-1])/l[i])
860 l.append(1.0)
861 z.append(0.0)
862 b = [False for i in range(3)]
863 c = [False for i in range(4)]
864 d = [False for i in range(3)]
865 c[3] = 0.0
866 for i in range(2,-1,-1):
867 c[i] = z[i]-u[i]*c[i+1]
868 b[i] = (a[i+1]-a[i])/h[i]-h[i]*(c[i+1]+2.0*c[i])/3.0
869 d[i] = (c[i+1]-c[i])/(3.0*h[i])
870 for i in range(3):
871 result.append([a[i], b[i], c[i], d[i], x[i]])
872 spline = [result[1], result[4], result[7]]
874 return(spline)
877 # return a list with new vertex location vectors, a list with face vertex
878 # integers, and the highest vertex integer in the virtual mesh
879 def bridge_calculate_geometry(bm, lines, vertex_normals, segments,
880 interpolation, cubic_strength, min_width, max_vert_index):
881 new_verts = []
882 faces = []
884 # calculate location based on interpolation method
885 def get_location(line, segment, splines):
886 v1 = bm.verts[lines[line][0]].co
887 v2 = bm.verts[lines[line][1]].co
888 if interpolation == 'linear':
889 return v1 + (segment/segments) * (v2-v1)
890 else: # interpolation == 'cubic'
891 m = (segment/segments)
892 ax,bx,cx,dx,tx = splines[line][0]
893 x = ax+bx*m+cx*m**2+dx*m**3
894 ay,by,cy,dy,ty = splines[line][1]
895 y = ay+by*m+cy*m**2+dy*m**3
896 az,bz,cz,dz,tz = splines[line][2]
897 z = az+bz*m+cz*m**2+dz*m**3
898 return mathutils.Vector((x, y, z))
900 # no interpolation needed
901 if segments == 1:
902 for i, line in enumerate(lines):
903 if i < len(lines)-1:
904 faces.append([line[0], lines[i+1][0], lines[i+1][1], line[1]])
905 # more than 1 segment, interpolate
906 else:
907 # calculate splines (if necessary) once, so no recalculations needed
908 if interpolation == 'cubic':
909 splines = []
910 for line in lines:
911 v1 = bm.verts[line[0]].co
912 v2 = bm.verts[line[1]].co
913 size = (v2-v1).length * cubic_strength
914 splines.append(bridge_calculate_cubic_spline(bm,
915 [v1+size*vertex_normals[line[0]], v1, v2,
916 v2+size*vertex_normals[line[1]]]))
917 else:
918 splines = False
920 # create starting situation
921 virtual_width = [(bm.verts[lines[i][0]].co -
922 bm.verts[lines[i+1][0]].co).length for i
923 in range(len(lines)-1)]
924 new_verts = [get_location(0, seg, splines) for seg in range(1,
925 segments)]
926 first_line_indices = [i for i in range(max_vert_index+1,
927 max_vert_index+segments)]
929 prev_verts = new_verts[:] # vertex locations of verts on previous line
930 prev_vert_indices = first_line_indices[:]
931 max_vert_index += segments - 1 # highest vertex index in virtual mesh
932 next_verts = [] # vertex locations of verts on current line
933 next_vert_indices = []
935 for i, line in enumerate(lines):
936 if i < len(lines)-1:
937 v1 = line[0]
938 v2 = lines[i+1][0]
939 end_face = True
940 for seg in range(1, segments):
941 loc1 = prev_verts[seg-1]
942 loc2 = get_location(i+1, seg, splines)
943 if (loc1-loc2).length < (min_width/100)*virtual_width[i] \
944 and line[1]==lines[i+1][1]:
945 # triangle, no new vertex
946 faces.append([v1, v2, prev_vert_indices[seg-1],
947 prev_vert_indices[seg-1]])
948 next_verts += prev_verts[seg-1:]
949 next_vert_indices += prev_vert_indices[seg-1:]
950 end_face = False
951 break
952 else:
953 if i == len(lines)-2 and lines[0] == lines[-1]:
954 # quad with first line, no new vertex
955 faces.append([v1, v2, first_line_indices[seg-1],
956 prev_vert_indices[seg-1]])
957 v2 = first_line_indices[seg-1]
958 v1 = prev_vert_indices[seg-1]
959 else:
960 # quad, add new vertex
961 max_vert_index += 1
962 faces.append([v1, v2, max_vert_index,
963 prev_vert_indices[seg-1]])
964 v2 = max_vert_index
965 v1 = prev_vert_indices[seg-1]
966 new_verts.append(loc2)
967 next_verts.append(loc2)
968 next_vert_indices.append(max_vert_index)
969 if end_face:
970 faces.append([v1, v2, lines[i+1][1], line[1]])
972 prev_verts = next_verts[:]
973 prev_vert_indices = next_vert_indices[:]
974 next_verts = []
975 next_vert_indices = []
977 return(new_verts, faces, max_vert_index)
980 # calculate lines (list of lists, vertex indices) that are used for bridging
981 def bridge_calculate_lines(bm, loops, mode, twist, reverse):
982 lines = []
983 loop1, loop2 = [i[0] for i in loops]
984 loop1_circular, loop2_circular = [i[1] for i in loops]
985 circular = loop1_circular or loop2_circular
986 circle_full = False
988 # calculate loop centers
989 centers = []
990 for loop in [loop1, loop2]:
991 center = mathutils.Vector()
992 for vertex in loop:
993 center += bm.verts[vertex].co
994 center /= len(loop)
995 centers.append(center)
996 for i, loop in enumerate([loop1, loop2]):
997 for vertex in loop:
998 if bm.verts[vertex].co == centers[i]:
999 # prevent zero-length vectors in angle comparisons
1000 centers[i] += mathutils.Vector((0.01, 0, 0))
1001 break
1002 center1, center2 = centers
1004 # calculate the normals of the virtual planes that the loops are on
1005 normals = []
1006 normal_plurity = False
1007 for i, loop in enumerate([loop1, loop2]):
1008 # covariance matrix
1009 mat = mathutils.Matrix(((0.0, 0.0, 0.0),
1010 (0.0, 0.0, 0.0),
1011 (0.0, 0.0, 0.0)))
1012 x, y, z = centers[i]
1013 for loc in [bm.verts[vertex].co for vertex in loop]:
1014 mat[0][0] += (loc[0]-x)**2
1015 mat[1][0] += (loc[0]-x)*(loc[1]-y)
1016 mat[2][0] += (loc[0]-x)*(loc[2]-z)
1017 mat[0][1] += (loc[1]-y)*(loc[0]-x)
1018 mat[1][1] += (loc[1]-y)**2
1019 mat[2][1] += (loc[1]-y)*(loc[2]-z)
1020 mat[0][2] += (loc[2]-z)*(loc[0]-x)
1021 mat[1][2] += (loc[2]-z)*(loc[1]-y)
1022 mat[2][2] += (loc[2]-z)**2
1023 # plane normal
1024 normal = False
1025 if sum(mat[0]) < 1e-6 or sum(mat[1]) < 1e-6 or sum(mat[2]) < 1e-6:
1026 normal_plurity = True
1027 try:
1028 mat.invert()
1029 except:
1030 if sum(mat[0]) == 0:
1031 normal = mathutils.Vector((1.0, 0.0, 0.0))
1032 elif sum(mat[1]) == 0:
1033 normal = mathutils.Vector((0.0, 1.0, 0.0))
1034 elif sum(mat[2]) == 0:
1035 normal = mathutils.Vector((0.0, 0.0, 1.0))
1036 if not normal:
1037 # warning! this is different from .normalize()
1038 itermax = 500
1039 iter = 0
1040 vec = mathutils.Vector((1.0, 1.0, 1.0))
1041 vec2 = (mat * vec)/(mat * vec).length
1042 while vec != vec2 and iter<itermax:
1043 iter+=1
1044 vec = vec2
1045 vec2 = mat * vec
1046 if vec2.length != 0:
1047 vec2 /= vec2.length
1048 if vec2.length == 0:
1049 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
1050 normal = vec2
1051 normals.append(normal)
1052 # have plane normals face in the same direction (maximum angle: 90 degrees)
1053 if ((center1 + normals[0]) - center2).length < \
1054 ((center1 - normals[0]) - center2).length:
1055 normals[0].negate()
1056 if ((center2 + normals[1]) - center1).length > \
1057 ((center2 - normals[1]) - center1).length:
1058 normals[1].negate()
1060 # rotation matrix, representing the difference between the plane normals
1061 axis = normals[0].cross(normals[1])
1062 axis = mathutils.Vector([loc if abs(loc) > 1e-8 else 0 for loc in axis])
1063 if axis.angle(mathutils.Vector((0, 0, 1)), 0) > 1.5707964:
1064 axis.negate()
1065 angle = normals[0].dot(normals[1])
1066 rotation_matrix = mathutils.Matrix.Rotation(angle, 4, axis)
1068 # if circular, rotate loops so they are aligned
1069 if circular:
1070 # make sure loop1 is the circular one (or both are circular)
1071 if loop2_circular and not loop1_circular:
1072 loop1_circular, loop2_circular = True, False
1073 loop1, loop2 = loop2, loop1
1075 # match start vertex of loop1 with loop2
1076 target_vector = bm.verts[loop2[0]].co - center2
1077 dif_angles = [[(rotation_matrix * (bm.verts[vertex].co - center1)
1078 ).angle(target_vector, 0), False, i] for
1079 i, vertex in enumerate(loop1)]
1080 dif_angles.sort()
1081 if len(loop1) != len(loop2):
1082 angle_limit = dif_angles[0][0] * 1.2 # 20% margin
1083 dif_angles = [[(bm.verts[loop2[0]].co - \
1084 bm.verts[loop1[index]].co).length, angle, index] for \
1085 angle, distance, index in dif_angles if angle <= angle_limit]
1086 dif_angles.sort()
1087 loop1 = loop1[dif_angles[0][2]:] + loop1[:dif_angles[0][2]]
1089 # have both loops face the same way
1090 if normal_plurity and not circular:
1091 second_to_first, second_to_second, second_to_last = \
1092 [(bm.verts[loop1[1]].co - center1).\
1093 angle(bm.verts[loop2[i]].co - center2) for i in [0, 1, -1]]
1094 last_to_first, last_to_second = [(bm.verts[loop1[-1]].co - \
1095 center1).angle(bm.verts[loop2[i]].co - center2) for \
1096 i in [0, 1]]
1097 if (min(last_to_first, last_to_second)*1.1 < min(second_to_first, \
1098 second_to_second)) or (loop2_circular and second_to_last*1.1 < \
1099 min(second_to_first, second_to_second)):
1100 loop1.reverse()
1101 if circular:
1102 loop1 = [loop1[-1]] + loop1[:-1]
1103 else:
1104 angle = (bm.verts[loop1[0]].co - center1).\
1105 cross(bm.verts[loop1[1]].co - center1).angle(normals[0], 0)
1106 target_angle = (bm.verts[loop2[0]].co - center2).\
1107 cross(bm.verts[loop2[1]].co - center2).angle(normals[1], 0)
1108 limit = 1.5707964 # 0.5*pi, 90 degrees
1109 if not ((angle > limit and target_angle > limit) or \
1110 (angle < limit and target_angle < limit)):
1111 loop1.reverse()
1112 if circular:
1113 loop1 = [loop1[-1]] + loop1[:-1]
1114 elif normals[0].angle(normals[1]) > limit:
1115 loop1.reverse()
1116 if circular:
1117 loop1 = [loop1[-1]] + loop1[:-1]
1119 # both loops have the same length
1120 if len(loop1) == len(loop2):
1121 # manual override
1122 if twist:
1123 if abs(twist) < len(loop1):
1124 loop1 = loop1[twist:]+loop1[:twist]
1125 if reverse:
1126 loop1.reverse()
1128 lines.append([loop1[0], loop2[0]])
1129 for i in range(1, len(loop1)):
1130 lines.append([loop1[i], loop2[i]])
1132 # loops of different lengths
1133 else:
1134 # make loop1 longest loop
1135 if len(loop2) > len(loop1):
1136 loop1, loop2 = loop2, loop1
1137 loop1_circular, loop2_circular = loop2_circular, loop1_circular
1139 # manual override
1140 if twist:
1141 if abs(twist) < len(loop1):
1142 loop1 = loop1[twist:]+loop1[:twist]
1143 if reverse:
1144 loop1.reverse()
1146 # shortest angle difference doesn't always give correct start vertex
1147 if loop1_circular and not loop2_circular:
1148 shifting = 1
1149 while shifting:
1150 if len(loop1) - shifting < len(loop2):
1151 shifting = False
1152 break
1153 to_last, to_first = [(rotation_matrix *
1154 (bm.verts[loop1[-1]].co - center1)).angle((bm.\
1155 verts[loop2[i]].co - center2), 0) for i in [-1, 0]]
1156 if to_first < to_last:
1157 loop1 = [loop1[-1]] + loop1[:-1]
1158 shifting += 1
1159 else:
1160 shifting = False
1161 break
1163 # basic shortest side first
1164 if mode == 'basic':
1165 lines.append([loop1[0], loop2[0]])
1166 for i in range(1, len(loop1)):
1167 if i >= len(loop2) - 1:
1168 # triangles
1169 lines.append([loop1[i], loop2[-1]])
1170 else:
1171 # quads
1172 lines.append([loop1[i], loop2[i]])
1174 # shortest edge algorithm
1175 else: # mode == 'shortest'
1176 lines.append([loop1[0], loop2[0]])
1177 prev_vert2 = 0
1178 for i in range(len(loop1) -1):
1179 if prev_vert2 == len(loop2) - 1 and not loop2_circular:
1180 # force triangles, reached end of loop2
1181 tri, quad = 0, 1
1182 elif prev_vert2 == len(loop2) - 1 and loop2_circular:
1183 # at end of loop2, but circular, so check with first vert
1184 tri, quad = [(bm.verts[loop1[i+1]].co -
1185 bm.verts[loop2[j]].co).length
1186 for j in [prev_vert2, 0]]
1187 circle_full = 2
1188 elif len(loop1) - 1 - i == len(loop2) - 1 - prev_vert2 and \
1189 not circle_full:
1190 # force quads, otherwise won't make it to end of loop2
1191 tri, quad = 1, 0
1192 else:
1193 # calculate if tri or quad gives shortest edge
1194 tri, quad = [(bm.verts[loop1[i+1]].co -
1195 bm.verts[loop2[j]].co).length
1196 for j in range(prev_vert2, prev_vert2+2)]
1198 # triangle
1199 if tri < quad:
1200 lines.append([loop1[i+1], loop2[prev_vert2]])
1201 if circle_full == 2:
1202 circle_full = False
1203 # quad
1204 elif not circle_full:
1205 lines.append([loop1[i+1], loop2[prev_vert2+1]])
1206 prev_vert2 += 1
1207 # quad to first vertex of loop2
1208 else:
1209 lines.append([loop1[i+1], loop2[0]])
1210 prev_vert2 = 0
1211 circle_full = True
1213 # final face for circular loops
1214 if loop1_circular and loop2_circular:
1215 lines.append([loop1[0], loop2[0]])
1217 return(lines)
1220 # calculate number of segments needed
1221 def bridge_calculate_segments(bm, lines, loops, segments):
1222 # return if amount of segments is set by user
1223 if segments != 0:
1224 return segments
1226 # edge lengths
1227 average_edge_length = [(bm.verts[vertex].co - \
1228 bm.verts[loop[0][i+1]].co).length for loop in loops for \
1229 i, vertex in enumerate(loop[0][:-1])]
1230 # closing edges of circular loops
1231 average_edge_length += [(bm.verts[loop[0][-1]].co - \
1232 bm.verts[loop[0][0]].co).length for loop in loops if loop[1]]
1234 # average lengths
1235 average_edge_length = sum(average_edge_length) / len(average_edge_length)
1236 average_bridge_length = sum([(bm.verts[v1].co - \
1237 bm.verts[v2].co).length for v1, v2 in lines]) / len(lines)
1239 segments = max(1, round(average_bridge_length / average_edge_length))
1241 return(segments)
1244 # return dictionary with vertex index as key, and the normal vector as value
1245 def bridge_calculate_virtual_vertex_normals(bm, lines, loops, edge_faces,
1246 edgekey_to_edge):
1247 if not edge_faces: # interpolation isn't set to cubic
1248 return False
1250 # pity reduce() isn't one of the basic functions in python anymore
1251 def average_vector_dictionary(dic):
1252 for key, vectors in dic.items():
1253 #if type(vectors) == type([]) and len(vectors) > 1:
1254 if len(vectors) > 1:
1255 average = mathutils.Vector()
1256 for vector in vectors:
1257 average += vector
1258 average /= len(vectors)
1259 dic[key] = [average]
1260 return dic
1262 # get all edges of the loop
1263 edges = [[edgekey_to_edge[tuple(sorted([loops[j][0][i],
1264 loops[j][0][i+1]]))] for i in range(len(loops[j][0])-1)] for \
1265 j in [0,1]]
1266 edges = edges[0] + edges[1]
1267 for j in [0, 1]:
1268 if loops[j][1]: # circular
1269 edges.append(edgekey_to_edge[tuple(sorted([loops[j][0][0],
1270 loops[j][0][-1]]))])
1273 calculation based on face topology (assign edge-normals to vertices)
1275 edge_normal = face_normal x edge_vector
1276 vertex_normal = average(edge_normals)
1278 vertex_normals = dict([(vertex, []) for vertex in loops[0][0]+loops[1][0]])
1279 for edge in edges:
1280 faces = edge_faces[edgekey(edge)] # valid faces connected to edge
1282 if faces:
1283 # get edge coordinates
1284 v1, v2 = [bm.verts[edgekey(edge)[i]].co for i in [0,1]]
1285 edge_vector = v1 - v2
1286 if edge_vector.length < 1e-4:
1287 # zero-length edge, vertices at same location
1288 continue
1289 edge_center = (v1 + v2) / 2
1291 # average face coordinates, if connected to more than 1 valid face
1292 if len(faces) > 1:
1293 face_normal = mathutils.Vector()
1294 face_center = mathutils.Vector()
1295 for face in faces:
1296 face_normal += face.normal
1297 face_center += face.calc_center_median()
1298 face_normal /= len(faces)
1299 face_center /= len(faces)
1300 else:
1301 face_normal = faces[0].normal
1302 face_center = faces[0].calc_center_median()
1303 if face_normal.length < 1e-4:
1304 # faces with a surface of 0 have no face normal
1305 continue
1307 # calculate virtual edge normal
1308 edge_normal = edge_vector.cross(face_normal)
1309 edge_normal.length = 0.01
1310 if (face_center - (edge_center + edge_normal)).length > \
1311 (face_center - (edge_center - edge_normal)).length:
1312 # make normal face the correct way
1313 edge_normal.negate()
1314 edge_normal.normalize()
1315 # add virtual edge normal as entry for both vertices it connects
1316 for vertex in edgekey(edge):
1317 vertex_normals[vertex].append(edge_normal)
1320 calculation based on connection with other loop (vertex focused method)
1321 - used for vertices that aren't connected to any valid faces
1323 plane_normal = edge_vector x connection_vector
1324 vertex_normal = plane_normal x edge_vector
1326 vertices = [vertex for vertex, normal in vertex_normals.items() if not \
1327 normal]
1329 if vertices:
1330 # edge vectors connected to vertices
1331 edge_vectors = dict([[vertex, []] for vertex in vertices])
1332 for edge in edges:
1333 for v in edgekey(edge):
1334 if v in edge_vectors:
1335 edge_vector = bm.verts[edgekey(edge)[0]].co - \
1336 bm.verts[edgekey(edge)[1]].co
1337 if edge_vector.length < 1e-4:
1338 # zero-length edge, vertices at same location
1339 continue
1340 edge_vectors[v].append(edge_vector)
1342 # connection vectors between vertices of both loops
1343 connection_vectors = dict([[vertex, []] for vertex in vertices])
1344 connections = dict([[vertex, []] for vertex in vertices])
1345 for v1, v2 in lines:
1346 if v1 in connection_vectors or v2 in connection_vectors:
1347 new_vector = bm.verts[v1].co - bm.verts[v2].co
1348 if new_vector.length < 1e-4:
1349 # zero-length connection vector,
1350 # vertices in different loops at same location
1351 continue
1352 if v1 in connection_vectors:
1353 connection_vectors[v1].append(new_vector)
1354 connections[v1].append(v2)
1355 if v2 in connection_vectors:
1356 connection_vectors[v2].append(new_vector)
1357 connections[v2].append(v1)
1358 connection_vectors = average_vector_dictionary(connection_vectors)
1359 connection_vectors = dict([[vertex, vector[0]] if vector else \
1360 [vertex, []] for vertex, vector in connection_vectors.items()])
1362 for vertex, values in edge_vectors.items():
1363 # vertex normal doesn't matter, just assign a random vector to it
1364 if not connection_vectors[vertex]:
1365 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1366 continue
1368 # calculate to what location the vertex is connected,
1369 # used to determine what way to flip the normal
1370 connected_center = mathutils.Vector()
1371 for v in connections[vertex]:
1372 connected_center += bm.verts[v].co
1373 if len(connections[vertex]) > 1:
1374 connected_center /= len(connections[vertex])
1375 if len(connections[vertex]) == 0:
1376 # shouldn't be possible, but better safe than sorry
1377 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1378 continue
1380 # can't do proper calculations, because of zero-length vector
1381 if not values:
1382 if (connected_center - (bm.verts[vertex].co + \
1383 connection_vectors[vertex])).length < (connected_center - \
1384 (bm.verts[vertex].co - connection_vectors[vertex])).\
1385 length:
1386 connection_vectors[vertex].negate()
1387 vertex_normals[vertex] = [connection_vectors[vertex].\
1388 normalized()]
1389 continue
1391 # calculate vertex normals using edge-vectors,
1392 # connection-vectors and the derived plane normal
1393 for edge_vector in values:
1394 plane_normal = edge_vector.cross(connection_vectors[vertex])
1395 vertex_normal = edge_vector.cross(plane_normal)
1396 vertex_normal.length = 0.1
1397 if (connected_center - (bm.verts[vertex].co + \
1398 vertex_normal)).length < (connected_center - \
1399 (bm.verts[vertex].co - vertex_normal)).length:
1400 # make normal face the correct way
1401 vertex_normal.negate()
1402 vertex_normal.normalize()
1403 vertex_normals[vertex].append(vertex_normal)
1405 # average virtual vertex normals, based on all edges it's connected to
1406 vertex_normals = average_vector_dictionary(vertex_normals)
1407 vertex_normals = dict([[vertex, vector[0]] for vertex, vector in \
1408 vertex_normals.items()])
1410 return(vertex_normals)
1413 # add vertices to mesh
1414 def bridge_create_vertices(bm, vertices):
1415 for i in range(len(vertices)):
1416 bm.verts.new(vertices[i])
1417 bm.verts.ensure_lookup_table()
1420 # add faces to mesh
1421 def bridge_create_faces(object, bm, faces, twist):
1422 # have the normal point the correct way
1423 if twist < 0:
1424 [face.reverse() for face in faces]
1425 faces = [face[2:]+face[:2] if face[0]==face[1] else face for \
1426 face in faces]
1428 # eekadoodle prevention
1429 for i in range(len(faces)):
1430 if not faces[i][-1]:
1431 if faces[i][0] == faces[i][-1]:
1432 faces[i] = [faces[i][1], faces[i][2], faces[i][3], faces[i][1]]
1433 else:
1434 faces[i] = [faces[i][-1]] + faces[i][:-1]
1435 # result of converting from pre-bmesh period
1436 if faces[i][-1] == faces[i][-2]:
1437 faces[i] = faces[i][:-1]
1439 new_faces = []
1440 for i in range(len(faces)):
1441 new_faces.append(bm.faces.new([bm.verts[v] for v in faces[i]]))
1442 bm.normal_update()
1443 object.data.update(calc_edges=True) # calc_edges prevents memory-corruption
1445 bm.verts.ensure_lookup_table()
1446 bm.edges.ensure_lookup_table()
1447 bm.faces.ensure_lookup_table()
1449 return(new_faces)
1452 # calculate input loops
1453 def bridge_get_input(bm):
1454 # create list of internal edges, which should be skipped
1455 eks_of_selected_faces = [item for sublist in [face_edgekeys(face) for \
1456 face in bm.faces if face.select and not face.hide] for item in sublist]
1457 edge_count = {}
1458 for ek in eks_of_selected_faces:
1459 if ek in edge_count:
1460 edge_count[ek] += 1
1461 else:
1462 edge_count[ek] = 1
1463 internal_edges = [ek for ek in edge_count if edge_count[ek] > 1]
1465 # sort correct edges into loops
1466 selected_edges = [edgekey(edge) for edge in bm.edges if edge.select \
1467 and not edge.hide and edgekey(edge) not in internal_edges]
1468 loops = get_connected_selections(selected_edges)
1470 return(loops)
1473 # return values needed by the bridge operator
1474 def bridge_initialise(bm, interpolation):
1475 if interpolation == 'cubic':
1476 # dict with edge-key as key and list of connected valid faces as value
1477 face_blacklist = [face.index for face in bm.faces if face.select or \
1478 face.hide]
1479 edge_faces = dict([[edgekey(edge), []] for edge in bm.edges if not \
1480 edge.hide])
1481 for face in bm.faces:
1482 if face.index in face_blacklist:
1483 continue
1484 for key in face_edgekeys(face):
1485 edge_faces[key].append(face)
1486 # dictionary with the edge-key as key and edge as value
1487 edgekey_to_edge = dict([[edgekey(edge), edge] for edge in \
1488 bm.edges if edge.select and not edge.hide])
1489 else:
1490 edge_faces = False
1491 edgekey_to_edge = False
1493 # selected faces input
1494 old_selected_faces = [face.index for face in bm.faces if face.select \
1495 and not face.hide]
1497 # find out if faces created by bridging should be smoothed
1498 smooth = False
1499 if bm.faces:
1500 if sum([face.smooth for face in bm.faces])/len(bm.faces) \
1501 >= 0.5:
1502 smooth = True
1504 return(edge_faces, edgekey_to_edge, old_selected_faces, smooth)
1507 # return a string with the input method
1508 def bridge_input_method(loft, loft_loop):
1509 method = ""
1510 if loft:
1511 if loft_loop:
1512 method = "Loft loop"
1513 else:
1514 method = "Loft no-loop"
1515 else:
1516 method = "Bridge"
1518 return(method)
1521 # match up loops in pairs, used for multi-input bridging
1522 def bridge_match_loops(bm, loops):
1523 # calculate average loop normals and centers
1524 normals = []
1525 centers = []
1526 for vertices, circular in loops:
1527 normal = mathutils.Vector()
1528 center = mathutils.Vector()
1529 for vertex in vertices:
1530 normal += bm.verts[vertex].normal
1531 center += bm.verts[vertex].co
1532 normals.append(normal / len(vertices) / 10)
1533 centers.append(center / len(vertices))
1535 # possible matches if loop normals are faced towards the center
1536 # of the other loop
1537 matches = dict([[i, []] for i in range(len(loops))])
1538 matches_amount = 0
1539 for i in range(len(loops) + 1):
1540 for j in range(i+1, len(loops)):
1541 if (centers[i] - centers[j]).length > (centers[i] - (centers[j] \
1542 + normals[j])).length and (centers[j] - centers[i]).length > \
1543 (centers[j] - (centers[i] + normals[i])).length:
1544 matches_amount += 1
1545 matches[i].append([(centers[i] - centers[j]).length, i, j])
1546 matches[j].append([(centers[i] - centers[j]).length, j, i])
1547 # if no loops face each other, just make matches between all the loops
1548 if matches_amount == 0:
1549 for i in range(len(loops) + 1):
1550 for j in range(i+1, len(loops)):
1551 matches[i].append([(centers[i] - centers[j]).length, i, j])
1552 matches[j].append([(centers[i] - centers[j]).length, j, i])
1553 for key, value in matches.items():
1554 value.sort()
1556 # matches based on distance between centers and number of vertices in loops
1557 new_order = []
1558 for loop_index in range(len(loops)):
1559 if loop_index in new_order:
1560 continue
1561 loop_matches = matches[loop_index]
1562 if not loop_matches:
1563 continue
1564 shortest_distance = loop_matches[0][0]
1565 shortest_distance *= 1.1
1566 loop_matches = [[abs(len(loops[loop_index][0]) - \
1567 len(loops[loop[2]][0])), loop[0], loop[1], loop[2]] for loop in \
1568 loop_matches if loop[0] < shortest_distance]
1569 loop_matches.sort()
1570 for match in loop_matches:
1571 if match[3] not in new_order:
1572 new_order += [loop_index, match[3]]
1573 break
1575 # reorder loops based on matches
1576 if len(new_order) >= 2:
1577 loops = [loops[i] for i in new_order]
1579 return(loops)
1582 # remove old_selected_faces
1583 def bridge_remove_internal_faces(bm, old_selected_faces):
1584 # collect bmesh faces and internal bmesh edges
1585 remove_faces = [bm.faces[face] for face in old_selected_faces]
1586 edges = collections.Counter([edge.index for face in remove_faces for \
1587 edge in face.edges])
1588 remove_edges = [bm.edges[edge] for edge in edges if edges[edge] > 1]
1590 # remove internal faces and edges
1591 for face in remove_faces:
1592 bm.faces.remove(face)
1593 for edge in remove_edges:
1594 bm.edges.remove(edge)
1596 bm.faces.ensure_lookup_table()
1597 bm.edges.ensure_lookup_table()
1598 bm.verts.ensure_lookup_table()
1601 # update list of internal faces that are flagged for removal
1602 def bridge_save_unused_faces(bm, old_selected_faces, loops):
1603 # key: vertex index, value: lists of selected faces using it
1605 vertex_to_face = dict([[i, []] for i in range(len(bm.verts))])
1606 [[vertex_to_face[vertex.index].append(face) for vertex in \
1607 bm.faces[face].verts] for face in old_selected_faces]
1609 # group selected faces that are connected
1610 groups = []
1611 grouped_faces = []
1612 for face in old_selected_faces:
1613 if face in grouped_faces:
1614 continue
1615 grouped_faces.append(face)
1616 group = [face]
1617 new_faces = [face]
1618 while new_faces:
1619 grow_face = new_faces[0]
1620 for vertex in bm.faces[grow_face].verts:
1621 vertex_face_group = [face for face in vertex_to_face[\
1622 vertex.index] if face not in grouped_faces]
1623 new_faces += vertex_face_group
1624 grouped_faces += vertex_face_group
1625 group += vertex_face_group
1626 new_faces.pop(0)
1627 groups.append(group)
1629 # key: vertex index, value: True/False (is it in a loop that is used)
1630 used_vertices = dict([[i, 0] for i in range(len(bm.verts))])
1631 for loop in loops:
1632 for vertex in loop[0]:
1633 used_vertices[vertex] = True
1635 # check if group is bridged, if not remove faces from internal faces list
1636 for group in groups:
1637 used = False
1638 for face in group:
1639 if used:
1640 break
1641 for vertex in bm.faces[face].verts:
1642 if used_vertices[vertex.index]:
1643 used = True
1644 break
1645 if not used:
1646 for face in group:
1647 old_selected_faces.remove(face)
1650 # add the newly created faces to the selection
1651 def bridge_select_new_faces(new_faces, smooth):
1652 for face in new_faces:
1653 face.select_set(True)
1654 face.smooth = smooth
1657 # sort loops, so they are connected in the correct order when lofting
1658 def bridge_sort_loops(bm, loops, loft_loop):
1659 # simplify loops to single points, and prepare for pathfinding
1660 x, y, z = [[sum([bm.verts[i].co[j] for i in loop[0]]) / \
1661 len(loop[0]) for loop in loops] for j in range(3)]
1662 nodes = [mathutils.Vector((x[i], y[i], z[i])) for i in range(len(loops))]
1664 active_node = 0
1665 open = [i for i in range(1, len(loops))]
1666 path = [[0,0]]
1667 # connect node to path, that is shortest to active_node
1668 while len(open) > 0:
1669 distances = [(nodes[active_node] - nodes[i]).length for i in open]
1670 active_node = open[distances.index(min(distances))]
1671 open.remove(active_node)
1672 path.append([active_node, min(distances)])
1673 # check if we didn't start in the middle of the path
1674 for i in range(2, len(path)):
1675 if (nodes[path[i][0]]-nodes[0]).length < path[i][1]:
1676 temp = path[:i]
1677 path.reverse()
1678 path = path[:-i] + temp
1679 break
1681 # reorder loops
1682 loops = [loops[i[0]] for i in path]
1683 # if requested, duplicate first loop at last position, so loft can loop
1684 if loft_loop:
1685 loops = loops + [loops[0]]
1687 return(loops)
1690 # remapping old indices to new position in list
1691 def bridge_update_old_selection(bm, old_selected_faces):
1692 #old_indices = old_selected_faces[:]
1693 #old_selected_faces = []
1694 #for i, face in enumerate(bm.faces):
1695 # if face.index in old_indices:
1696 # old_selected_faces.append(i)
1698 old_selected_faces = [i for i, face in enumerate(bm.faces) if face.index \
1699 in old_selected_faces]
1701 return(old_selected_faces)
1704 ##########################################
1705 ####### Circle functions #################
1706 ##########################################
1708 # convert 3d coordinates to 2d coordinates on plane
1709 def circle_3d_to_2d(bm_mod, loop, com, normal):
1710 # project vertices onto the plane
1711 verts = [bm_mod.verts[v] for v in loop[0]]
1712 verts_projected = [[v.co - (v.co - com).dot(normal) * normal, v.index]
1713 for v in verts]
1715 # calculate two vectors (p and q) along the plane
1716 m = mathutils.Vector((normal[0] + 1.0, normal[1], normal[2]))
1717 p = m - (m.dot(normal) * normal)
1718 if p.dot(p) < 1e-6:
1719 m = mathutils.Vector((normal[0], normal[1] + 1.0, normal[2]))
1720 p = m - (m.dot(normal) * normal)
1721 q = p.cross(normal)
1723 # change to 2d coordinates using perpendicular projection
1724 locs_2d = []
1725 for loc, vert in verts_projected:
1726 vloc = loc - com
1727 x = p.dot(vloc) / p.dot(p)
1728 y = q.dot(vloc) / q.dot(q)
1729 locs_2d.append([x, y, vert])
1731 return(locs_2d, p, q)
1734 # calculate a best-fit circle to the 2d locations on the plane
1735 def circle_calculate_best_fit(locs_2d):
1736 # initial guess
1737 x0 = 0.0
1738 y0 = 0.0
1739 r = 1.0
1741 # calculate center and radius (non-linear least squares solution)
1742 for iter in range(500):
1743 jmat = []
1744 k = []
1745 for v in locs_2d:
1746 d = (v[0]**2-2.0*x0*v[0]+v[1]**2-2.0*y0*v[1]+x0**2+y0**2)**0.5
1747 jmat.append([(x0-v[0])/d, (y0-v[1])/d, -1.0])
1748 k.append(-(((v[0]-x0)**2+(v[1]-y0)**2)**0.5-r))
1749 jmat2 = mathutils.Matrix(((0.0, 0.0, 0.0),
1750 (0.0, 0.0, 0.0),
1751 (0.0, 0.0, 0.0),
1753 k2 = mathutils.Vector((0.0, 0.0, 0.0))
1754 for i in range(len(jmat)):
1755 k2 += mathutils.Vector(jmat[i])*k[i]
1756 jmat2[0][0] += jmat[i][0]**2
1757 jmat2[1][0] += jmat[i][0]*jmat[i][1]
1758 jmat2[2][0] += jmat[i][0]*jmat[i][2]
1759 jmat2[1][1] += jmat[i][1]**2
1760 jmat2[2][1] += jmat[i][1]*jmat[i][2]
1761 jmat2[2][2] += jmat[i][2]**2
1762 jmat2[0][1] = jmat2[1][0]
1763 jmat2[0][2] = jmat2[2][0]
1764 jmat2[1][2] = jmat2[2][1]
1765 try:
1766 jmat2.invert()
1767 except:
1768 pass
1769 dx0, dy0, dr = jmat2 * k2
1770 x0 += dx0
1771 y0 += dy0
1772 r += dr
1773 # stop iterating if we're close enough to optimal solution
1774 if abs(dx0)<1e-6 and abs(dy0)<1e-6 and abs(dr)<1e-6:
1775 break
1777 # return center of circle and radius
1778 return(x0, y0, r)
1781 # calculate circle so no vertices have to be moved away from the center
1782 def circle_calculate_min_fit(locs_2d):
1783 # center of circle
1784 x0 = (min([i[0] for i in locs_2d])+max([i[0] for i in locs_2d]))/2.0
1785 y0 = (min([i[1] for i in locs_2d])+max([i[1] for i in locs_2d]))/2.0
1786 center = mathutils.Vector([x0, y0])
1787 # radius of circle
1788 r = min([(mathutils.Vector([i[0], i[1]])-center).length for i in locs_2d])
1790 # return center of circle and radius
1791 return(x0, y0, r)
1794 # calculate the new locations of the vertices that need to be moved
1795 def circle_calculate_verts(flatten, bm_mod, locs_2d, com, p, q, normal):
1796 # changing 2d coordinates back to 3d coordinates
1797 locs_3d = []
1798 for loc in locs_2d:
1799 locs_3d.append([loc[2], loc[0]*p + loc[1]*q + com])
1801 if flatten: # flat circle
1802 return(locs_3d)
1804 else: # project the locations on the existing mesh
1805 vert_edges = dict_vert_edges(bm_mod)
1806 vert_faces = dict_vert_faces(bm_mod)
1807 faces = [f for f in bm_mod.faces if not f.hide]
1808 rays = [normal, -normal]
1809 new_locs = []
1810 for loc in locs_3d:
1811 projection = False
1812 if bm_mod.verts[loc[0]].co == loc[1]: # vertex hasn't moved
1813 projection = loc[1]
1814 else:
1815 dif = normal.angle(loc[1]-bm_mod.verts[loc[0]].co)
1816 if -1e-6 < dif < 1e-6 or math.pi-1e-6 < dif < math.pi+1e-6:
1817 # original location is already along projection normal
1818 projection = bm_mod.verts[loc[0]].co
1819 else:
1820 # quick search through adjacent faces
1821 for face in vert_faces[loc[0]]:
1822 verts = [v.co for v in bm_mod.faces[face].verts]
1823 if len(verts) == 3: # triangle
1824 v1, v2, v3 = verts
1825 v4 = False
1826 else: # assume quad
1827 v1, v2, v3, v4 = verts[:4]
1828 for ray in rays:
1829 intersect = mathutils.geometry.\
1830 intersect_ray_tri(v1, v2, v3, ray, loc[1])
1831 if intersect:
1832 projection = intersect
1833 break
1834 elif v4:
1835 intersect = mathutils.geometry.\
1836 intersect_ray_tri(v1, v3, v4, ray, loc[1])
1837 if intersect:
1838 projection = intersect
1839 break
1840 if projection:
1841 break
1842 if not projection:
1843 # check if projection is on adjacent edges
1844 for edgekey in vert_edges[loc[0]]:
1845 line1 = bm_mod.verts[edgekey[0]].co
1846 line2 = bm_mod.verts[edgekey[1]].co
1847 intersect, dist = mathutils.geometry.intersect_point_line(\
1848 loc[1], line1, line2)
1849 if 1e-6 < dist < 1 - 1e-6:
1850 projection = intersect
1851 break
1852 if not projection:
1853 # full search through the entire mesh
1854 hits = []
1855 for face in faces:
1856 verts = [v.co for v in face.verts]
1857 if len(verts) == 3: # triangle
1858 v1, v2, v3 = verts
1859 v4 = False
1860 else: # assume quad
1861 v1, v2, v3, v4 = verts[:4]
1862 for ray in rays:
1863 intersect = mathutils.geometry.intersect_ray_tri(\
1864 v1, v2, v3, ray, loc[1])
1865 if intersect:
1866 hits.append([(loc[1] - intersect).length,
1867 intersect])
1868 break
1869 elif v4:
1870 intersect = mathutils.geometry.intersect_ray_tri(\
1871 v1, v3, v4, ray, loc[1])
1872 if intersect:
1873 hits.append([(loc[1] - intersect).length,
1874 intersect])
1875 break
1876 if len(hits) >= 1:
1877 # if more than 1 hit with mesh, closest hit is new loc
1878 hits.sort()
1879 projection = hits[0][1]
1880 if not projection:
1881 # nothing to project on, remain at flat location
1882 projection = loc[1]
1883 new_locs.append([loc[0], projection])
1885 # return new positions of projected circle
1886 return(new_locs)
1889 # check loops and only return valid ones
1890 def circle_check_loops(single_loops, loops, mapping, bm_mod):
1891 valid_single_loops = {}
1892 valid_loops = []
1893 for i, [loop, circular] in enumerate(loops):
1894 # loop needs to have at least 3 vertices
1895 if len(loop) < 3:
1896 continue
1897 # loop needs at least 1 vertex in the original, non-mirrored mesh
1898 if mapping:
1899 all_virtual = True
1900 for vert in loop:
1901 if mapping[vert] > -1:
1902 all_virtual = False
1903 break
1904 if all_virtual:
1905 continue
1906 # loop has to be non-collinear
1907 collinear = True
1908 loc0 = mathutils.Vector(bm_mod.verts[loop[0]].co[:])
1909 loc1 = mathutils.Vector(bm_mod.verts[loop[1]].co[:])
1910 for v in loop[2:]:
1911 locn = mathutils.Vector(bm_mod.verts[v].co[:])
1912 if loc0 == loc1 or loc1 == locn:
1913 loc0 = loc1
1914 loc1 = locn
1915 continue
1916 d1 = loc1-loc0
1917 d2 = locn-loc1
1918 if -1e-6 < d1.angle(d2, 0) < 1e-6:
1919 loc0 = loc1
1920 loc1 = locn
1921 continue
1922 collinear = False
1923 break
1924 if collinear:
1925 continue
1926 # passed all tests, loop is valid
1927 valid_loops.append([loop, circular])
1928 valid_single_loops[len(valid_loops)-1] = single_loops[i]
1930 return(valid_single_loops, valid_loops)
1933 # calculate the location of single input vertices that need to be flattened
1934 def circle_flatten_singles(bm_mod, com, p, q, normal, single_loop):
1935 new_locs = []
1936 for vert in single_loop:
1937 loc = mathutils.Vector(bm_mod.verts[vert].co[:])
1938 new_locs.append([vert, loc - (loc-com).dot(normal)*normal])
1940 return(new_locs)
1943 # calculate input loops
1944 def circle_get_input(object, bm, scene):
1945 # get mesh with modifiers applied
1946 derived, bm_mod = get_derived_bmesh(object, bm, scene)
1948 # create list of edge-keys based on selection state
1949 faces = False
1950 for face in bm.faces:
1951 if face.select and not face.hide:
1952 faces = True
1953 break
1954 if faces:
1955 # get selected, non-hidden , non-internal edge-keys
1956 eks_selected = [key for keys in [face_edgekeys(face) for face in \
1957 bm_mod.faces if face.select and not face.hide] for key in keys]
1958 edge_count = {}
1959 for ek in eks_selected:
1960 if ek in edge_count:
1961 edge_count[ek] += 1
1962 else:
1963 edge_count[ek] = 1
1964 edge_keys = [edgekey(edge) for edge in bm_mod.edges if edge.select \
1965 and not edge.hide and edge_count.get(edgekey(edge), 1)==1]
1966 else:
1967 # no faces, so no internal edges either
1968 edge_keys = [edgekey(edge) for edge in bm_mod.edges if edge.select \
1969 and not edge.hide]
1971 # add edge-keys around single vertices
1972 verts_connected = dict([[vert, 1] for edge in [edge for edge in \
1973 bm_mod.edges if edge.select and not edge.hide] for vert in \
1974 edgekey(edge)])
1975 single_vertices = [vert.index for vert in bm_mod.verts if \
1976 vert.select and not vert.hide and not \
1977 verts_connected.get(vert.index, False)]
1979 if single_vertices and len(bm.faces)>0:
1980 vert_to_single = dict([[v.index, []] for v in bm_mod.verts \
1981 if not v.hide])
1982 for face in [face for face in bm_mod.faces if not face.select \
1983 and not face.hide]:
1984 for vert in face.verts:
1985 vert = vert.index
1986 if vert in single_vertices:
1987 for ek in face_edgekeys(face):
1988 if not vert in ek:
1989 edge_keys.append(ek)
1990 if vert not in vert_to_single[ek[0]]:
1991 vert_to_single[ek[0]].append(vert)
1992 if vert not in vert_to_single[ek[1]]:
1993 vert_to_single[ek[1]].append(vert)
1994 break
1996 # sort edge-keys into loops
1997 loops = get_connected_selections(edge_keys)
1999 # find out to which loops the single vertices belong
2000 single_loops = dict([[i, []] for i in range(len(loops))])
2001 if single_vertices and len(bm.faces)>0:
2002 for i, [loop, circular] in enumerate(loops):
2003 for vert in loop:
2004 if vert_to_single[vert]:
2005 for single in vert_to_single[vert]:
2006 if single not in single_loops[i]:
2007 single_loops[i].append(single)
2009 return(derived, bm_mod, single_vertices, single_loops, loops)
2012 # recalculate positions based on the influence of the circle shape
2013 def circle_influence_locs(locs_2d, new_locs_2d, influence):
2014 for i in range(len(locs_2d)):
2015 oldx, oldy, j = locs_2d[i]
2016 newx, newy, k = new_locs_2d[i]
2017 altx = newx*(influence/100)+ oldx*((100-influence)/100)
2018 alty = newy*(influence/100)+ oldy*((100-influence)/100)
2019 locs_2d[i] = [altx, alty, j]
2021 return(locs_2d)
2024 # project 2d locations on circle, respecting distance relations between verts
2025 def circle_project_non_regular(locs_2d, x0, y0, r):
2026 for i in range(len(locs_2d)):
2027 x, y, j = locs_2d[i]
2028 loc = mathutils.Vector([x-x0, y-y0])
2029 loc.length = r
2030 locs_2d[i] = [loc[0], loc[1], j]
2032 return(locs_2d)
2035 # project 2d locations on circle, with equal distance between all vertices
2036 def circle_project_regular(locs_2d, x0, y0, r):
2037 # find offset angle and circling direction
2038 x, y, i = locs_2d[0]
2039 loc = mathutils.Vector([x-x0, y-y0])
2040 loc.length = r
2041 offset_angle = loc.angle(mathutils.Vector([1.0, 0.0]), 0.0)
2042 loca = mathutils.Vector([x-x0, y-y0, 0.0])
2043 if loc[1] < -1e-6:
2044 offset_angle *= -1
2045 x, y, j = locs_2d[1]
2046 locb = mathutils.Vector([x-x0, y-y0, 0.0])
2047 if loca.cross(locb)[2] >= 0:
2048 ccw = 1
2049 else:
2050 ccw = -1
2051 # distribute vertices along the circle
2052 for i in range(len(locs_2d)):
2053 t = offset_angle + ccw * (i / len(locs_2d) * 2 * math.pi)
2054 x = math.cos(t) * r
2055 y = math.sin(t) * r
2056 locs_2d[i] = [x, y, locs_2d[i][2]]
2058 return(locs_2d)
2061 # shift loop, so the first vertex is closest to the center
2062 def circle_shift_loop(bm_mod, loop, com):
2063 verts, circular = loop
2064 distances = [[(bm_mod.verts[vert].co - com).length, i] \
2065 for i, vert in enumerate(verts)]
2066 distances.sort()
2067 shift = distances[0][1]
2068 loop = [verts[shift:] + verts[:shift], circular]
2070 return(loop)
2073 ##########################################
2074 ####### Curve functions ##################
2075 ##########################################
2077 # create lists with knots and points, all correctly sorted
2078 def curve_calculate_knots(loop, verts_selected):
2079 knots = [v for v in loop[0] if v in verts_selected]
2080 points = loop[0][:]
2081 # circular loop, potential for weird splines
2082 if loop[1]:
2083 offset = int(len(loop[0]) / 4)
2084 kpos = []
2085 for k in knots:
2086 kpos.append(loop[0].index(k))
2087 kdif = []
2088 for i in range(len(kpos) - 1):
2089 kdif.append(kpos[i+1] - kpos[i])
2090 kdif.append(len(loop[0]) - kpos[-1] + kpos[0])
2091 kadd = []
2092 for k in kdif:
2093 if k > 2 * offset:
2094 kadd.append([kdif.index(k), True])
2095 # next 2 lines are optional, they insert
2096 # an extra control point in small gaps
2097 #elif k > offset:
2098 # kadd.append([kdif.index(k), False])
2099 kins = []
2100 krot = False
2101 for k in kadd: # extra knots to be added
2102 if k[1]: # big gap (break circular spline)
2103 kpos = loop[0].index(knots[k[0]]) + offset
2104 if kpos > len(loop[0]) - 1:
2105 kpos -= len(loop[0])
2106 kins.append([knots[k[0]], loop[0][kpos]])
2107 kpos2 = k[0] + 1
2108 if kpos2 > len(knots)-1:
2109 kpos2 -= len(knots)
2110 kpos2 = loop[0].index(knots[kpos2]) - offset
2111 if kpos2 < 0:
2112 kpos2 += len(loop[0])
2113 kins.append([loop[0][kpos], loop[0][kpos2]])
2114 krot = loop[0][kpos2]
2115 else: # small gap (keep circular spline)
2116 k1 = loop[0].index(knots[k[0]])
2117 k2 = k[0] + 1
2118 if k2 > len(knots)-1:
2119 k2 -= len(knots)
2120 k2 = loop[0].index(knots[k2])
2121 if k2 < k1:
2122 dif = len(loop[0]) - 1 - k1 + k2
2123 else:
2124 dif = k2 - k1
2125 kn = k1 + int(dif/2)
2126 if kn > len(loop[0]) - 1:
2127 kn -= len(loop[0])
2128 kins.append([loop[0][k1], loop[0][kn]])
2129 for j in kins: # insert new knots
2130 knots.insert(knots.index(j[0]) + 1, j[1])
2131 if not krot: # circular loop
2132 knots.append(knots[0])
2133 points = loop[0][loop[0].index(knots[0]):]
2134 points += loop[0][0:loop[0].index(knots[0]) + 1]
2135 else: # non-circular loop (broken by script)
2136 krot = knots.index(krot)
2137 knots = knots[krot:] + knots[0:krot]
2138 if loop[0].index(knots[0]) > loop[0].index(knots[-1]):
2139 points = loop[0][loop[0].index(knots[0]):]
2140 points += loop[0][0:loop[0].index(knots[-1])+1]
2141 else:
2142 points = loop[0][loop[0].index(knots[0]):\
2143 loop[0].index(knots[-1]) + 1]
2144 # non-circular loop, add first and last point as knots
2145 else:
2146 if loop[0][0] not in knots:
2147 knots.insert(0, loop[0][0])
2148 if loop[0][-1] not in knots:
2149 knots.append(loop[0][-1])
2151 return(knots, points)
2154 # calculate relative positions compared to first knot
2155 def curve_calculate_t(bm_mod, knots, points, pknots, regular, circular):
2156 tpoints = []
2157 loc_prev = False
2158 len_total = 0
2160 for p in points:
2161 if p in knots:
2162 loc = pknots[knots.index(p)] # use projected knot location
2163 else:
2164 loc = mathutils.Vector(bm_mod.verts[p].co[:])
2165 if not loc_prev:
2166 loc_prev = loc
2167 len_total += (loc-loc_prev).length
2168 tpoints.append(len_total)
2169 loc_prev = loc
2170 tknots = []
2171 for p in points:
2172 if p in knots:
2173 tknots.append(tpoints[points.index(p)])
2174 if circular:
2175 tknots[-1] = tpoints[-1]
2177 # regular option
2178 if regular:
2179 tpoints_average = tpoints[-1] / (len(tpoints) - 1)
2180 for i in range(1, len(tpoints) - 1):
2181 tpoints[i] = i * tpoints_average
2182 for i in range(len(knots)):
2183 tknots[i] = tpoints[points.index(knots[i])]
2184 if circular:
2185 tknots[-1] = tpoints[-1]
2187 return(tknots, tpoints)
2190 # change the location of non-selected points to their place on the spline
2191 def curve_calculate_vertices(bm_mod, knots, tknots, points, tpoints, splines,
2192 interpolation, restriction):
2193 newlocs = {}
2194 move = []
2196 for p in points:
2197 if p in knots:
2198 continue
2199 m = tpoints[points.index(p)]
2200 if m in tknots:
2201 n = tknots.index(m)
2202 else:
2203 t = tknots[:]
2204 t.append(m)
2205 t.sort()
2206 n = t.index(m) - 1
2207 if n > len(splines) - 1:
2208 n = len(splines) - 1
2209 elif n < 0:
2210 n = 0
2212 if interpolation == 'cubic':
2213 ax, bx, cx, dx, tx = splines[n][0]
2214 x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
2215 ay, by, cy, dy, ty = splines[n][1]
2216 y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
2217 az, bz, cz, dz, tz = splines[n][2]
2218 z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
2219 newloc = mathutils.Vector([x,y,z])
2220 else: # interpolation == 'linear'
2221 a, d, t, u = splines[n]
2222 newloc = ((m-t)/u)*d + a
2224 if restriction != 'none': # vertex movement is restricted
2225 newlocs[p] = newloc
2226 else: # set the vertex to its new location
2227 move.append([p, newloc])
2229 if restriction != 'none': # vertex movement is restricted
2230 for p in points:
2231 if p in newlocs:
2232 newloc = newlocs[p]
2233 else:
2234 move.append([p, bm_mod.verts[p].co])
2235 continue
2236 oldloc = bm_mod.verts[p].co
2237 normal = bm_mod.verts[p].normal
2238 dloc = newloc - oldloc
2239 if dloc.length < 1e-6:
2240 move.append([p, newloc])
2241 elif restriction == 'extrude': # only extrusions
2242 if dloc.angle(normal, 0) < 0.5 * math.pi + 1e-6:
2243 move.append([p, newloc])
2244 else: # restriction == 'indent' only indentations
2245 if dloc.angle(normal) > 0.5 * math.pi - 1e-6:
2246 move.append([p, newloc])
2248 return(move)
2251 # trim loops to part between first and last selected vertices (including)
2252 def curve_cut_boundaries(bm_mod, loops):
2253 cut_loops = []
2254 for loop, circular in loops:
2255 if circular:
2256 # don't cut
2257 cut_loops.append([loop, circular])
2258 continue
2259 selected = [bm_mod.verts[v].select for v in loop]
2260 first = selected.index(True)
2261 selected.reverse()
2262 last = -selected.index(True)
2263 if last == 0:
2264 cut_loops.append([loop[first:], circular])
2265 else:
2266 cut_loops.append([loop[first:last], circular])
2268 return(cut_loops)
2271 # calculate input loops
2272 def curve_get_input(object, bm, boundaries, scene):
2273 # get mesh with modifiers applied
2274 derived, bm_mod = get_derived_bmesh(object, bm, scene)
2276 # vertices that still need a loop to run through it
2277 verts_unsorted = [v.index for v in bm_mod.verts if \
2278 v.select and not v.hide]
2279 # necessary dictionaries
2280 vert_edges = dict_vert_edges(bm_mod)
2281 edge_faces = dict_edge_faces(bm_mod)
2282 correct_loops = []
2283 # find loops through each selected vertex
2284 while len(verts_unsorted) > 0:
2285 loops = curve_vertex_loops(bm_mod, verts_unsorted[0], vert_edges,
2286 edge_faces)
2287 verts_unsorted.pop(0)
2289 # check if loop is fully selected
2290 search_perpendicular = False
2291 i = -1
2292 for loop, circular in loops:
2293 i += 1
2294 selected = [v for v in loop if bm_mod.verts[v].select]
2295 if len(selected) < 2:
2296 # only one selected vertex on loop, don't use
2297 loops.pop(i)
2298 continue
2299 elif len(selected) == len(loop):
2300 search_perpendicular = loop
2301 break
2302 # entire loop is selected, find perpendicular loops
2303 if search_perpendicular:
2304 for vert in loop:
2305 if vert in verts_unsorted:
2306 verts_unsorted.remove(vert)
2307 perp_loops = curve_perpendicular_loops(bm_mod, loop,
2308 vert_edges, edge_faces)
2309 for perp_loop in perp_loops:
2310 correct_loops.append(perp_loop)
2311 # normal input
2312 else:
2313 for loop, circular in loops:
2314 correct_loops.append([loop, circular])
2316 # boundaries option
2317 if boundaries:
2318 correct_loops = curve_cut_boundaries(bm_mod, correct_loops)
2320 return(derived, bm_mod, correct_loops)
2323 # return all loops that are perpendicular to the given one
2324 def curve_perpendicular_loops(bm_mod, start_loop, vert_edges, edge_faces):
2325 # find perpendicular loops
2326 perp_loops = []
2327 for start_vert in start_loop:
2328 loops = curve_vertex_loops(bm_mod, start_vert, vert_edges,
2329 edge_faces)
2330 for loop, circular in loops:
2331 selected = [v for v in loop if bm_mod.verts[v].select]
2332 if len(selected) == len(loop):
2333 continue
2334 else:
2335 perp_loops.append([loop, circular, loop.index(start_vert)])
2337 # trim loops to same lengths
2338 shortest = [[len(loop[0]), i] for i, loop in enumerate(perp_loops)\
2339 if not loop[1]]
2340 if not shortest:
2341 # all loops are circular, not trimming
2342 return([[loop[0], loop[1]] for loop in perp_loops])
2343 else:
2344 shortest = min(shortest)
2345 shortest_start = perp_loops[shortest[1]][2]
2346 before_start = shortest_start
2347 after_start = shortest[0] - shortest_start - 1
2348 bigger_before = before_start > after_start
2349 trimmed_loops = []
2350 for loop in perp_loops:
2351 # have the loop face the same direction as the shortest one
2352 if bigger_before:
2353 if loop[2] < len(loop[0]) / 2:
2354 loop[0].reverse()
2355 loop[2] = len(loop[0]) - loop[2] - 1
2356 else:
2357 if loop[2] > len(loop[0]) / 2:
2358 loop[0].reverse()
2359 loop[2] = len(loop[0]) - loop[2] - 1
2360 # circular loops can shift, to prevent wrong trimming
2361 if loop[1]:
2362 shift = shortest_start - loop[2]
2363 if loop[2] + shift > 0 and loop[2] + shift < len(loop[0]):
2364 loop[0] = loop[0][-shift:] + loop[0][:-shift]
2365 loop[2] += shift
2366 if loop[2] < 0:
2367 loop[2] += len(loop[0])
2368 elif loop[2] > len(loop[0]) -1:
2369 loop[2] -= len(loop[0])
2370 # trim
2371 start = max(0, loop[2] - before_start)
2372 end = min(len(loop[0]), loop[2] + after_start + 1)
2373 trimmed_loops.append([loop[0][start:end], False])
2375 return(trimmed_loops)
2378 # project knots on non-selected geometry
2379 def curve_project_knots(bm_mod, verts_selected, knots, points, circular):
2380 # function to project vertex on edge
2381 def project(v1, v2, v3):
2382 # v1 and v2 are part of a line
2383 # v3 is projected onto it
2384 v2 -= v1
2385 v3 -= v1
2386 p = v3.project(v2)
2387 return(p + v1)
2389 if circular: # project all knots
2390 start = 0
2391 end = len(knots)
2392 pknots = []
2393 else: # first and last knot shouldn't be projected
2394 start = 1
2395 end = -1
2396 pknots = [mathutils.Vector(bm_mod.verts[knots[0]].co[:])]
2397 for knot in knots[start:end]:
2398 if knot in verts_selected:
2399 knot_left = knot_right = False
2400 for i in range(points.index(knot)-1, -1*len(points), -1):
2401 if points[i] not in knots:
2402 knot_left = points[i]
2403 break
2404 for i in range(points.index(knot)+1, 2*len(points)):
2405 if i > len(points) - 1:
2406 i -= len(points)
2407 if points[i] not in knots:
2408 knot_right = points[i]
2409 break
2410 if knot_left and knot_right and knot_left != knot_right:
2411 knot_left = mathutils.Vector(\
2412 bm_mod.verts[knot_left].co[:])
2413 knot_right = mathutils.Vector(\
2414 bm_mod.verts[knot_right].co[:])
2415 knot = mathutils.Vector(bm_mod.verts[knot].co[:])
2416 pknots.append(project(knot_left, knot_right, knot))
2417 else:
2418 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2419 else: # knot isn't selected, so shouldn't be changed
2420 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2421 if not circular:
2422 pknots.append(mathutils.Vector(bm_mod.verts[knots[-1]].co[:]))
2424 return(pknots)
2427 # find all loops through a given vertex
2428 def curve_vertex_loops(bm_mod, start_vert, vert_edges, edge_faces):
2429 edges_used = []
2430 loops = []
2432 for edge in vert_edges[start_vert]:
2433 if edge in edges_used:
2434 continue
2435 loop = []
2436 circular = False
2437 for vert in edge:
2438 active_faces = edge_faces[edge]
2439 new_vert = vert
2440 growing = True
2441 while growing:
2442 growing = False
2443 new_edges = vert_edges[new_vert]
2444 loop.append(new_vert)
2445 if len(loop) > 1:
2446 edges_used.append(tuple(sorted([loop[-1], loop[-2]])))
2447 if len(new_edges) < 3 or len(new_edges) > 4:
2448 # pole
2449 break
2450 else:
2451 # find next edge
2452 for new_edge in new_edges:
2453 if new_edge in edges_used:
2454 continue
2455 eliminate = False
2456 for new_face in edge_faces[new_edge]:
2457 if new_face in active_faces:
2458 eliminate = True
2459 break
2460 if eliminate:
2461 continue
2462 # found correct new edge
2463 active_faces = edge_faces[new_edge]
2464 v1, v2 = new_edge
2465 if v1 != new_vert:
2466 new_vert = v1
2467 else:
2468 new_vert = v2
2469 if new_vert == loop[0]:
2470 circular = True
2471 else:
2472 growing = True
2473 break
2474 if circular:
2475 break
2476 loop.reverse()
2477 loops.append([loop, circular])
2479 return(loops)
2482 ##########################################
2483 ####### Flatten functions ################
2484 ##########################################
2486 # sort input into loops
2487 def flatten_get_input(bm):
2488 vert_verts = dict_vert_verts([edgekey(edge) for edge in bm.edges \
2489 if edge.select and not edge.hide])
2490 verts = [v.index for v in bm.verts if v.select and not v.hide]
2492 # no connected verts, consider all selected verts as a single input
2493 if not vert_verts:
2494 return([[verts, False]])
2496 loops = []
2497 while len(verts) > 0:
2498 # start of loop
2499 loop = [verts[0]]
2500 verts.pop(0)
2501 if loop[-1] in vert_verts:
2502 to_grow = vert_verts[loop[-1]]
2503 else:
2504 to_grow = []
2505 # grow loop
2506 while len(to_grow) > 0:
2507 new_vert = to_grow[0]
2508 to_grow.pop(0)
2509 if new_vert in loop:
2510 continue
2511 loop.append(new_vert)
2512 verts.remove(new_vert)
2513 to_grow += vert_verts[new_vert]
2514 # add loop to loops
2515 loops.append([loop, False])
2517 return(loops)
2520 # calculate position of vertex projections on plane
2521 def flatten_project(bm, loop, com, normal):
2522 verts = [bm.verts[v] for v in loop[0]]
2523 verts_projected = [[v.index, mathutils.Vector(v.co[:]) - \
2524 (mathutils.Vector(v.co[:])-com).dot(normal)*normal] for v in verts]
2526 return(verts_projected)
2529 ##########################################
2530 ####### Gstretch functions ###############
2531 ##########################################
2533 # fake stroke class, used to create custom strokes if no GP data is found
2534 class gstretch_fake_stroke():
2535 def __init__(self, points):
2536 self.points = [gstretch_fake_stroke_point(p) for p in points]
2539 # fake stroke point class, used in fake strokes
2540 class gstretch_fake_stroke_point():
2541 def __init__(self, loc):
2542 self.co = loc
2545 # flips loops, if necessary, to obtain maximum alignment to stroke
2546 def gstretch_align_pairs(ls_pairs, object, bm_mod, method):
2547 # returns total distance between all verts in loop and corresponding stroke
2548 def distance_loop_stroke(loop, stroke, object, bm_mod, method):
2549 stroke_lengths_cache = False
2550 loop_length = len(loop[0])
2551 total_distance = 0
2553 if method != 'regular':
2554 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2556 for i, v_index in enumerate(loop[0]):
2557 if method == 'regular':
2558 relative_distance = i / (loop_length - 1)
2559 else:
2560 relative_distance = relative_lengths[i]
2562 loc1 = object.matrix_world * bm_mod.verts[v_index].co
2563 loc2, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2564 relative_distance, stroke_lengths_cache)
2565 total_distance += (loc2 - loc1).length
2567 return(total_distance)
2569 if ls_pairs:
2570 for (loop, stroke) in ls_pairs:
2571 total_dist = distance_loop_stroke(loop, stroke, object, bm_mod,
2572 method)
2573 loop[0].reverse()
2574 total_dist_rev = distance_loop_stroke(loop, stroke, object, bm_mod,
2575 method)
2576 if total_dist_rev > total_dist:
2577 loop[0].reverse()
2579 return(ls_pairs)
2582 # calculate vertex positions on stroke
2583 def gstretch_calculate_verts(loop, stroke, object, bm_mod, method):
2584 move = []
2585 stroke_lengths_cache = False
2586 loop_length = len(loop[0])
2587 matrix_inverse = object.matrix_world.inverted()
2589 # return intersection of line with stroke, or None
2590 def intersect_line_stroke(vec1, vec2, stroke):
2591 for i, p in enumerate(stroke.points[1:]):
2592 intersections = mathutils.geometry.intersect_line_line(vec1, vec2,
2593 p.co, stroke.points[i].co)
2594 if intersections and \
2595 (intersections[0] - intersections[1]).length < 1e-2:
2596 x, dist = mathutils.geometry.intersect_point_line(
2597 intersections[0], p.co, stroke.points[i].co)
2598 if -1 < dist < 1:
2599 return(intersections[0])
2600 return(None)
2602 if method == 'project':
2603 projection_vectors = []
2604 vert_edges = dict_vert_edges(bm_mod)
2606 for v_index in loop[0]:
2607 intersection = None
2608 for ek in vert_edges[v_index]:
2609 v1, v2 = ek
2610 v1 = bm_mod.verts[v1]
2611 v2 = bm_mod.verts[v2]
2612 if v1.select + v2.select == 1 and not v1.hide and not v2.hide:
2613 vec1 = object.matrix_world * v1.co
2614 vec2 = object.matrix_world * v2.co
2615 intersection = intersect_line_stroke(vec1, vec2, stroke)
2616 if intersection:
2617 break
2618 if not intersection:
2619 v = bm_mod.verts[v_index]
2620 intersection = intersect_line_stroke(v.co, v.co + v.normal,
2621 stroke)
2622 if intersection:
2623 move.append([v_index, matrix_inverse * intersection])
2625 else:
2626 if method == 'irregular':
2627 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2629 for i, v_index in enumerate(loop[0]):
2630 if method == 'regular':
2631 relative_distance = i / (loop_length - 1)
2632 else: # method == 'irregular'
2633 relative_distance = relative_lengths[i]
2634 loc, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2635 relative_distance, stroke_lengths_cache)
2636 loc = matrix_inverse * loc
2637 move.append([v_index, loc])
2639 return(move)
2642 # create new vertices, based on GP strokes
2643 def gstretch_create_verts(object, bm_mod, strokes, method, conversion,
2644 conversion_distance, conversion_max, conversion_min, conversion_vertices):
2645 move = []
2646 stroke_verts = []
2647 mat_world = object.matrix_world.inverted()
2648 singles = gstretch_match_single_verts(bm_mod, strokes, mat_world)
2650 for stroke in strokes:
2651 stroke_verts.append([stroke, []])
2652 min_end_point = 0
2653 if conversion == 'vertices':
2654 min_end_point = conversion_vertices
2655 end_point = conversion_vertices
2656 elif conversion == 'limit_vertices':
2657 min_end_point = conversion_min
2658 end_point = conversion_max
2659 else:
2660 end_point = len(stroke.points)
2661 # creation of new vertices at fixed user-defined distances
2662 if conversion == 'distance':
2663 method = 'project'
2664 prev_point = stroke.points[0]
2665 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * \
2666 prev_point.co))
2667 distance = 0
2668 limit = conversion_distance
2669 for point in stroke.points:
2670 new_distance = distance + (point.co - prev_point.co).length
2671 iteration = 0
2672 while new_distance > limit:
2673 to_cover = limit - distance + (limit * iteration)
2674 new_loc = prev_point.co + to_cover * \
2675 (point.co - prev_point.co).normalized()
2676 stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * \
2677 new_loc))
2678 new_distance -= limit
2679 iteration += 1
2680 distance = new_distance
2681 prev_point = point
2682 # creation of new vertices for other methods
2683 else:
2684 # add vertices at stroke points
2685 for point in stroke.points[:end_point]:
2686 stroke_verts[-1][1].append(bm_mod.verts.new(\
2687 mat_world * point.co))
2688 # add more vertices, beyond the points that are available
2689 if min_end_point > min(len(stroke.points), end_point):
2690 for i in range(min_end_point -
2691 (min(len(stroke.points), end_point))):
2692 stroke_verts[-1][1].append(bm_mod.verts.new(\
2693 mat_world * point.co))
2694 # force even spreading of points, so they are placed on stroke
2695 method = 'regular'
2696 bm_mod.verts.ensure_lookup_table()
2697 bm_mod.verts.index_update()
2698 for stroke, verts_seq in stroke_verts:
2699 if len(verts_seq) < 2:
2700 continue
2701 # spread vertices evenly over the stroke
2702 if method == 'regular':
2703 loop = [[vert.index for vert in verts_seq], False]
2704 move += gstretch_calculate_verts(loop, stroke, object, bm_mod,
2705 method)
2706 # create edges
2707 for i, vert in enumerate(verts_seq):
2708 if i > 0:
2709 bm_mod.edges.new((verts_seq[i-1], verts_seq[i]))
2710 vert.select = True
2711 # connect single vertices to the closest stroke
2712 if singles:
2713 for vert, m_stroke, point in singles:
2714 if m_stroke != stroke:
2715 continue
2716 bm_mod.edges.new((vert, verts_seq[point]))
2717 bm_mod.edges.ensure_lookup_table()
2718 bmesh.update_edit_mesh(object.data)
2720 return(move)
2723 # erases the grease pencil stroke
2724 def gstretch_erase_stroke(stroke, context):
2725 # change 3d coordinate into a stroke-point
2726 def sp(loc, context):
2727 lib = {'name': "",
2728 'pen_flip': False,
2729 'is_start': False,
2730 'location': (0, 0, 0),
2731 'mouse': (view3d_utils.location_3d_to_region_2d(\
2732 context.region, context.space_data.region_3d, loc)),
2733 'pressure': 1,
2734 'size': 0,
2735 'time': 0}
2736 return(lib)
2738 if type(stroke) != bpy.types.GPencilStroke:
2739 # fake stroke, there is nothing to delete
2740 return
2742 erase_stroke = [sp(p.co, context) for p in stroke.points]
2743 if erase_stroke:
2744 erase_stroke[0]['is_start'] = True
2745 bpy.ops.gpencil.draw(mode='ERASER', stroke=erase_stroke)
2748 # get point on stroke, given by relative distance (0.0 - 1.0)
2749 def gstretch_eval_stroke(stroke, distance, stroke_lengths_cache=False):
2750 # use cache if available
2751 if not stroke_lengths_cache:
2752 lengths = [0]
2753 for i, p in enumerate(stroke.points[1:]):
2754 lengths.append((p.co - stroke.points[i].co).length + \
2755 lengths[-1])
2756 total_length = max(lengths[-1], 1e-7)
2757 stroke_lengths_cache = [length / total_length for length in
2758 lengths]
2759 stroke_lengths = stroke_lengths_cache[:]
2761 if distance in stroke_lengths:
2762 loc = stroke.points[stroke_lengths.index(distance)].co
2763 elif distance > stroke_lengths[-1]:
2764 # should be impossible, but better safe than sorry
2765 loc = stroke.points[-1].co
2766 else:
2767 stroke_lengths.append(distance)
2768 stroke_lengths.sort()
2769 stroke_index = stroke_lengths.index(distance)
2770 interval_length = stroke_lengths[stroke_index+1] - \
2771 stroke_lengths[stroke_index-1]
2772 distance_relative = (distance - stroke_lengths[stroke_index-1]) / \
2773 interval_length
2774 interval_vector = stroke.points[stroke_index].co - \
2775 stroke.points[stroke_index-1].co
2776 loc = stroke.points[stroke_index-1].co + \
2777 distance_relative * interval_vector
2779 return(loc, stroke_lengths_cache)
2782 # create fake grease pencil strokes for the active object
2783 def gstretch_get_fake_strokes(object, bm_mod, loops):
2784 strokes = []
2785 for loop in loops:
2786 p1 = object.matrix_world * bm_mod.verts[loop[0][0]].co
2787 p2 = object.matrix_world * bm_mod.verts[loop[0][-1]].co
2788 strokes.append(gstretch_fake_stroke([p1, p2]))
2790 return(strokes)
2793 # get grease pencil strokes for the active object
2794 def gstretch_get_strokes(object, context):
2795 gp = get_grease_pencil(object, context)
2796 if not gp:
2797 return(None)
2798 layer = gp.layers.active
2799 if not layer:
2800 return(None)
2801 frame = layer.active_frame
2802 if not frame:
2803 return(None)
2804 strokes = frame.strokes
2805 if len(strokes) < 1:
2806 return(None)
2808 return(strokes)
2811 # returns a list with loop-stroke pairs
2812 def gstretch_match_loops_strokes(loops, strokes, object, bm_mod):
2813 if not loops or not strokes:
2814 return(None)
2816 # calculate loop centers
2817 loop_centers = []
2818 bm_mod.verts.ensure_lookup_table()
2819 for loop in loops:
2820 center = mathutils.Vector()
2821 for v_index in loop[0]:
2822 center += bm_mod.verts[v_index].co
2823 center /= len(loop[0])
2824 center = object.matrix_world * center
2825 loop_centers.append([center, loop])
2827 # calculate stroke centers
2828 stroke_centers = []
2829 for stroke in strokes:
2830 center = mathutils.Vector()
2831 for p in stroke.points:
2832 center += p.co
2833 center /= len(stroke.points)
2834 stroke_centers.append([center, stroke, 0])
2836 # match, first by stroke use count, then by distance
2837 ls_pairs = []
2838 for lc in loop_centers:
2839 distances = []
2840 for i, sc in enumerate(stroke_centers):
2841 distances.append([sc[2], (lc[0] - sc[0]).length, i])
2842 distances.sort()
2843 best_stroke = distances[0][2]
2844 ls_pairs.append([lc[1], stroke_centers[best_stroke][1]])
2845 stroke_centers[best_stroke][2] += 1 # increase stroke use count
2847 return(ls_pairs)
2850 # match single selected vertices to the closest stroke endpoint
2851 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2852 def gstretch_match_single_verts(bm_mod, strokes, mat_world):
2853 # calculate stroke endpoints in object space
2854 endpoints = []
2855 for stroke in strokes:
2856 endpoints.append((mat_world * stroke.points[0].co, stroke, 0))
2857 endpoints.append((mat_world * stroke.points[-1].co, stroke, -1))
2859 distances = []
2860 # find single vertices (not connected to other selected verts)
2861 for vert in bm_mod.verts:
2862 if not vert.select:
2863 continue
2864 single = True
2865 for edge in vert.link_edges:
2866 if edge.other_vert(vert).select:
2867 single = False
2868 break
2869 if not single:
2870 continue
2871 # calculate distances from vertex to endpoints
2872 distance = [((vert.co - loc).length, vert, stroke, stroke_point,
2873 endpoint_index) for endpoint_index, (loc, stroke, stroke_point) in
2874 enumerate(endpoints)]
2875 distance.sort()
2876 distances.append(distance[0])
2878 # create matches, based on shortest distance first
2879 singles = []
2880 while distances:
2881 distances.sort()
2882 singles.append((distances[0][1], distances[0][2], distances[0][3]))
2883 endpoints.pop(distances[0][4])
2884 distances.pop(0)
2885 distances_new = []
2886 for (i, vert, j, k, l) in distances:
2887 distance_new = [((vert.co - loc).length, vert, stroke, stroke_point,
2888 endpoint_index) for endpoint_index, (loc, stroke,
2889 stroke_point) in enumerate(endpoints)]
2890 distance_new.sort()
2891 distances_new.append(distance_new[0])
2892 distances = distances_new
2894 return(singles)
2897 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2898 def gstretch_relative_lengths(loop, bm_mod):
2899 lengths = [0]
2900 for i, v_index in enumerate(loop[0][1:]):
2901 lengths.append((bm_mod.verts[v_index].co - \
2902 bm_mod.verts[loop[0][i]].co).length + lengths[-1])
2903 total_length = max(lengths[-1], 1e-7)
2904 relative_lengths = [length / total_length for length in
2905 lengths]
2907 return(relative_lengths)
2910 # convert cache-stored strokes into usable (fake) GP strokes
2911 def gstretch_safe_to_true_strokes(safe_strokes):
2912 strokes = []
2913 for safe_stroke in safe_strokes:
2914 strokes.append(gstretch_fake_stroke(safe_stroke))
2916 return(strokes)
2919 # convert a GP stroke into a list of points which can be stored in cache
2920 def gstretch_true_to_safe_strokes(strokes):
2921 safe_strokes = []
2922 for stroke in strokes:
2923 safe_strokes.append([p.co.copy() for p in stroke.points])
2925 return(safe_strokes)
2928 # force consistency in GUI, max value can never be lower than min value
2929 def gstretch_update_max(self, context):
2930 # called from operator settings (after execution)
2931 if 'conversion_min' in self.keys():
2932 if self.conversion_min > self.conversion_max:
2933 self.conversion_max = self.conversion_min
2934 # called from toolbar
2935 else:
2936 lt = context.window_manager.looptools
2937 if lt.gstretch_conversion_min > lt.gstretch_conversion_max:
2938 lt.gstretch_conversion_max = lt.gstretch_conversion_min
2941 # force consistency in GUI, min value can never be higher than max value
2942 def gstretch_update_min(self, context):
2943 # called from operator settings (after execution)
2944 if 'conversion_max' in self.keys():
2945 if self.conversion_max < self.conversion_min:
2946 self.conversion_min = self.conversion_max
2947 # called from toolbar
2948 else:
2949 lt = context.window_manager.looptools
2950 if lt.gstretch_conversion_max < lt.gstretch_conversion_min:
2951 lt.gstretch_conversion_min = lt.gstretch_conversion_max
2954 ##########################################
2955 ####### Relax functions ##################
2956 ##########################################
2958 # create lists with knots and points, all correctly sorted
2959 def relax_calculate_knots(loops):
2960 all_knots = []
2961 all_points = []
2962 for loop, circular in loops:
2963 knots = [[], []]
2964 points = [[], []]
2965 if circular:
2966 if len(loop)%2 == 1: # odd
2967 extend = [False, True, 0, 1, 0, 1]
2968 else: # even
2969 extend = [True, False, 0, 1, 1, 2]
2970 else:
2971 if len(loop)%2 == 1: # odd
2972 extend = [False, False, 0, 1, 1, 2]
2973 else: # even
2974 extend = [False, False, 0, 1, 1, 2]
2975 for j in range(2):
2976 if extend[j]:
2977 loop = [loop[-1]] + loop + [loop[0]]
2978 for i in range(extend[2+2*j], len(loop), 2):
2979 knots[j].append(loop[i])
2980 for i in range(extend[3+2*j], len(loop), 2):
2981 if loop[i] == loop[-1] and not circular:
2982 continue
2983 if len(points[j]) == 0:
2984 points[j].append(loop[i])
2985 elif loop[i] != points[j][0]:
2986 points[j].append(loop[i])
2987 if circular:
2988 if knots[j][0] != knots[j][-1]:
2989 knots[j].append(knots[j][0])
2990 if len(points[1]) == 0:
2991 knots.pop(1)
2992 points.pop(1)
2993 for k in knots:
2994 all_knots.append(k)
2995 for p in points:
2996 all_points.append(p)
2998 return(all_knots, all_points)
3001 # calculate relative positions compared to first knot
3002 def relax_calculate_t(bm_mod, knots, points, regular):
3003 all_tknots = []
3004 all_tpoints = []
3005 for i in range(len(knots)):
3006 amount = len(knots[i]) + len(points[i])
3007 mix = []
3008 for j in range(amount):
3009 if j%2 == 0:
3010 mix.append([True, knots[i][round(j/2)]])
3011 elif j == amount-1:
3012 mix.append([True, knots[i][-1]])
3013 else:
3014 mix.append([False, points[i][int(j/2)]])
3015 len_total = 0
3016 loc_prev = False
3017 tknots = []
3018 tpoints = []
3019 for m in mix:
3020 loc = mathutils.Vector(bm_mod.verts[m[1]].co[:])
3021 if not loc_prev:
3022 loc_prev = loc
3023 len_total += (loc - loc_prev).length
3024 if m[0]:
3025 tknots.append(len_total)
3026 else:
3027 tpoints.append(len_total)
3028 loc_prev = loc
3029 if regular:
3030 tpoints = []
3031 for p in range(len(points[i])):
3032 tpoints.append((tknots[p] + tknots[p+1]) / 2)
3033 all_tknots.append(tknots)
3034 all_tpoints.append(tpoints)
3036 return(all_tknots, all_tpoints)
3039 # change the location of the points to their place on the spline
3040 def relax_calculate_verts(bm_mod, interpolation, tknots, knots, tpoints,
3041 points, splines):
3042 change = []
3043 move = []
3044 for i in range(len(knots)):
3045 for p in points[i]:
3046 m = tpoints[i][points[i].index(p)]
3047 if m in tknots[i]:
3048 n = tknots[i].index(m)
3049 else:
3050 t = tknots[i][:]
3051 t.append(m)
3052 t.sort()
3053 n = t.index(m)-1
3054 if n > len(splines[i]) - 1:
3055 n = len(splines[i]) - 1
3056 elif n < 0:
3057 n = 0
3059 if interpolation == 'cubic':
3060 ax, bx, cx, dx, tx = splines[i][n][0]
3061 x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
3062 ay, by, cy, dy, ty = splines[i][n][1]
3063 y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
3064 az, bz, cz, dz, tz = splines[i][n][2]
3065 z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
3066 change.append([p, mathutils.Vector([x,y,z])])
3067 else: # interpolation == 'linear'
3068 a, d, t, u = splines[i][n]
3069 if u == 0:
3070 u = 1e-8
3071 change.append([p, ((m-t)/u)*d + a])
3072 for c in change:
3073 move.append([c[0], (bm_mod.verts[c[0]].co + c[1]) / 2])
3075 return(move)
3078 ##########################################
3079 ####### Space functions ##################
3080 ##########################################
3082 # calculate relative positions compared to first knot
3083 def space_calculate_t(bm_mod, knots):
3084 tknots = []
3085 loc_prev = False
3086 len_total = 0
3087 for k in knots:
3088 loc = mathutils.Vector(bm_mod.verts[k].co[:])
3089 if not loc_prev:
3090 loc_prev = loc
3091 len_total += (loc - loc_prev).length
3092 tknots.append(len_total)
3093 loc_prev = loc
3094 amount = len(knots)
3095 t_per_segment = len_total / (amount - 1)
3096 tpoints = [i * t_per_segment for i in range(amount)]
3098 return(tknots, tpoints)
3101 # change the location of the points to their place on the spline
3102 def space_calculate_verts(bm_mod, interpolation, tknots, tpoints, points,
3103 splines):
3104 move = []
3105 for p in points:
3106 m = tpoints[points.index(p)]
3107 if m in tknots:
3108 n = tknots.index(m)
3109 else:
3110 t = tknots[:]
3111 t.append(m)
3112 t.sort()
3113 n = t.index(m) - 1
3114 if n > len(splines) - 1:
3115 n = len(splines) - 1
3116 elif n < 0:
3117 n = 0
3119 if interpolation == 'cubic':
3120 ax, bx, cx, dx, tx = splines[n][0]
3121 x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
3122 ay, by, cy, dy, ty = splines[n][1]
3123 y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
3124 az, bz, cz, dz, tz = splines[n][2]
3125 z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
3126 move.append([p, mathutils.Vector([x,y,z])])
3127 else: # interpolation == 'linear'
3128 a, d, t, u = splines[n]
3129 move.append([p, ((m-t)/u)*d + a])
3131 return(move)
3134 ##########################################
3135 ####### Operators ########################
3136 ##########################################
3138 # bridge operator
3139 class Bridge(bpy.types.Operator):
3140 bl_idname = 'mesh.looptools_bridge'
3141 bl_label = "Bridge / Loft"
3142 bl_description = "Bridge two, or loft several, loops of vertices"
3143 bl_options = {'REGISTER', 'UNDO'}
3145 cubic_strength = bpy.props.FloatProperty(name = "Strength",
3146 description = "Higher strength results in more fluid curves",
3147 default = 1.0,
3148 soft_min = -3.0,
3149 soft_max = 3.0)
3150 interpolation = bpy.props.EnumProperty(name = "Interpolation mode",
3151 items = (('cubic', "Cubic", "Gives curved results"),
3152 ('linear', "Linear", "Basic, fast, straight interpolation")),
3153 description = "Interpolation mode: algorithm used when creating "\
3154 "segments",
3155 default = 'cubic')
3156 loft = bpy.props.BoolProperty(name = "Loft",
3157 description = "Loft multiple loops, instead of considering them as "\
3158 "a multi-input for bridging",
3159 default = False)
3160 loft_loop = bpy.props.BoolProperty(name = "Loop",
3161 description = "Connect the first and the last loop with each other",
3162 default = False)
3163 min_width = bpy.props.IntProperty(name = "Minimum width",
3164 description = "Segments with an edge smaller than this are merged "\
3165 "(compared to base edge)",
3166 default = 0,
3167 min = 0,
3168 max = 100,
3169 subtype = 'PERCENTAGE')
3170 mode = bpy.props.EnumProperty(name = "Mode",
3171 items = (('basic', "Basic", "Fast algorithm"), ('shortest',
3172 "Shortest edge", "Slower algorithm with better vertex matching")),
3173 description = "Algorithm used for bridging",
3174 default = 'shortest')
3175 remove_faces = bpy.props.BoolProperty(name = "Remove faces",
3176 description = "Remove faces that are internal after bridging",
3177 default = True)
3178 reverse = bpy.props.BoolProperty(name = "Reverse",
3179 description = "Manually override the direction in which the loops "\
3180 "are bridged. Only use if the tool gives the wrong " \
3181 "result",
3182 default = False)
3183 segments = bpy.props.IntProperty(name = "Segments",
3184 description = "Number of segments used to bridge the gap "\
3185 "(0 = automatic)",
3186 default = 1,
3187 min = 0,
3188 soft_max = 20)
3189 twist = bpy.props.IntProperty(name = "Twist",
3190 description = "Twist what vertices are connected to each other",
3191 default = 0)
3193 @classmethod
3194 def poll(cls, context):
3195 ob = context.active_object
3196 return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3198 def draw(self, context):
3199 layout = self.layout
3200 #layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3202 # top row
3203 col_top = layout.column(align=True)
3204 row = col_top.row(align=True)
3205 col_left = row.column(align=True)
3206 col_right = row.column(align=True)
3207 col_right.active = self.segments != 1
3208 col_left.prop(self, "segments")
3209 col_right.prop(self, "min_width", text="")
3210 # bottom row
3211 bottom_left = col_left.row()
3212 bottom_left.active = self.segments != 1
3213 bottom_left.prop(self, "interpolation", text="")
3214 bottom_right = col_right.row()
3215 bottom_right.active = self.interpolation == 'cubic'
3216 bottom_right.prop(self, "cubic_strength")
3217 # boolean properties
3218 col_top.prop(self, "remove_faces")
3219 if self.loft:
3220 col_top.prop(self, "loft_loop")
3222 # override properties
3223 col_top.separator()
3224 row = layout.row(align = True)
3225 row.prop(self, "twist")
3226 row.prop(self, "reverse")
3228 def invoke(self, context, event):
3229 # load custom settings
3230 context.window_manager.looptools.bridge_loft = self.loft
3231 settings_load(self)
3232 return self.execute(context)
3234 def execute(self, context):
3235 # initialise
3236 global_undo, object, bm = initialise()
3237 edge_faces, edgekey_to_edge, old_selected_faces, smooth = \
3238 bridge_initialise(bm, self.interpolation)
3239 settings_write(self)
3241 # check cache to see if we can save time
3242 input_method = bridge_input_method(self.loft, self.loft_loop)
3243 cached, single_loops, loops, derived, mapping = cache_read("Bridge",
3244 object, bm, input_method, False)
3245 if not cached:
3246 # get loops
3247 loops = bridge_get_input(bm)
3248 if loops:
3249 # reorder loops if there are more than 2
3250 if len(loops) > 2:
3251 if self.loft:
3252 loops = bridge_sort_loops(bm, loops, self.loft_loop)
3253 else:
3254 loops = bridge_match_loops(bm, loops)
3256 # saving cache for faster execution next time
3257 if not cached:
3258 cache_write("Bridge", object, bm, input_method, False, False,
3259 loops, False, False)
3261 if loops:
3262 # calculate new geometry
3263 vertices = []
3264 faces = []
3265 max_vert_index = len(bm.verts)-1
3266 for i in range(1, len(loops)):
3267 if not self.loft and i%2 == 0:
3268 continue
3269 lines = bridge_calculate_lines(bm, loops[i-1:i+1],
3270 self.mode, self.twist, self.reverse)
3271 vertex_normals = bridge_calculate_virtual_vertex_normals(bm,
3272 lines, loops[i-1:i+1], edge_faces, edgekey_to_edge)
3273 segments = bridge_calculate_segments(bm, lines,
3274 loops[i-1:i+1], self.segments)
3275 new_verts, new_faces, max_vert_index = \
3276 bridge_calculate_geometry(bm, lines, vertex_normals,
3277 segments, self.interpolation, self.cubic_strength,
3278 self.min_width, max_vert_index)
3279 if new_verts:
3280 vertices += new_verts
3281 if new_faces:
3282 faces += new_faces
3283 # make sure faces in loops that aren't used, aren't removed
3284 if self.remove_faces and old_selected_faces:
3285 bridge_save_unused_faces(bm, old_selected_faces, loops)
3286 # create vertices
3287 if vertices:
3288 bridge_create_vertices(bm, vertices)
3289 # create faces
3290 if faces:
3291 new_faces = bridge_create_faces(object, bm, faces, self.twist)
3292 old_selected_faces = [i for i, face in enumerate(bm.faces) \
3293 if face.index in old_selected_faces] # updating list
3294 bridge_select_new_faces(new_faces, smooth)
3295 # edge-data could have changed, can't use cache next run
3296 if faces and not vertices:
3297 cache_delete("Bridge")
3298 # delete internal faces
3299 if self.remove_faces and old_selected_faces:
3300 bridge_remove_internal_faces(bm, old_selected_faces)
3301 # make sure normals are facing outside
3302 bmesh.update_edit_mesh(object.data, tessface=False,
3303 destructive=True)
3304 bpy.ops.mesh.normals_make_consistent()
3306 # cleaning up
3307 terminate(global_undo)
3309 return{'FINISHED'}
3312 # circle operator
3313 class Circle(bpy.types.Operator):
3314 bl_idname = "mesh.looptools_circle"
3315 bl_label = "Circle"
3316 bl_description = "Move selected vertices into a circle shape"
3317 bl_options = {'REGISTER', 'UNDO'}
3319 custom_radius = bpy.props.BoolProperty(name = "Radius",
3320 description = "Force a custom radius",
3321 default = False)
3322 fit = bpy.props.EnumProperty(name = "Method",
3323 items = (("best", "Best fit", "Non-linear least squares"),
3324 ("inside", "Fit inside","Only move vertices towards the center")),
3325 description = "Method used for fitting a circle to the vertices",
3326 default = 'best')
3327 flatten = bpy.props.BoolProperty(name = "Flatten",
3328 description = "Flatten the circle, instead of projecting it on the " \
3329 "mesh",
3330 default = True)
3331 influence = bpy.props.FloatProperty(name = "Influence",
3332 description = "Force of the tool",
3333 default = 100.0,
3334 min = 0.0,
3335 max = 100.0,
3336 precision = 1,
3337 subtype = 'PERCENTAGE')
3338 lock_x = bpy.props.BoolProperty(name = "Lock X",
3339 description = "Lock editing of the x-coordinate",
3340 default = False)
3341 lock_y = bpy.props.BoolProperty(name = "Lock Y",
3342 description = "Lock editing of the y-coordinate",
3343 default = False)
3344 lock_z = bpy.props.BoolProperty(name = "Lock Z",
3345 description = "Lock editing of the z-coordinate",
3346 default = False)
3347 radius = bpy.props.FloatProperty(name = "Radius",
3348 description = "Custom radius for circle",
3349 default = 1.0,
3350 min = 0.0,
3351 soft_max = 1000.0)
3352 regular = bpy.props.BoolProperty(name = "Regular",
3353 description = "Distribute vertices at constant distances along the " \
3354 "circle",
3355 default = True)
3357 @classmethod
3358 def poll(cls, context):
3359 ob = context.active_object
3360 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3362 def draw(self, context):
3363 layout = self.layout
3364 col = layout.column()
3366 col.prop(self, "fit")
3367 col.separator()
3369 col.prop(self, "flatten")
3370 row = col.row(align=True)
3371 row.prop(self, "custom_radius")
3372 row_right = row.row(align=True)
3373 row_right.active = self.custom_radius
3374 row_right.prop(self, "radius", text="")
3375 col.prop(self, "regular")
3376 col.separator()
3378 col_move = col.column(align=True)
3379 row = col_move.row(align=True)
3380 if self.lock_x:
3381 row.prop(self, "lock_x", text = "X", icon='LOCKED')
3382 else:
3383 row.prop(self, "lock_x", text = "X", icon='UNLOCKED')
3384 if self.lock_y:
3385 row.prop(self, "lock_y", text = "Y", icon='LOCKED')
3386 else:
3387 row.prop(self, "lock_y", text = "Y", icon='UNLOCKED')
3388 if self.lock_z:
3389 row.prop(self, "lock_z", text = "Z", icon='LOCKED')
3390 else:
3391 row.prop(self, "lock_z", text = "Z", icon='UNLOCKED')
3392 col_move.prop(self, "influence")
3394 def invoke(self, context, event):
3395 # load custom settings
3396 settings_load(self)
3397 return self.execute(context)
3399 def execute(self, context):
3400 # initialise
3401 global_undo, object, bm = initialise()
3402 settings_write(self)
3403 # check cache to see if we can save time
3404 cached, single_loops, loops, derived, mapping = cache_read("Circle",
3405 object, bm, False, False)
3406 if cached:
3407 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
3408 else:
3409 # find loops
3410 derived, bm_mod, single_vertices, single_loops, loops = \
3411 circle_get_input(object, bm, context.scene)
3412 mapping = get_mapping(derived, bm, bm_mod, single_vertices,
3413 False, loops)
3414 single_loops, loops = circle_check_loops(single_loops, loops,
3415 mapping, bm_mod)
3417 # saving cache for faster execution next time
3418 if not cached:
3419 cache_write("Circle", object, bm, False, False, single_loops,
3420 loops, derived, mapping)
3422 move = []
3423 for i, loop in enumerate(loops):
3424 # best fitting flat plane
3425 com, normal = calculate_plane(bm_mod, loop)
3426 # if circular, shift loop so we get a good starting vertex
3427 if loop[1]:
3428 loop = circle_shift_loop(bm_mod, loop, com)
3429 # flatten vertices on plane
3430 locs_2d, p, q = circle_3d_to_2d(bm_mod, loop, com, normal)
3431 # calculate circle
3432 if self.fit == 'best':
3433 x0, y0, r = circle_calculate_best_fit(locs_2d)
3434 else: # self.fit == 'inside'
3435 x0, y0, r = circle_calculate_min_fit(locs_2d)
3436 # radius override
3437 if self.custom_radius:
3438 r = self.radius / p.length
3439 # calculate positions on circle
3440 if self.regular:
3441 new_locs_2d = circle_project_regular(locs_2d[:], x0, y0, r)
3442 else:
3443 new_locs_2d = circle_project_non_regular(locs_2d[:], x0, y0, r)
3444 # take influence into account
3445 locs_2d = circle_influence_locs(locs_2d, new_locs_2d,
3446 self.influence)
3447 # calculate 3d positions of the created 2d input
3448 move.append(circle_calculate_verts(self.flatten, bm_mod,
3449 locs_2d, com, p, q, normal))
3450 # flatten single input vertices on plane defined by loop
3451 if self.flatten and single_loops:
3452 move.append(circle_flatten_singles(bm_mod, com, p, q,
3453 normal, single_loops[i]))
3455 # move vertices to new locations
3456 if self.lock_x or self.lock_y or self.lock_z:
3457 lock = [self.lock_x, self.lock_y, self.lock_z]
3458 else:
3459 lock = False
3460 move_verts(object, bm, mapping, move, lock, -1)
3462 # cleaning up
3463 if derived:
3464 bm_mod.free()
3465 terminate(global_undo)
3467 return{'FINISHED'}
3470 # curve operator
3471 class Curve(bpy.types.Operator):
3472 bl_idname = "mesh.looptools_curve"
3473 bl_label = "Curve"
3474 bl_description = "Turn a loop into a smooth curve"
3475 bl_options = {'REGISTER', 'UNDO'}
3477 boundaries = bpy.props.BoolProperty(name = "Boundaries",
3478 description = "Limit the tool to work within the boundaries of the "\
3479 "selected vertices",
3480 default = False)
3481 influence = bpy.props.FloatProperty(name = "Influence",
3482 description = "Force of the tool",
3483 default = 100.0,
3484 min = 0.0,
3485 max = 100.0,
3486 precision = 1,
3487 subtype = 'PERCENTAGE')
3488 interpolation = bpy.props.EnumProperty(name = "Interpolation",
3489 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3490 ("linear", "Linear", "Simple and fast linear algorithm")),
3491 description = "Algorithm used for interpolation",
3492 default = 'cubic')
3493 lock_x = bpy.props.BoolProperty(name = "Lock X",
3494 description = "Lock editing of the x-coordinate",
3495 default = False)
3496 lock_y = bpy.props.BoolProperty(name = "Lock Y",
3497 description = "Lock editing of the y-coordinate",
3498 default = False)
3499 lock_z = bpy.props.BoolProperty(name = "Lock Z",
3500 description = "Lock editing of the z-coordinate",
3501 default = False)
3502 regular = bpy.props.BoolProperty(name = "Regular",
3503 description = "Distribute vertices at constant distances along the" \
3504 "curve",
3505 default = True)
3506 restriction = bpy.props.EnumProperty(name = "Restriction",
3507 items = (("none", "None", "No restrictions on vertex movement"),
3508 ("extrude", "Extrude only","Only allow extrusions (no "\
3509 "indentations)"),
3510 ("indent", "Indent only", "Only allow indentation (no "\
3511 "extrusions)")),
3512 description = "Restrictions on how the vertices can be moved",
3513 default = 'none')
3515 @classmethod
3516 def poll(cls, context):
3517 ob = context.active_object
3518 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3520 def draw(self, context):
3521 layout = self.layout
3522 col = layout.column()
3524 col.prop(self, "interpolation")
3525 col.prop(self, "restriction")
3526 col.prop(self, "boundaries")
3527 col.prop(self, "regular")
3528 col.separator()
3530 col_move = col.column(align=True)
3531 row = col_move.row(align=True)
3532 if self.lock_x:
3533 row.prop(self, "lock_x", text = "X", icon='LOCKED')
3534 else:
3535 row.prop(self, "lock_x", text = "X", icon='UNLOCKED')
3536 if self.lock_y:
3537 row.prop(self, "lock_y", text = "Y", icon='LOCKED')
3538 else:
3539 row.prop(self, "lock_y", text = "Y", icon='UNLOCKED')
3540 if self.lock_z:
3541 row.prop(self, "lock_z", text = "Z", icon='LOCKED')
3542 else:
3543 row.prop(self, "lock_z", text = "Z", icon='UNLOCKED')
3544 col_move.prop(self, "influence")
3546 def invoke(self, context, event):
3547 # load custom settings
3548 settings_load(self)
3549 return self.execute(context)
3551 def execute(self, context):
3552 # initialise
3553 global_undo, object, bm = initialise()
3554 settings_write(self)
3555 # check cache to see if we can save time
3556 cached, single_loops, loops, derived, mapping = cache_read("Curve",
3557 object, bm, False, self.boundaries)
3558 if cached:
3559 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
3560 else:
3561 # find loops
3562 derived, bm_mod, loops = curve_get_input(object, bm,
3563 self.boundaries, context.scene)
3564 mapping = get_mapping(derived, bm, bm_mod, False, True, loops)
3565 loops = check_loops(loops, mapping, bm_mod)
3566 verts_selected = [v.index for v in bm_mod.verts if v.select \
3567 and not v.hide]
3569 # saving cache for faster execution next time
3570 if not cached:
3571 cache_write("Curve", object, bm, False, self.boundaries, False,
3572 loops, derived, mapping)
3574 move = []
3575 for loop in loops:
3576 knots, points = curve_calculate_knots(loop, verts_selected)
3577 pknots = curve_project_knots(bm_mod, verts_selected, knots,
3578 points, loop[1])
3579 tknots, tpoints = curve_calculate_t(bm_mod, knots, points,
3580 pknots, self.regular, loop[1])
3581 splines = calculate_splines(self.interpolation, bm_mod,
3582 tknots, knots)
3583 move.append(curve_calculate_vertices(bm_mod, knots, tknots,
3584 points, tpoints, splines, self.interpolation,
3585 self.restriction))
3587 # move vertices to new locations
3588 if self.lock_x or self.lock_y or self.lock_z:
3589 lock = [self.lock_x, self.lock_y, self.lock_z]
3590 else:
3591 lock = False
3592 move_verts(object, bm, mapping, move, lock, self.influence)
3594 # cleaning up
3595 if derived:
3596 bm_mod.free()
3597 terminate(global_undo)
3599 return{'FINISHED'}
3602 # flatten operator
3603 class Flatten(bpy.types.Operator):
3604 bl_idname = "mesh.looptools_flatten"
3605 bl_label = "Flatten"
3606 bl_description = "Flatten vertices on a best-fitting plane"
3607 bl_options = {'REGISTER', 'UNDO'}
3609 influence = bpy.props.FloatProperty(name = "Influence",
3610 description = "Force of the tool",
3611 default = 100.0,
3612 min = 0.0,
3613 max = 100.0,
3614 precision = 1,
3615 subtype = 'PERCENTAGE')
3616 lock_x = bpy.props.BoolProperty(name = "Lock X",
3617 description = "Lock editing of the x-coordinate",
3618 default = False)
3619 lock_y = bpy.props.BoolProperty(name = "Lock Y",
3620 description = "Lock editing of the y-coordinate",
3621 default = False)
3622 lock_z = bpy.props.BoolProperty(name = "Lock Z",
3623 description = "Lock editing of the z-coordinate",
3624 default = False)
3625 plane = bpy.props.EnumProperty(name = "Plane",
3626 items = (("best_fit", "Best fit", "Calculate a best fitting plane"),
3627 ("normal", "Normal", "Derive plane from averaging vertex "\
3628 "normals"),
3629 ("view", "View", "Flatten on a plane perpendicular to the "\
3630 "viewing angle")),
3631 description = "Plane on which vertices are flattened",
3632 default = 'best_fit')
3633 restriction = bpy.props.EnumProperty(name = "Restriction",
3634 items = (("none", "None", "No restrictions on vertex movement"),
3635 ("bounding_box", "Bounding box", "Vertices are restricted to "\
3636 "movement inside the bounding box of the selection")),
3637 description = "Restrictions on how the vertices can be moved",
3638 default = 'none')
3640 @classmethod
3641 def poll(cls, context):
3642 ob = context.active_object
3643 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3645 def draw(self, context):
3646 layout = self.layout
3647 col = layout.column()
3649 col.prop(self, "plane")
3650 #col.prop(self, "restriction")
3651 col.separator()
3653 col_move = col.column(align=True)
3654 row = col_move.row(align=True)
3655 if self.lock_x:
3656 row.prop(self, "lock_x", text = "X", icon='LOCKED')
3657 else:
3658 row.prop(self, "lock_x", text = "X", icon='UNLOCKED')
3659 if self.lock_y:
3660 row.prop(self, "lock_y", text = "Y", icon='LOCKED')
3661 else:
3662 row.prop(self, "lock_y", text = "Y", icon='UNLOCKED')
3663 if self.lock_z:
3664 row.prop(self, "lock_z", text = "Z", icon='LOCKED')
3665 else:
3666 row.prop(self, "lock_z", text = "Z", icon='UNLOCKED')
3667 col_move.prop(self, "influence")
3669 def invoke(self, context, event):
3670 # load custom settings
3671 settings_load(self)
3672 return self.execute(context)
3674 def execute(self, context):
3675 # initialise
3676 global_undo, object, bm = initialise()
3677 settings_write(self)
3678 # check cache to see if we can save time
3679 cached, single_loops, loops, derived, mapping = cache_read("Flatten",
3680 object, bm, False, False)
3681 if not cached:
3682 # order input into virtual loops
3683 loops = flatten_get_input(bm)
3684 loops = check_loops(loops, mapping, bm)
3686 # saving cache for faster execution next time
3687 if not cached:
3688 cache_write("Flatten", object, bm, False, False, False, loops,
3689 False, False)
3691 move = []
3692 for loop in loops:
3693 # calculate plane and position of vertices on them
3694 com, normal = calculate_plane(bm, loop, method=self.plane,
3695 object=object)
3696 to_move = flatten_project(bm, loop, com, normal)
3697 if self.restriction == 'none':
3698 move.append(to_move)
3699 else:
3700 move.append(to_move)
3702 # move vertices to new locations
3703 if self.lock_x or self.lock_y or self.lock_z:
3704 lock = [self.lock_x, self.lock_y, self.lock_z]
3705 else:
3706 lock = False
3707 move_verts(object, bm, False, move, lock, self.influence)
3709 # cleaning up
3710 terminate(global_undo)
3712 return{'FINISHED'}
3715 # gstretch operator
3716 class GStretch(bpy.types.Operator):
3717 bl_idname = "mesh.looptools_gstretch"
3718 bl_label = "Gstretch"
3719 bl_description = "Stretch selected vertices to Grease Pencil stroke"
3720 bl_options = {'REGISTER', 'UNDO'}
3722 conversion = bpy.props.EnumProperty(name = "Conversion",
3723 items = (("distance", "Distance", "Set the distance between vertices "\
3724 "of the converted grease pencil stroke"),
3725 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "\
3726 "number of vertices that converted GP strokes will have"),
3727 ("vertices", "Exact vertices", "Set the exact number of vertices "\
3728 "that converted grease pencil strokes will have. Short strokes "\
3729 "with few points may contain less vertices than this number."),
3730 ("none", "No simplification", "Convert each grease pencil point "\
3731 "to a vertex")),
3732 description = "If grease pencil strokes are converted to geometry, "\
3733 "use this simplification method",
3734 default = 'limit_vertices')
3735 conversion_distance = bpy.props.FloatProperty(name = "Distance",
3736 description = "Absolute distance between vertices along the converted "\
3737 "grease pencil stroke",
3738 default = 0.1,
3739 min = 0.000001,
3740 soft_min = 0.01,
3741 soft_max = 100)
3742 conversion_max = bpy.props.IntProperty(name = "Max Vertices",
3743 description = "Maximum number of vertices grease pencil strokes will "\
3744 "have, when they are converted to geomtery",
3745 default = 32,
3746 min = 3,
3747 soft_max = 500,
3748 update = gstretch_update_min)
3749 conversion_min = bpy.props.IntProperty(name = "Min Vertices",
3750 description = "Minimum number of vertices grease pencil strokes will "\
3751 "have, when they are converted to geomtery",
3752 default = 8,
3753 min = 3,
3754 soft_max = 500,
3755 update = gstretch_update_max)
3756 conversion_vertices = bpy.props.IntProperty(name = "Vertices",
3757 description = "Number of vertices grease pencil strokes will "\
3758 "have, when they are converted to geometry. If strokes have less "\
3759 "points than required, the 'Spread evenly' method is used",
3760 default = 32,
3761 min = 3,
3762 soft_max = 500)
3763 delete_strokes = bpy.props.BoolProperty(name="Delete strokes",
3764 description = "Remove Grease Pencil strokes if they have been used "\
3765 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
3766 default = False)
3767 influence = bpy.props.FloatProperty(name = "Influence",
3768 description = "Force of the tool",
3769 default = 100.0,
3770 min = 0.0,
3771 max = 100.0,
3772 precision = 1,
3773 subtype = 'PERCENTAGE')
3774 lock_x = bpy.props.BoolProperty(name = "Lock X",
3775 description = "Lock editing of the x-coordinate",
3776 default = False)
3777 lock_y = bpy.props.BoolProperty(name = "Lock Y",
3778 description = "Lock editing of the y-coordinate",
3779 default = False)
3780 lock_z = bpy.props.BoolProperty(name = "Lock Z",
3781 description = "Lock editing of the z-coordinate",
3782 default = False)
3783 method = bpy.props.EnumProperty(name = "Method",
3784 items = (("project", "Project", "Project vertices onto the stroke, "\
3785 "using vertex normals and connected edges"),
3786 ("irregular", "Spread", "Distribute vertices along the full "\
3787 "stroke, retaining relative distances between the vertices"),
3788 ("regular", "Spread evenly", "Distribute vertices at regular "\
3789 "distances along the full stroke")),
3790 description = "Method of distributing the vertices over the Grease "\
3791 "Pencil stroke",
3792 default = 'regular')
3794 @classmethod
3795 def poll(cls, context):
3796 ob = context.active_object
3797 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3799 def draw(self, context):
3800 layout = self.layout
3801 col = layout.column()
3803 col.prop(self, "method")
3804 col.prop(self, "delete_strokes")
3805 col.separator()
3807 col_conv = col.column(align=True)
3808 col_conv.prop(self, "conversion", text="")
3809 if self.conversion == 'distance':
3810 col_conv.prop(self, "conversion_distance")
3811 elif self.conversion == 'limit_vertices':
3812 row = col_conv.row(align=True)
3813 row.prop(self, "conversion_min", text="Min")
3814 row.prop(self, "conversion_max", text="Max")
3815 elif self.conversion == 'vertices':
3816 col_conv.prop(self, "conversion_vertices")
3817 col.separator()
3819 col_move = col.column(align=True)
3820 row = col_move.row(align=True)
3821 if self.lock_x:
3822 row.prop(self, "lock_x", text = "X", icon='LOCKED')
3823 else:
3824 row.prop(self, "lock_x", text = "X", icon='UNLOCKED')
3825 if self.lock_y:
3826 row.prop(self, "lock_y", text = "Y", icon='LOCKED')
3827 else:
3828 row.prop(self, "lock_y", text = "Y", icon='UNLOCKED')
3829 if self.lock_z:
3830 row.prop(self, "lock_z", text = "Z", icon='LOCKED')
3831 else:
3832 row.prop(self, "lock_z", text = "Z", icon='UNLOCKED')
3833 col_move.prop(self, "influence")
3835 def invoke(self, context, event):
3836 # flush cached strokes
3837 if 'Gstretch' in looptools_cache:
3838 looptools_cache['Gstretch']['single_loops'] = []
3839 # load custom settings
3840 settings_load(self)
3841 return self.execute(context)
3843 def execute(self, context):
3844 # initialise
3845 global_undo, object, bm = initialise()
3846 settings_write(self)
3848 # check cache to see if we can save time
3849 cached, safe_strokes, loops, derived, mapping = cache_read("Gstretch",
3850 object, bm, False, False)
3851 if cached:
3852 straightening = False
3853 if safe_strokes:
3854 strokes = gstretch_safe_to_true_strokes(safe_strokes)
3855 # cached strokes were flushed (see operator's invoke function)
3856 elif get_grease_pencil(object, context):
3857 strokes = gstretch_get_strokes(object, context)
3858 else:
3859 # straightening function (no GP) -> loops ignore modifiers
3860 straightening = True
3861 derived = False
3862 bm_mod = bm.copy()
3863 bm_mod.verts.ensure_lookup_table()
3864 bm_mod.edges.ensure_lookup_table()
3865 bm_mod.faces.ensure_lookup_table()
3866 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
3867 if not straightening:
3868 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
3869 else:
3870 # get loops and strokes
3871 if get_grease_pencil(object, context):
3872 # find loops
3873 derived, bm_mod, loops = get_connected_input(object, bm,
3874 context.scene, input='selected')
3875 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
3876 loops = check_loops(loops, mapping, bm_mod)
3877 # get strokes
3878 strokes = gstretch_get_strokes(object, context)
3879 else:
3880 # straightening function (no GP) -> loops ignore modifiers
3881 derived = False
3882 mapping = False
3883 bm_mod = bm.copy()
3884 bm_mod.verts.ensure_lookup_table()
3885 bm_mod.edges.ensure_lookup_table()
3886 bm_mod.faces.ensure_lookup_table()
3887 edge_keys = [edgekey(edge) for edge in bm_mod.edges if \
3888 edge.select and not edge.hide]
3889 loops = get_connected_selections(edge_keys)
3890 loops = check_loops(loops, mapping, bm_mod)
3891 # create fake strokes
3892 strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
3894 # saving cache for faster execution next time
3895 if not cached:
3896 if strokes:
3897 safe_strokes = gstretch_true_to_safe_strokes(strokes)
3898 else:
3899 safe_strokes = []
3900 cache_write("Gstretch", object, bm, False, False,
3901 safe_strokes, loops, derived, mapping)
3903 # pair loops and strokes
3904 ls_pairs = gstretch_match_loops_strokes(loops, strokes, object, bm_mod)
3905 ls_pairs = gstretch_align_pairs(ls_pairs, object, bm_mod, self.method)
3907 move = []
3908 if not loops:
3909 # no selected geometry, convert GP to verts
3910 if strokes:
3911 move.append(gstretch_create_verts(object, bm, strokes,
3912 self.method, self.conversion, self.conversion_distance,
3913 self.conversion_max, self.conversion_min,
3914 self.conversion_vertices))
3915 for stroke in strokes:
3916 gstretch_erase_stroke(stroke, context)
3917 elif ls_pairs:
3918 for (loop, stroke) in ls_pairs:
3919 move.append(gstretch_calculate_verts(loop, stroke, object,
3920 bm_mod, self.method))
3921 if self.delete_strokes:
3922 if type(stroke) != bpy.types.GPencilStroke:
3923 # in case of cached fake stroke, get the real one
3924 if get_grease_pencil(object, context):
3925 strokes = gstretch_get_strokes(object, context)
3926 if loops and strokes:
3927 ls_pairs = gstretch_match_loops_strokes(loops,
3928 strokes, object, bm_mod)
3929 ls_pairs = gstretch_align_pairs(ls_pairs,
3930 object, bm_mod, self.method)
3931 for (l, s) in ls_pairs:
3932 if l == loop:
3933 stroke = s
3934 break
3935 gstretch_erase_stroke(stroke, context)
3937 # move vertices to new locations
3938 if self.lock_x or self.lock_y or self.lock_z:
3939 lock = [self.lock_x, self.lock_y, self.lock_z]
3940 else:
3941 lock = False
3942 bmesh.update_edit_mesh(object.data, tessface=True, destructive=True)
3943 move_verts(object, bm, mapping, move, lock, self.influence)
3945 # cleaning up
3946 if derived:
3947 bm_mod.free()
3948 terminate(global_undo)
3950 return{'FINISHED'}
3953 # relax operator
3954 class Relax(bpy.types.Operator):
3955 bl_idname = "mesh.looptools_relax"
3956 bl_label = "Relax"
3957 bl_description = "Relax the loop, so it is smoother"
3958 bl_options = {'REGISTER', 'UNDO'}
3960 input = bpy.props.EnumProperty(name = "Input",
3961 items = (("all", "Parallel (all)", "Also use non-selected "\
3962 "parallel loops as input"),
3963 ("selected", "Selection","Only use selected vertices as input")),
3964 description = "Loops that are relaxed",
3965 default = 'selected')
3966 interpolation = bpy.props.EnumProperty(name = "Interpolation",
3967 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3968 ("linear", "Linear", "Simple and fast linear algorithm")),
3969 description = "Algorithm used for interpolation",
3970 default = 'cubic')
3971 iterations = bpy.props.EnumProperty(name = "Iterations",
3972 items = (("1", "1", "One"),
3973 ("3", "3", "Three"),
3974 ("5", "5", "Five"),
3975 ("10", "10", "Ten"),
3976 ("25", "25", "Twenty-five")),
3977 description = "Number of times the loop is relaxed",
3978 default = "1")
3979 regular = bpy.props.BoolProperty(name = "Regular",
3980 description = "Distribute vertices at constant distances along the" \
3981 "loop",
3982 default = True)
3984 @classmethod
3985 def poll(cls, context):
3986 ob = context.active_object
3987 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3989 def draw(self, context):
3990 layout = self.layout
3991 col = layout.column()
3993 col.prop(self, "interpolation")
3994 col.prop(self, "input")
3995 col.prop(self, "iterations")
3996 col.prop(self, "regular")
3998 def invoke(self, context, event):
3999 # load custom settings
4000 settings_load(self)
4001 return self.execute(context)
4003 def execute(self, context):
4004 # initialise
4005 global_undo, object, bm = initialise()
4006 settings_write(self)
4007 # check cache to see if we can save time
4008 cached, single_loops, loops, derived, mapping = cache_read("Relax",
4009 object, bm, self.input, False)
4010 if cached:
4011 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
4012 else:
4013 # find loops
4014 derived, bm_mod, loops = get_connected_input(object, bm,
4015 context.scene, self.input)
4016 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4017 loops = check_loops(loops, mapping, bm_mod)
4018 knots, points = relax_calculate_knots(loops)
4020 # saving cache for faster execution next time
4021 if not cached:
4022 cache_write("Relax", object, bm, self.input, False, False, loops,
4023 derived, mapping)
4025 for iteration in range(int(self.iterations)):
4026 # calculate splines and new positions
4027 tknots, tpoints = relax_calculate_t(bm_mod, knots, points,
4028 self.regular)
4029 splines = []
4030 for i in range(len(knots)):
4031 splines.append(calculate_splines(self.interpolation, bm_mod,
4032 tknots[i], knots[i]))
4033 move = [relax_calculate_verts(bm_mod, self.interpolation,
4034 tknots, knots, tpoints, points, splines)]
4035 move_verts(object, bm, mapping, move, False, -1)
4037 # cleaning up
4038 if derived:
4039 bm_mod.free()
4040 terminate(global_undo)
4042 return{'FINISHED'}
4045 # space operator
4046 class Space(bpy.types.Operator):
4047 bl_idname = "mesh.looptools_space"
4048 bl_label = "Space"
4049 bl_description = "Space the vertices in a regular distrubtion on the loop"
4050 bl_options = {'REGISTER', 'UNDO'}
4052 influence = bpy.props.FloatProperty(name = "Influence",
4053 description = "Force of the tool",
4054 default = 100.0,
4055 min = 0.0,
4056 max = 100.0,
4057 precision = 1,
4058 subtype = 'PERCENTAGE')
4059 input = bpy.props.EnumProperty(name = "Input",
4060 items = (("all", "Parallel (all)", "Also use non-selected "\
4061 "parallel loops as input"),
4062 ("selected", "Selection","Only use selected vertices as input")),
4063 description = "Loops that are spaced",
4064 default = 'selected')
4065 interpolation = bpy.props.EnumProperty(name = "Interpolation",
4066 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4067 ("linear", "Linear", "Vertices are projected on existing edges")),
4068 description = "Algorithm used for interpolation",
4069 default = 'cubic')
4070 lock_x = bpy.props.BoolProperty(name = "Lock X",
4071 description = "Lock editing of the x-coordinate",
4072 default = False)
4073 lock_y = bpy.props.BoolProperty(name = "Lock Y",
4074 description = "Lock editing of the y-coordinate",
4075 default = False)
4076 lock_z = bpy.props.BoolProperty(name = "Lock Z",
4077 description = "Lock editing of the z-coordinate",
4078 default = False)
4080 @classmethod
4081 def poll(cls, context):
4082 ob = context.active_object
4083 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
4085 def draw(self, context):
4086 layout = self.layout
4087 col = layout.column()
4089 col.prop(self, "interpolation")
4090 col.prop(self, "input")
4091 col.separator()
4093 col_move = col.column(align=True)
4094 row = col_move.row(align=True)
4095 if self.lock_x:
4096 row.prop(self, "lock_x", text = "X", icon='LOCKED')
4097 else:
4098 row.prop(self, "lock_x", text = "X", icon='UNLOCKED')
4099 if self.lock_y:
4100 row.prop(self, "lock_y", text = "Y", icon='LOCKED')
4101 else:
4102 row.prop(self, "lock_y", text = "Y", icon='UNLOCKED')
4103 if self.lock_z:
4104 row.prop(self, "lock_z", text = "Z", icon='LOCKED')
4105 else:
4106 row.prop(self, "lock_z", text = "Z", icon='UNLOCKED')
4107 col_move.prop(self, "influence")
4109 def invoke(self, context, event):
4110 # load custom settings
4111 settings_load(self)
4112 return self.execute(context)
4114 def execute(self, context):
4115 # initialise
4116 global_undo, object, bm = initialise()
4117 settings_write(self)
4118 # check cache to see if we can save time
4119 cached, single_loops, loops, derived, mapping = cache_read("Space",
4120 object, bm, self.input, False)
4121 if cached:
4122 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
4123 else:
4124 # find loops
4125 derived, bm_mod, loops = get_connected_input(object, bm,
4126 context.scene, self.input)
4127 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
4128 loops = check_loops(loops, mapping, bm_mod)
4130 # saving cache for faster execution next time
4131 if not cached:
4132 cache_write("Space", object, bm, self.input, False, False, loops,
4133 derived, mapping)
4135 move = []
4136 for loop in loops:
4137 # calculate splines and new positions
4138 if loop[1]: # circular
4139 loop[0].append(loop[0][0])
4140 tknots, tpoints = space_calculate_t(bm_mod, loop[0][:])
4141 splines = calculate_splines(self.interpolation, bm_mod,
4142 tknots, loop[0][:])
4143 move.append(space_calculate_verts(bm_mod, self.interpolation,
4144 tknots, tpoints, loop[0][:-1], splines))
4145 # move vertices to new locations
4146 if self.lock_x or self.lock_y or self.lock_z:
4147 lock = [self.lock_x, self.lock_y, self.lock_z]
4148 else:
4149 lock = False
4150 move_verts(object, bm, mapping, move, lock, self.influence)
4152 # cleaning up
4153 if derived:
4154 bm_mod.free()
4155 terminate(global_undo)
4157 return{'FINISHED'}
4160 ##########################################
4161 ####### GUI and registration #############
4162 ##########################################
4164 # menu containing all tools
4165 class VIEW3D_MT_edit_mesh_looptools(bpy.types.Menu):
4166 bl_label = "LoopTools"
4168 def draw(self, context):
4169 layout = self.layout
4171 layout.operator("mesh.looptools_bridge", text="Bridge").loft = False
4172 layout.operator("mesh.looptools_circle")
4173 layout.operator("mesh.looptools_curve")
4174 layout.operator("mesh.looptools_flatten")
4175 layout.operator("mesh.looptools_gstretch")
4176 layout.operator("mesh.looptools_bridge", text="Loft").loft = True
4177 layout.operator("mesh.looptools_relax")
4178 layout.operator("mesh.looptools_space")
4181 # panel containing all tools
4182 class VIEW3D_PT_tools_looptools(bpy.types.Panel):
4183 bl_space_type = 'VIEW_3D'
4184 bl_region_type = 'TOOLS'
4185 bl_category = 'Tools'
4186 bl_context = "mesh_edit"
4187 bl_label = "LoopTools"
4188 bl_options = {'DEFAULT_CLOSED'}
4190 def draw(self, context):
4191 layout = self.layout
4192 col = layout.column(align=True)
4193 lt = context.window_manager.looptools
4195 # bridge - first line
4196 split = col.split(percentage=0.15, align=True)
4197 if lt.display_bridge:
4198 split.prop(lt, "display_bridge", text="", icon='DOWNARROW_HLT')
4199 else:
4200 split.prop(lt, "display_bridge", text="", icon='RIGHTARROW')
4201 split.operator("mesh.looptools_bridge", text="Bridge").loft = False
4202 # bridge - settings
4203 if lt.display_bridge:
4204 box = col.column(align=True).box().column()
4205 #box.prop(self, "mode")
4207 # top row
4208 col_top = box.column(align=True)
4209 row = col_top.row(align=True)
4210 col_left = row.column(align=True)
4211 col_right = row.column(align=True)
4212 col_right.active = lt.bridge_segments != 1
4213 col_left.prop(lt, "bridge_segments")
4214 col_right.prop(lt, "bridge_min_width", text="")
4215 # bottom row
4216 bottom_left = col_left.row()
4217 bottom_left.active = lt.bridge_segments != 1
4218 bottom_left.prop(lt, "bridge_interpolation", text="")
4219 bottom_right = col_right.row()
4220 bottom_right.active = lt.bridge_interpolation == 'cubic'
4221 bottom_right.prop(lt, "bridge_cubic_strength")
4222 # boolean properties
4223 col_top.prop(lt, "bridge_remove_faces")
4225 # override properties
4226 col_top.separator()
4227 row = box.row(align = True)
4228 row.prop(lt, "bridge_twist")
4229 row.prop(lt, "bridge_reverse")
4231 # circle - first line
4232 split = col.split(percentage=0.15, align=True)
4233 if lt.display_circle:
4234 split.prop(lt, "display_circle", text="", icon='DOWNARROW_HLT')
4235 else:
4236 split.prop(lt, "display_circle", text="", icon='RIGHTARROW')
4237 split.operator("mesh.looptools_circle")
4238 # circle - settings
4239 if lt.display_circle:
4240 box = col.column(align=True).box().column()
4241 box.prop(lt, "circle_fit")
4242 box.separator()
4244 box.prop(lt, "circle_flatten")
4245 row = box.row(align=True)
4246 row.prop(lt, "circle_custom_radius")
4247 row_right = row.row(align=True)
4248 row_right.active = lt.circle_custom_radius
4249 row_right.prop(lt, "circle_radius", text="")
4250 box.prop(lt, "circle_regular")
4251 box.separator()
4253 col_move = box.column(align=True)
4254 row = col_move.row(align=True)
4255 if lt.circle_lock_x:
4256 row.prop(lt, "circle_lock_x", text = "X", icon='LOCKED')
4257 else:
4258 row.prop(lt, "circle_lock_x", text = "X", icon='UNLOCKED')
4259 if lt.circle_lock_y:
4260 row.prop(lt, "circle_lock_y", text = "Y", icon='LOCKED')
4261 else:
4262 row.prop(lt, "circle_lock_y", text = "Y", icon='UNLOCKED')
4263 if lt.circle_lock_z:
4264 row.prop(lt, "circle_lock_z", text = "Z", icon='LOCKED')
4265 else:
4266 row.prop(lt, "circle_lock_z", text = "Z", icon='UNLOCKED')
4267 col_move.prop(lt, "circle_influence")
4269 # curve - first line
4270 split = col.split(percentage=0.15, align=True)
4271 if lt.display_curve:
4272 split.prop(lt, "display_curve", text="", icon='DOWNARROW_HLT')
4273 else:
4274 split.prop(lt, "display_curve", text="", icon='RIGHTARROW')
4275 split.operator("mesh.looptools_curve")
4276 # curve - settings
4277 if lt.display_curve:
4278 box = col.column(align=True).box().column()
4279 box.prop(lt, "curve_interpolation")
4280 box.prop(lt, "curve_restriction")
4281 box.prop(lt, "curve_boundaries")
4282 box.prop(lt, "curve_regular")
4283 box.separator()
4285 col_move = box.column(align=True)
4286 row = col_move.row(align=True)
4287 if lt.curve_lock_x:
4288 row.prop(lt, "curve_lock_x", text = "X", icon='LOCKED')
4289 else:
4290 row.prop(lt, "curve_lock_x", text = "X", icon='UNLOCKED')
4291 if lt.curve_lock_y:
4292 row.prop(lt, "curve_lock_y", text = "Y", icon='LOCKED')
4293 else:
4294 row.prop(lt, "curve_lock_y", text = "Y", icon='UNLOCKED')
4295 if lt.curve_lock_z:
4296 row.prop(lt, "curve_lock_z", text = "Z", icon='LOCKED')
4297 else:
4298 row.prop(lt, "curve_lock_z", text = "Z", icon='UNLOCKED')
4299 col_move.prop(lt, "curve_influence")
4301 # flatten - first line
4302 split = col.split(percentage=0.15, align=True)
4303 if lt.display_flatten:
4304 split.prop(lt, "display_flatten", text="", icon='DOWNARROW_HLT')
4305 else:
4306 split.prop(lt, "display_flatten", text="", icon='RIGHTARROW')
4307 split.operator("mesh.looptools_flatten")
4308 # flatten - settings
4309 if lt.display_flatten:
4310 box = col.column(align=True).box().column()
4311 box.prop(lt, "flatten_plane")
4312 #box.prop(lt, "flatten_restriction")
4313 box.separator()
4315 col_move = box.column(align=True)
4316 row = col_move.row(align=True)
4317 if lt.flatten_lock_x:
4318 row.prop(lt, "flatten_lock_x", text = "X", icon='LOCKED')
4319 else:
4320 row.prop(lt, "flatten_lock_x", text = "X", icon='UNLOCKED')
4321 if lt.flatten_lock_y:
4322 row.prop(lt, "flatten_lock_y", text = "Y", icon='LOCKED')
4323 else:
4324 row.prop(lt, "flatten_lock_y", text = "Y", icon='UNLOCKED')
4325 if lt.flatten_lock_z:
4326 row.prop(lt, "flatten_lock_z", text = "Z", icon='LOCKED')
4327 else:
4328 row.prop(lt, "flatten_lock_z", text = "Z", icon='UNLOCKED')
4329 col_move.prop(lt, "flatten_influence")
4331 # gstretch - first line
4332 split = col.split(percentage=0.15, align=True)
4333 if lt.display_gstretch:
4334 split.prop(lt, "display_gstretch", text="", icon='DOWNARROW_HLT')
4335 else:
4336 split.prop(lt, "display_gstretch", text="", icon='RIGHTARROW')
4337 split.operator("mesh.looptools_gstretch")
4338 # gstretch settings
4339 if lt.display_gstretch:
4340 box = col.column(align=True).box().column()
4341 box.prop(lt, "gstretch_method")
4342 box.prop(lt, "gstretch_delete_strokes")
4343 box.separator()
4345 col_conv = box.column(align=True)
4346 col_conv.prop(lt, "gstretch_conversion", text="")
4347 if lt.gstretch_conversion == 'distance':
4348 col_conv.prop(lt, "gstretch_conversion_distance")
4349 elif lt.gstretch_conversion == 'limit_vertices':
4350 row = col_conv.row(align=True)
4351 row.prop(lt, "gstretch_conversion_min", text="Min")
4352 row.prop(lt, "gstretch_conversion_max", text="Max")
4353 elif lt.gstretch_conversion == 'vertices':
4354 col_conv.prop(lt, "gstretch_conversion_vertices")
4355 box.separator()
4357 col_move = box.column(align=True)
4358 row = col_move.row(align=True)
4359 if lt.gstretch_lock_x:
4360 row.prop(lt, "gstretch_lock_x", text = "X", icon='LOCKED')
4361 else:
4362 row.prop(lt, "gstretch_lock_x", text = "X", icon='UNLOCKED')
4363 if lt.gstretch_lock_y:
4364 row.prop(lt, "gstretch_lock_y", text = "Y", icon='LOCKED')
4365 else:
4366 row.prop(lt, "gstretch_lock_y", text = "Y", icon='UNLOCKED')
4367 if lt.gstretch_lock_z:
4368 row.prop(lt, "gstretch_lock_z", text = "Z", icon='LOCKED')
4369 else:
4370 row.prop(lt, "gstretch_lock_z", text = "Z", icon='UNLOCKED')
4371 col_move.prop(lt, "gstretch_influence")
4373 # loft - first line
4374 split = col.split(percentage=0.15, align=True)
4375 if lt.display_loft:
4376 split.prop(lt, "display_loft", text="", icon='DOWNARROW_HLT')
4377 else:
4378 split.prop(lt, "display_loft", text="", icon='RIGHTARROW')
4379 split.operator("mesh.looptools_bridge", text="Loft").loft = True
4380 # loft - settings
4381 if lt.display_loft:
4382 box = col.column(align=True).box().column()
4383 #box.prop(self, "mode")
4385 # top row
4386 col_top = box.column(align=True)
4387 row = col_top.row(align=True)
4388 col_left = row.column(align=True)
4389 col_right = row.column(align=True)
4390 col_right.active = lt.bridge_segments != 1
4391 col_left.prop(lt, "bridge_segments")
4392 col_right.prop(lt, "bridge_min_width", text="")
4393 # bottom row
4394 bottom_left = col_left.row()
4395 bottom_left.active = lt.bridge_segments != 1
4396 bottom_left.prop(lt, "bridge_interpolation", text="")
4397 bottom_right = col_right.row()
4398 bottom_right.active = lt.bridge_interpolation == 'cubic'
4399 bottom_right.prop(lt, "bridge_cubic_strength")
4400 # boolean properties
4401 col_top.prop(lt, "bridge_remove_faces")
4402 col_top.prop(lt, "bridge_loft_loop")
4404 # override properties
4405 col_top.separator()
4406 row = box.row(align = True)
4407 row.prop(lt, "bridge_twist")
4408 row.prop(lt, "bridge_reverse")
4410 # relax - first line
4411 split = col.split(percentage=0.15, align=True)
4412 if lt.display_relax:
4413 split.prop(lt, "display_relax", text="", icon='DOWNARROW_HLT')
4414 else:
4415 split.prop(lt, "display_relax", text="", icon='RIGHTARROW')
4416 split.operator("mesh.looptools_relax")
4417 # relax - settings
4418 if lt.display_relax:
4419 box = col.column(align=True).box().column()
4420 box.prop(lt, "relax_interpolation")
4421 box.prop(lt, "relax_input")
4422 box.prop(lt, "relax_iterations")
4423 box.prop(lt, "relax_regular")
4425 # space - first line
4426 split = col.split(percentage=0.15, align=True)
4427 if lt.display_space:
4428 split.prop(lt, "display_space", text="", icon='DOWNARROW_HLT')
4429 else:
4430 split.prop(lt, "display_space", text="", icon='RIGHTARROW')
4431 split.operator("mesh.looptools_space")
4432 # space - settings
4433 if lt.display_space:
4434 box = col.column(align=True).box().column()
4435 box.prop(lt, "space_interpolation")
4436 box.prop(lt, "space_input")
4437 box.separator()
4439 col_move = box.column(align=True)
4440 row = col_move.row(align=True)
4441 if lt.space_lock_x:
4442 row.prop(lt, "space_lock_x", text = "X", icon='LOCKED')
4443 else:
4444 row.prop(lt, "space_lock_x", text = "X", icon='UNLOCKED')
4445 if lt.space_lock_y:
4446 row.prop(lt, "space_lock_y", text = "Y", icon='LOCKED')
4447 else:
4448 row.prop(lt, "space_lock_y", text = "Y", icon='UNLOCKED')
4449 if lt.space_lock_z:
4450 row.prop(lt, "space_lock_z", text = "Z", icon='LOCKED')
4451 else:
4452 row.prop(lt, "space_lock_z", text = "Z", icon='UNLOCKED')
4453 col_move.prop(lt, "space_influence")
4456 # property group containing all properties for the gui in the panel
4457 class LoopToolsProps(bpy.types.PropertyGroup):
4459 Fake module like class
4460 bpy.context.window_manager.looptools
4463 # general display properties
4464 display_bridge = bpy.props.BoolProperty(name = "Bridge settings",
4465 description = "Display settings of the Bridge tool",
4466 default = False)
4467 display_circle = bpy.props.BoolProperty(name = "Circle settings",
4468 description = "Display settings of the Circle tool",
4469 default = False)
4470 display_curve = bpy.props.BoolProperty(name = "Curve settings",
4471 description = "Display settings of the Curve tool",
4472 default = False)
4473 display_flatten = bpy.props.BoolProperty(name = "Flatten settings",
4474 description = "Display settings of the Flatten tool",
4475 default = False)
4476 display_gstretch = bpy.props.BoolProperty(name = "Gstretch settings",
4477 description = "Display settings of the Gstretch tool",
4478 default = False)
4479 display_loft = bpy.props.BoolProperty(name = "Loft settings",
4480 description = "Display settings of the Loft tool",
4481 default = False)
4482 display_relax = bpy.props.BoolProperty(name = "Relax settings",
4483 description = "Display settings of the Relax tool",
4484 default = False)
4485 display_space = bpy.props.BoolProperty(name = "Space settings",
4486 description = "Display settings of the Space tool",
4487 default = False)
4489 # bridge properties
4490 bridge_cubic_strength = bpy.props.FloatProperty(name = "Strength",
4491 description = "Higher strength results in more fluid curves",
4492 default = 1.0,
4493 soft_min = -3.0,
4494 soft_max = 3.0)
4495 bridge_interpolation = bpy.props.EnumProperty(name = "Interpolation mode",
4496 items = (('cubic', "Cubic", "Gives curved results"),
4497 ('linear', "Linear", "Basic, fast, straight interpolation")),
4498 description = "Interpolation mode: algorithm used when creating "\
4499 "segments",
4500 default = 'cubic')
4501 bridge_loft = bpy.props.BoolProperty(name = "Loft",
4502 description = "Loft multiple loops, instead of considering them as "\
4503 "a multi-input for bridging",
4504 default = False)
4505 bridge_loft_loop = bpy.props.BoolProperty(name = "Loop",
4506 description = "Connect the first and the last loop with each other",
4507 default = False)
4508 bridge_min_width = bpy.props.IntProperty(name = "Minimum width",
4509 description = "Segments with an edge smaller than this are merged "\
4510 "(compared to base edge)",
4511 default = 0,
4512 min = 0,
4513 max = 100,
4514 subtype = 'PERCENTAGE')
4515 bridge_mode = bpy.props.EnumProperty(name = "Mode",
4516 items = (('basic', "Basic", "Fast algorithm"),
4517 ('shortest', "Shortest edge", "Slower algorithm with " \
4518 "better vertex matching")),
4519 description = "Algorithm used for bridging",
4520 default = 'shortest')
4521 bridge_remove_faces = bpy.props.BoolProperty(name = "Remove faces",
4522 description = "Remove faces that are internal after bridging",
4523 default = True)
4524 bridge_reverse = bpy.props.BoolProperty(name = "Reverse",
4525 description = "Manually override the direction in which the loops "\
4526 "are bridged. Only use if the tool gives the wrong " \
4527 "result",
4528 default = False)
4529 bridge_segments = bpy.props.IntProperty(name = "Segments",
4530 description = "Number of segments used to bridge the gap "\
4531 "(0 = automatic)",
4532 default = 1,
4533 min = 0,
4534 soft_max = 20)
4535 bridge_twist = bpy.props.IntProperty(name = "Twist",
4536 description = "Twist what vertices are connected to each other",
4537 default = 0)
4539 # circle properties
4540 circle_custom_radius = bpy.props.BoolProperty(name = "Radius",
4541 description = "Force a custom radius",
4542 default = False)
4543 circle_fit = bpy.props.EnumProperty(name = "Method",
4544 items = (("best", "Best fit", "Non-linear least squares"),
4545 ("inside", "Fit inside","Only move vertices towards the center")),
4546 description = "Method used for fitting a circle to the vertices",
4547 default = 'best')
4548 circle_flatten = bpy.props.BoolProperty(name = "Flatten",
4549 description = "Flatten the circle, instead of projecting it on the " \
4550 "mesh",
4551 default = True)
4552 circle_influence = bpy.props.FloatProperty(name = "Influence",
4553 description = "Force of the tool",
4554 default = 100.0,
4555 min = 0.0,
4556 max = 100.0,
4557 precision = 1,
4558 subtype = 'PERCENTAGE')
4559 circle_lock_x = bpy.props.BoolProperty(name = "Lock X",
4560 description = "Lock editing of the x-coordinate",
4561 default = False)
4562 circle_lock_y = bpy.props.BoolProperty(name = "Lock Y",
4563 description = "Lock editing of the y-coordinate",
4564 default = False)
4565 circle_lock_z = bpy.props.BoolProperty(name = "Lock Z",
4566 description = "Lock editing of the z-coordinate",
4567 default = False)
4568 circle_radius = bpy.props.FloatProperty(name = "Radius",
4569 description = "Custom radius for circle",
4570 default = 1.0,
4571 min = 0.0,
4572 soft_max = 1000.0)
4573 circle_regular = bpy.props.BoolProperty(name = "Regular",
4574 description = "Distribute vertices at constant distances along the " \
4575 "circle",
4576 default = True)
4578 # curve properties
4579 curve_boundaries = bpy.props.BoolProperty(name = "Boundaries",
4580 description = "Limit the tool to work within the boundaries of the "\
4581 "selected vertices",
4582 default = False)
4583 curve_influence = bpy.props.FloatProperty(name = "Influence",
4584 description = "Force of the tool",
4585 default = 100.0,
4586 min = 0.0,
4587 max = 100.0,
4588 precision = 1,
4589 subtype = 'PERCENTAGE')
4590 curve_interpolation = bpy.props.EnumProperty(name = "Interpolation",
4591 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4592 ("linear", "Linear", "Simple and fast linear algorithm")),
4593 description = "Algorithm used for interpolation",
4594 default = 'cubic')
4595 curve_lock_x = bpy.props.BoolProperty(name = "Lock X",
4596 description = "Lock editing of the x-coordinate",
4597 default = False)
4598 curve_lock_y = bpy.props.BoolProperty(name = "Lock Y",
4599 description = "Lock editing of the y-coordinate",
4600 default = False)
4601 curve_lock_z = bpy.props.BoolProperty(name = "Lock Z",
4602 description = "Lock editing of the z-coordinate",
4603 default = False)
4604 curve_regular = bpy.props.BoolProperty(name = "Regular",
4605 description = "Distribute vertices at constant distances along the " \
4606 "curve",
4607 default = True)
4608 curve_restriction = bpy.props.EnumProperty(name = "Restriction",
4609 items = (("none", "None", "No restrictions on vertex movement"),
4610 ("extrude", "Extrude only","Only allow extrusions (no "\
4611 "indentations)"),
4612 ("indent", "Indent only", "Only allow indentation (no "\
4613 "extrusions)")),
4614 description = "Restrictions on how the vertices can be moved",
4615 default = 'none')
4617 # flatten properties
4618 flatten_influence = bpy.props.FloatProperty(name = "Influence",
4619 description = "Force of the tool",
4620 default = 100.0,
4621 min = 0.0,
4622 max = 100.0,
4623 precision = 1,
4624 subtype = 'PERCENTAGE')
4625 flatten_lock_x = bpy.props.BoolProperty(name = "Lock X",
4626 description = "Lock editing of the x-coordinate",
4627 default = False)
4628 flatten_lock_y = bpy.props.BoolProperty(name = "Lock Y",
4629 description = "Lock editing of the y-coordinate",
4630 default = False)
4631 flatten_lock_z = bpy.props.BoolProperty(name = "Lock Z",
4632 description = "Lock editing of the z-coordinate",
4633 default = False)
4634 flatten_plane = bpy.props.EnumProperty(name = "Plane",
4635 items = (("best_fit", "Best fit", "Calculate a best fitting plane"),
4636 ("normal", "Normal", "Derive plane from averaging vertex "\
4637 "normals"),
4638 ("view", "View", "Flatten on a plane perpendicular to the "\
4639 "viewing angle")),
4640 description = "Plane on which vertices are flattened",
4641 default = 'best_fit')
4642 flatten_restriction = bpy.props.EnumProperty(name = "Restriction",
4643 items = (("none", "None", "No restrictions on vertex movement"),
4644 ("bounding_box", "Bounding box", "Vertices are restricted to "\
4645 "movement inside the bounding box of the selection")),
4646 description = "Restrictions on how the vertices can be moved",
4647 default = 'none')
4649 # gstretch properties
4650 gstretch_conversion = bpy.props.EnumProperty(name = "Conversion",
4651 items = (("distance", "Distance", "Set the distance between vertices "\
4652 "of the converted grease pencil stroke"),
4653 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "\
4654 "number of vertices that converted GP strokes will have"),
4655 ("vertices", "Exact vertices", "Set the exact number of vertices "\
4656 "that converted grease pencil strokes will have. Short strokes "\
4657 "with few points may contain less vertices than this number."),
4658 ("none", "No simplification", "Convert each grease pencil point "\
4659 "to a vertex")),
4660 description = "If grease pencil strokes are converted to geometry, "\
4661 "use this simplification method",
4662 default = 'limit_vertices')
4663 gstretch_conversion_distance = bpy.props.FloatProperty(name = "Distance",
4664 description = "Absolute distance between vertices along the converted "\
4665 "grease pencil stroke",
4666 default = 0.1,
4667 min = 0.000001,
4668 soft_min = 0.01,
4669 soft_max = 100)
4670 gstretch_conversion_max = bpy.props.IntProperty(name = "Max Vertices",
4671 description = "Maximum number of vertices grease pencil strokes will "\
4672 "have, when they are converted to geomtery",
4673 default = 32,
4674 min = 3,
4675 soft_max = 500,
4676 update = gstretch_update_min)
4677 gstretch_conversion_min = bpy.props.IntProperty(name = "Min Vertices",
4678 description = "Minimum number of vertices grease pencil strokes will "\
4679 "have, when they are converted to geomtery",
4680 default = 8,
4681 min = 3,
4682 soft_max = 500,
4683 update = gstretch_update_max)
4684 gstretch_conversion_vertices = bpy.props.IntProperty(name = "Vertices",
4685 description = "Number of vertices grease pencil strokes will "\
4686 "have, when they are converted to geometry. If strokes have less "\
4687 "points than required, the 'Spread evenly' method is used",
4688 default = 32,
4689 min = 3,
4690 soft_max = 500)
4691 gstretch_delete_strokes = bpy.props.BoolProperty(name="Delete strokes",
4692 description = "Remove Grease Pencil strokes if they have been used "\
4693 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
4694 default = False)
4695 gstretch_influence = bpy.props.FloatProperty(name = "Influence",
4696 description = "Force of the tool",
4697 default = 100.0,
4698 min = 0.0,
4699 max = 100.0,
4700 precision = 1,
4701 subtype = 'PERCENTAGE')
4702 gstretch_lock_x = bpy.props.BoolProperty(name = "Lock X",
4703 description = "Lock editing of the x-coordinate",
4704 default = False)
4705 gstretch_lock_y = bpy.props.BoolProperty(name = "Lock Y",
4706 description = "Lock editing of the y-coordinate",
4707 default = False)
4708 gstretch_lock_z = bpy.props.BoolProperty(name = "Lock Z",
4709 description = "Lock editing of the z-coordinate",
4710 default = False)
4711 gstretch_method = bpy.props.EnumProperty(name = "Method",
4712 items = (("project", "Project", "Project vertices onto the stroke, "\
4713 "using vertex normals and connected edges"),
4714 ("irregular", "Spread", "Distribute vertices along the full "\
4715 "stroke, retaining relative distances between the vertices"),
4716 ("regular", "Spread evenly", "Distribute vertices at regular "\
4717 "distances along the full stroke")),
4718 description = "Method of distributing the vertices over the Grease "\
4719 "Pencil stroke",
4720 default = 'regular')
4722 # relax properties
4723 relax_input = bpy.props.EnumProperty(name = "Input",
4724 items = (("all", "Parallel (all)", "Also use non-selected "\
4725 "parallel loops as input"),
4726 ("selected", "Selection","Only use selected vertices as input")),
4727 description = "Loops that are relaxed",
4728 default = 'selected')
4729 relax_interpolation = bpy.props.EnumProperty(name = "Interpolation",
4730 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4731 ("linear", "Linear", "Simple and fast linear algorithm")),
4732 description = "Algorithm used for interpolation",
4733 default = 'cubic')
4734 relax_iterations = bpy.props.EnumProperty(name = "Iterations",
4735 items = (("1", "1", "One"),
4736 ("3", "3", "Three"),
4737 ("5", "5", "Five"),
4738 ("10", "10", "Ten"),
4739 ("25", "25", "Twenty-five")),
4740 description = "Number of times the loop is relaxed",
4741 default = "1")
4742 relax_regular = bpy.props.BoolProperty(name = "Regular",
4743 description = "Distribute vertices at constant distances along the" \
4744 "loop",
4745 default = True)
4747 # space properties
4748 space_influence = bpy.props.FloatProperty(name = "Influence",
4749 description = "Force of the tool",
4750 default = 100.0,
4751 min = 0.0,
4752 max = 100.0,
4753 precision = 1,
4754 subtype = 'PERCENTAGE')
4755 space_input = bpy.props.EnumProperty(name = "Input",
4756 items = (("all", "Parallel (all)", "Also use non-selected "\
4757 "parallel loops as input"),
4758 ("selected", "Selection","Only use selected vertices as input")),
4759 description = "Loops that are spaced",
4760 default = 'selected')
4761 space_interpolation = bpy.props.EnumProperty(name = "Interpolation",
4762 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4763 ("linear", "Linear", "Vertices are projected on existing edges")),
4764 description = "Algorithm used for interpolation",
4765 default = 'cubic')
4766 space_lock_x = bpy.props.BoolProperty(name = "Lock X",
4767 description = "Lock editing of the x-coordinate",
4768 default = False)
4769 space_lock_y = bpy.props.BoolProperty(name = "Lock Y",
4770 description = "Lock editing of the y-coordinate",
4771 default = False)
4772 space_lock_z = bpy.props.BoolProperty(name = "Lock Z",
4773 description = "Lock editing of the z-coordinate",
4774 default = False)
4777 # draw function for integration in menus
4778 def menu_func(self, context):
4779 self.layout.menu("VIEW3D_MT_edit_mesh_looptools")
4780 self.layout.separator()
4783 # define classes for registration
4784 classes = [VIEW3D_MT_edit_mesh_looptools,
4785 VIEW3D_PT_tools_looptools,
4786 LoopToolsProps,
4787 Bridge,
4788 Circle,
4789 Curve,
4790 Flatten,
4791 GStretch,
4792 Relax,
4793 Space]
4796 # registering and menu integration
4797 def register():
4798 for c in classes:
4799 bpy.utils.register_class(c)
4800 bpy.types.VIEW3D_MT_edit_mesh_specials.prepend(menu_func)
4801 bpy.types.WindowManager.looptools = bpy.props.PointerProperty(\
4802 type = LoopToolsProps)
4805 # unregistering and removing menus
4806 def unregister():
4807 for c in classes:
4808 bpy.utils.unregister_class(c)
4809 bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func)
4810 try:
4811 del bpy.types.WindowManager.looptools
4812 except:
4813 pass
4816 if __name__ == "__main__":
4817 register()