fix for error reading gzip'd x3d/vrml files
[blender-addons.git] / mesh_looptools.py
blob20c9b6735162c4f73d742f1df41f7e563a809d87
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, 2, 0),
23 "blender": (2, 63, 0),
24 "location": "View3D > Toolbar and View3D > Specials (W-key)",
25 "warning": "",
26 "description": "Mesh modelling toolkit. Several tools to aid modelling",
27 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
28 "Scripts/Modeling/LoopTools",
29 "tracker_url": "http://projects.blender.org/tracker/index.php?"
30 "func=detail&aid=26189",
31 "category": "Mesh"}
34 import bmesh
35 import bpy
36 import collections
37 import mathutils
38 import math
39 from bpy_extras import view3d_utils
42 ##########################################
43 ####### General functions ################
44 ##########################################
47 # used by all tools to improve speed on reruns
48 looptools_cache = {}
51 # force a full recalculation next time
52 def cache_delete(tool):
53 if tool in looptools_cache:
54 del looptools_cache[tool]
57 # check cache for stored information
58 def cache_read(tool, object, bm, input_method, boundaries):
59 # current tool not cached yet
60 if tool not in looptools_cache:
61 return(False, False, False, False, False)
62 # check if selected object didn't change
63 if object.name != looptools_cache[tool]["object"]:
64 return(False, False, False, False, False)
65 # check if input didn't change
66 if input_method != looptools_cache[tool]["input_method"]:
67 return(False, False, False, False, False)
68 if boundaries != looptools_cache[tool]["boundaries"]:
69 return(False, False, False, False, False)
70 modifiers = [mod.name for mod in object.modifiers if mod.show_viewport \
71 and mod.type == 'MIRROR']
72 if modifiers != looptools_cache[tool]["modifiers"]:
73 return(False, False, False, False, False)
74 input = [v.index for v in bm.verts if v.select and not v.hide]
75 if input != looptools_cache[tool]["input"]:
76 return(False, False, False, False, False)
77 # reading values
78 single_loops = looptools_cache[tool]["single_loops"]
79 loops = looptools_cache[tool]["loops"]
80 derived = looptools_cache[tool]["derived"]
81 mapping = looptools_cache[tool]["mapping"]
83 return(True, single_loops, loops, derived, mapping)
86 # store information in the cache
87 def cache_write(tool, object, bm, input_method, boundaries, single_loops,
88 loops, derived, mapping):
89 # clear cache of current tool
90 if tool in looptools_cache:
91 del looptools_cache[tool]
92 # prepare values to be saved to cache
93 input = [v.index for v in bm.verts if v.select and not v.hide]
94 modifiers = [mod.name for mod in object.modifiers if mod.show_viewport \
95 and mod.type == 'MIRROR']
96 # update cache
97 looptools_cache[tool] = {"input": input, "object": object.name,
98 "input_method": input_method, "boundaries": boundaries,
99 "single_loops": single_loops, "loops": loops,
100 "derived": derived, "mapping": mapping, "modifiers": modifiers}
103 # calculates natural cubic splines through all given knots
104 def calculate_cubic_splines(bm_mod, tknots, knots):
105 # hack for circular loops
106 if knots[0] == knots[-1] and len(knots) > 1:
107 circular = True
108 k_new1 = []
109 for k in range(-1, -5, -1):
110 if k - 1 < -len(knots):
111 k += len(knots)
112 k_new1.append(knots[k-1])
113 k_new2 = []
114 for k in range(4):
115 if k + 1 > len(knots) - 1:
116 k -= len(knots)
117 k_new2.append(knots[k+1])
118 for k in k_new1:
119 knots.insert(0, k)
120 for k in k_new2:
121 knots.append(k)
122 t_new1 = []
123 total1 = 0
124 for t in range(-1, -5, -1):
125 if t - 1 < -len(tknots):
126 t += len(tknots)
127 total1 += tknots[t] - tknots[t-1]
128 t_new1.append(tknots[0] - total1)
129 t_new2 = []
130 total2 = 0
131 for t in range(4):
132 if t + 1 > len(tknots) - 1:
133 t -= len(tknots)
134 total2 += tknots[t+1] - tknots[t]
135 t_new2.append(tknots[-1] + total2)
136 for t in t_new1:
137 tknots.insert(0, t)
138 for t in t_new2:
139 tknots.append(t)
140 else:
141 circular = False
142 # end of hack
144 n = len(knots)
145 if n < 2:
146 return False
147 x = tknots[:]
148 locs = [bm_mod.verts[k].co[:] for k in knots]
149 result = []
150 for j in range(3):
151 a = []
152 for i in locs:
153 a.append(i[j])
154 h = []
155 for i in range(n-1):
156 if x[i+1] - x[i] == 0:
157 h.append(1e-8)
158 else:
159 h.append(x[i+1] - x[i])
160 q = [False]
161 for i in range(1, n-1):
162 q.append(3/h[i]*(a[i+1]-a[i]) - 3/h[i-1]*(a[i]-a[i-1]))
163 l = [1.0]
164 u = [0.0]
165 z = [0.0]
166 for i in range(1, n-1):
167 l.append(2*(x[i+1]-x[i-1]) - h[i-1]*u[i-1])
168 if l[i] == 0:
169 l[i] = 1e-8
170 u.append(h[i] / l[i])
171 z.append((q[i] - h[i-1] * z[i-1]) / l[i])
172 l.append(1.0)
173 z.append(0.0)
174 b = [False for i in range(n-1)]
175 c = [False for i in range(n)]
176 d = [False for i in range(n-1)]
177 c[n-1] = 0.0
178 for i in range(n-2, -1, -1):
179 c[i] = z[i] - u[i]*c[i+1]
180 b[i] = (a[i+1]-a[i])/h[i] - h[i]*(c[i+1]+2*c[i])/3
181 d[i] = (c[i+1]-c[i]) / (3*h[i])
182 for i in range(n-1):
183 result.append([a[i], b[i], c[i], d[i], x[i]])
184 splines = []
185 for i in range(len(knots)-1):
186 splines.append([result[i], result[i+n-1], result[i+(n-1)*2]])
187 if circular: # cleaning up after hack
188 knots = knots[4:-4]
189 tknots = tknots[4:-4]
191 return(splines)
194 # calculates linear splines through all given knots
195 def calculate_linear_splines(bm_mod, tknots, knots):
196 splines = []
197 for i in range(len(knots)-1):
198 a = bm_mod.verts[knots[i]].co
199 b = bm_mod.verts[knots[i+1]].co
200 d = b-a
201 t = tknots[i]
202 u = tknots[i+1]-t
203 splines.append([a, d, t, u]) # [locStart, locDif, tStart, tDif]
205 return(splines)
208 # calculate a best-fit plane to the given vertices
209 def calculate_plane(bm_mod, loop, method="best_fit", object=False):
210 # getting the vertex locations
211 locs = [bm_mod.verts[v].co.copy() for v in loop[0]]
213 # calculating the center of masss
214 com = mathutils.Vector()
215 for loc in locs:
216 com += loc
217 com /= len(locs)
218 x, y, z = com
220 if method == 'best_fit':
221 # creating the covariance matrix
222 mat = mathutils.Matrix(((0.0, 0.0, 0.0),
223 (0.0, 0.0, 0.0),
224 (0.0, 0.0, 0.0),
226 for loc in locs:
227 mat[0][0] += (loc[0]-x)**2
228 mat[1][0] += (loc[0]-x)*(loc[1]-y)
229 mat[2][0] += (loc[0]-x)*(loc[2]-z)
230 mat[0][1] += (loc[1]-y)*(loc[0]-x)
231 mat[1][1] += (loc[1]-y)**2
232 mat[2][1] += (loc[1]-y)*(loc[2]-z)
233 mat[0][2] += (loc[2]-z)*(loc[0]-x)
234 mat[1][2] += (loc[2]-z)*(loc[1]-y)
235 mat[2][2] += (loc[2]-z)**2
237 # calculating the normal to the plane
238 normal = False
239 try:
240 mat.invert()
241 except:
242 if sum(mat[0]) == 0.0:
243 normal = mathutils.Vector((1.0, 0.0, 0.0))
244 elif sum(mat[1]) == 0.0:
245 normal = mathutils.Vector((0.0, 1.0, 0.0))
246 elif sum(mat[2]) == 0.0:
247 normal = mathutils.Vector((0.0, 0.0, 1.0))
248 if not normal:
249 # warning! this is different from .normalize()
250 itermax = 500
251 iter = 0
252 vec = mathutils.Vector((1.0, 1.0, 1.0))
253 vec2 = (mat * vec)/(mat * vec).length
254 while vec != vec2 and iter<itermax:
255 iter+=1
256 vec = vec2
257 vec2 = mat * vec
258 if vec2.length != 0:
259 vec2 /= vec2.length
260 if vec2.length == 0:
261 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
262 normal = vec2
264 elif method == 'normal':
265 # averaging the vertex normals
266 v_normals = [bm_mod.verts[v].normal for v in loop[0]]
267 normal = mathutils.Vector()
268 for v_normal in v_normals:
269 normal += v_normal
270 normal /= len(v_normals)
271 normal.normalize()
273 elif method == 'view':
274 # calculate view normal
275 rotation = bpy.context.space_data.region_3d.view_matrix.to_3x3().\
276 inverted()
277 normal = rotation * mathutils.Vector((0.0, 0.0, 1.0))
278 if object:
279 normal = object.matrix_world.inverted().to_euler().to_matrix() * \
280 normal
282 return(com, normal)
285 # calculate splines based on given interpolation method (controller function)
286 def calculate_splines(interpolation, bm_mod, tknots, knots):
287 if interpolation == 'cubic':
288 splines = calculate_cubic_splines(bm_mod, tknots, knots[:])
289 else: # interpolations == 'linear'
290 splines = calculate_linear_splines(bm_mod, tknots, knots[:])
292 return(splines)
295 # check loops and only return valid ones
296 def check_loops(loops, mapping, bm_mod):
297 valid_loops = []
298 for loop, circular in loops:
299 # loop needs to have at least 3 vertices
300 if len(loop) < 3:
301 continue
302 # loop needs at least 1 vertex in the original, non-mirrored mesh
303 if mapping:
304 all_virtual = True
305 for vert in loop:
306 if mapping[vert] > -1:
307 all_virtual = False
308 break
309 if all_virtual:
310 continue
311 # vertices can not all be at the same location
312 stacked = True
313 for i in range(len(loop) - 1):
314 if (bm_mod.verts[loop[i]].co - \
315 bm_mod.verts[loop[i+1]].co).length > 1e-6:
316 stacked = False
317 break
318 if stacked:
319 continue
320 # passed all tests, loop is valid
321 valid_loops.append([loop, circular])
323 return(valid_loops)
326 # input: bmesh, output: dict with the edge-key as key and face-index as value
327 def dict_edge_faces(bm):
328 edge_faces = dict([[edgekey(edge), []] for edge in bm.edges if \
329 not edge.hide])
330 for face in bm.faces:
331 if face.hide:
332 continue
333 for key in face_edgekeys(face):
334 edge_faces[key].append(face.index)
336 return(edge_faces)
339 # input: bmesh (edge-faces optional), output: dict with face-face connections
340 def dict_face_faces(bm, edge_faces=False):
341 if not edge_faces:
342 edge_faces = dict_edge_faces(bm)
344 connected_faces = dict([[face.index, []] for face in bm.faces if \
345 not face.hide])
346 for face in bm.faces:
347 if face.hide:
348 continue
349 for edge_key in face_edgekeys(face):
350 for connected_face in edge_faces[edge_key]:
351 if connected_face == face.index:
352 continue
353 connected_faces[face.index].append(connected_face)
355 return(connected_faces)
358 # input: bmesh, output: dict with the vert index as key and edge-keys as value
359 def dict_vert_edges(bm):
360 vert_edges = dict([[v.index, []] for v in bm.verts if not v.hide])
361 for edge in bm.edges:
362 if edge.hide:
363 continue
364 ek = edgekey(edge)
365 for vert in ek:
366 vert_edges[vert].append(ek)
368 return(vert_edges)
371 # input: bmesh, output: dict with the vert index as key and face index as value
372 def dict_vert_faces(bm):
373 vert_faces = dict([[v.index, []] for v in bm.verts if not v.hide])
374 for face in bm.faces:
375 if not face.hide:
376 for vert in face.verts:
377 vert_faces[vert.index].append(face.index)
379 return(vert_faces)
382 # input: list of edge-keys, output: dictionary with vertex-vertex connections
383 def dict_vert_verts(edge_keys):
384 # create connection data
385 vert_verts = {}
386 for ek in edge_keys:
387 for i in range(2):
388 if ek[i] in vert_verts:
389 vert_verts[ek[i]].append(ek[1-i])
390 else:
391 vert_verts[ek[i]] = [ek[1-i]]
393 return(vert_verts)
396 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
397 def edgekey(edge):
398 return(tuple(sorted([edge.verts[0].index, edge.verts[1].index])))
401 # returns the edgekeys of a bmesh face
402 def face_edgekeys(face):
403 return([tuple(sorted([edge.verts[0].index, edge.verts[1].index])) for \
404 edge in face.edges])
407 # calculate input loops
408 def get_connected_input(object, bm, scene, input):
409 # get mesh with modifiers applied
410 derived, bm_mod = get_derived_bmesh(object, bm, scene)
412 # calculate selected loops
413 edge_keys = [edgekey(edge) for edge in bm_mod.edges if \
414 edge.select and not edge.hide]
415 loops = get_connected_selections(edge_keys)
417 # if only selected loops are needed, we're done
418 if input == 'selected':
419 return(derived, bm_mod, loops)
420 # elif input == 'all':
421 loops = get_parallel_loops(bm_mod, loops)
423 return(derived, bm_mod, loops)
426 # sorts all edge-keys into a list of loops
427 def get_connected_selections(edge_keys):
428 # create connection data
429 vert_verts = dict_vert_verts(edge_keys)
431 # find loops consisting of connected selected edges
432 loops = []
433 while len(vert_verts) > 0:
434 loop = [iter(vert_verts.keys()).__next__()]
435 growing = True
436 flipped = False
438 # extend loop
439 while growing:
440 # no more connection data for current vertex
441 if loop[-1] not in vert_verts:
442 if not flipped:
443 loop.reverse()
444 flipped = True
445 else:
446 growing = False
447 else:
448 extended = False
449 for i, next_vert in enumerate(vert_verts[loop[-1]]):
450 if next_vert not in loop:
451 vert_verts[loop[-1]].pop(i)
452 if len(vert_verts[loop[-1]]) == 0:
453 del vert_verts[loop[-1]]
454 # remove connection both ways
455 if next_vert in vert_verts:
456 if len(vert_verts[next_vert]) == 1:
457 del vert_verts[next_vert]
458 else:
459 vert_verts[next_vert].remove(loop[-1])
460 loop.append(next_vert)
461 extended = True
462 break
463 if not extended:
464 # found one end of the loop, continue with next
465 if not flipped:
466 loop.reverse()
467 flipped = True
468 # found both ends of the loop, stop growing
469 else:
470 growing = False
472 # check if loop is circular
473 if loop[0] in vert_verts:
474 if loop[-1] in vert_verts[loop[0]]:
475 # is circular
476 if len(vert_verts[loop[0]]) == 1:
477 del vert_verts[loop[0]]
478 else:
479 vert_verts[loop[0]].remove(loop[-1])
480 if len(vert_verts[loop[-1]]) == 1:
481 del vert_verts[loop[-1]]
482 else:
483 vert_verts[loop[-1]].remove(loop[0])
484 loop = [loop, True]
485 else:
486 # not circular
487 loop = [loop, False]
488 else:
489 # not circular
490 loop = [loop, False]
492 loops.append(loop)
494 return(loops)
497 # get the derived mesh data, if there is a mirror modifier
498 def get_derived_bmesh(object, bm, scene):
499 # check for mirror modifiers
500 if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
501 derived = True
502 # disable other modifiers
503 show_viewport = [mod.name for mod in object.modifiers if \
504 mod.show_viewport]
505 for mod in object.modifiers:
506 if mod.type != 'MIRROR':
507 mod.show_viewport = False
508 # get derived mesh
509 bm_mod = bmesh.new()
510 mesh_mod = object.to_mesh(scene, True, 'PREVIEW')
511 bm_mod.from_mesh(mesh_mod)
512 bpy.context.blend_data.meshes.remove(mesh_mod)
513 # re-enable other modifiers
514 for mod_name in show_viewport:
515 object.modifiers[mod_name].show_viewport = True
516 # no mirror modifiers, so no derived mesh necessary
517 else:
518 derived = False
519 bm_mod = bm
521 return(derived, bm_mod)
524 # return a mapping of derived indices to indices
525 def get_mapping(derived, bm, bm_mod, single_vertices, full_search, loops):
526 if not derived:
527 return(False)
529 if full_search:
530 verts = [v for v in bm.verts if not v.hide]
531 else:
532 verts = [v for v in bm.verts if v.select and not v.hide]
534 # non-selected vertices around single vertices also need to be mapped
535 if single_vertices:
536 mapping = dict([[vert, -1] for vert in single_vertices])
537 verts_mod = [bm_mod.verts[vert] for vert in single_vertices]
538 for v in verts:
539 for v_mod in verts_mod:
540 if (v.co - v_mod.co).length < 1e-6:
541 mapping[v_mod.index] = v.index
542 break
543 real_singles = [v_real for v_real in mapping.values() if v_real>-1]
545 verts_indices = [vert.index for vert in verts]
546 for face in [face for face in bm.faces if not face.select \
547 and not face.hide]:
548 for vert in face.verts:
549 if vert.index in real_singles:
550 for v in face.verts:
551 if not v.index in verts_indices:
552 if v not in verts:
553 verts.append(v)
554 break
556 # create mapping of derived indices to indices
557 mapping = dict([[vert, -1] for loop in loops for vert in loop[0]])
558 if single_vertices:
559 for single in single_vertices:
560 mapping[single] = -1
561 verts_mod = [bm_mod.verts[i] for i in mapping.keys()]
562 for v in verts:
563 for v_mod in verts_mod:
564 if (v.co - v_mod.co).length < 1e-6:
565 mapping[v_mod.index] = v.index
566 verts_mod.remove(v_mod)
567 break
569 return(mapping)
572 # returns a list of all loops parallel to the input, input included
573 def get_parallel_loops(bm_mod, loops):
574 # get required dictionaries
575 edge_faces = dict_edge_faces(bm_mod)
576 connected_faces = dict_face_faces(bm_mod, edge_faces)
577 # turn vertex loops into edge loops
578 edgeloops = []
579 for loop in loops:
580 edgeloop = [[sorted([loop[0][i], loop[0][i+1]]) for i in \
581 range(len(loop[0])-1)], loop[1]]
582 if loop[1]: # circular
583 edgeloop[0].append(sorted([loop[0][-1], loop[0][0]]))
584 edgeloops.append(edgeloop[:])
585 # variables to keep track while iterating
586 all_edgeloops = []
587 has_branches = False
589 for loop in edgeloops:
590 # initialise with original loop
591 all_edgeloops.append(loop[0])
592 newloops = [loop[0]]
593 verts_used = []
594 for edge in loop[0]:
595 if edge[0] not in verts_used:
596 verts_used.append(edge[0])
597 if edge[1] not in verts_used:
598 verts_used.append(edge[1])
600 # find parallel loops
601 while len(newloops) > 0:
602 side_a = []
603 side_b = []
604 for i in newloops[-1]:
605 i = tuple(i)
606 forbidden_side = False
607 if not i in edge_faces:
608 # weird input with branches
609 has_branches = True
610 break
611 for face in edge_faces[i]:
612 if len(side_a) == 0 and forbidden_side != "a":
613 side_a.append(face)
614 if forbidden_side:
615 break
616 forbidden_side = "a"
617 continue
618 elif side_a[-1] in connected_faces[face] and \
619 forbidden_side != "a":
620 side_a.append(face)
621 if forbidden_side:
622 break
623 forbidden_side = "a"
624 continue
625 if len(side_b) == 0 and forbidden_side != "b":
626 side_b.append(face)
627 if forbidden_side:
628 break
629 forbidden_side = "b"
630 continue
631 elif side_b[-1] in connected_faces[face] and \
632 forbidden_side != "b":
633 side_b.append(face)
634 if forbidden_side:
635 break
636 forbidden_side = "b"
637 continue
639 if has_branches:
640 # weird input with branches
641 break
643 newloops.pop(-1)
644 sides = []
645 if side_a:
646 sides.append(side_a)
647 if side_b:
648 sides.append(side_b)
650 for side in sides:
651 extraloop = []
652 for fi in side:
653 for key in face_edgekeys(bm_mod.faces[fi]):
654 if key[0] not in verts_used and key[1] not in \
655 verts_used:
656 extraloop.append(key)
657 break
658 if extraloop:
659 for key in extraloop:
660 for new_vert in key:
661 if new_vert not in verts_used:
662 verts_used.append(new_vert)
663 newloops.append(extraloop)
664 all_edgeloops.append(extraloop)
666 # input contains branches, only return selected loop
667 if has_branches:
668 return(loops)
670 # change edgeloops into normal loops
671 loops = []
672 for edgeloop in all_edgeloops:
673 loop = []
674 # grow loop by comparing vertices between consecutive edge-keys
675 for i in range(len(edgeloop)-1):
676 for vert in range(2):
677 if edgeloop[i][vert] in edgeloop[i+1]:
678 loop.append(edgeloop[i][vert])
679 break
680 if loop:
681 # add starting vertex
682 for vert in range(2):
683 if edgeloop[0][vert] != loop[0]:
684 loop = [edgeloop[0][vert]] + loop
685 break
686 # add ending vertex
687 for vert in range(2):
688 if edgeloop[-1][vert] != loop[-1]:
689 loop.append(edgeloop[-1][vert])
690 break
691 # check if loop is circular
692 if loop[0] == loop[-1]:
693 circular = True
694 loop = loop[:-1]
695 else:
696 circular = False
697 loops.append([loop, circular])
699 return(loops)
702 # gather initial data
703 def initialise():
704 global_undo = bpy.context.user_preferences.edit.use_global_undo
705 bpy.context.user_preferences.edit.use_global_undo = False
706 object = bpy.context.active_object
707 if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
708 # ensure that selection is synced for the derived mesh
709 bpy.ops.object.mode_set(mode='OBJECT')
710 bpy.ops.object.mode_set(mode='EDIT')
711 bm = bmesh.from_edit_mesh(object.data)
713 return(global_undo, object, bm)
716 # move the vertices to their new locations
717 def move_verts(object, bm, mapping, move, influence):
718 for loop in move:
719 for index, loc in loop:
720 if mapping:
721 if mapping[index] == -1:
722 continue
723 else:
724 index = mapping[index]
725 if influence >= 0:
726 bm.verts[index].co = loc*(influence/100) + \
727 bm.verts[index].co*((100-influence)/100)
728 else:
729 bm.verts[index].co = loc
730 bm.normal_update()
731 object.data.update()
734 # load custom tool settings
735 def settings_load(self):
736 lt = bpy.context.window_manager.looptools
737 tool = self.name.split()[0].lower()
738 keys = self.as_keywords().keys()
739 for key in keys:
740 setattr(self, key, getattr(lt, tool + "_" + key))
743 # store custom tool settings
744 def settings_write(self):
745 lt = bpy.context.window_manager.looptools
746 tool = self.name.split()[0].lower()
747 keys = self.as_keywords().keys()
748 for key in keys:
749 setattr(lt, tool + "_" + key, getattr(self, key))
752 # clean up and set settings back to original state
753 def terminate(global_undo):
754 context = bpy.context
756 # update editmesh cached data
757 obj = context.active_object
758 if obj.mode == 'EDIT':
759 bmesh.update_edit_mesh(obj.data, tessface=True, destructive=True)
761 context.user_preferences.edit.use_global_undo = global_undo
764 ##########################################
765 ####### Bridge functions #################
766 ##########################################
768 # calculate a cubic spline through the middle section of 4 given coordinates
769 def bridge_calculate_cubic_spline(bm, coordinates):
770 result = []
771 x = [0, 1, 2, 3]
773 for j in range(3):
774 a = []
775 for i in coordinates:
776 a.append(float(i[j]))
777 h = []
778 for i in range(3):
779 h.append(x[i+1]-x[i])
780 q = [False]
781 for i in range(1,3):
782 q.append(3.0/h[i]*(a[i+1]-a[i])-3.0/h[i-1]*(a[i]-a[i-1]))
783 l = [1.0]
784 u = [0.0]
785 z = [0.0]
786 for i in range(1,3):
787 l.append(2.0*(x[i+1]-x[i-1])-h[i-1]*u[i-1])
788 u.append(h[i]/l[i])
789 z.append((q[i]-h[i-1]*z[i-1])/l[i])
790 l.append(1.0)
791 z.append(0.0)
792 b = [False for i in range(3)]
793 c = [False for i in range(4)]
794 d = [False for i in range(3)]
795 c[3] = 0.0
796 for i in range(2,-1,-1):
797 c[i] = z[i]-u[i]*c[i+1]
798 b[i] = (a[i+1]-a[i])/h[i]-h[i]*(c[i+1]+2.0*c[i])/3.0
799 d[i] = (c[i+1]-c[i])/(3.0*h[i])
800 for i in range(3):
801 result.append([a[i], b[i], c[i], d[i], x[i]])
802 spline = [result[1], result[4], result[7]]
804 return(spline)
807 # return a list with new vertex location vectors, a list with face vertex
808 # integers, and the highest vertex integer in the virtual mesh
809 def bridge_calculate_geometry(bm, lines, vertex_normals, segments,
810 interpolation, cubic_strength, min_width, max_vert_index):
811 new_verts = []
812 faces = []
814 # calculate location based on interpolation method
815 def get_location(line, segment, splines):
816 v1 = bm.verts[lines[line][0]].co
817 v2 = bm.verts[lines[line][1]].co
818 if interpolation == 'linear':
819 return v1 + (segment/segments) * (v2-v1)
820 else: # interpolation == 'cubic'
821 m = (segment/segments)
822 ax,bx,cx,dx,tx = splines[line][0]
823 x = ax+bx*m+cx*m**2+dx*m**3
824 ay,by,cy,dy,ty = splines[line][1]
825 y = ay+by*m+cy*m**2+dy*m**3
826 az,bz,cz,dz,tz = splines[line][2]
827 z = az+bz*m+cz*m**2+dz*m**3
828 return mathutils.Vector((x, y, z))
830 # no interpolation needed
831 if segments == 1:
832 for i, line in enumerate(lines):
833 if i < len(lines)-1:
834 faces.append([line[0], lines[i+1][0], lines[i+1][1], line[1]])
835 # more than 1 segment, interpolate
836 else:
837 # calculate splines (if necessary) once, so no recalculations needed
838 if interpolation == 'cubic':
839 splines = []
840 for line in lines:
841 v1 = bm.verts[line[0]].co
842 v2 = bm.verts[line[1]].co
843 size = (v2-v1).length * cubic_strength
844 splines.append(bridge_calculate_cubic_spline(bm,
845 [v1+size*vertex_normals[line[0]], v1, v2,
846 v2+size*vertex_normals[line[1]]]))
847 else:
848 splines = False
850 # create starting situation
851 virtual_width = [(bm.verts[lines[i][0]].co -
852 bm.verts[lines[i+1][0]].co).length for i
853 in range(len(lines)-1)]
854 new_verts = [get_location(0, seg, splines) for seg in range(1,
855 segments)]
856 first_line_indices = [i for i in range(max_vert_index+1,
857 max_vert_index+segments)]
859 prev_verts = new_verts[:] # vertex locations of verts on previous line
860 prev_vert_indices = first_line_indices[:]
861 max_vert_index += segments - 1 # highest vertex index in virtual mesh
862 next_verts = [] # vertex locations of verts on current line
863 next_vert_indices = []
865 for i, line in enumerate(lines):
866 if i < len(lines)-1:
867 v1 = line[0]
868 v2 = lines[i+1][0]
869 end_face = True
870 for seg in range(1, segments):
871 loc1 = prev_verts[seg-1]
872 loc2 = get_location(i+1, seg, splines)
873 if (loc1-loc2).length < (min_width/100)*virtual_width[i] \
874 and line[1]==lines[i+1][1]:
875 # triangle, no new vertex
876 faces.append([v1, v2, prev_vert_indices[seg-1],
877 prev_vert_indices[seg-1]])
878 next_verts += prev_verts[seg-1:]
879 next_vert_indices += prev_vert_indices[seg-1:]
880 end_face = False
881 break
882 else:
883 if i == len(lines)-2 and lines[0] == lines[-1]:
884 # quad with first line, no new vertex
885 faces.append([v1, v2, first_line_indices[seg-1],
886 prev_vert_indices[seg-1]])
887 v2 = first_line_indices[seg-1]
888 v1 = prev_vert_indices[seg-1]
889 else:
890 # quad, add new vertex
891 max_vert_index += 1
892 faces.append([v1, v2, max_vert_index,
893 prev_vert_indices[seg-1]])
894 v2 = max_vert_index
895 v1 = prev_vert_indices[seg-1]
896 new_verts.append(loc2)
897 next_verts.append(loc2)
898 next_vert_indices.append(max_vert_index)
899 if end_face:
900 faces.append([v1, v2, lines[i+1][1], line[1]])
902 prev_verts = next_verts[:]
903 prev_vert_indices = next_vert_indices[:]
904 next_verts = []
905 next_vert_indices = []
907 return(new_verts, faces, max_vert_index)
910 # calculate lines (list of lists, vertex indices) that are used for bridging
911 def bridge_calculate_lines(bm, loops, mode, twist, reverse):
912 lines = []
913 loop1, loop2 = [i[0] for i in loops]
914 loop1_circular, loop2_circular = [i[1] for i in loops]
915 circular = loop1_circular or loop2_circular
916 circle_full = False
918 # calculate loop centers
919 centers = []
920 for loop in [loop1, loop2]:
921 center = mathutils.Vector()
922 for vertex in loop:
923 center += bm.verts[vertex].co
924 center /= len(loop)
925 centers.append(center)
926 for i, loop in enumerate([loop1, loop2]):
927 for vertex in loop:
928 if bm.verts[vertex].co == centers[i]:
929 # prevent zero-length vectors in angle comparisons
930 centers[i] += mathutils.Vector((0.01, 0, 0))
931 break
932 center1, center2 = centers
934 # calculate the normals of the virtual planes that the loops are on
935 normals = []
936 normal_plurity = False
937 for i, loop in enumerate([loop1, loop2]):
938 # covariance matrix
939 mat = mathutils.Matrix(((0.0, 0.0, 0.0),
940 (0.0, 0.0, 0.0),
941 (0.0, 0.0, 0.0)))
942 x, y, z = centers[i]
943 for loc in [bm.verts[vertex].co for vertex in loop]:
944 mat[0][0] += (loc[0]-x)**2
945 mat[1][0] += (loc[0]-x)*(loc[1]-y)
946 mat[2][0] += (loc[0]-x)*(loc[2]-z)
947 mat[0][1] += (loc[1]-y)*(loc[0]-x)
948 mat[1][1] += (loc[1]-y)**2
949 mat[2][1] += (loc[1]-y)*(loc[2]-z)
950 mat[0][2] += (loc[2]-z)*(loc[0]-x)
951 mat[1][2] += (loc[2]-z)*(loc[1]-y)
952 mat[2][2] += (loc[2]-z)**2
953 # plane normal
954 normal = False
955 if sum(mat[0]) < 1e-6 or sum(mat[1]) < 1e-6 or sum(mat[2]) < 1e-6:
956 normal_plurity = True
957 try:
958 mat.invert()
959 except:
960 if sum(mat[0]) == 0:
961 normal = mathutils.Vector((1.0, 0.0, 0.0))
962 elif sum(mat[1]) == 0:
963 normal = mathutils.Vector((0.0, 1.0, 0.0))
964 elif sum(mat[2]) == 0:
965 normal = mathutils.Vector((0.0, 0.0, 1.0))
966 if not normal:
967 # warning! this is different from .normalize()
968 itermax = 500
969 iter = 0
970 vec = mathutils.Vector((1.0, 1.0, 1.0))
971 vec2 = (mat * vec)/(mat * vec).length
972 while vec != vec2 and iter<itermax:
973 iter+=1
974 vec = vec2
975 vec2 = mat * vec
976 if vec2.length != 0:
977 vec2 /= vec2.length
978 if vec2.length == 0:
979 vec2 = mathutils.Vector((1.0, 1.0, 1.0))
980 normal = vec2
981 normals.append(normal)
982 # have plane normals face in the same direction (maximum angle: 90 degrees)
983 if ((center1 + normals[0]) - center2).length < \
984 ((center1 - normals[0]) - center2).length:
985 normals[0].negate()
986 if ((center2 + normals[1]) - center1).length > \
987 ((center2 - normals[1]) - center1).length:
988 normals[1].negate()
990 # rotation matrix, representing the difference between the plane normals
991 axis = normals[0].cross(normals[1])
992 axis = mathutils.Vector([loc if abs(loc) > 1e-8 else 0 for loc in axis])
993 if axis.angle(mathutils.Vector((0, 0, 1)), 0) > 1.5707964:
994 axis.negate()
995 angle = normals[0].dot(normals[1])
996 rotation_matrix = mathutils.Matrix.Rotation(angle, 4, axis)
998 # if circular, rotate loops so they are aligned
999 if circular:
1000 # make sure loop1 is the circular one (or both are circular)
1001 if loop2_circular and not loop1_circular:
1002 loop1_circular, loop2_circular = True, False
1003 loop1, loop2 = loop2, loop1
1005 # match start vertex of loop1 with loop2
1006 target_vector = bm.verts[loop2[0]].co - center2
1007 dif_angles = [[(rotation_matrix * (bm.verts[vertex].co - center1)
1008 ).angle(target_vector, 0), False, i] for
1009 i, vertex in enumerate(loop1)]
1010 dif_angles.sort()
1011 if len(loop1) != len(loop2):
1012 angle_limit = dif_angles[0][0] * 1.2 # 20% margin
1013 dif_angles = [[(bm.verts[loop2[0]].co - \
1014 bm.verts[loop1[index]].co).length, angle, index] for \
1015 angle, distance, index in dif_angles if angle <= angle_limit]
1016 dif_angles.sort()
1017 loop1 = loop1[dif_angles[0][2]:] + loop1[:dif_angles[0][2]]
1019 # have both loops face the same way
1020 if normal_plurity and not circular:
1021 second_to_first, second_to_second, second_to_last = \
1022 [(bm.verts[loop1[1]].co - center1).\
1023 angle(bm.verts[loop2[i]].co - center2) for i in [0, 1, -1]]
1024 last_to_first, last_to_second = [(bm.verts[loop1[-1]].co - \
1025 center1).angle(bm.verts[loop2[i]].co - center2) for \
1026 i in [0, 1]]
1027 if (min(last_to_first, last_to_second)*1.1 < min(second_to_first, \
1028 second_to_second)) or (loop2_circular and second_to_last*1.1 < \
1029 min(second_to_first, second_to_second)):
1030 loop1.reverse()
1031 if circular:
1032 loop1 = [loop1[-1]] + loop1[:-1]
1033 else:
1034 angle = (bm.verts[loop1[0]].co - center1).\
1035 cross(bm.verts[loop1[1]].co - center1).angle(normals[0], 0)
1036 target_angle = (bm.verts[loop2[0]].co - center2).\
1037 cross(bm.verts[loop2[1]].co - center2).angle(normals[1], 0)
1038 limit = 1.5707964 # 0.5*pi, 90 degrees
1039 if not ((angle > limit and target_angle > limit) or \
1040 (angle < limit and target_angle < limit)):
1041 loop1.reverse()
1042 if circular:
1043 loop1 = [loop1[-1]] + loop1[:-1]
1044 elif normals[0].angle(normals[1]) > limit:
1045 loop1.reverse()
1046 if circular:
1047 loop1 = [loop1[-1]] + loop1[:-1]
1049 # both loops have the same length
1050 if len(loop1) == len(loop2):
1051 # manual override
1052 if twist:
1053 if abs(twist) < len(loop1):
1054 loop1 = loop1[twist:]+loop1[:twist]
1055 if reverse:
1056 loop1.reverse()
1058 lines.append([loop1[0], loop2[0]])
1059 for i in range(1, len(loop1)):
1060 lines.append([loop1[i], loop2[i]])
1062 # loops of different lengths
1063 else:
1064 # make loop1 longest loop
1065 if len(loop2) > len(loop1):
1066 loop1, loop2 = loop2, loop1
1067 loop1_circular, loop2_circular = loop2_circular, loop1_circular
1069 # manual override
1070 if twist:
1071 if abs(twist) < len(loop1):
1072 loop1 = loop1[twist:]+loop1[:twist]
1073 if reverse:
1074 loop1.reverse()
1076 # shortest angle difference doesn't always give correct start vertex
1077 if loop1_circular and not loop2_circular:
1078 shifting = 1
1079 while shifting:
1080 if len(loop1) - shifting < len(loop2):
1081 shifting = False
1082 break
1083 to_last, to_first = [(rotation_matrix *
1084 (bm.verts[loop1[-1]].co - center1)).angle((bm.\
1085 verts[loop2[i]].co - center2), 0) for i in [-1, 0]]
1086 if to_first < to_last:
1087 loop1 = [loop1[-1]] + loop1[:-1]
1088 shifting += 1
1089 else:
1090 shifting = False
1091 break
1093 # basic shortest side first
1094 if mode == 'basic':
1095 lines.append([loop1[0], loop2[0]])
1096 for i in range(1, len(loop1)):
1097 if i >= len(loop2) - 1:
1098 # triangles
1099 lines.append([loop1[i], loop2[-1]])
1100 else:
1101 # quads
1102 lines.append([loop1[i], loop2[i]])
1104 # shortest edge algorithm
1105 else: # mode == 'shortest'
1106 lines.append([loop1[0], loop2[0]])
1107 prev_vert2 = 0
1108 for i in range(len(loop1) -1):
1109 if prev_vert2 == len(loop2) - 1 and not loop2_circular:
1110 # force triangles, reached end of loop2
1111 tri, quad = 0, 1
1112 elif prev_vert2 == len(loop2) - 1 and loop2_circular:
1113 # at end of loop2, but circular, so check with first vert
1114 tri, quad = [(bm.verts[loop1[i+1]].co -
1115 bm.verts[loop2[j]].co).length
1116 for j in [prev_vert2, 0]]
1118 circle_full = 2
1119 elif len(loop1) - 1 - i == len(loop2) - 1 - prev_vert2 and \
1120 not circle_full:
1121 # force quads, otherwise won't make it to end of loop2
1122 tri, quad = 1, 0
1123 else:
1124 # calculate if tri or quad gives shortest edge
1125 tri, quad = [(bm.verts[loop1[i+1]].co -
1126 bm.verts[loop2[j]].co).length
1127 for j in range(prev_vert2, prev_vert2+2)]
1129 # triangle
1130 if tri < quad:
1131 lines.append([loop1[i+1], loop2[prev_vert2]])
1132 if circle_full == 2:
1133 circle_full = False
1134 # quad
1135 elif not circle_full:
1136 lines.append([loop1[i+1], loop2[prev_vert2+1]])
1137 prev_vert2 += 1
1138 # quad to first vertex of loop2
1139 else:
1140 lines.append([loop1[i+1], loop2[0]])
1141 prev_vert2 = 0
1142 circle_full = True
1144 # final face for circular loops
1145 if loop1_circular and loop2_circular:
1146 lines.append([loop1[0], loop2[0]])
1148 return(lines)
1151 # calculate number of segments needed
1152 def bridge_calculate_segments(bm, lines, loops, segments):
1153 # return if amount of segments is set by user
1154 if segments != 0:
1155 return segments
1157 # edge lengths
1158 average_edge_length = [(bm.verts[vertex].co - \
1159 bm.verts[loop[0][i+1]].co).length for loop in loops for \
1160 i, vertex in enumerate(loop[0][:-1])]
1161 # closing edges of circular loops
1162 average_edge_length += [(bm.verts[loop[0][-1]].co - \
1163 bm.verts[loop[0][0]].co).length for loop in loops if loop[1]]
1165 # average lengths
1166 average_edge_length = sum(average_edge_length) / len(average_edge_length)
1167 average_bridge_length = sum([(bm.verts[v1].co - \
1168 bm.verts[v2].co).length for v1, v2 in lines]) / len(lines)
1170 segments = max(1, round(average_bridge_length / average_edge_length))
1172 return(segments)
1175 # return dictionary with vertex index as key, and the normal vector as value
1176 def bridge_calculate_virtual_vertex_normals(bm, lines, loops, edge_faces,
1177 edgekey_to_edge):
1178 if not edge_faces: # interpolation isn't set to cubic
1179 return False
1181 # pity reduce() isn't one of the basic functions in python anymore
1182 def average_vector_dictionary(dic):
1183 for key, vectors in dic.items():
1184 #if type(vectors) == type([]) and len(vectors) > 1:
1185 if len(vectors) > 1:
1186 average = mathutils.Vector()
1187 for vector in vectors:
1188 average += vector
1189 average /= len(vectors)
1190 dic[key] = [average]
1191 return dic
1193 # get all edges of the loop
1194 edges = [[edgekey_to_edge[tuple(sorted([loops[j][0][i],
1195 loops[j][0][i+1]]))] for i in range(len(loops[j][0])-1)] for \
1196 j in [0,1]]
1197 edges = edges[0] + edges[1]
1198 for j in [0, 1]:
1199 if loops[j][1]: # circular
1200 edges.append(edgekey_to_edge[tuple(sorted([loops[j][0][0],
1201 loops[j][0][-1]]))])
1204 calculation based on face topology (assign edge-normals to vertices)
1206 edge_normal = face_normal x edge_vector
1207 vertex_normal = average(edge_normals)
1209 vertex_normals = dict([(vertex, []) for vertex in loops[0][0]+loops[1][0]])
1210 for edge in edges:
1211 faces = edge_faces[edgekey(edge)] # valid faces connected to edge
1213 if faces:
1214 # get edge coordinates
1215 v1, v2 = [bm.verts[edgekey(edge)[i]].co for i in [0,1]]
1216 edge_vector = v1 - v2
1217 if edge_vector.length < 1e-4:
1218 # zero-length edge, vertices at same location
1219 continue
1220 edge_center = (v1 + v2) / 2
1222 # average face coordinates, if connected to more than 1 valid face
1223 if len(faces) > 1:
1224 face_normal = mathutils.Vector()
1225 face_center = mathutils.Vector()
1226 for face in faces:
1227 face_normal += face.normal
1228 face_center += face.calc_center_median()
1229 face_normal /= len(faces)
1230 face_center /= len(faces)
1231 else:
1232 face_normal = faces[0].normal
1233 face_center = faces[0].calc_center_median()
1234 if face_normal.length < 1e-4:
1235 # faces with a surface of 0 have no face normal
1236 continue
1238 # calculate virtual edge normal
1239 edge_normal = edge_vector.cross(face_normal)
1240 edge_normal.length = 0.01
1241 if (face_center - (edge_center + edge_normal)).length > \
1242 (face_center - (edge_center - edge_normal)).length:
1243 # make normal face the correct way
1244 edge_normal.negate()
1245 edge_normal.normalize()
1246 # add virtual edge normal as entry for both vertices it connects
1247 for vertex in edgekey(edge):
1248 vertex_normals[vertex].append(edge_normal)
1250 """
1251 calculation based on connection with other loop (vertex focused method)
1252 - used for vertices that aren't connected to any valid faces
1254 plane_normal = edge_vector x connection_vector
1255 vertex_normal = plane_normal x edge_vector
1257 vertices = [vertex for vertex, normal in vertex_normals.items() if not \
1258 normal]
1260 if vertices:
1261 # edge vectors connected to vertices
1262 edge_vectors = dict([[vertex, []] for vertex in vertices])
1263 for edge in edges:
1264 for v in edgekey(edge):
1265 if v in edge_vectors:
1266 edge_vector = bm.verts[edgekey(edge)[0]].co - \
1267 bm.verts[edgekey(edge)[1]].co
1268 if edge_vector.length < 1e-4:
1269 # zero-length edge, vertices at same location
1270 continue
1271 edge_vectors[v].append(edge_vector)
1273 # connection vectors between vertices of both loops
1274 connection_vectors = dict([[vertex, []] for vertex in vertices])
1275 connections = dict([[vertex, []] for vertex in vertices])
1276 for v1, v2 in lines:
1277 if v1 in connection_vectors or v2 in connection_vectors:
1278 new_vector = bm.verts[v1].co - bm.verts[v2].co
1279 if new_vector.length < 1e-4:
1280 # zero-length connection vector,
1281 # vertices in different loops at same location
1282 continue
1283 if v1 in connection_vectors:
1284 connection_vectors[v1].append(new_vector)
1285 connections[v1].append(v2)
1286 if v2 in connection_vectors:
1287 connection_vectors[v2].append(new_vector)
1288 connections[v2].append(v1)
1289 connection_vectors = average_vector_dictionary(connection_vectors)
1290 connection_vectors = dict([[vertex, vector[0]] if vector else \
1291 [vertex, []] for vertex, vector in connection_vectors.items()])
1293 for vertex, values in edge_vectors.items():
1294 # vertex normal doesn't matter, just assign a random vector to it
1295 if not connection_vectors[vertex]:
1296 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1297 continue
1299 # calculate to what location the vertex is connected,
1300 # used to determine what way to flip the normal
1301 connected_center = mathutils.Vector()
1302 for v in connections[vertex]:
1303 connected_center += bm.verts[v].co
1304 if len(connections[vertex]) > 1:
1305 connected_center /= len(connections[vertex])
1306 if len(connections[vertex]) == 0:
1307 # shouldn't be possible, but better safe than sorry
1308 vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1309 continue
1311 # can't do proper calculations, because of zero-length vector
1312 if not values:
1313 if (connected_center - (bm.verts[vertex].co + \
1314 connection_vectors[vertex])).length < (connected_center - \
1315 (bm.verts[vertex].co - connection_vectors[vertex])).\
1316 length:
1317 connection_vectors[vertex].negate()
1318 vertex_normals[vertex] = [connection_vectors[vertex].\
1319 normalized()]
1320 continue
1322 # calculate vertex normals using edge-vectors,
1323 # connection-vectors and the derived plane normal
1324 for edge_vector in values:
1325 plane_normal = edge_vector.cross(connection_vectors[vertex])
1326 vertex_normal = edge_vector.cross(plane_normal)
1327 vertex_normal.length = 0.1
1328 if (connected_center - (bm.verts[vertex].co + \
1329 vertex_normal)).length < (connected_center - \
1330 (bm.verts[vertex].co - vertex_normal)).length:
1331 # make normal face the correct way
1332 vertex_normal.negate()
1333 vertex_normal.normalize()
1334 vertex_normals[vertex].append(vertex_normal)
1336 # average virtual vertex normals, based on all edges it's connected to
1337 vertex_normals = average_vector_dictionary(vertex_normals)
1338 vertex_normals = dict([[vertex, vector[0]] for vertex, vector in \
1339 vertex_normals.items()])
1341 return(vertex_normals)
1344 # add vertices to mesh
1345 def bridge_create_vertices(bm, vertices):
1346 for i in range(len(vertices)):
1347 bm.verts.new(vertices[i])
1350 # add faces to mesh
1351 def bridge_create_faces(object, bm, faces, twist):
1352 # have the normal point the correct way
1353 if twist < 0:
1354 [face.reverse() for face in faces]
1355 faces = [face[2:]+face[:2] if face[0]==face[1] else face for \
1356 face in faces]
1358 # eekadoodle prevention
1359 for i in range(len(faces)):
1360 if not faces[i][-1]:
1361 if faces[i][0] == faces[i][-1]:
1362 faces[i] = [faces[i][1], faces[i][2], faces[i][3], faces[i][1]]
1363 else:
1364 faces[i] = [faces[i][-1]] + faces[i][:-1]
1365 # result of converting from pre-bmesh period
1366 if faces[i][-1] == faces[i][-2]:
1367 faces[i] = faces[i][:-1]
1369 for i in range(len(faces)):
1370 bm.faces.new([bm.verts[v] for v in faces[i]])
1371 bm.normal_update()
1372 object.data.update(calc_edges=True) # calc_edges prevents memory-corruption
1375 # calculate input loops
1376 def bridge_get_input(bm):
1377 # create list of internal edges, which should be skipped
1378 eks_of_selected_faces = [item for sublist in [face_edgekeys(face) for \
1379 face in bm.faces if face.select and not face.hide] for item in sublist]
1380 edge_count = {}
1381 for ek in eks_of_selected_faces:
1382 if ek in edge_count:
1383 edge_count[ek] += 1
1384 else:
1385 edge_count[ek] = 1
1386 internal_edges = [ek for ek in edge_count if edge_count[ek] > 1]
1388 # sort correct edges into loops
1389 selected_edges = [edgekey(edge) for edge in bm.edges if edge.select \
1390 and not edge.hide and edgekey(edge) not in internal_edges]
1391 loops = get_connected_selections(selected_edges)
1393 return(loops)
1396 # return values needed by the bridge operator
1397 def bridge_initialise(bm, interpolation):
1398 if interpolation == 'cubic':
1399 # dict with edge-key as key and list of connected valid faces as value
1400 face_blacklist = [face.index for face in bm.faces if face.select or \
1401 face.hide]
1402 edge_faces = dict([[edgekey(edge), []] for edge in bm.edges if not \
1403 edge.hide])
1404 for face in bm.faces:
1405 if face.index in face_blacklist:
1406 continue
1407 for key in face_edgekeys(face):
1408 edge_faces[key].append(face)
1409 # dictionary with the edge-key as key and edge as value
1410 edgekey_to_edge = dict([[edgekey(edge), edge] for edge in \
1411 bm.edges if edge.select and not edge.hide])
1412 else:
1413 edge_faces = False
1414 edgekey_to_edge = False
1416 # selected faces input
1417 old_selected_faces = [face.index for face in bm.faces if face.select \
1418 and not face.hide]
1420 # find out if faces created by bridging should be smoothed
1421 smooth = False
1422 if bm.faces:
1423 if sum([face.smooth for face in bm.faces])/len(bm.faces) \
1424 >= 0.5:
1425 smooth = True
1427 return(edge_faces, edgekey_to_edge, old_selected_faces, smooth)
1430 # return a string with the input method
1431 def bridge_input_method(loft, loft_loop):
1432 method = ""
1433 if loft:
1434 if loft_loop:
1435 method = "Loft loop"
1436 else:
1437 method = "Loft no-loop"
1438 else:
1439 method = "Bridge"
1441 return(method)
1444 # match up loops in pairs, used for multi-input bridging
1445 def bridge_match_loops(bm, loops):
1446 # calculate average loop normals and centers
1447 normals = []
1448 centers = []
1449 for vertices, circular in loops:
1450 normal = mathutils.Vector()
1451 center = mathutils.Vector()
1452 for vertex in vertices:
1453 normal += bm.verts[vertex].normal
1454 center += bm.verts[vertex].co
1455 normals.append(normal / len(vertices) / 10)
1456 centers.append(center / len(vertices))
1458 # possible matches if loop normals are faced towards the center
1459 # of the other loop
1460 matches = dict([[i, []] for i in range(len(loops))])
1461 matches_amount = 0
1462 for i in range(len(loops) + 1):
1463 for j in range(i+1, len(loops)):
1464 if (centers[i] - centers[j]).length > (centers[i] - (centers[j] \
1465 + normals[j])).length and (centers[j] - centers[i]).length > \
1466 (centers[j] - (centers[i] + normals[i])).length:
1467 matches_amount += 1
1468 matches[i].append([(centers[i] - centers[j]).length, i, j])
1469 matches[j].append([(centers[i] - centers[j]).length, j, i])
1470 # if no loops face each other, just make matches between all the loops
1471 if matches_amount == 0:
1472 for i in range(len(loops) + 1):
1473 for j in range(i+1, len(loops)):
1474 matches[i].append([(centers[i] - centers[j]).length, i, j])
1475 matches[j].append([(centers[i] - centers[j]).length, j, i])
1476 for key, value in matches.items():
1477 value.sort()
1479 # matches based on distance between centers and number of vertices in loops
1480 new_order = []
1481 for loop_index in range(len(loops)):
1482 if loop_index in new_order:
1483 continue
1484 loop_matches = matches[loop_index]
1485 if not loop_matches:
1486 continue
1487 shortest_distance = loop_matches[0][0]
1488 shortest_distance *= 1.1
1489 loop_matches = [[abs(len(loops[loop_index][0]) - \
1490 len(loops[loop[2]][0])), loop[0], loop[1], loop[2]] for loop in \
1491 loop_matches if loop[0] < shortest_distance]
1492 loop_matches.sort()
1493 for match in loop_matches:
1494 if match[3] not in new_order:
1495 new_order += [loop_index, match[3]]
1496 break
1498 # reorder loops based on matches
1499 if len(new_order) >= 2:
1500 loops = [loops[i] for i in new_order]
1502 return(loops)
1505 # remove old_selected_faces
1506 def bridge_remove_internal_faces(bm, old_selected_faces):
1507 # collect bmesh faces and internal bmesh edges
1508 remove_faces = [bm.faces[face] for face in old_selected_faces]
1509 edges = collections.Counter([edge.index for face in remove_faces for \
1510 edge in face.edges])
1511 remove_edges = [bm.edges[edge] for edge in edges if edges[edge] > 1]
1513 # remove internal faces and edges
1514 for face in remove_faces:
1515 bm.faces.remove(face)
1516 for edge in remove_edges:
1517 bm.edges.remove(edge)
1520 # update list of internal faces that are flagged for removal
1521 def bridge_save_unused_faces(bm, old_selected_faces, loops):
1522 # key: vertex index, value: lists of selected faces using it
1523 vertex_to_face = dict([[i, []] for i in range(len(bm.verts))])
1524 [[vertex_to_face[vertex.index].append(face) for vertex in \
1525 bm.faces[face].verts] for face in old_selected_faces]
1527 # group selected faces that are connected
1528 groups = []
1529 grouped_faces = []
1530 for face in old_selected_faces:
1531 if face in grouped_faces:
1532 continue
1533 grouped_faces.append(face)
1534 group = [face]
1535 new_faces = [face]
1536 while new_faces:
1537 grow_face = new_faces[0]
1538 for vertex in bm.faces[grow_face].verts:
1539 vertex_face_group = [face for face in vertex_to_face[\
1540 vertex.index] if face not in grouped_faces]
1541 new_faces += vertex_face_group
1542 grouped_faces += vertex_face_group
1543 group += vertex_face_group
1544 new_faces.pop(0)
1545 groups.append(group)
1547 # key: vertex index, value: True/False (is it in a loop that is used)
1548 used_vertices = dict([[i, 0] for i in range(len(bm.verts))])
1549 for loop in loops:
1550 for vertex in loop[0]:
1551 used_vertices[vertex] = True
1553 # check if group is bridged, if not remove faces from internal faces list
1554 for group in groups:
1555 used = False
1556 for face in group:
1557 if used:
1558 break
1559 for vertex in bm.faces[face].verts:
1560 if used_vertices[vertex.index]:
1561 used = True
1562 break
1563 if not used:
1564 for face in group:
1565 old_selected_faces.remove(face)
1568 # add the newly created faces to the selection
1569 def bridge_select_new_faces(bm, amount, smooth):
1570 for i in range(amount):
1571 bm.faces[-(i+1)].select_set(True)
1572 bm.faces[-(i+1)].smooth = smooth
1575 # sort loops, so they are connected in the correct order when lofting
1576 def bridge_sort_loops(bm, loops, loft_loop):
1577 # simplify loops to single points, and prepare for pathfinding
1578 x, y, z = [[sum([bm.verts[i].co[j] for i in loop[0]]) / \
1579 len(loop[0]) for loop in loops] for j in range(3)]
1580 nodes = [mathutils.Vector((x[i], y[i], z[i])) for i in range(len(loops))]
1582 active_node = 0
1583 open = [i for i in range(1, len(loops))]
1584 path = [[0,0]]
1585 # connect node to path, that is shortest to active_node
1586 while len(open) > 0:
1587 distances = [(nodes[active_node] - nodes[i]).length for i in open]
1588 active_node = open[distances.index(min(distances))]
1589 open.remove(active_node)
1590 path.append([active_node, min(distances)])
1591 # check if we didn't start in the middle of the path
1592 for i in range(2, len(path)):
1593 if (nodes[path[i][0]]-nodes[0]).length < path[i][1]:
1594 temp = path[:i]
1595 path.reverse()
1596 path = path[:-i] + temp
1597 break
1599 # reorder loops
1600 loops = [loops[i[0]] for i in path]
1601 # if requested, duplicate first loop at last position, so loft can loop
1602 if loft_loop:
1603 loops = loops + [loops[0]]
1605 return(loops)
1608 ##########################################
1609 ####### Circle functions #################
1610 ##########################################
1612 # convert 3d coordinates to 2d coordinates on plane
1613 def circle_3d_to_2d(bm_mod, loop, com, normal):
1614 # project vertices onto the plane
1615 verts = [bm_mod.verts[v] for v in loop[0]]
1616 verts_projected = [[v.co - (v.co - com).dot(normal) * normal, v.index]
1617 for v in verts]
1619 # calculate two vectors (p and q) along the plane
1620 m = mathutils.Vector((normal[0] + 1.0, normal[1], normal[2]))
1621 p = m - (m.dot(normal) * normal)
1622 if p.dot(p) == 0.0:
1623 m = mathutils.Vector((normal[0], normal[1] + 1.0, normal[2]))
1624 p = m - (m.dot(normal) * normal)
1625 q = p.cross(normal)
1627 # change to 2d coordinates using perpendicular projection
1628 locs_2d = []
1629 for loc, vert in verts_projected:
1630 vloc = loc - com
1631 x = p.dot(vloc) / p.dot(p)
1632 y = q.dot(vloc) / q.dot(q)
1633 locs_2d.append([x, y, vert])
1635 return(locs_2d, p, q)
1638 # calculate a best-fit circle to the 2d locations on the plane
1639 def circle_calculate_best_fit(locs_2d):
1640 # initial guess
1641 x0 = 0.0
1642 y0 = 0.0
1643 r = 1.0
1645 # calculate center and radius (non-linear least squares solution)
1646 for iter in range(500):
1647 jmat = []
1648 k = []
1649 for v in locs_2d:
1650 d = (v[0]**2-2.0*x0*v[0]+v[1]**2-2.0*y0*v[1]+x0**2+y0**2)**0.5
1651 jmat.append([(x0-v[0])/d, (y0-v[1])/d, -1.0])
1652 k.append(-(((v[0]-x0)**2+(v[1]-y0)**2)**0.5-r))
1653 jmat2 = mathutils.Matrix(((0.0, 0.0, 0.0),
1654 (0.0, 0.0, 0.0),
1655 (0.0, 0.0, 0.0),
1657 k2 = mathutils.Vector((0.0, 0.0, 0.0))
1658 for i in range(len(jmat)):
1659 k2 += mathutils.Vector(jmat[i])*k[i]
1660 jmat2[0][0] += jmat[i][0]**2
1661 jmat2[1][0] += jmat[i][0]*jmat[i][1]
1662 jmat2[2][0] += jmat[i][0]*jmat[i][2]
1663 jmat2[1][1] += jmat[i][1]**2
1664 jmat2[2][1] += jmat[i][1]*jmat[i][2]
1665 jmat2[2][2] += jmat[i][2]**2
1666 jmat2[0][1] = jmat2[1][0]
1667 jmat2[0][2] = jmat2[2][0]
1668 jmat2[1][2] = jmat2[2][1]
1669 try:
1670 jmat2.invert()
1671 except:
1672 pass
1673 dx0, dy0, dr = jmat2 * k2
1674 x0 += dx0
1675 y0 += dy0
1676 r += dr
1677 # stop iterating if we're close enough to optimal solution
1678 if abs(dx0)<1e-6 and abs(dy0)<1e-6 and abs(dr)<1e-6:
1679 break
1681 # return center of circle and radius
1682 return(x0, y0, r)
1685 # calculate circle so no vertices have to be moved away from the center
1686 def circle_calculate_min_fit(locs_2d):
1687 # center of circle
1688 x0 = (min([i[0] for i in locs_2d])+max([i[0] for i in locs_2d]))/2.0
1689 y0 = (min([i[1] for i in locs_2d])+max([i[1] for i in locs_2d]))/2.0
1690 center = mathutils.Vector([x0, y0])
1691 # radius of circle
1692 r = min([(mathutils.Vector([i[0], i[1]])-center).length for i in locs_2d])
1694 # return center of circle and radius
1695 return(x0, y0, r)
1698 # calculate the new locations of the vertices that need to be moved
1699 def circle_calculate_verts(flatten, bm_mod, locs_2d, com, p, q, normal):
1700 # changing 2d coordinates back to 3d coordinates
1701 locs_3d = []
1702 for loc in locs_2d:
1703 locs_3d.append([loc[2], loc[0]*p + loc[1]*q + com])
1705 if flatten: # flat circle
1706 return(locs_3d)
1708 else: # project the locations on the existing mesh
1709 vert_edges = dict_vert_edges(bm_mod)
1710 vert_faces = dict_vert_faces(bm_mod)
1711 faces = [f for f in bm_mod.faces if not f.hide]
1712 rays = [normal, -normal]
1713 new_locs = []
1714 for loc in locs_3d:
1715 projection = False
1716 if bm_mod.verts[loc[0]].co == loc[1]: # vertex hasn't moved
1717 projection = loc[1]
1718 else:
1719 dif = normal.angle(loc[1]-bm_mod.verts[loc[0]].co)
1720 if -1e-6 < dif < 1e-6 or math.pi-1e-6 < dif < math.pi+1e-6:
1721 # original location is already along projection normal
1722 projection = bm_mod.verts[loc[0]].co
1723 else:
1724 # quick search through adjacent faces
1725 for face in vert_faces[loc[0]]:
1726 verts = [v.co for v in bm_mod.faces[face].verts]
1727 if len(verts) == 3: # triangle
1728 v1, v2, v3 = verts
1729 v4 = False
1730 else: # assume quad
1731 v1, v2, v3, v4 = verts[:4]
1732 for ray in rays:
1733 intersect = mathutils.geometry.\
1734 intersect_ray_tri(v1, v2, v3, ray, loc[1])
1735 if intersect:
1736 projection = intersect
1737 break
1738 elif v4:
1739 intersect = mathutils.geometry.\
1740 intersect_ray_tri(v1, v3, v4, ray, loc[1])
1741 if intersect:
1742 projection = intersect
1743 break
1744 if projection:
1745 break
1746 if not projection:
1747 # check if projection is on adjacent edges
1748 for edgekey in vert_edges[loc[0]]:
1749 line1 = bm_mod.verts[edgekey[0]].co
1750 line2 = bm_mod.verts[edgekey[1]].co
1751 intersect, dist = mathutils.geometry.intersect_point_line(\
1752 loc[1], line1, line2)
1753 if 1e-6 < dist < 1 - 1e-6:
1754 projection = intersect
1755 break
1756 if not projection:
1757 # full search through the entire mesh
1758 hits = []
1759 for face in faces:
1760 verts = [v.co for v in face.verts]
1761 if len(verts) == 3: # triangle
1762 v1, v2, v3 = verts
1763 v4 = False
1764 else: # assume quad
1765 v1, v2, v3, v4 = verts[:4]
1766 for ray in rays:
1767 intersect = mathutils.geometry.intersect_ray_tri(\
1768 v1, v2, v3, ray, loc[1])
1769 if intersect:
1770 hits.append([(loc[1] - intersect).length,
1771 intersect])
1772 break
1773 elif v4:
1774 intersect = mathutils.geometry.intersect_ray_tri(\
1775 v1, v3, v4, ray, loc[1])
1776 if intersect:
1777 hits.append([(loc[1] - intersect).length,
1778 intersect])
1779 break
1780 if len(hits) >= 1:
1781 # if more than 1 hit with mesh, closest hit is new loc
1782 hits.sort()
1783 projection = hits[0][1]
1784 if not projection:
1785 # nothing to project on, remain at flat location
1786 projection = loc[1]
1787 new_locs.append([loc[0], projection])
1789 # return new positions of projected circle
1790 return(new_locs)
1793 # check loops and only return valid ones
1794 def circle_check_loops(single_loops, loops, mapping, bm_mod):
1795 valid_single_loops = {}
1796 valid_loops = []
1797 for i, [loop, circular] in enumerate(loops):
1798 # loop needs to have at least 3 vertices
1799 if len(loop) < 3:
1800 continue
1801 # loop needs at least 1 vertex in the original, non-mirrored mesh
1802 if mapping:
1803 all_virtual = True
1804 for vert in loop:
1805 if mapping[vert] > -1:
1806 all_virtual = False
1807 break
1808 if all_virtual:
1809 continue
1810 # loop has to be non-collinear
1811 collinear = True
1812 loc0 = mathutils.Vector(bm_mod.verts[loop[0]].co[:])
1813 loc1 = mathutils.Vector(bm_mod.verts[loop[1]].co[:])
1814 for v in loop[2:]:
1815 locn = mathutils.Vector(bm_mod.verts[v].co[:])
1816 if loc0 == loc1 or loc1 == locn:
1817 loc0 = loc1
1818 loc1 = locn
1819 continue
1820 d1 = loc1-loc0
1821 d2 = locn-loc1
1822 if -1e-6 < d1.angle(d2, 0) < 1e-6:
1823 loc0 = loc1
1824 loc1 = locn
1825 continue
1826 collinear = False
1827 break
1828 if collinear:
1829 continue
1830 # passed all tests, loop is valid
1831 valid_loops.append([loop, circular])
1832 valid_single_loops[len(valid_loops)-1] = single_loops[i]
1834 return(valid_single_loops, valid_loops)
1837 # calculate the location of single input vertices that need to be flattened
1838 def circle_flatten_singles(bm_mod, com, p, q, normal, single_loop):
1839 new_locs = []
1840 for vert in single_loop:
1841 loc = mathutils.Vector(bm_mod.verts[vert].co[:])
1842 new_locs.append([vert, loc - (loc-com).dot(normal)*normal])
1844 return(new_locs)
1847 # calculate input loops
1848 def circle_get_input(object, bm, scene):
1849 # get mesh with modifiers applied
1850 derived, bm_mod = get_derived_bmesh(object, bm, scene)
1852 # create list of edge-keys based on selection state
1853 faces = False
1854 for face in bm.faces:
1855 if face.select and not face.hide:
1856 faces = True
1857 break
1858 if faces:
1859 # get selected, non-hidden , non-internal edge-keys
1860 eks_selected = [key for keys in [face_edgekeys(face) for face in \
1861 bm_mod.faces if face.select and not face.hide] for key in keys]
1862 edge_count = {}
1863 for ek in eks_selected:
1864 if ek in edge_count:
1865 edge_count[ek] += 1
1866 else:
1867 edge_count[ek] = 1
1868 edge_keys = [edgekey(edge) for edge in bm_mod.edges if edge.select \
1869 and not edge.hide and edge_count.get(edgekey(edge), 1)==1]
1870 else:
1871 # no faces, so no internal edges either
1872 edge_keys = [edgekey(edge) for edge in bm_mod.edges if edge.select \
1873 and not edge.hide]
1875 # add edge-keys around single vertices
1876 verts_connected = dict([[vert, 1] for edge in [edge for edge in \
1877 bm_mod.edges if edge.select and not edge.hide] for vert in \
1878 edgekey(edge)])
1879 single_vertices = [vert.index for vert in bm_mod.verts if \
1880 vert.select and not vert.hide and not \
1881 verts_connected.get(vert.index, False)]
1883 if single_vertices and len(bm.faces)>0:
1884 vert_to_single = dict([[v.index, []] for v in bm_mod.verts \
1885 if not v.hide])
1886 for face in [face for face in bm_mod.faces if not face.select \
1887 and not face.hide]:
1888 for vert in face.verts:
1889 vert = vert.index
1890 if vert in single_vertices:
1891 for ek in face_edgekeys(face):
1892 if not vert in ek:
1893 edge_keys.append(ek)
1894 if vert not in vert_to_single[ek[0]]:
1895 vert_to_single[ek[0]].append(vert)
1896 if vert not in vert_to_single[ek[1]]:
1897 vert_to_single[ek[1]].append(vert)
1898 break
1900 # sort edge-keys into loops
1901 loops = get_connected_selections(edge_keys)
1903 # find out to which loops the single vertices belong
1904 single_loops = dict([[i, []] for i in range(len(loops))])
1905 if single_vertices and len(bm.faces)>0:
1906 for i, [loop, circular] in enumerate(loops):
1907 for vert in loop:
1908 if vert_to_single[vert]:
1909 for single in vert_to_single[vert]:
1910 if single not in single_loops[i]:
1911 single_loops[i].append(single)
1913 return(derived, bm_mod, single_vertices, single_loops, loops)
1916 # recalculate positions based on the influence of the circle shape
1917 def circle_influence_locs(locs_2d, new_locs_2d, influence):
1918 for i in range(len(locs_2d)):
1919 oldx, oldy, j = locs_2d[i]
1920 newx, newy, k = new_locs_2d[i]
1921 altx = newx*(influence/100)+ oldx*((100-influence)/100)
1922 alty = newy*(influence/100)+ oldy*((100-influence)/100)
1923 locs_2d[i] = [altx, alty, j]
1925 return(locs_2d)
1928 # project 2d locations on circle, respecting distance relations between verts
1929 def circle_project_non_regular(locs_2d, x0, y0, r):
1930 for i in range(len(locs_2d)):
1931 x, y, j = locs_2d[i]
1932 loc = mathutils.Vector([x-x0, y-y0])
1933 loc.length = r
1934 locs_2d[i] = [loc[0], loc[1], j]
1936 return(locs_2d)
1939 # project 2d locations on circle, with equal distance between all vertices
1940 def circle_project_regular(locs_2d, x0, y0, r):
1941 # find offset angle and circling direction
1942 x, y, i = locs_2d[0]
1943 loc = mathutils.Vector([x-x0, y-y0])
1944 loc.length = r
1945 offset_angle = loc.angle(mathutils.Vector([1.0, 0.0]), 0.0)
1946 loca = mathutils.Vector([x-x0, y-y0, 0.0])
1947 if loc[1] < -1e-6:
1948 offset_angle *= -1
1949 x, y, j = locs_2d[1]
1950 locb = mathutils.Vector([x-x0, y-y0, 0.0])
1951 if loca.cross(locb)[2] >= 0:
1952 ccw = 1
1953 else:
1954 ccw = -1
1955 # distribute vertices along the circle
1956 for i in range(len(locs_2d)):
1957 t = offset_angle + ccw * (i / len(locs_2d) * 2 * math.pi)
1958 x = math.cos(t) * r
1959 y = math.sin(t) * r
1960 locs_2d[i] = [x, y, locs_2d[i][2]]
1962 return(locs_2d)
1965 # shift loop, so the first vertex is closest to the center
1966 def circle_shift_loop(bm_mod, loop, com):
1967 verts, circular = loop
1968 distances = [[(bm_mod.verts[vert].co - com).length, i] \
1969 for i, vert in enumerate(verts)]
1970 distances.sort()
1971 shift = distances[0][1]
1972 loop = [verts[shift:] + verts[:shift], circular]
1974 return(loop)
1977 ##########################################
1978 ####### Curve functions ##################
1979 ##########################################
1981 # create lists with knots and points, all correctly sorted
1982 def curve_calculate_knots(loop, verts_selected):
1983 knots = [v for v in loop[0] if v in verts_selected]
1984 points = loop[0][:]
1985 # circular loop, potential for weird splines
1986 if loop[1]:
1987 offset = int(len(loop[0]) / 4)
1988 kpos = []
1989 for k in knots:
1990 kpos.append(loop[0].index(k))
1991 kdif = []
1992 for i in range(len(kpos) - 1):
1993 kdif.append(kpos[i+1] - kpos[i])
1994 kdif.append(len(loop[0]) - kpos[-1] + kpos[0])
1995 kadd = []
1996 for k in kdif:
1997 if k > 2 * offset:
1998 kadd.append([kdif.index(k), True])
1999 # next 2 lines are optional, they insert
2000 # an extra control point in small gaps
2001 #elif k > offset:
2002 # kadd.append([kdif.index(k), False])
2003 kins = []
2004 krot = False
2005 for k in kadd: # extra knots to be added
2006 if k[1]: # big gap (break circular spline)
2007 kpos = loop[0].index(knots[k[0]]) + offset
2008 if kpos > len(loop[0]) - 1:
2009 kpos -= len(loop[0])
2010 kins.append([knots[k[0]], loop[0][kpos]])
2011 kpos2 = k[0] + 1
2012 if kpos2 > len(knots)-1:
2013 kpos2 -= len(knots)
2014 kpos2 = loop[0].index(knots[kpos2]) - offset
2015 if kpos2 < 0:
2016 kpos2 += len(loop[0])
2017 kins.append([loop[0][kpos], loop[0][kpos2]])
2018 krot = loop[0][kpos2]
2019 else: # small gap (keep circular spline)
2020 k1 = loop[0].index(knots[k[0]])
2021 k2 = k[0] + 1
2022 if k2 > len(knots)-1:
2023 k2 -= len(knots)
2024 k2 = loop[0].index(knots[k2])
2025 if k2 < k1:
2026 dif = len(loop[0]) - 1 - k1 + k2
2027 else:
2028 dif = k2 - k1
2029 kn = k1 + int(dif/2)
2030 if kn > len(loop[0]) - 1:
2031 kn -= len(loop[0])
2032 kins.append([loop[0][k1], loop[0][kn]])
2033 for j in kins: # insert new knots
2034 knots.insert(knots.index(j[0]) + 1, j[1])
2035 if not krot: # circular loop
2036 knots.append(knots[0])
2037 points = loop[0][loop[0].index(knots[0]):]
2038 points += loop[0][0:loop[0].index(knots[0]) + 1]
2039 else: # non-circular loop (broken by script)
2040 krot = knots.index(krot)
2041 knots = knots[krot:] + knots[0:krot]
2042 if loop[0].index(knots[0]) > loop[0].index(knots[-1]):
2043 points = loop[0][loop[0].index(knots[0]):]
2044 points += loop[0][0:loop[0].index(knots[-1])+1]
2045 else:
2046 points = loop[0][loop[0].index(knots[0]):\
2047 loop[0].index(knots[-1]) + 1]
2048 # non-circular loop, add first and last point as knots
2049 else:
2050 if loop[0][0] not in knots:
2051 knots.insert(0, loop[0][0])
2052 if loop[0][-1] not in knots:
2053 knots.append(loop[0][-1])
2055 return(knots, points)
2058 # calculate relative positions compared to first knot
2059 def curve_calculate_t(bm_mod, knots, points, pknots, regular, circular):
2060 tpoints = []
2061 loc_prev = False
2062 len_total = 0
2064 for p in points:
2065 if p in knots:
2066 loc = pknots[knots.index(p)] # use projected knot location
2067 else:
2068 loc = mathutils.Vector(bm_mod.verts[p].co[:])
2069 if not loc_prev:
2070 loc_prev = loc
2071 len_total += (loc-loc_prev).length
2072 tpoints.append(len_total)
2073 loc_prev = loc
2074 tknots = []
2075 for p in points:
2076 if p in knots:
2077 tknots.append(tpoints[points.index(p)])
2078 if circular:
2079 tknots[-1] = tpoints[-1]
2081 # regular option
2082 if regular:
2083 tpoints_average = tpoints[-1] / (len(tpoints) - 1)
2084 for i in range(1, len(tpoints) - 1):
2085 tpoints[i] = i * tpoints_average
2086 for i in range(len(knots)):
2087 tknots[i] = tpoints[points.index(knots[i])]
2088 if circular:
2089 tknots[-1] = tpoints[-1]
2092 return(tknots, tpoints)
2095 # change the location of non-selected points to their place on the spline
2096 def curve_calculate_vertices(bm_mod, knots, tknots, points, tpoints, splines,
2097 interpolation, restriction):
2098 newlocs = {}
2099 move = []
2101 for p in points:
2102 if p in knots:
2103 continue
2104 m = tpoints[points.index(p)]
2105 if m in tknots:
2106 n = tknots.index(m)
2107 else:
2108 t = tknots[:]
2109 t.append(m)
2110 t.sort()
2111 n = t.index(m) - 1
2112 if n > len(splines) - 1:
2113 n = len(splines) - 1
2114 elif n < 0:
2115 n = 0
2117 if interpolation == 'cubic':
2118 ax, bx, cx, dx, tx = splines[n][0]
2119 x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
2120 ay, by, cy, dy, ty = splines[n][1]
2121 y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
2122 az, bz, cz, dz, tz = splines[n][2]
2123 z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
2124 newloc = mathutils.Vector([x,y,z])
2125 else: # interpolation == 'linear'
2126 a, d, t, u = splines[n]
2127 newloc = ((m-t)/u)*d + a
2129 if restriction != 'none': # vertex movement is restricted
2130 newlocs[p] = newloc
2131 else: # set the vertex to its new location
2132 move.append([p, newloc])
2134 if restriction != 'none': # vertex movement is restricted
2135 for p in points:
2136 if p in newlocs:
2137 newloc = newlocs[p]
2138 else:
2139 move.append([p, bm_mod.verts[p].co])
2140 continue
2141 oldloc = bm_mod.verts[p].co
2142 normal = bm_mod.verts[p].normal
2143 dloc = newloc - oldloc
2144 if dloc.length < 1e-6:
2145 move.append([p, newloc])
2146 elif restriction == 'extrude': # only extrusions
2147 if dloc.angle(normal, 0) < 0.5 * math.pi + 1e-6:
2148 move.append([p, newloc])
2149 else: # restriction == 'indent' only indentations
2150 if dloc.angle(normal) > 0.5 * math.pi - 1e-6:
2151 move.append([p, newloc])
2153 return(move)
2156 # trim loops to part between first and last selected vertices (including)
2157 def curve_cut_boundaries(bm_mod, loops):
2158 cut_loops = []
2159 for loop, circular in loops:
2160 if circular:
2161 # don't cut
2162 cut_loops.append([loop, circular])
2163 continue
2164 selected = [bm_mod.verts[v].select for v in loop]
2165 first = selected.index(True)
2166 selected.reverse()
2167 last = -selected.index(True)
2168 if last == 0:
2169 cut_loops.append([loop[first:], circular])
2170 else:
2171 cut_loops.append([loop[first:last], circular])
2173 return(cut_loops)
2176 # calculate input loops
2177 def curve_get_input(object, bm, boundaries, scene):
2178 # get mesh with modifiers applied
2179 derived, bm_mod = get_derived_bmesh(object, bm, scene)
2181 # vertices that still need a loop to run through it
2182 verts_unsorted = [v.index for v in bm_mod.verts if \
2183 v.select and not v.hide]
2184 # necessary dictionaries
2185 vert_edges = dict_vert_edges(bm_mod)
2186 edge_faces = dict_edge_faces(bm_mod)
2187 correct_loops = []
2189 # find loops through each selected vertex
2190 while len(verts_unsorted) > 0:
2191 loops = curve_vertex_loops(bm_mod, verts_unsorted[0], vert_edges,
2192 edge_faces)
2193 verts_unsorted.pop(0)
2195 # check if loop is fully selected
2196 search_perpendicular = False
2197 i = -1
2198 for loop, circular in loops:
2199 i += 1
2200 selected = [v for v in loop if bm_mod.verts[v].select]
2201 if len(selected) < 2:
2202 # only one selected vertex on loop, don't use
2203 loops.pop(i)
2204 continue
2205 elif len(selected) == len(loop):
2206 search_perpendicular = loop
2207 break
2208 # entire loop is selected, find perpendicular loops
2209 if search_perpendicular:
2210 for vert in loop:
2211 if vert in verts_unsorted:
2212 verts_unsorted.remove(vert)
2213 perp_loops = curve_perpendicular_loops(bm_mod, loop,
2214 vert_edges, edge_faces)
2215 for perp_loop in perp_loops:
2216 correct_loops.append(perp_loop)
2217 # normal input
2218 else:
2219 for loop, circular in loops:
2220 correct_loops.append([loop, circular])
2222 # boundaries option
2223 if boundaries:
2224 correct_loops = curve_cut_boundaries(bm_mod, correct_loops)
2226 return(derived, bm_mod, correct_loops)
2229 # return all loops that are perpendicular to the given one
2230 def curve_perpendicular_loops(bm_mod, start_loop, vert_edges, edge_faces):
2231 # find perpendicular loops
2232 perp_loops = []
2233 for start_vert in start_loop:
2234 loops = curve_vertex_loops(bm_mod, start_vert, vert_edges,
2235 edge_faces)
2236 for loop, circular in loops:
2237 selected = [v for v in loop if bm_mod.verts[v].select]
2238 if len(selected) == len(loop):
2239 continue
2240 else:
2241 perp_loops.append([loop, circular, loop.index(start_vert)])
2243 # trim loops to same lengths
2244 shortest = [[len(loop[0]), i] for i, loop in enumerate(perp_loops)\
2245 if not loop[1]]
2246 if not shortest:
2247 # all loops are circular, not trimming
2248 return([[loop[0], loop[1]] for loop in perp_loops])
2249 else:
2250 shortest = min(shortest)
2251 shortest_start = perp_loops[shortest[1]][2]
2252 before_start = shortest_start
2253 after_start = shortest[0] - shortest_start - 1
2254 bigger_before = before_start > after_start
2255 trimmed_loops = []
2256 for loop in perp_loops:
2257 # have the loop face the same direction as the shortest one
2258 if bigger_before:
2259 if loop[2] < len(loop[0]) / 2:
2260 loop[0].reverse()
2261 loop[2] = len(loop[0]) - loop[2] - 1
2262 else:
2263 if loop[2] > len(loop[0]) / 2:
2264 loop[0].reverse()
2265 loop[2] = len(loop[0]) - loop[2] - 1
2266 # circular loops can shift, to prevent wrong trimming
2267 if loop[1]:
2268 shift = shortest_start - loop[2]
2269 if loop[2] + shift > 0 and loop[2] + shift < len(loop[0]):
2270 loop[0] = loop[0][-shift:] + loop[0][:-shift]
2271 loop[2] += shift
2272 if loop[2] < 0:
2273 loop[2] += len(loop[0])
2274 elif loop[2] > len(loop[0]) -1:
2275 loop[2] -= len(loop[0])
2276 # trim
2277 start = max(0, loop[2] - before_start)
2278 end = min(len(loop[0]), loop[2] + after_start + 1)
2279 trimmed_loops.append([loop[0][start:end], False])
2281 return(trimmed_loops)
2284 # project knots on non-selected geometry
2285 def curve_project_knots(bm_mod, verts_selected, knots, points, circular):
2286 # function to project vertex on edge
2287 def project(v1, v2, v3):
2288 # v1 and v2 are part of a line
2289 # v3 is projected onto it
2290 v2 -= v1
2291 v3 -= v1
2292 p = v3.project(v2)
2293 return(p + v1)
2295 if circular: # project all knots
2296 start = 0
2297 end = len(knots)
2298 pknots = []
2299 else: # first and last knot shouldn't be projected
2300 start = 1
2301 end = -1
2302 pknots = [mathutils.Vector(bm_mod.verts[knots[0]].co[:])]
2303 for knot in knots[start:end]:
2304 if knot in verts_selected:
2305 knot_left = knot_right = False
2306 for i in range(points.index(knot)-1, -1*len(points), -1):
2307 if points[i] not in knots:
2308 knot_left = points[i]
2309 break
2310 for i in range(points.index(knot)+1, 2*len(points)):
2311 if i > len(points) - 1:
2312 i -= len(points)
2313 if points[i] not in knots:
2314 knot_right = points[i]
2315 break
2316 if knot_left and knot_right and knot_left != knot_right:
2317 knot_left = mathutils.Vector(\
2318 bm_mod.verts[knot_left].co[:])
2319 knot_right = mathutils.Vector(\
2320 bm_mod.verts[knot_right].co[:])
2321 knot = mathutils.Vector(bm_mod.verts[knot].co[:])
2322 pknots.append(project(knot_left, knot_right, knot))
2323 else:
2324 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2325 else: # knot isn't selected, so shouldn't be changed
2326 pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
2327 if not circular:
2328 pknots.append(mathutils.Vector(bm_mod.verts[knots[-1]].co[:]))
2330 return(pknots)
2333 # find all loops through a given vertex
2334 def curve_vertex_loops(bm_mod, start_vert, vert_edges, edge_faces):
2335 edges_used = []
2336 loops = []
2338 for edge in vert_edges[start_vert]:
2339 if edge in edges_used:
2340 continue
2341 loop = []
2342 circular = False
2343 for vert in edge:
2344 active_faces = edge_faces[edge]
2345 new_vert = vert
2346 growing = True
2347 while growing:
2348 growing = False
2349 new_edges = vert_edges[new_vert]
2350 loop.append(new_vert)
2351 if len(loop) > 1:
2352 edges_used.append(tuple(sorted([loop[-1], loop[-2]])))
2353 if len(new_edges) < 3 or len(new_edges) > 4:
2354 # pole
2355 break
2356 else:
2357 # find next edge
2358 for new_edge in new_edges:
2359 if new_edge in edges_used:
2360 continue
2361 eliminate = False
2362 for new_face in edge_faces[new_edge]:
2363 if new_face in active_faces:
2364 eliminate = True
2365 break
2366 if eliminate:
2367 continue
2368 # found correct new edge
2369 active_faces = edge_faces[new_edge]
2370 v1, v2 = new_edge
2371 if v1 != new_vert:
2372 new_vert = v1
2373 else:
2374 new_vert = v2
2375 if new_vert == loop[0]:
2376 circular = True
2377 else:
2378 growing = True
2379 break
2380 if circular:
2381 break
2382 loop.reverse()
2383 loops.append([loop, circular])
2385 return(loops)
2388 ##########################################
2389 ####### Flatten functions ################
2390 ##########################################
2392 # sort input into loops
2393 def flatten_get_input(bm):
2394 vert_verts = dict_vert_verts([edgekey(edge) for edge in bm.edges \
2395 if edge.select and not edge.hide])
2396 verts = [v.index for v in bm.verts if v.select and not v.hide]
2398 # no connected verts, consider all selected verts as a single input
2399 if not vert_verts:
2400 return([[verts, False]])
2402 loops = []
2403 while len(verts) > 0:
2404 # start of loop
2405 loop = [verts[0]]
2406 verts.pop(0)
2407 if loop[-1] in vert_verts:
2408 to_grow = vert_verts[loop[-1]]
2409 else:
2410 to_grow = []
2411 # grow loop
2412 while len(to_grow) > 0:
2413 new_vert = to_grow[0]
2414 to_grow.pop(0)
2415 if new_vert in loop:
2416 continue
2417 loop.append(new_vert)
2418 verts.remove(new_vert)
2419 to_grow += vert_verts[new_vert]
2420 # add loop to loops
2421 loops.append([loop, False])
2423 return(loops)
2426 # calculate position of vertex projections on plane
2427 def flatten_project(bm, loop, com, normal):
2428 verts = [bm.verts[v] for v in loop[0]]
2429 verts_projected = [[v.index, mathutils.Vector(v.co[:]) - \
2430 (mathutils.Vector(v.co[:])-com).dot(normal)*normal] for v in verts]
2432 return(verts_projected)
2437 ##########################################
2438 ####### Gstretch functions ###############
2439 ##########################################
2441 # flips loops, if necessary, to obtain maximum alignment to stroke
2442 def gstretch_align_pairs(ls_pairs, object, bm_mod, method):
2443 # returns total distance between all verts in loop and corresponding stroke
2444 def distance_loop_stroke(loop, stroke, object, bm_mod, method):
2445 stroke_lengths_cache = False
2446 loop_length = len(loop[0])
2447 total_distance = 0
2449 if method != 'regular':
2450 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2452 for i, v_index in enumerate(loop[0]):
2453 if method == 'regular':
2454 relative_distance = i / (loop_length - 1)
2455 else:
2456 relative_distance = relative_lengths[i]
2458 loc1 = object.matrix_world * bm_mod.verts[v_index].co
2459 loc2, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2460 relative_distance, stroke_lengths_cache)
2461 total_distance += (loc2 - loc1).length
2463 return(total_distance)
2465 if ls_pairs:
2466 for (loop, stroke) in ls_pairs:
2467 distance_loop_stroke
2468 total_dist = distance_loop_stroke(loop, stroke, object, bm_mod,
2469 method)
2470 loop[0].reverse()
2471 total_dist_rev = distance_loop_stroke(loop, stroke, object, bm_mod,
2472 method)
2473 if total_dist_rev > total_dist:
2474 loop[0].reverse()
2476 return(ls_pairs)
2479 # calculate vertex positions on stroke
2480 def gstretch_calculate_verts(loop, stroke, object, bm_mod, method):
2481 move = []
2482 stroke_lengths_cache = False
2483 loop_length = len(loop[0])
2484 matrix_inverse = object.matrix_world.inverted()
2486 # return intersection of line with stroke, or None
2487 def intersect_line_stroke(vec1, vec2, stroke):
2488 for i, p in enumerate(stroke.points[1:]):
2489 intersections = mathutils.geometry.intersect_line_line(vec1, vec2,
2490 p.co, stroke.points[i].co)
2491 if intersections and \
2492 (intersections[0] - intersections[1]).length < 1e-2:
2493 x, dist = mathutils.geometry.intersect_point_line(
2494 intersections[0], p.co, stroke.points[i].co)
2495 if -1 < dist < 1:
2496 return(intersections[0])
2497 return(None)
2499 if method == 'project':
2500 projection_vectors = []
2501 vert_edges = dict_vert_edges(bm_mod)
2503 for v_index in loop[0]:
2504 for ek in vert_edges[v_index]:
2505 v1, v2 = ek
2506 v1 = bm_mod.verts[v1]
2507 v2 = bm_mod.verts[v2]
2508 if v1.select + v2.select == 1 and not v1.hide and not v2.hide:
2509 vec1 = object.matrix_world * v1.co
2510 vec2 = object.matrix_world * v2.co
2511 intersection = intersect_line_stroke(vec1, vec2, stroke)
2512 if intersection:
2513 break
2514 if not intersection:
2515 v = bm_mod.verts[v_index]
2516 intersection = intersect_line_stroke(v.co, v.co + v.normal,
2517 stroke)
2518 if intersection:
2519 move.append([v_index, matrix_inverse * intersection])
2521 else:
2522 if method == 'irregular':
2523 relative_lengths = gstretch_relative_lengths(loop, bm_mod)
2525 for i, v_index in enumerate(loop[0]):
2526 if method == 'regular':
2527 relative_distance = i / (loop_length - 1)
2528 else: # method == 'irregular'
2529 relative_distance = relative_lengths[i]
2530 loc, stroke_lengths_cache = gstretch_eval_stroke(stroke,
2531 relative_distance, stroke_lengths_cache)
2532 loc = matrix_inverse * loc
2533 move.append([v_index, loc])
2535 return(move)
2538 # erases the grease pencil stroke
2539 def gstretch_erase_stroke(stroke, context):
2540 # change 3d coordinate into a stroke-point
2541 def sp(loc, context):
2542 lib = {'name': "",
2543 'pen_flip': False,
2544 'is_start': False,
2545 'location': (0, 0, 0),
2546 'mouse': (view3d_utils.location_3d_to_region_2d(\
2547 context.region, context.space_data.region_3d, loc)),
2548 'pressure': 1,
2549 'time': 0}
2550 return(lib)
2552 erase_stroke = [sp(p.co, context) for p in stroke.points]
2553 if erase_stroke:
2554 erase_stroke[0]['is_start'] = True
2555 bpy.ops.gpencil.draw(mode='ERASER', stroke=erase_stroke)
2558 # get point on stroke, given by relative distance (0.0 - 1.0)
2559 def gstretch_eval_stroke(stroke, distance, stroke_lengths_cache=False):
2560 # use cache if available
2561 if not stroke_lengths_cache:
2562 lengths = [0]
2563 for i, p in enumerate(stroke.points[1:]):
2564 lengths.append((p.co - stroke.points[i].co).length + \
2565 lengths[-1])
2566 total_length = max(lengths[-1], 1e-7)
2567 stroke_lengths_cache = [length / total_length for length in
2568 lengths]
2569 stroke_lengths = stroke_lengths_cache[:]
2571 if distance in stroke_lengths:
2572 loc = stroke.points[stroke_lengths.index(distance)].co
2573 elif distance > stroke_lengths[-1]:
2574 # should be impossible, but better safe than sorry
2575 loc = stroke.points[-1].co
2576 else:
2577 stroke_lengths.append(distance)
2578 stroke_lengths.sort()
2579 stroke_index = stroke_lengths.index(distance)
2580 interval_length = stroke_lengths[stroke_index+1] - \
2581 stroke_lengths[stroke_index-1]
2582 distance_relative = (distance - stroke_lengths[stroke_index-1]) / \
2583 interval_length
2584 interval_vector = stroke.points[stroke_index].co - \
2585 stroke.points[stroke_index-1].co
2586 loc = stroke.points[stroke_index-1].co + \
2587 distance_relative * interval_vector
2589 return(loc, stroke_lengths_cache)
2592 # get grease pencil strokes for the active object
2593 def gstretch_get_strokes(object):
2594 gp = object.grease_pencil
2595 if not gp:
2596 return(None)
2597 layer = gp.layers.active
2598 if not layer:
2599 return(None)
2600 frame = layer.active_frame
2601 if not frame:
2602 return(None)
2603 strokes = frame.strokes
2604 if len(strokes) < 1:
2605 return(None)
2607 return(strokes)
2610 # returns a list with loop-stroke pairs
2611 def gstretch_match_loops_strokes(loops, strokes, object, bm_mod):
2612 if not loops or not strokes:
2613 return(None)
2615 # calculate loop centers
2616 loop_centers = []
2617 for loop in loops:
2618 center = mathutils.Vector()
2619 for v_index in loop[0]:
2620 center += bm_mod.verts[v_index].co
2621 center /= len(loop[0])
2622 center = object.matrix_world * center
2623 loop_centers.append([center, loop])
2625 # calculate stroke centers
2626 stroke_centers = []
2627 for stroke in strokes:
2628 center = mathutils.Vector()
2629 for p in stroke.points:
2630 center += p.co
2631 center /= len(stroke.points)
2632 stroke_centers.append([center, stroke, 0])
2634 # match, first by stroke use count, then by distance
2635 ls_pairs = []
2636 for lc in loop_centers:
2637 distances = []
2638 for i, sc in enumerate(stroke_centers):
2639 distances.append([sc[2], (lc[0] - sc[0]).length, i])
2640 distances.sort()
2641 best_stroke = distances[0][2]
2642 ls_pairs.append([lc[1], stroke_centers[best_stroke][1]])
2643 stroke_centers[best_stroke][2] += 1 # increase stroke use count
2645 return(ls_pairs)
2648 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2649 def gstretch_relative_lengths(loop, bm_mod):
2650 lengths = [0]
2651 for i, v_index in enumerate(loop[0][1:]):
2652 lengths.append((bm_mod.verts[v_index].co - \
2653 bm_mod.verts[loop[0][i]].co).length + lengths[-1])
2654 total_length = max(lengths[-1], 1e-7)
2655 relative_lengths = [length / total_length for length in
2656 lengths]
2658 return(relative_lengths)
2661 ##########################################
2662 ####### Relax functions ##################
2663 ##########################################
2665 # create lists with knots and points, all correctly sorted
2666 def relax_calculate_knots(loops):
2667 all_knots = []
2668 all_points = []
2669 for loop, circular in loops:
2670 knots = [[], []]
2671 points = [[], []]
2672 if circular:
2673 if len(loop)%2 == 1: # odd
2674 extend = [False, True, 0, 1, 0, 1]
2675 else: # even
2676 extend = [True, False, 0, 1, 1, 2]
2677 else:
2678 if len(loop)%2 == 1: # odd
2679 extend = [False, False, 0, 1, 1, 2]
2680 else: # even
2681 extend = [False, False, 0, 1, 1, 2]
2682 for j in range(2):
2683 if extend[j]:
2684 loop = [loop[-1]] + loop + [loop[0]]
2685 for i in range(extend[2+2*j], len(loop), 2):
2686 knots[j].append(loop[i])
2687 for i in range(extend[3+2*j], len(loop), 2):
2688 if loop[i] == loop[-1] and not circular:
2689 continue
2690 if len(points[j]) == 0:
2691 points[j].append(loop[i])
2692 elif loop[i] != points[j][0]:
2693 points[j].append(loop[i])
2694 if circular:
2695 if knots[j][0] != knots[j][-1]:
2696 knots[j].append(knots[j][0])
2697 if len(points[1]) == 0:
2698 knots.pop(1)
2699 points.pop(1)
2700 for k in knots:
2701 all_knots.append(k)
2702 for p in points:
2703 all_points.append(p)
2705 return(all_knots, all_points)
2708 # calculate relative positions compared to first knot
2709 def relax_calculate_t(bm_mod, knots, points, regular):
2710 all_tknots = []
2711 all_tpoints = []
2712 for i in range(len(knots)):
2713 amount = len(knots[i]) + len(points[i])
2714 mix = []
2715 for j in range(amount):
2716 if j%2 == 0:
2717 mix.append([True, knots[i][round(j/2)]])
2718 elif j == amount-1:
2719 mix.append([True, knots[i][-1]])
2720 else:
2721 mix.append([False, points[i][int(j/2)]])
2722 len_total = 0
2723 loc_prev = False
2724 tknots = []
2725 tpoints = []
2726 for m in mix:
2727 loc = mathutils.Vector(bm_mod.verts[m[1]].co[:])
2728 if not loc_prev:
2729 loc_prev = loc
2730 len_total += (loc - loc_prev).length
2731 if m[0]:
2732 tknots.append(len_total)
2733 else:
2734 tpoints.append(len_total)
2735 loc_prev = loc
2736 if regular:
2737 tpoints = []
2738 for p in range(len(points[i])):
2739 tpoints.append((tknots[p] + tknots[p+1]) / 2)
2740 all_tknots.append(tknots)
2741 all_tpoints.append(tpoints)
2743 return(all_tknots, all_tpoints)
2746 # change the location of the points to their place on the spline
2747 def relax_calculate_verts(bm_mod, interpolation, tknots, knots, tpoints,
2748 points, splines):
2749 change = []
2750 move = []
2751 for i in range(len(knots)):
2752 for p in points[i]:
2753 m = tpoints[i][points[i].index(p)]
2754 if m in tknots[i]:
2755 n = tknots[i].index(m)
2756 else:
2757 t = tknots[i][:]
2758 t.append(m)
2759 t.sort()
2760 n = t.index(m)-1
2761 if n > len(splines[i]) - 1:
2762 n = len(splines[i]) - 1
2763 elif n < 0:
2764 n = 0
2766 if interpolation == 'cubic':
2767 ax, bx, cx, dx, tx = splines[i][n][0]
2768 x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
2769 ay, by, cy, dy, ty = splines[i][n][1]
2770 y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
2771 az, bz, cz, dz, tz = splines[i][n][2]
2772 z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
2773 change.append([p, mathutils.Vector([x,y,z])])
2774 else: # interpolation == 'linear'
2775 a, d, t, u = splines[i][n]
2776 if u == 0:
2777 u = 1e-8
2778 change.append([p, ((m-t)/u)*d + a])
2779 for c in change:
2780 move.append([c[0], (bm_mod.verts[c[0]].co + c[1]) / 2])
2782 return(move)
2785 ##########################################
2786 ####### Space functions ##################
2787 ##########################################
2789 # calculate relative positions compared to first knot
2790 def space_calculate_t(bm_mod, knots):
2791 tknots = []
2792 loc_prev = False
2793 len_total = 0
2794 for k in knots:
2795 loc = mathutils.Vector(bm_mod.verts[k].co[:])
2796 if not loc_prev:
2797 loc_prev = loc
2798 len_total += (loc - loc_prev).length
2799 tknots.append(len_total)
2800 loc_prev = loc
2801 amount = len(knots)
2802 t_per_segment = len_total / (amount - 1)
2803 tpoints = [i * t_per_segment for i in range(amount)]
2805 return(tknots, tpoints)
2808 # change the location of the points to their place on the spline
2809 def space_calculate_verts(bm_mod, interpolation, tknots, tpoints, points,
2810 splines):
2811 move = []
2812 for p in points:
2813 m = tpoints[points.index(p)]
2814 if m in tknots:
2815 n = tknots.index(m)
2816 else:
2817 t = tknots[:]
2818 t.append(m)
2819 t.sort()
2820 n = t.index(m) - 1
2821 if n > len(splines) - 1:
2822 n = len(splines) - 1
2823 elif n < 0:
2824 n = 0
2826 if interpolation == 'cubic':
2827 ax, bx, cx, dx, tx = splines[n][0]
2828 x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
2829 ay, by, cy, dy, ty = splines[n][1]
2830 y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
2831 az, bz, cz, dz, tz = splines[n][2]
2832 z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
2833 move.append([p, mathutils.Vector([x,y,z])])
2834 else: # interpolation == 'linear'
2835 a, d, t, u = splines[n]
2836 move.append([p, ((m-t)/u)*d + a])
2838 return(move)
2841 ##########################################
2842 ####### Operators ########################
2843 ##########################################
2845 # bridge operator
2846 class Bridge(bpy.types.Operator):
2847 bl_idname = 'mesh.looptools_bridge'
2848 bl_label = "Bridge / Loft"
2849 bl_description = "Bridge two, or loft several, loops of vertices"
2850 bl_options = {'REGISTER', 'UNDO'}
2852 cubic_strength = bpy.props.FloatProperty(name = "Strength",
2853 description = "Higher strength results in more fluid curves",
2854 default = 1.0,
2855 soft_min = -3.0,
2856 soft_max = 3.0)
2857 interpolation = bpy.props.EnumProperty(name = "Interpolation mode",
2858 items = (('cubic', "Cubic", "Gives curved results"),
2859 ('linear', "Linear", "Basic, fast, straight interpolation")),
2860 description = "Interpolation mode: algorithm used when creating "\
2861 "segments",
2862 default = 'cubic')
2863 loft = bpy.props.BoolProperty(name = "Loft",
2864 description = "Loft multiple loops, instead of considering them as "\
2865 "a multi-input for bridging",
2866 default = False)
2867 loft_loop = bpy.props.BoolProperty(name = "Loop",
2868 description = "Connect the first and the last loop with each other",
2869 default = False)
2870 min_width = bpy.props.IntProperty(name = "Minimum width",
2871 description = "Segments with an edge smaller than this are merged "\
2872 "(compared to base edge)",
2873 default = 0,
2874 min = 0,
2875 max = 100,
2876 subtype = 'PERCENTAGE')
2877 mode = bpy.props.EnumProperty(name = "Mode",
2878 items = (('basic', "Basic", "Fast algorithm"), ('shortest',
2879 "Shortest edge", "Slower algorithm with better vertex matching")),
2880 description = "Algorithm used for bridging",
2881 default = 'shortest')
2882 remove_faces = bpy.props.BoolProperty(name = "Remove faces",
2883 description = "Remove faces that are internal after bridging",
2884 default = True)
2885 reverse = bpy.props.BoolProperty(name = "Reverse",
2886 description = "Manually override the direction in which the loops "\
2887 "are bridged. Only use if the tool gives the wrong " \
2888 "result",
2889 default = False)
2890 segments = bpy.props.IntProperty(name = "Segments",
2891 description = "Number of segments used to bridge the gap "\
2892 "(0 = automatic)",
2893 default = 1,
2894 min = 0,
2895 soft_max = 20)
2896 twist = bpy.props.IntProperty(name = "Twist",
2897 description = "Twist what vertices are connected to each other",
2898 default = 0)
2900 @classmethod
2901 def poll(cls, context):
2902 ob = context.active_object
2903 return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
2905 def draw(self, context):
2906 layout = self.layout
2907 #layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
2909 # top row
2910 col_top = layout.column(align=True)
2911 row = col_top.row(align=True)
2912 col_left = row.column(align=True)
2913 col_right = row.column(align=True)
2914 col_right.active = self.segments != 1
2915 col_left.prop(self, "segments")
2916 col_right.prop(self, "min_width", text="")
2917 # bottom row
2918 bottom_left = col_left.row()
2919 bottom_left.active = self.segments != 1
2920 bottom_left.prop(self, "interpolation", text="")
2921 bottom_right = col_right.row()
2922 bottom_right.active = self.interpolation == 'cubic'
2923 bottom_right.prop(self, "cubic_strength")
2924 # boolean properties
2925 col_top.prop(self, "remove_faces")
2926 if self.loft:
2927 col_top.prop(self, "loft_loop")
2929 # override properties
2930 col_top.separator()
2931 row = layout.row(align = True)
2932 row.prop(self, "twist")
2933 row.prop(self, "reverse")
2935 def invoke(self, context, event):
2936 # load custom settings
2937 context.window_manager.looptools.bridge_loft = self.loft
2938 settings_load(self)
2939 return self.execute(context)
2941 def execute(self, context):
2942 # initialise
2943 global_undo, object, bm = initialise()
2944 edge_faces, edgekey_to_edge, old_selected_faces, smooth = \
2945 bridge_initialise(bm, self.interpolation)
2946 settings_write(self)
2948 # check cache to see if we can save time
2949 input_method = bridge_input_method(self.loft, self.loft_loop)
2950 cached, single_loops, loops, derived, mapping = cache_read("Bridge",
2951 object, bm, input_method, False)
2952 if not cached:
2953 # get loops
2954 loops = bridge_get_input(bm)
2955 if loops:
2956 # reorder loops if there are more than 2
2957 if len(loops) > 2:
2958 if self.loft:
2959 loops = bridge_sort_loops(bm, loops, self.loft_loop)
2960 else:
2961 loops = bridge_match_loops(bm, loops)
2963 # saving cache for faster execution next time
2964 if not cached:
2965 cache_write("Bridge", object, bm, input_method, False, False,
2966 loops, False, False)
2968 if loops:
2969 # calculate new geometry
2970 vertices = []
2971 faces = []
2972 max_vert_index = len(bm.verts)-1
2973 for i in range(1, len(loops)):
2974 if not self.loft and i%2 == 0:
2975 continue
2976 lines = bridge_calculate_lines(bm, loops[i-1:i+1],
2977 self.mode, self.twist, self.reverse)
2978 vertex_normals = bridge_calculate_virtual_vertex_normals(bm,
2979 lines, loops[i-1:i+1], edge_faces, edgekey_to_edge)
2980 segments = bridge_calculate_segments(bm, lines,
2981 loops[i-1:i+1], self.segments)
2982 new_verts, new_faces, max_vert_index = \
2983 bridge_calculate_geometry(bm, lines, vertex_normals,
2984 segments, self.interpolation, self.cubic_strength,
2985 self.min_width, max_vert_index)
2986 if new_verts:
2987 vertices += new_verts
2988 if new_faces:
2989 faces += new_faces
2990 # make sure faces in loops that aren't used, aren't removed
2991 if self.remove_faces and old_selected_faces:
2992 bridge_save_unused_faces(bm, old_selected_faces, loops)
2993 # create vertices
2994 if vertices:
2995 bridge_create_vertices(bm, vertices)
2996 # create faces
2997 if faces:
2998 bridge_create_faces(object, bm, faces, self.twist)
2999 bridge_select_new_faces(bm, len(faces), smooth)
3000 # edge-data could have changed, can't use cache next run
3001 if faces and not vertices:
3002 cache_delete("Bridge")
3003 # delete internal faces
3004 if self.remove_faces and old_selected_faces:
3005 bridge_remove_internal_faces(bm, old_selected_faces)
3006 # make sure normals are facing outside
3007 bmesh.update_edit_mesh(object.data, tessface=False, destructive=True)
3008 bpy.ops.mesh.normals_make_consistent()
3010 # cleaning up
3011 terminate(global_undo)
3013 return{'FINISHED'}
3016 # circle operator
3017 class Circle(bpy.types.Operator):
3018 bl_idname = "mesh.looptools_circle"
3019 bl_label = "Circle"
3020 bl_description = "Move selected vertices into a circle shape"
3021 bl_options = {'REGISTER', 'UNDO'}
3023 custom_radius = bpy.props.BoolProperty(name = "Radius",
3024 description = "Force a custom radius",
3025 default = False)
3026 fit = bpy.props.EnumProperty(name = "Method",
3027 items = (("best", "Best fit", "Non-linear least squares"),
3028 ("inside", "Fit inside","Only move vertices towards the center")),
3029 description = "Method used for fitting a circle to the vertices",
3030 default = 'best')
3031 flatten = bpy.props.BoolProperty(name = "Flatten",
3032 description = "Flatten the circle, instead of projecting it on the " \
3033 "mesh",
3034 default = True)
3035 influence = bpy.props.FloatProperty(name = "Influence",
3036 description = "Force of the tool",
3037 default = 100.0,
3038 min = 0.0,
3039 max = 100.0,
3040 precision = 1,
3041 subtype = 'PERCENTAGE')
3042 radius = bpy.props.FloatProperty(name = "Radius",
3043 description = "Custom radius for circle",
3044 default = 1.0,
3045 min = 0.0,
3046 soft_max = 1000.0)
3047 regular = bpy.props.BoolProperty(name = "Regular",
3048 description = "Distribute vertices at constant distances along the " \
3049 "circle",
3050 default = True)
3052 @classmethod
3053 def poll(cls, context):
3054 ob = context.active_object
3055 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3057 def draw(self, context):
3058 layout = self.layout
3059 col = layout.column()
3061 col.prop(self, "fit")
3062 col.separator()
3064 col.prop(self, "flatten")
3065 row = col.row(align=True)
3066 row.prop(self, "custom_radius")
3067 row_right = row.row(align=True)
3068 row_right.active = self.custom_radius
3069 row_right.prop(self, "radius", text="")
3070 col.prop(self, "regular")
3071 col.separator()
3073 col.prop(self, "influence")
3075 def invoke(self, context, event):
3076 # load custom settings
3077 settings_load(self)
3078 return self.execute(context)
3080 def execute(self, context):
3081 # initialise
3082 global_undo, object, bm = initialise()
3083 settings_write(self)
3084 # check cache to see if we can save time
3085 cached, single_loops, loops, derived, mapping = cache_read("Circle",
3086 object, bm, False, False)
3087 if cached:
3088 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
3089 else:
3090 # find loops
3091 derived, bm_mod, single_vertices, single_loops, loops = \
3092 circle_get_input(object, bm, context.scene)
3093 mapping = get_mapping(derived, bm, bm_mod, single_vertices,
3094 False, loops)
3095 single_loops, loops = circle_check_loops(single_loops, loops,
3096 mapping, bm_mod)
3098 # saving cache for faster execution next time
3099 if not cached:
3100 cache_write("Circle", object, bm, False, False, single_loops,
3101 loops, derived, mapping)
3103 move = []
3104 for i, loop in enumerate(loops):
3105 # best fitting flat plane
3106 com, normal = calculate_plane(bm_mod, loop)
3107 # if circular, shift loop so we get a good starting vertex
3108 if loop[1]:
3109 loop = circle_shift_loop(bm_mod, loop, com)
3110 # flatten vertices on plane
3111 locs_2d, p, q = circle_3d_to_2d(bm_mod, loop, com, normal)
3112 # calculate circle
3113 if self.fit == 'best':
3114 x0, y0, r = circle_calculate_best_fit(locs_2d)
3115 else: # self.fit == 'inside'
3116 x0, y0, r = circle_calculate_min_fit(locs_2d)
3117 # radius override
3118 if self.custom_radius:
3119 r = self.radius / p.length
3120 # calculate positions on circle
3121 if self.regular:
3122 new_locs_2d = circle_project_regular(locs_2d[:], x0, y0, r)
3123 else:
3124 new_locs_2d = circle_project_non_regular(locs_2d[:], x0, y0, r)
3125 # take influence into account
3126 locs_2d = circle_influence_locs(locs_2d, new_locs_2d,
3127 self.influence)
3128 # calculate 3d positions of the created 2d input
3129 move.append(circle_calculate_verts(self.flatten, bm_mod,
3130 locs_2d, com, p, q, normal))
3131 # flatten single input vertices on plane defined by loop
3132 if self.flatten and single_loops:
3133 move.append(circle_flatten_singles(bm_mod, com, p, q,
3134 normal, single_loops[i]))
3136 # move vertices to new locations
3137 move_verts(object, bm, mapping, move, -1)
3139 # cleaning up
3140 if derived:
3141 bm_mod.free()
3142 terminate(global_undo)
3144 return{'FINISHED'}
3147 # curve operator
3148 class Curve(bpy.types.Operator):
3149 bl_idname = "mesh.looptools_curve"
3150 bl_label = "Curve"
3151 bl_description = "Turn a loop into a smooth curve"
3152 bl_options = {'REGISTER', 'UNDO'}
3154 boundaries = bpy.props.BoolProperty(name = "Boundaries",
3155 description = "Limit the tool to work within the boundaries of the "\
3156 "selected vertices",
3157 default = False)
3158 influence = bpy.props.FloatProperty(name = "Influence",
3159 description = "Force of the tool",
3160 default = 100.0,
3161 min = 0.0,
3162 max = 100.0,
3163 precision = 1,
3164 subtype = 'PERCENTAGE')
3165 interpolation = bpy.props.EnumProperty(name = "Interpolation",
3166 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3167 ("linear", "Linear", "Simple and fast linear algorithm")),
3168 description = "Algorithm used for interpolation",
3169 default = 'cubic')
3170 regular = bpy.props.BoolProperty(name = "Regular",
3171 description = "Distribute vertices at constant distances along the" \
3172 "curve",
3173 default = True)
3174 restriction = bpy.props.EnumProperty(name = "Restriction",
3175 items = (("none", "None", "No restrictions on vertex movement"),
3176 ("extrude", "Extrude only","Only allow extrusions (no "\
3177 "indentations)"),
3178 ("indent", "Indent only", "Only allow indentation (no "\
3179 "extrusions)")),
3180 description = "Restrictions on how the vertices can be moved",
3181 default = 'none')
3183 @classmethod
3184 def poll(cls, context):
3185 ob = context.active_object
3186 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3188 def draw(self, context):
3189 layout = self.layout
3190 col = layout.column()
3192 col.prop(self, "interpolation")
3193 col.prop(self, "restriction")
3194 col.prop(self, "boundaries")
3195 col.prop(self, "regular")
3196 col.separator()
3198 col.prop(self, "influence")
3200 def invoke(self, context, event):
3201 # load custom settings
3202 settings_load(self)
3203 return self.execute(context)
3205 def execute(self, context):
3206 # initialise
3207 global_undo, object, bm = initialise()
3208 settings_write(self)
3209 # check cache to see if we can save time
3210 cached, single_loops, loops, derived, mapping = cache_read("Curve",
3211 object, bm, False, self.boundaries)
3212 if cached:
3213 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
3214 else:
3215 # find loops
3216 derived, bm_mod, loops = curve_get_input(object, bm,
3217 self.boundaries, context.scene)
3218 mapping = get_mapping(derived, bm, bm_mod, False, True, loops)
3219 loops = check_loops(loops, mapping, bm_mod)
3220 verts_selected = [v.index for v in bm_mod.verts if v.select \
3221 and not v.hide]
3223 # saving cache for faster execution next time
3224 if not cached:
3225 cache_write("Curve", object, bm, False, self.boundaries, False,
3226 loops, derived, mapping)
3228 move = []
3229 for loop in loops:
3230 knots, points = curve_calculate_knots(loop, verts_selected)
3231 pknots = curve_project_knots(bm_mod, verts_selected, knots,
3232 points, loop[1])
3233 tknots, tpoints = curve_calculate_t(bm_mod, knots, points,
3234 pknots, self.regular, loop[1])
3235 splines = calculate_splines(self.interpolation, bm_mod,
3236 tknots, knots)
3237 move.append(curve_calculate_vertices(bm_mod, knots, tknots,
3238 points, tpoints, splines, self.interpolation,
3239 self.restriction))
3241 # move vertices to new locations
3242 move_verts(object, bm, mapping, move, self.influence)
3244 # cleaning up
3245 if derived:
3246 bm_mod.free()
3247 terminate(global_undo)
3249 return{'FINISHED'}
3252 # flatten operator
3253 class Flatten(bpy.types.Operator):
3254 bl_idname = "mesh.looptools_flatten"
3255 bl_label = "Flatten"
3256 bl_description = "Flatten vertices on a best-fitting plane"
3257 bl_options = {'REGISTER', 'UNDO'}
3259 influence = bpy.props.FloatProperty(name = "Influence",
3260 description = "Force of the tool",
3261 default = 100.0,
3262 min = 0.0,
3263 max = 100.0,
3264 precision = 1,
3265 subtype = 'PERCENTAGE')
3266 plane = bpy.props.EnumProperty(name = "Plane",
3267 items = (("best_fit", "Best fit", "Calculate a best fitting plane"),
3268 ("normal", "Normal", "Derive plane from averaging vertex "\
3269 "normals"),
3270 ("view", "View", "Flatten on a plane perpendicular to the "\
3271 "viewing angle")),
3272 description = "Plane on which vertices are flattened",
3273 default = 'best_fit')
3274 restriction = bpy.props.EnumProperty(name = "Restriction",
3275 items = (("none", "None", "No restrictions on vertex movement"),
3276 ("bounding_box", "Bounding box", "Vertices are restricted to "\
3277 "movement inside the bounding box of the selection")),
3278 description = "Restrictions on how the vertices can be moved",
3279 default = 'none')
3281 @classmethod
3282 def poll(cls, context):
3283 ob = context.active_object
3284 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3286 def draw(self, context):
3287 layout = self.layout
3288 col = layout.column()
3290 col.prop(self, "plane")
3291 #col.prop(self, "restriction")
3292 col.separator()
3294 col.prop(self, "influence")
3296 def invoke(self, context, event):
3297 # load custom settings
3298 settings_load(self)
3299 return self.execute(context)
3301 def execute(self, context):
3302 # initialise
3303 global_undo, object, bm = initialise()
3304 settings_write(self)
3305 # check cache to see if we can save time
3306 cached, single_loops, loops, derived, mapping = cache_read("Flatten",
3307 object, bm, False, False)
3308 if not cached:
3309 # order input into virtual loops
3310 loops = flatten_get_input(bm)
3311 loops = check_loops(loops, mapping, bm)
3313 # saving cache for faster execution next time
3314 if not cached:
3315 cache_write("Flatten", object, bm, False, False, False, loops,
3316 False, False)
3318 move = []
3319 for loop in loops:
3320 # calculate plane and position of vertices on them
3321 com, normal = calculate_plane(bm, loop, method=self.plane,
3322 object=object)
3323 to_move = flatten_project(bm, loop, com, normal)
3324 if self.restriction == 'none':
3325 move.append(to_move)
3326 else:
3327 move.append(to_move)
3328 move_verts(object, bm, False, move, self.influence)
3330 # cleaning up
3331 terminate(global_undo)
3333 return{'FINISHED'}
3336 # gstretch operator
3337 class GStretch(bpy.types.Operator):
3338 bl_idname = "mesh.looptools_gstretch"
3339 bl_label = "Gstretch"
3340 bl_description = "Stretch selected vertices to Grease Pencil stroke"
3341 bl_options = {'REGISTER', 'UNDO'}
3343 delete_strokes = bpy.props.BoolProperty(name="Delete strokes",
3344 description = "Remove Grease Pencil strokes if they have been used "\
3345 "for Gstretch",
3346 default = False)
3347 influence = bpy.props.FloatProperty(name = "Influence",
3348 description = "Force of the tool",
3349 default = 100.0,
3350 min = 0.0,
3351 max = 100.0,
3352 precision = 1,
3353 subtype = 'PERCENTAGE')
3354 method = bpy.props.EnumProperty(name = "Method",
3355 items = (("project", "Project", "Project vertices onto the stroke, "\
3356 "using vertex normals and connected edges"),
3357 ("irregular", "Spread", "Distribute vertices along the full "\
3358 "stroke, retaining relative distances between the vertices"),
3359 ("regular", "Spread evenly", "Distribute vertices at regular "\
3360 "distances along the full stroke")),
3361 description = "Method of distributing the vertices over the Grease "\
3362 "Pencil stroke",
3363 default = 'regular')
3365 @classmethod
3366 def poll(cls, context):
3367 ob = context.active_object
3368 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH'
3369 and ob.grease_pencil)
3371 def draw(self, context):
3372 layout = self.layout
3373 col = layout.column()
3375 col.prop(self, "delete_strokes")
3376 col.prop(self, "method")
3377 col.separator()
3378 col.prop(self, "influence")
3380 def invoke(self, context, event):
3381 # load custom settings
3382 settings_load(self)
3383 return self.execute(context)
3385 def execute(self, context):
3386 # initialise
3387 global_undo, object, bm = initialise()
3388 settings_write(self)
3390 # check cache to see if we can save time
3391 cached, single_loops, loops, derived, mapping = cache_read("Gstretch",
3392 object, bm, False, False)
3393 if cached:
3394 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
3395 else:
3396 # find loops
3397 derived, bm_mod, loops = get_connected_input(object, bm,
3398 context.scene, input='selected')
3399 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
3400 loops = check_loops(loops, mapping, bm_mod)
3401 strokes = gstretch_get_strokes(object)
3403 # saving cache for faster execution next time
3404 if not cached:
3405 cache_write("Gstretch", object, bm, False, False, False, loops,
3406 derived, mapping)
3408 # pair loops and strokes
3409 ls_pairs = gstretch_match_loops_strokes(loops, strokes, object, bm_mod)
3410 ls_pairs = gstretch_align_pairs(ls_pairs, object, bm_mod, self.method)
3412 move = []
3413 if ls_pairs:
3414 for (loop, stroke) in ls_pairs:
3415 move.append(gstretch_calculate_verts(loop, stroke, object,
3416 bm_mod, self.method))
3417 if self.delete_strokes:
3418 gstretch_erase_stroke(stroke, context)
3420 # move vertices to new locations
3421 move_verts(object, bm, mapping, move, self.influence)
3423 # cleaning up
3424 if derived:
3425 bm_mod.free()
3426 terminate(global_undo)
3428 return{'FINISHED'}
3431 # relax operator
3432 class Relax(bpy.types.Operator):
3433 bl_idname = "mesh.looptools_relax"
3434 bl_label = "Relax"
3435 bl_description = "Relax the loop, so it is smoother"
3436 bl_options = {'REGISTER', 'UNDO'}
3438 input = bpy.props.EnumProperty(name = "Input",
3439 items = (("all", "Parallel (all)", "Also use non-selected "\
3440 "parallel loops as input"),
3441 ("selected", "Selection","Only use selected vertices as input")),
3442 description = "Loops that are relaxed",
3443 default = 'selected')
3444 interpolation = bpy.props.EnumProperty(name = "Interpolation",
3445 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3446 ("linear", "Linear", "Simple and fast linear algorithm")),
3447 description = "Algorithm used for interpolation",
3448 default = 'cubic')
3449 iterations = bpy.props.EnumProperty(name = "Iterations",
3450 items = (("1", "1", "One"),
3451 ("3", "3", "Three"),
3452 ("5", "5", "Five"),
3453 ("10", "10", "Ten"),
3454 ("25", "25", "Twenty-five")),
3455 description = "Number of times the loop is relaxed",
3456 default = "1")
3457 regular = bpy.props.BoolProperty(name = "Regular",
3458 description = "Distribute vertices at constant distances along the" \
3459 "loop",
3460 default = True)
3462 @classmethod
3463 def poll(cls, context):
3464 ob = context.active_object
3465 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3467 def draw(self, context):
3468 layout = self.layout
3469 col = layout.column()
3471 col.prop(self, "interpolation")
3472 col.prop(self, "input")
3473 col.prop(self, "iterations")
3474 col.prop(self, "regular")
3476 def invoke(self, context, event):
3477 # load custom settings
3478 settings_load(self)
3479 return self.execute(context)
3481 def execute(self, context):
3482 # initialise
3483 global_undo, object, bm = initialise()
3484 settings_write(self)
3485 # check cache to see if we can save time
3486 cached, single_loops, loops, derived, mapping = cache_read("Relax",
3487 object, bm, self.input, False)
3488 if cached:
3489 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
3490 else:
3491 # find loops
3492 derived, bm_mod, loops = get_connected_input(object, bm,
3493 context.scene, self.input)
3494 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
3495 loops = check_loops(loops, mapping, bm_mod)
3496 knots, points = relax_calculate_knots(loops)
3498 # saving cache for faster execution next time
3499 if not cached:
3500 cache_write("Relax", object, bm, self.input, False, False, loops,
3501 derived, mapping)
3503 for iteration in range(int(self.iterations)):
3504 # calculate splines and new positions
3505 tknots, tpoints = relax_calculate_t(bm_mod, knots, points,
3506 self.regular)
3507 splines = []
3508 for i in range(len(knots)):
3509 splines.append(calculate_splines(self.interpolation, bm_mod,
3510 tknots[i], knots[i]))
3511 move = [relax_calculate_verts(bm_mod, self.interpolation,
3512 tknots, knots, tpoints, points, splines)]
3513 move_verts(object, bm, mapping, move, -1)
3515 # cleaning up
3516 if derived:
3517 bm_mod.free()
3518 terminate(global_undo)
3520 return{'FINISHED'}
3523 # space operator
3524 class Space(bpy.types.Operator):
3525 bl_idname = "mesh.looptools_space"
3526 bl_label = "Space"
3527 bl_description = "Space the vertices in a regular distrubtion on the loop"
3528 bl_options = {'REGISTER', 'UNDO'}
3530 influence = bpy.props.FloatProperty(name = "Influence",
3531 description = "Force of the tool",
3532 default = 100.0,
3533 min = 0.0,
3534 max = 100.0,
3535 precision = 1,
3536 subtype = 'PERCENTAGE')
3537 input = bpy.props.EnumProperty(name = "Input",
3538 items = (("all", "Parallel (all)", "Also use non-selected "\
3539 "parallel loops as input"),
3540 ("selected", "Selection","Only use selected vertices as input")),
3541 description = "Loops that are spaced",
3542 default = 'selected')
3543 interpolation = bpy.props.EnumProperty(name = "Interpolation",
3544 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3545 ("linear", "Linear", "Vertices are projected on existing edges")),
3546 description = "Algorithm used for interpolation",
3547 default = 'cubic')
3549 @classmethod
3550 def poll(cls, context):
3551 ob = context.active_object
3552 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3554 def draw(self, context):
3555 layout = self.layout
3556 col = layout.column()
3558 col.prop(self, "interpolation")
3559 col.prop(self, "input")
3560 col.separator()
3562 col.prop(self, "influence")
3564 def invoke(self, context, event):
3565 # load custom settings
3566 settings_load(self)
3567 return self.execute(context)
3569 def execute(self, context):
3570 # initialise
3571 global_undo, object, bm = initialise()
3572 settings_write(self)
3573 # check cache to see if we can save time
3574 cached, single_loops, loops, derived, mapping = cache_read("Space",
3575 object, bm, self.input, False)
3576 if cached:
3577 derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
3578 else:
3579 # find loops
3580 derived, bm_mod, loops = get_connected_input(object, bm,
3581 context.scene, self.input)
3582 mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
3583 loops = check_loops(loops, mapping, bm_mod)
3585 # saving cache for faster execution next time
3586 if not cached:
3587 cache_write("Space", object, bm, self.input, False, False, loops,
3588 derived, mapping)
3590 move = []
3591 for loop in loops:
3592 # calculate splines and new positions
3593 if loop[1]: # circular
3594 loop[0].append(loop[0][0])
3595 tknots, tpoints = space_calculate_t(bm_mod, loop[0][:])
3596 splines = calculate_splines(self.interpolation, bm_mod,
3597 tknots, loop[0][:])
3598 move.append(space_calculate_verts(bm_mod, self.interpolation,
3599 tknots, tpoints, loop[0][:-1], splines))
3600 # move vertices to new locations
3601 move_verts(object, bm, mapping, move, self.influence)
3603 # cleaning up
3604 if derived:
3605 bm_mod.free()
3606 terminate(global_undo)
3608 return{'FINISHED'}
3611 ##########################################
3612 ####### GUI and registration #############
3613 ##########################################
3615 # menu containing all tools
3616 class VIEW3D_MT_edit_mesh_looptools(bpy.types.Menu):
3617 bl_label = "LoopTools"
3619 def draw(self, context):
3620 layout = self.layout
3622 layout.operator("mesh.looptools_bridge", text="Bridge").loft = False
3623 layout.operator("mesh.looptools_circle")
3624 layout.operator("mesh.looptools_curve")
3625 layout.operator("mesh.looptools_flatten")
3626 layout.operator("mesh.looptools_gstretch")
3627 layout.operator("mesh.looptools_bridge", text="Loft").loft = True
3628 layout.operator("mesh.looptools_relax")
3629 layout.operator("mesh.looptools_space")
3632 # panel containing all tools
3633 class VIEW3D_PT_tools_looptools(bpy.types.Panel):
3634 bl_space_type = 'VIEW_3D'
3635 bl_region_type = 'TOOLS'
3636 bl_context = "mesh_edit"
3637 bl_label = "LoopTools"
3638 bl_options = {'DEFAULT_CLOSED'}
3640 def draw(self, context):
3641 layout = self.layout
3642 col = layout.column(align=True)
3643 lt = context.window_manager.looptools
3645 # bridge - first line
3646 split = col.split(percentage=0.15)
3647 if lt.display_bridge:
3648 split.prop(lt, "display_bridge", text="", icon='DOWNARROW_HLT')
3649 else:
3650 split.prop(lt, "display_bridge", text="", icon='RIGHTARROW')
3651 split.operator("mesh.looptools_bridge", text="Bridge").loft = False
3652 # bridge - settings
3653 if lt.display_bridge:
3654 box = col.column(align=True).box().column()
3655 #box.prop(self, "mode")
3657 # top row
3658 col_top = box.column(align=True)
3659 row = col_top.row(align=True)
3660 col_left = row.column(align=True)
3661 col_right = row.column(align=True)
3662 col_right.active = lt.bridge_segments != 1
3663 col_left.prop(lt, "bridge_segments")
3664 col_right.prop(lt, "bridge_min_width", text="")
3665 # bottom row
3666 bottom_left = col_left.row()
3667 bottom_left.active = lt.bridge_segments != 1
3668 bottom_left.prop(lt, "bridge_interpolation", text="")
3669 bottom_right = col_right.row()
3670 bottom_right.active = lt.bridge_interpolation == 'cubic'
3671 bottom_right.prop(lt, "bridge_cubic_strength")
3672 # boolean properties
3673 col_top.prop(lt, "bridge_remove_faces")
3675 # override properties
3676 col_top.separator()
3677 row = box.row(align = True)
3678 row.prop(lt, "bridge_twist")
3679 row.prop(lt, "bridge_reverse")
3681 # circle - first line
3682 split = col.split(percentage=0.15)
3683 if lt.display_circle:
3684 split.prop(lt, "display_circle", text="", icon='DOWNARROW_HLT')
3685 else:
3686 split.prop(lt, "display_circle", text="", icon='RIGHTARROW')
3687 split.operator("mesh.looptools_circle")
3688 # circle - settings
3689 if lt.display_circle:
3690 box = col.column(align=True).box().column()
3691 box.prop(lt, "circle_fit")
3692 box.separator()
3694 box.prop(lt, "circle_flatten")
3695 row = box.row(align=True)
3696 row.prop(lt, "circle_custom_radius")
3697 row_right = row.row(align=True)
3698 row_right.active = lt.circle_custom_radius
3699 row_right.prop(lt, "circle_radius", text="")
3700 box.prop(lt, "circle_regular")
3701 box.separator()
3703 box.prop(lt, "circle_influence")
3705 # curve - first line
3706 split = col.split(percentage=0.15)
3707 if lt.display_curve:
3708 split.prop(lt, "display_curve", text="", icon='DOWNARROW_HLT')
3709 else:
3710 split.prop(lt, "display_curve", text="", icon='RIGHTARROW')
3711 split.operator("mesh.looptools_curve")
3712 # curve - settings
3713 if lt.display_curve:
3714 box = col.column(align=True).box().column()
3715 box.prop(lt, "curve_interpolation")
3716 box.prop(lt, "curve_restriction")
3717 box.prop(lt, "curve_boundaries")
3718 box.prop(lt, "curve_regular")
3719 box.separator()
3721 box.prop(lt, "curve_influence")
3723 # flatten - first line
3724 split = col.split(percentage=0.15)
3725 if lt.display_flatten:
3726 split.prop(lt, "display_flatten", text="", icon='DOWNARROW_HLT')
3727 else:
3728 split.prop(lt, "display_flatten", text="", icon='RIGHTARROW')
3729 split.operator("mesh.looptools_flatten")
3730 # flatten - settings
3731 if lt.display_flatten:
3732 box = col.column(align=True).box().column()
3733 box.prop(lt, "flatten_plane")
3734 #box.prop(lt, "flatten_restriction")
3735 box.separator()
3737 box.prop(lt, "flatten_influence")
3739 # gstretch - first line
3740 split = col.split(percentage=0.15)
3741 if lt.display_gstretch:
3742 split.prop(lt, "display_gstretch", text="", icon='DOWNARROW_HLT')
3743 else:
3744 split.prop(lt, "display_gstretch", text="", icon='RIGHTARROW')
3745 split.operator("mesh.looptools_gstretch")
3746 # gstretch settings
3747 if lt.display_gstretch:
3748 box = col.column(align=True).box().column()
3749 box.prop(lt, "gstretch_delete_strokes")
3750 box.prop(lt, "gstretch_method")
3751 box.separator()
3752 box.prop(lt, "gstretch_influence")
3754 # loft - first line
3755 split = col.split(percentage=0.15)
3756 if lt.display_loft:
3757 split.prop(lt, "display_loft", text="", icon='DOWNARROW_HLT')
3758 else:
3759 split.prop(lt, "display_loft", text="", icon='RIGHTARROW')
3760 split.operator("mesh.looptools_bridge", text="Loft").loft = True
3761 # loft - settings
3762 if lt.display_loft:
3763 box = col.column(align=True).box().column()
3764 #box.prop(self, "mode")
3766 # top row
3767 col_top = box.column(align=True)
3768 row = col_top.row(align=True)
3769 col_left = row.column(align=True)
3770 col_right = row.column(align=True)
3771 col_right.active = lt.bridge_segments != 1
3772 col_left.prop(lt, "bridge_segments")
3773 col_right.prop(lt, "bridge_min_width", text="")
3774 # bottom row
3775 bottom_left = col_left.row()
3776 bottom_left.active = lt.bridge_segments != 1
3777 bottom_left.prop(lt, "bridge_interpolation", text="")
3778 bottom_right = col_right.row()
3779 bottom_right.active = lt.bridge_interpolation == 'cubic'
3780 bottom_right.prop(lt, "bridge_cubic_strength")
3781 # boolean properties
3782 col_top.prop(lt, "bridge_remove_faces")
3783 col_top.prop(lt, "bridge_loft_loop")
3785 # override properties
3786 col_top.separator()
3787 row = box.row(align = True)
3788 row.prop(lt, "bridge_twist")
3789 row.prop(lt, "bridge_reverse")
3791 # relax - first line
3792 split = col.split(percentage=0.15)
3793 if lt.display_relax:
3794 split.prop(lt, "display_relax", text="", icon='DOWNARROW_HLT')
3795 else:
3796 split.prop(lt, "display_relax", text="", icon='RIGHTARROW')
3797 split.operator("mesh.looptools_relax")
3798 # relax - settings
3799 if lt.display_relax:
3800 box = col.column(align=True).box().column()
3801 box.prop(lt, "relax_interpolation")
3802 box.prop(lt, "relax_input")
3803 box.prop(lt, "relax_iterations")
3804 box.prop(lt, "relax_regular")
3806 # space - first line
3807 split = col.split(percentage=0.15)
3808 if lt.display_space:
3809 split.prop(lt, "display_space", text="", icon='DOWNARROW_HLT')
3810 else:
3811 split.prop(lt, "display_space", text="", icon='RIGHTARROW')
3812 split.operator("mesh.looptools_space")
3813 # space - settings
3814 if lt.display_space:
3815 box = col.column(align=True).box().column()
3816 box.prop(lt, "space_interpolation")
3817 box.prop(lt, "space_input")
3818 box.separator()
3820 box.prop(lt, "space_influence")
3823 # property group containing all properties for the gui in the panel
3824 class LoopToolsProps(bpy.types.PropertyGroup):
3826 Fake module like class
3827 bpy.context.window_manager.looptools
3830 # general display properties
3831 display_bridge = bpy.props.BoolProperty(name = "Bridge settings",
3832 description = "Display settings of the Bridge tool",
3833 default = False)
3834 display_circle = bpy.props.BoolProperty(name = "Circle settings",
3835 description = "Display settings of the Circle tool",
3836 default = False)
3837 display_curve = bpy.props.BoolProperty(name = "Curve settings",
3838 description = "Display settings of the Curve tool",
3839 default = False)
3840 display_flatten = bpy.props.BoolProperty(name = "Flatten settings",
3841 description = "Display settings of the Flatten tool",
3842 default = False)
3843 display_gstretch = bpy.props.BoolProperty(name = "Gstretch settings",
3844 description = "Display settings of the Gstretch tool",
3845 default = False)
3846 display_loft = bpy.props.BoolProperty(name = "Loft settings",
3847 description = "Display settings of the Loft tool",
3848 default = False)
3849 display_relax = bpy.props.BoolProperty(name = "Relax settings",
3850 description = "Display settings of the Relax tool",
3851 default = False)
3852 display_space = bpy.props.BoolProperty(name = "Space settings",
3853 description = "Display settings of the Space tool",
3854 default = False)
3856 # bridge properties
3857 bridge_cubic_strength = bpy.props.FloatProperty(name = "Strength",
3858 description = "Higher strength results in more fluid curves",
3859 default = 1.0,
3860 soft_min = -3.0,
3861 soft_max = 3.0)
3862 bridge_interpolation = bpy.props.EnumProperty(name = "Interpolation mode",
3863 items = (('cubic', "Cubic", "Gives curved results"),
3864 ('linear', "Linear", "Basic, fast, straight interpolation")),
3865 description = "Interpolation mode: algorithm used when creating "\
3866 "segments",
3867 default = 'cubic')
3868 bridge_loft = bpy.props.BoolProperty(name = "Loft",
3869 description = "Loft multiple loops, instead of considering them as "\
3870 "a multi-input for bridging",
3871 default = False)
3872 bridge_loft_loop = bpy.props.BoolProperty(name = "Loop",
3873 description = "Connect the first and the last loop with each other",
3874 default = False)
3875 bridge_min_width = bpy.props.IntProperty(name = "Minimum width",
3876 description = "Segments with an edge smaller than this are merged "\
3877 "(compared to base edge)",
3878 default = 0,
3879 min = 0,
3880 max = 100,
3881 subtype = 'PERCENTAGE')
3882 bridge_mode = bpy.props.EnumProperty(name = "Mode",
3883 items = (('basic', "Basic", "Fast algorithm"),
3884 ('shortest', "Shortest edge", "Slower algorithm with " \
3885 "better vertex matching")),
3886 description = "Algorithm used for bridging",
3887 default = 'shortest')
3888 bridge_remove_faces = bpy.props.BoolProperty(name = "Remove faces",
3889 description = "Remove faces that are internal after bridging",
3890 default = True)
3891 bridge_reverse = bpy.props.BoolProperty(name = "Reverse",
3892 description = "Manually override the direction in which the loops "\
3893 "are bridged. Only use if the tool gives the wrong " \
3894 "result",
3895 default = False)
3896 bridge_segments = bpy.props.IntProperty(name = "Segments",
3897 description = "Number of segments used to bridge the gap "\
3898 "(0 = automatic)",
3899 default = 1,
3900 min = 0,
3901 soft_max = 20)
3902 bridge_twist = bpy.props.IntProperty(name = "Twist",
3903 description = "Twist what vertices are connected to each other",
3904 default = 0)
3906 # circle properties
3907 circle_custom_radius = bpy.props.BoolProperty(name = "Radius",
3908 description = "Force a custom radius",
3909 default = False)
3910 circle_fit = bpy.props.EnumProperty(name = "Method",
3911 items = (("best", "Best fit", "Non-linear least squares"),
3912 ("inside", "Fit inside","Only move vertices towards the center")),
3913 description = "Method used for fitting a circle to the vertices",
3914 default = 'best')
3915 circle_flatten = bpy.props.BoolProperty(name = "Flatten",
3916 description = "Flatten the circle, instead of projecting it on the " \
3917 "mesh",
3918 default = True)
3919 circle_influence = bpy.props.FloatProperty(name = "Influence",
3920 description = "Force of the tool",
3921 default = 100.0,
3922 min = 0.0,
3923 max = 100.0,
3924 precision = 1,
3925 subtype = 'PERCENTAGE')
3926 circle_radius = bpy.props.FloatProperty(name = "Radius",
3927 description = "Custom radius for circle",
3928 default = 1.0,
3929 min = 0.0,
3930 soft_max = 1000.0)
3931 circle_regular = bpy.props.BoolProperty(name = "Regular",
3932 description = "Distribute vertices at constant distances along the " \
3933 "circle",
3934 default = True)
3936 # curve properties
3937 curve_boundaries = bpy.props.BoolProperty(name = "Boundaries",
3938 description = "Limit the tool to work within the boundaries of the "\
3939 "selected vertices",
3940 default = False)
3941 curve_influence = bpy.props.FloatProperty(name = "Influence",
3942 description = "Force of the tool",
3943 default = 100.0,
3944 min = 0.0,
3945 max = 100.0,
3946 precision = 1,
3947 subtype = 'PERCENTAGE')
3948 curve_interpolation = bpy.props.EnumProperty(name = "Interpolation",
3949 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3950 ("linear", "Linear", "Simple and fast linear algorithm")),
3951 description = "Algorithm used for interpolation",
3952 default = 'cubic')
3953 curve_regular = bpy.props.BoolProperty(name = "Regular",
3954 description = "Distribute vertices at constant distances along the " \
3955 "curve",
3956 default = True)
3957 curve_restriction = bpy.props.EnumProperty(name = "Restriction",
3958 items = (("none", "None", "No restrictions on vertex movement"),
3959 ("extrude", "Extrude only","Only allow extrusions (no "\
3960 "indentations)"),
3961 ("indent", "Indent only", "Only allow indentation (no "\
3962 "extrusions)")),
3963 description = "Restrictions on how the vertices can be moved",
3964 default = 'none')
3966 # flatten properties
3967 flatten_influence = bpy.props.FloatProperty(name = "Influence",
3968 description = "Force of the tool",
3969 default = 100.0,
3970 min = 0.0,
3971 max = 100.0,
3972 precision = 1,
3973 subtype = 'PERCENTAGE')
3974 flatten_plane = bpy.props.EnumProperty(name = "Plane",
3975 items = (("best_fit", "Best fit", "Calculate a best fitting plane"),
3976 ("normal", "Normal", "Derive plane from averaging vertex "\
3977 "normals"),
3978 ("view", "View", "Flatten on a plane perpendicular to the "\
3979 "viewing angle")),
3980 description = "Plane on which vertices are flattened",
3981 default = 'best_fit')
3982 flatten_restriction = bpy.props.EnumProperty(name = "Restriction",
3983 items = (("none", "None", "No restrictions on vertex movement"),
3984 ("bounding_box", "Bounding box", "Vertices are restricted to "\
3985 "movement inside the bounding box of the selection")),
3986 description = "Restrictions on how the vertices can be moved",
3987 default = 'none')
3989 # gstretch properties
3990 gstretch_delete_strokes = bpy.props.BoolProperty(name="Delete strokes",
3991 description = "Remove Grease Pencil strokes if they have been used "\
3992 "for Gstretch",
3993 default = False)
3994 gstretch_influence = bpy.props.FloatProperty(name = "Influence",
3995 description = "Force of the tool",
3996 default = 100.0,
3997 min = 0.0,
3998 max = 100.0,
3999 precision = 1,
4000 subtype = 'PERCENTAGE')
4001 gstretch_method = bpy.props.EnumProperty(name = "Method",
4002 items = (("project", "Project", "Project vertices onto the stroke, "\
4003 "using vertex normals and connected edges"),
4004 ("irregular", "Spread", "Distribute vertices along the full "\
4005 "stroke, retaining relative distances between the vertices"),
4006 ("regular", "Spread evenly", "Distribute vertices at regular "\
4007 "distances along the full stroke")),
4008 description = "Method of distributing the vertices over the Grease "\
4009 "Pencil stroke",
4010 default = 'regular')
4012 # relax properties
4013 relax_input = bpy.props.EnumProperty(name = "Input",
4014 items = (("all", "Parallel (all)", "Also use non-selected "\
4015 "parallel loops as input"),
4016 ("selected", "Selection","Only use selected vertices as input")),
4017 description = "Loops that are relaxed",
4018 default = 'selected')
4019 relax_interpolation = bpy.props.EnumProperty(name = "Interpolation",
4020 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4021 ("linear", "Linear", "Simple and fast linear algorithm")),
4022 description = "Algorithm used for interpolation",
4023 default = 'cubic')
4024 relax_iterations = bpy.props.EnumProperty(name = "Iterations",
4025 items = (("1", "1", "One"),
4026 ("3", "3", "Three"),
4027 ("5", "5", "Five"),
4028 ("10", "10", "Ten"),
4029 ("25", "25", "Twenty-five")),
4030 description = "Number of times the loop is relaxed",
4031 default = "1")
4032 relax_regular = bpy.props.BoolProperty(name = "Regular",
4033 description = "Distribute vertices at constant distances along the" \
4034 "loop",
4035 default = True)
4037 # space properties
4038 space_influence = bpy.props.FloatProperty(name = "Influence",
4039 description = "Force of the tool",
4040 default = 100.0,
4041 min = 0.0,
4042 max = 100.0,
4043 precision = 1,
4044 subtype = 'PERCENTAGE')
4045 space_input = bpy.props.EnumProperty(name = "Input",
4046 items = (("all", "Parallel (all)", "Also use non-selected "\
4047 "parallel loops as input"),
4048 ("selected", "Selection","Only use selected vertices as input")),
4049 description = "Loops that are spaced",
4050 default = 'selected')
4051 space_interpolation = bpy.props.EnumProperty(name = "Interpolation",
4052 items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4053 ("linear", "Linear", "Vertices are projected on existing edges")),
4054 description = "Algorithm used for interpolation",
4055 default = 'cubic')
4058 # draw function for integration in menus
4059 def menu_func(self, context):
4060 self.layout.menu("VIEW3D_MT_edit_mesh_looptools")
4061 self.layout.separator()
4064 # define classes for registration
4065 classes = [VIEW3D_MT_edit_mesh_looptools,
4066 VIEW3D_PT_tools_looptools,
4067 LoopToolsProps,
4068 Bridge,
4069 Circle,
4070 Curve,
4071 Flatten,
4072 GStretch,
4073 Relax,
4074 Space]
4077 # registering and menu integration
4078 def register():
4079 for c in classes:
4080 bpy.utils.register_class(c)
4081 bpy.types.VIEW3D_MT_edit_mesh_specials.prepend(menu_func)
4082 bpy.types.WindowManager.looptools = bpy.props.PointerProperty(\
4083 type = LoopToolsProps)
4086 # unregistering and removing menus
4087 def unregister():
4088 for c in classes:
4089 bpy.utils.unregister_class(c)
4090 bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func)
4091 try:
4092 del bpy.types.WindowManager.looptools
4093 except:
4094 pass
4097 if __name__ == "__main__":
4098 register()