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