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