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 #####
21 "author": "Bart Crouch",
23 "blender": (2, 71, 3),
24 "location": "View3D > Toolbar and View3D > Specials (W-key)",
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",
38 from bpy_extras
import view3d_utils
41 ##########################################
42 ####### General functions ################
43 ##########################################
46 # used by all tools to improve speed on reruns
50 # force a full recalculation next time
51 def cache_delete(tool
):
52 if tool
in looptools_cache
:
53 del looptools_cache
[tool
]
56 # check cache for stored information
57 def cache_read(tool
, object, bm
, input_method
, boundaries
):
58 # current tool not cached yet
59 if tool
not in looptools_cache
:
60 return(False, False, False, False, False)
61 # check if selected object didn't change
62 if object.name
!= looptools_cache
[tool
]["object"]:
63 return(False, False, False, False, False)
64 # check if input didn't change
65 if input_method
!= looptools_cache
[tool
]["input_method"]:
66 return(False, False, False, False, False)
67 if boundaries
!= looptools_cache
[tool
]["boundaries"]:
68 return(False, False, False, False, False)
69 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport \
70 and mod
.type == 'MIRROR']
71 if modifiers
!= looptools_cache
[tool
]["modifiers"]:
72 return(False, False, False, False, False)
73 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
74 if input != looptools_cache
[tool
]["input"]:
75 return(False, False, False, False, False)
77 single_loops
= looptools_cache
[tool
]["single_loops"]
78 loops
= looptools_cache
[tool
]["loops"]
79 derived
= looptools_cache
[tool
]["derived"]
80 mapping
= looptools_cache
[tool
]["mapping"]
82 return(True, single_loops
, loops
, derived
, mapping
)
85 # store information in the cache
86 def cache_write(tool
, object, bm
, input_method
, boundaries
, single_loops
,
87 loops
, derived
, mapping
):
88 # clear cache of current tool
89 if tool
in looptools_cache
:
90 del looptools_cache
[tool
]
91 # prepare values to be saved to cache
92 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
93 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport \
94 and mod
.type == 'MIRROR']
96 looptools_cache
[tool
] = {"input": input, "object": object.name
,
97 "input_method": input_method
, "boundaries": boundaries
,
98 "single_loops": single_loops
, "loops": loops
,
99 "derived": derived
, "mapping": mapping
, "modifiers": modifiers
}
102 # calculates natural cubic splines through all given knots
103 def calculate_cubic_splines(bm_mod
, tknots
, knots
):
104 # hack for circular loops
105 if knots
[0] == knots
[-1] and len(knots
) > 1:
108 for k
in range(-1, -5, -1):
109 if k
- 1 < -len(knots
):
111 k_new1
.append(knots
[k
-1])
114 if k
+ 1 > len(knots
) - 1:
116 k_new2
.append(knots
[k
+1])
123 for t
in range(-1, -5, -1):
124 if t
- 1 < -len(tknots
):
126 total1
+= tknots
[t
] - tknots
[t
-1]
127 t_new1
.append(tknots
[0] - total1
)
131 if t
+ 1 > len(tknots
) - 1:
133 total2
+= tknots
[t
+1] - tknots
[t
]
134 t_new2
.append(tknots
[-1] + total2
)
147 locs
= [bm_mod
.verts
[k
].co
[:] for k
in knots
]
155 if x
[i
+1] - x
[i
] == 0:
158 h
.append(x
[i
+1] - x
[i
])
160 for i
in range(1, n
-1):
161 q
.append(3/h
[i
]*(a
[i
+1]-a
[i
]) - 3/h
[i
-1]*(a
[i
]-a
[i
-1]))
165 for i
in range(1, n
-1):
166 l
.append(2*(x
[i
+1]-x
[i
-1]) - h
[i
-1]*u
[i
-1])
169 u
.append(h
[i
] / l
[i
])
170 z
.append((q
[i
] - h
[i
-1] * z
[i
-1]) / l
[i
])
173 b
= [False for i
in range(n
-1)]
174 c
= [False for i
in range(n
)]
175 d
= [False for i
in range(n
-1)]
177 for i
in range(n
-2, -1, -1):
178 c
[i
] = z
[i
] - u
[i
]*c
[i
+1]
179 b
[i
] = (a
[i
+1]-a
[i
])/h
[i
] - h
[i
]*(c
[i
+1]+2*c
[i
])/3
180 d
[i
] = (c
[i
+1]-c
[i
]) / (3*h
[i
])
182 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
184 for i
in range(len(knots
)-1):
185 splines
.append([result
[i
], result
[i
+n
-1], result
[i
+(n
-1)*2]])
186 if circular
: # cleaning up after hack
188 tknots
= tknots
[4:-4]
193 # calculates linear splines through all given knots
194 def calculate_linear_splines(bm_mod
, tknots
, knots
):
196 for i
in range(len(knots
)-1):
197 a
= bm_mod
.verts
[knots
[i
]].co
198 b
= bm_mod
.verts
[knots
[i
+1]].co
202 splines
.append([a
, d
, t
, u
]) # [locStart, locDif, tStart, tDif]
207 # calculate a best-fit plane to the given vertices
208 def calculate_plane(bm_mod
, loop
, method
="best_fit", object=False):
209 # getting the vertex locations
210 locs
= [bm_mod
.verts
[v
].co
.copy() for v
in loop
[0]]
212 # calculating the center of masss
213 com
= mathutils
.Vector()
219 if method
== 'best_fit':
220 # creating the covariance matrix
221 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
226 mat
[0][0] += (loc
[0]-x
)**2
227 mat
[1][0] += (loc
[0]-x
)*(loc
[1]-y
)
228 mat
[2][0] += (loc
[0]-x
)*(loc
[2]-z
)
229 mat
[0][1] += (loc
[1]-y
)*(loc
[0]-x
)
230 mat
[1][1] += (loc
[1]-y
)**2
231 mat
[2][1] += (loc
[1]-y
)*(loc
[2]-z
)
232 mat
[0][2] += (loc
[2]-z
)*(loc
[0]-x
)
233 mat
[1][2] += (loc
[2]-z
)*(loc
[1]-y
)
234 mat
[2][2] += (loc
[2]-z
)**2
236 # calculating the normal to the plane
239 mat
= matrix_invert(mat
)
242 if math
.fabs(sum(mat
[0])) < math
.fabs(sum(mat
[1])):
243 if math
.fabs(sum(mat
[0])) < math
.fabs(sum(mat
[2])):
245 elif math
.fabs(sum(mat
[1])) < math
.fabs(sum(mat
[2])):
248 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
250 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
252 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
254 # warning! this is different from .normalize()
257 vec
= mathutils
.Vector((1.0, 1.0, 1.0))
258 vec2
= (mat
* vec
)/(mat
* vec
).length
259 while vec
!= vec2
and iter<itermax
:
266 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
269 elif method
== 'normal':
270 # averaging the vertex normals
271 v_normals
= [bm_mod
.verts
[v
].normal
for v
in loop
[0]]
272 normal
= mathutils
.Vector()
273 for v_normal
in v_normals
:
275 normal
/= len(v_normals
)
278 elif method
== 'view':
279 # calculate view normal
280 rotation
= bpy
.context
.space_data
.region_3d
.view_matrix
.to_3x3().\
282 normal
= rotation
* mathutils
.Vector((0.0, 0.0, 1.0))
284 normal
= object.matrix_world
.inverted().to_euler().to_matrix() * \
290 # calculate splines based on given interpolation method (controller function)
291 def calculate_splines(interpolation
, bm_mod
, tknots
, knots
):
292 if interpolation
== 'cubic':
293 splines
= calculate_cubic_splines(bm_mod
, tknots
, knots
[:])
294 else: # interpolations == 'linear'
295 splines
= calculate_linear_splines(bm_mod
, tknots
, knots
[:])
300 # check loops and only return valid ones
301 def check_loops(loops
, mapping
, bm_mod
):
303 for loop
, circular
in loops
:
304 # loop needs to have at least 3 vertices
307 # loop needs at least 1 vertex in the original, non-mirrored mesh
311 if mapping
[vert
] > -1:
316 # vertices can not all be at the same location
318 for i
in range(len(loop
) - 1):
319 if (bm_mod
.verts
[loop
[i
]].co
- \
320 bm_mod
.verts
[loop
[i
+1]].co
).length
> 1e-6:
325 # passed all tests, loop is valid
326 valid_loops
.append([loop
, circular
])
331 # input: bmesh, output: dict with the edge-key as key and face-index as value
332 def dict_edge_faces(bm
):
333 edge_faces
= dict([[edgekey(edge
), []] for edge
in bm
.edges
if \
335 for face
in bm
.faces
:
338 for key
in face_edgekeys(face
):
339 edge_faces
[key
].append(face
.index
)
344 # input: bmesh (edge-faces optional), output: dict with face-face connections
345 def dict_face_faces(bm
, edge_faces
=False):
347 edge_faces
= dict_edge_faces(bm
)
349 connected_faces
= dict([[face
.index
, []] for face
in bm
.faces
if \
351 for face
in bm
.faces
:
354 for edge_key
in face_edgekeys(face
):
355 for connected_face
in edge_faces
[edge_key
]:
356 if connected_face
== face
.index
:
358 connected_faces
[face
.index
].append(connected_face
)
360 return(connected_faces
)
363 # input: bmesh, output: dict with the vert index as key and edge-keys as value
364 def dict_vert_edges(bm
):
365 vert_edges
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
366 for edge
in bm
.edges
:
371 vert_edges
[vert
].append(ek
)
376 # input: bmesh, output: dict with the vert index as key and face index as value
377 def dict_vert_faces(bm
):
378 vert_faces
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
379 for face
in bm
.faces
:
381 for vert
in face
.verts
:
382 vert_faces
[vert
.index
].append(face
.index
)
387 # input: list of edge-keys, output: dictionary with vertex-vertex connections
388 def dict_vert_verts(edge_keys
):
389 # create connection data
393 if ek
[i
] in vert_verts
:
394 vert_verts
[ek
[i
]].append(ek
[1-i
])
396 vert_verts
[ek
[i
]] = [ek
[1-i
]]
401 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
403 return(tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])))
406 # returns the edgekeys of a bmesh face
407 def face_edgekeys(face
):
408 return([tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])) for \
412 # calculate input loops
413 def get_connected_input(object, bm
, scene
, input):
414 # get mesh with modifiers applied
415 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
417 # calculate selected loops
418 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if \
419 edge
.select
and not edge
.hide
]
420 loops
= get_connected_selections(edge_keys
)
422 # if only selected loops are needed, we're done
423 if input == 'selected':
424 return(derived
, bm_mod
, loops
)
425 # elif input == 'all':
426 loops
= get_parallel_loops(bm_mod
, loops
)
428 return(derived
, bm_mod
, loops
)
431 # sorts all edge-keys into a list of loops
432 def get_connected_selections(edge_keys
):
433 # create connection data
434 vert_verts
= dict_vert_verts(edge_keys
)
436 # find loops consisting of connected selected edges
438 while len(vert_verts
) > 0:
439 loop
= [iter(vert_verts
.keys()).__next
__()]
445 # no more connection data for current vertex
446 if loop
[-1] not in vert_verts
:
454 for i
, next_vert
in enumerate(vert_verts
[loop
[-1]]):
455 if next_vert
not in loop
:
456 vert_verts
[loop
[-1]].pop(i
)
457 if len(vert_verts
[loop
[-1]]) == 0:
458 del vert_verts
[loop
[-1]]
459 # remove connection both ways
460 if next_vert
in vert_verts
:
461 if len(vert_verts
[next_vert
]) == 1:
462 del vert_verts
[next_vert
]
464 vert_verts
[next_vert
].remove(loop
[-1])
465 loop
.append(next_vert
)
469 # found one end of the loop, continue with next
473 # found both ends of the loop, stop growing
477 # check if loop is circular
478 if loop
[0] in vert_verts
:
479 if loop
[-1] in vert_verts
[loop
[0]]:
481 if len(vert_verts
[loop
[0]]) == 1:
482 del vert_verts
[loop
[0]]
484 vert_verts
[loop
[0]].remove(loop
[-1])
485 if len(vert_verts
[loop
[-1]]) == 1:
486 del vert_verts
[loop
[-1]]
488 vert_verts
[loop
[-1]].remove(loop
[0])
502 # get the derived mesh data, if there is a mirror modifier
503 def get_derived_bmesh(object, bm
, scene
):
504 # check for mirror modifiers
505 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
507 # disable other modifiers
508 show_viewport
= [mod
.name
for mod
in object.modifiers
if \
510 for mod
in object.modifiers
:
511 if mod
.type != 'MIRROR':
512 mod
.show_viewport
= False
515 mesh_mod
= object.to_mesh(scene
, True, 'PREVIEW')
516 bm_mod
.from_mesh(mesh_mod
)
517 bpy
.context
.blend_data
.meshes
.remove(mesh_mod
)
518 # re-enable other modifiers
519 for mod_name
in show_viewport
:
520 object.modifiers
[mod_name
].show_viewport
= True
521 # no mirror modifiers, so no derived mesh necessary
526 return(derived
, bm_mod
)
529 # return a mapping of derived indices to indices
530 def get_mapping(derived
, bm
, bm_mod
, single_vertices
, full_search
, loops
):
535 verts
= [v
for v
in bm
.verts
if not v
.hide
]
537 verts
= [v
for v
in bm
.verts
if v
.select
and not v
.hide
]
539 # non-selected vertices around single vertices also need to be mapped
541 mapping
= dict([[vert
, -1] for vert
in single_vertices
])
542 verts_mod
= [bm_mod
.verts
[vert
] for vert
in single_vertices
]
544 for v_mod
in verts_mod
:
545 if (v
.co
- v_mod
.co
).length
< 1e-6:
546 mapping
[v_mod
.index
] = v
.index
548 real_singles
= [v_real
for v_real
in mapping
.values() if v_real
>-1]
550 verts_indices
= [vert
.index
for vert
in verts
]
551 for face
in [face
for face
in bm
.faces
if not face
.select \
553 for vert
in face
.verts
:
554 if vert
.index
in real_singles
:
556 if not v
.index
in verts_indices
:
561 # create mapping of derived indices to indices
562 mapping
= dict([[vert
, -1] for loop
in loops
for vert
in loop
[0]])
564 for single
in single_vertices
:
566 verts_mod
= [bm_mod
.verts
[i
] for i
in mapping
.keys()]
568 for v_mod
in verts_mod
:
569 if (v
.co
- v_mod
.co
).length
< 1e-6:
570 mapping
[v_mod
.index
] = v
.index
571 verts_mod
.remove(v_mod
)
577 # calculate the determinant of a matrix
578 def matrix_determinant(m
):
579 determinant
= m
[0][0] * m
[1][1] * m
[2][2] + m
[0][1] * m
[1][2] * m
[2][0] \
580 + m
[0][2] * m
[1][0] * m
[2][1] - m
[0][2] * m
[1][1] * m
[2][0] \
581 - m
[0][1] * m
[1][0] * m
[2][2] - m
[0][0] * m
[1][2] * m
[2][1]
586 # custom matrix inversion, to provide higher precision than the built-in one
587 def matrix_invert(m
):
588 r
= mathutils
.Matrix((
589 (m
[1][1]*m
[2][2] - m
[1][2]*m
[2][1], m
[0][2]*m
[2][1] - m
[0][1]*m
[2][2],
590 m
[0][1]*m
[1][2] - m
[0][2]*m
[1][1]),
591 (m
[1][2]*m
[2][0] - m
[1][0]*m
[2][2], m
[0][0]*m
[2][2] - m
[0][2]*m
[2][0],
592 m
[0][2]*m
[1][0] - m
[0][0]*m
[1][2]),
593 (m
[1][0]*m
[2][1] - m
[1][1]*m
[2][0], m
[0][1]*m
[2][0] - m
[0][0]*m
[2][1],
594 m
[0][0]*m
[1][1] - m
[0][1]*m
[1][0])))
596 return (r
* (1 / matrix_determinant(m
)))
599 # returns a list of all loops parallel to the input, input included
600 def get_parallel_loops(bm_mod
, loops
):
601 # get required dictionaries
602 edge_faces
= dict_edge_faces(bm_mod
)
603 connected_faces
= dict_face_faces(bm_mod
, edge_faces
)
604 # turn vertex loops into edge loops
607 edgeloop
= [[sorted([loop
[0][i
], loop
[0][i
+1]]) for i
in \
608 range(len(loop
[0])-1)], loop
[1]]
609 if loop
[1]: # circular
610 edgeloop
[0].append(sorted([loop
[0][-1], loop
[0][0]]))
611 edgeloops
.append(edgeloop
[:])
612 # variables to keep track while iterating
616 for loop
in edgeloops
:
617 # initialise with original loop
618 all_edgeloops
.append(loop
[0])
622 if edge
[0] not in verts_used
:
623 verts_used
.append(edge
[0])
624 if edge
[1] not in verts_used
:
625 verts_used
.append(edge
[1])
627 # find parallel loops
628 while len(newloops
) > 0:
631 for i
in newloops
[-1]:
633 forbidden_side
= False
634 if not i
in edge_faces
:
635 # weird input with branches
638 for face
in edge_faces
[i
]:
639 if len(side_a
) == 0 and forbidden_side
!= "a":
645 elif side_a
[-1] in connected_faces
[face
] and \
646 forbidden_side
!= "a":
652 if len(side_b
) == 0 and forbidden_side
!= "b":
658 elif side_b
[-1] in connected_faces
[face
] and \
659 forbidden_side
!= "b":
667 # weird input with branches
680 for key
in face_edgekeys(bm_mod
.faces
[fi
]):
681 if key
[0] not in verts_used
and key
[1] not in \
683 extraloop
.append(key
)
686 for key
in extraloop
:
688 if new_vert
not in verts_used
:
689 verts_used
.append(new_vert
)
690 newloops
.append(extraloop
)
691 all_edgeloops
.append(extraloop
)
693 # input contains branches, only return selected loop
697 # change edgeloops into normal loops
699 for edgeloop
in all_edgeloops
:
701 # grow loop by comparing vertices between consecutive edge-keys
702 for i
in range(len(edgeloop
)-1):
703 for vert
in range(2):
704 if edgeloop
[i
][vert
] in edgeloop
[i
+1]:
705 loop
.append(edgeloop
[i
][vert
])
708 # add starting vertex
709 for vert
in range(2):
710 if edgeloop
[0][vert
] != loop
[0]:
711 loop
= [edgeloop
[0][vert
]] + loop
714 for vert
in range(2):
715 if edgeloop
[-1][vert
] != loop
[-1]:
716 loop
.append(edgeloop
[-1][vert
])
718 # check if loop is circular
719 if loop
[0] == loop
[-1]:
724 loops
.append([loop
, circular
])
729 # gather initial data
731 global_undo
= bpy
.context
.user_preferences
.edit
.use_global_undo
732 bpy
.context
.user_preferences
.edit
.use_global_undo
= False
733 object = bpy
.context
.active_object
734 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
735 # ensure that selection is synced for the derived mesh
736 bpy
.ops
.object.mode_set(mode
='OBJECT')
737 bpy
.ops
.object.mode_set(mode
='EDIT')
738 bm
= bmesh
.from_edit_mesh(object.data
)
740 return(global_undo
, object, bm
)
743 # move the vertices to their new locations
744 def move_verts(object, bm
, mapping
, move
, lock
, influence
):
746 lock_x
, lock_y
, lock_z
= lock
747 orientation
= bpy
.context
.space_data
.transform_orientation
748 custom
= bpy
.context
.space_data
.current_orientation
750 mat
= custom
.matrix
.to_4x4().inverted() * object.matrix_world
.copy()
751 elif orientation
== 'LOCAL':
752 mat
= mathutils
.Matrix
.Identity(4)
753 elif orientation
== 'VIEW':
754 mat
= bpy
.context
.region_data
.view_matrix
.copy() * \
755 object.matrix_world
.copy()
756 else: # orientation == 'GLOBAL'
757 mat
= object.matrix_world
.copy()
758 mat_inv
= mat
.inverted()
761 for index
, loc
in loop
:
763 if mapping
[index
] == -1:
766 index
= mapping
[index
]
768 delta
= (loc
- bm
.verts
[index
].co
) * mat_inv
776 loc
= bm
.verts
[index
].co
+ delta
780 new_loc
= loc
*(influence
/100) + \
781 bm
.verts
[index
].co
*((100-influence
)/100)
782 bm
.verts
[index
].co
= new_loc
787 # load custom tool settings
788 def settings_load(self
):
789 lt
= bpy
.context
.window_manager
.looptools
790 tool
= self
.name
.split()[0].lower()
791 keys
= self
.as_keywords().keys()
793 setattr(self
, key
, getattr(lt
, tool
+ "_" + key
))
796 # store custom tool settings
797 def settings_write(self
):
798 lt
= bpy
.context
.window_manager
.looptools
799 tool
= self
.name
.split()[0].lower()
800 keys
= self
.as_keywords().keys()
802 setattr(lt
, tool
+ "_" + key
, getattr(self
, key
))
805 # clean up and set settings back to original state
806 def terminate(global_undo
):
807 # update editmesh cached data
808 obj
= bpy
.context
.active_object
809 if obj
.mode
== 'EDIT':
810 bmesh
.update_edit_mesh(obj
.data
, tessface
=True, destructive
=True)
812 bpy
.context
.user_preferences
.edit
.use_global_undo
= global_undo
815 ##########################################
816 ####### Bridge functions #################
817 ##########################################
819 # calculate a cubic spline through the middle section of 4 given coordinates
820 def bridge_calculate_cubic_spline(bm
, coordinates
):
826 for i
in coordinates
:
827 a
.append(float(i
[j
]))
830 h
.append(x
[i
+1]-x
[i
])
833 q
.append(3.0/h
[i
]*(a
[i
+1]-a
[i
])-3.0/h
[i
-1]*(a
[i
]-a
[i
-1]))
838 l
.append(2.0*(x
[i
+1]-x
[i
-1])-h
[i
-1]*u
[i
-1])
840 z
.append((q
[i
]-h
[i
-1]*z
[i
-1])/l
[i
])
843 b
= [False for i
in range(3)]
844 c
= [False for i
in range(4)]
845 d
= [False for i
in range(3)]
847 for i
in range(2,-1,-1):
848 c
[i
] = z
[i
]-u
[i
]*c
[i
+1]
849 b
[i
] = (a
[i
+1]-a
[i
])/h
[i
]-h
[i
]*(c
[i
+1]+2.0*c
[i
])/3.0
850 d
[i
] = (c
[i
+1]-c
[i
])/(3.0*h
[i
])
852 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
853 spline
= [result
[1], result
[4], result
[7]]
858 # return a list with new vertex location vectors, a list with face vertex
859 # integers, and the highest vertex integer in the virtual mesh
860 def bridge_calculate_geometry(bm
, lines
, vertex_normals
, segments
,
861 interpolation
, cubic_strength
, min_width
, max_vert_index
):
865 # calculate location based on interpolation method
866 def get_location(line
, segment
, splines
):
867 v1
= bm
.verts
[lines
[line
][0]].co
868 v2
= bm
.verts
[lines
[line
][1]].co
869 if interpolation
== 'linear':
870 return v1
+ (segment
/segments
) * (v2
-v1
)
871 else: # interpolation == 'cubic'
872 m
= (segment
/segments
)
873 ax
,bx
,cx
,dx
,tx
= splines
[line
][0]
874 x
= ax
+bx
*m
+cx
*m
**2+dx
*m
**3
875 ay
,by
,cy
,dy
,ty
= splines
[line
][1]
876 y
= ay
+by
*m
+cy
*m
**2+dy
*m
**3
877 az
,bz
,cz
,dz
,tz
= splines
[line
][2]
878 z
= az
+bz
*m
+cz
*m
**2+dz
*m
**3
879 return mathutils
.Vector((x
, y
, z
))
881 # no interpolation needed
883 for i
, line
in enumerate(lines
):
885 faces
.append([line
[0], lines
[i
+1][0], lines
[i
+1][1], line
[1]])
886 # more than 1 segment, interpolate
888 # calculate splines (if necessary) once, so no recalculations needed
889 if interpolation
== 'cubic':
892 v1
= bm
.verts
[line
[0]].co
893 v2
= bm
.verts
[line
[1]].co
894 size
= (v2
-v1
).length
* cubic_strength
895 splines
.append(bridge_calculate_cubic_spline(bm
,
896 [v1
+size
*vertex_normals
[line
[0]], v1
, v2
,
897 v2
+size
*vertex_normals
[line
[1]]]))
901 # create starting situation
902 virtual_width
= [(bm
.verts
[lines
[i
][0]].co
-
903 bm
.verts
[lines
[i
+1][0]].co
).length
for i
904 in range(len(lines
)-1)]
905 new_verts
= [get_location(0, seg
, splines
) for seg
in range(1,
907 first_line_indices
= [i
for i
in range(max_vert_index
+1,
908 max_vert_index
+segments
)]
910 prev_verts
= new_verts
[:] # vertex locations of verts on previous line
911 prev_vert_indices
= first_line_indices
[:]
912 max_vert_index
+= segments
- 1 # highest vertex index in virtual mesh
913 next_verts
= [] # vertex locations of verts on current line
914 next_vert_indices
= []
916 for i
, line
in enumerate(lines
):
921 for seg
in range(1, segments
):
922 loc1
= prev_verts
[seg
-1]
923 loc2
= get_location(i
+1, seg
, splines
)
924 if (loc1
-loc2
).length
< (min_width
/100)*virtual_width
[i
] \
925 and line
[1]==lines
[i
+1][1]:
926 # triangle, no new vertex
927 faces
.append([v1
, v2
, prev_vert_indices
[seg
-1],
928 prev_vert_indices
[seg
-1]])
929 next_verts
+= prev_verts
[seg
-1:]
930 next_vert_indices
+= prev_vert_indices
[seg
-1:]
934 if i
== len(lines
)-2 and lines
[0] == lines
[-1]:
935 # quad with first line, no new vertex
936 faces
.append([v1
, v2
, first_line_indices
[seg
-1],
937 prev_vert_indices
[seg
-1]])
938 v2
= first_line_indices
[seg
-1]
939 v1
= prev_vert_indices
[seg
-1]
941 # quad, add new vertex
943 faces
.append([v1
, v2
, max_vert_index
,
944 prev_vert_indices
[seg
-1]])
946 v1
= prev_vert_indices
[seg
-1]
947 new_verts
.append(loc2
)
948 next_verts
.append(loc2
)
949 next_vert_indices
.append(max_vert_index
)
951 faces
.append([v1
, v2
, lines
[i
+1][1], line
[1]])
953 prev_verts
= next_verts
[:]
954 prev_vert_indices
= next_vert_indices
[:]
956 next_vert_indices
= []
958 return(new_verts
, faces
, max_vert_index
)
961 # calculate lines (list of lists, vertex indices) that are used for bridging
962 def bridge_calculate_lines(bm
, loops
, mode
, twist
, reverse
):
964 loop1
, loop2
= [i
[0] for i
in loops
]
965 loop1_circular
, loop2_circular
= [i
[1] for i
in loops
]
966 circular
= loop1_circular
or loop2_circular
969 # calculate loop centers
971 for loop
in [loop1
, loop2
]:
972 center
= mathutils
.Vector()
974 center
+= bm
.verts
[vertex
].co
976 centers
.append(center
)
977 for i
, loop
in enumerate([loop1
, loop2
]):
979 if bm
.verts
[vertex
].co
== centers
[i
]:
980 # prevent zero-length vectors in angle comparisons
981 centers
[i
] += mathutils
.Vector((0.01, 0, 0))
983 center1
, center2
= centers
985 # calculate the normals of the virtual planes that the loops are on
987 normal_plurity
= False
988 for i
, loop
in enumerate([loop1
, loop2
]):
990 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
994 for loc
in [bm
.verts
[vertex
].co
for vertex
in loop
]:
995 mat
[0][0] += (loc
[0]-x
)**2
996 mat
[1][0] += (loc
[0]-x
)*(loc
[1]-y
)
997 mat
[2][0] += (loc
[0]-x
)*(loc
[2]-z
)
998 mat
[0][1] += (loc
[1]-y
)*(loc
[0]-x
)
999 mat
[1][1] += (loc
[1]-y
)**2
1000 mat
[2][1] += (loc
[1]-y
)*(loc
[2]-z
)
1001 mat
[0][2] += (loc
[2]-z
)*(loc
[0]-x
)
1002 mat
[1][2] += (loc
[2]-z
)*(loc
[1]-y
)
1003 mat
[2][2] += (loc
[2]-z
)**2
1006 if sum(mat
[0]) < 1e-6 or sum(mat
[1]) < 1e-6 or sum(mat
[2]) < 1e-6:
1007 normal_plurity
= True
1011 if sum(mat
[0]) == 0:
1012 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
1013 elif sum(mat
[1]) == 0:
1014 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
1015 elif sum(mat
[2]) == 0:
1016 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
1018 # warning! this is different from .normalize()
1021 vec
= mathutils
.Vector((1.0, 1.0, 1.0))
1022 vec2
= (mat
* vec
)/(mat
* vec
).length
1023 while vec
!= vec2
and iter<itermax
:
1027 if vec2
.length
!= 0:
1029 if vec2
.length
== 0:
1030 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
1032 normals
.append(normal
)
1033 # have plane normals face in the same direction (maximum angle: 90 degrees)
1034 if ((center1
+ normals
[0]) - center2
).length
< \
1035 ((center1
- normals
[0]) - center2
).length
:
1037 if ((center2
+ normals
[1]) - center1
).length
> \
1038 ((center2
- normals
[1]) - center1
).length
:
1041 # rotation matrix, representing the difference between the plane normals
1042 axis
= normals
[0].cross(normals
[1])
1043 axis
= mathutils
.Vector([loc
if abs(loc
) > 1e-8 else 0 for loc
in axis
])
1044 if axis
.angle(mathutils
.Vector((0, 0, 1)), 0) > 1.5707964:
1046 angle
= normals
[0].dot(normals
[1])
1047 rotation_matrix
= mathutils
.Matrix
.Rotation(angle
, 4, axis
)
1049 # if circular, rotate loops so they are aligned
1051 # make sure loop1 is the circular one (or both are circular)
1052 if loop2_circular
and not loop1_circular
:
1053 loop1_circular
, loop2_circular
= True, False
1054 loop1
, loop2
= loop2
, loop1
1056 # match start vertex of loop1 with loop2
1057 target_vector
= bm
.verts
[loop2
[0]].co
- center2
1058 dif_angles
= [[(rotation_matrix
* (bm
.verts
[vertex
].co
- center1
)
1059 ).angle(target_vector
, 0), False, i
] for
1060 i
, vertex
in enumerate(loop1
)]
1062 if len(loop1
) != len(loop2
):
1063 angle_limit
= dif_angles
[0][0] * 1.2 # 20% margin
1064 dif_angles
= [[(bm
.verts
[loop2
[0]].co
- \
1065 bm
.verts
[loop1
[index
]].co
).length
, angle
, index
] for \
1066 angle
, distance
, index
in dif_angles
if angle
<= angle_limit
]
1068 loop1
= loop1
[dif_angles
[0][2]:] + loop1
[:dif_angles
[0][2]]
1070 # have both loops face the same way
1071 if normal_plurity
and not circular
:
1072 second_to_first
, second_to_second
, second_to_last
= \
1073 [(bm
.verts
[loop1
[1]].co
- center1
).\
1074 angle(bm
.verts
[loop2
[i
]].co
- center2
) for i
in [0, 1, -1]]
1075 last_to_first
, last_to_second
= [(bm
.verts
[loop1
[-1]].co
- \
1076 center1
).angle(bm
.verts
[loop2
[i
]].co
- center2
) for \
1078 if (min(last_to_first
, last_to_second
)*1.1 < min(second_to_first
, \
1079 second_to_second
)) or (loop2_circular
and second_to_last
*1.1 < \
1080 min(second_to_first
, second_to_second
)):
1083 loop1
= [loop1
[-1]] + loop1
[:-1]
1085 angle
= (bm
.verts
[loop1
[0]].co
- center1
).\
1086 cross(bm
.verts
[loop1
[1]].co
- center1
).angle(normals
[0], 0)
1087 target_angle
= (bm
.verts
[loop2
[0]].co
- center2
).\
1088 cross(bm
.verts
[loop2
[1]].co
- center2
).angle(normals
[1], 0)
1089 limit
= 1.5707964 # 0.5*pi, 90 degrees
1090 if not ((angle
> limit
and target_angle
> limit
) or \
1091 (angle
< limit
and target_angle
< limit
)):
1094 loop1
= [loop1
[-1]] + loop1
[:-1]
1095 elif normals
[0].angle(normals
[1]) > limit
:
1098 loop1
= [loop1
[-1]] + loop1
[:-1]
1100 # both loops have the same length
1101 if len(loop1
) == len(loop2
):
1104 if abs(twist
) < len(loop1
):
1105 loop1
= loop1
[twist
:]+loop1
[:twist
]
1109 lines
.append([loop1
[0], loop2
[0]])
1110 for i
in range(1, len(loop1
)):
1111 lines
.append([loop1
[i
], loop2
[i
]])
1113 # loops of different lengths
1115 # make loop1 longest loop
1116 if len(loop2
) > len(loop1
):
1117 loop1
, loop2
= loop2
, loop1
1118 loop1_circular
, loop2_circular
= loop2_circular
, loop1_circular
1122 if abs(twist
) < len(loop1
):
1123 loop1
= loop1
[twist
:]+loop1
[:twist
]
1127 # shortest angle difference doesn't always give correct start vertex
1128 if loop1_circular
and not loop2_circular
:
1131 if len(loop1
) - shifting
< len(loop2
):
1134 to_last
, to_first
= [(rotation_matrix
*
1135 (bm
.verts
[loop1
[-1]].co
- center1
)).angle((bm
.\
1136 verts
[loop2
[i
]].co
- center2
), 0) for i
in [-1, 0]]
1137 if to_first
< to_last
:
1138 loop1
= [loop1
[-1]] + loop1
[:-1]
1144 # basic shortest side first
1146 lines
.append([loop1
[0], loop2
[0]])
1147 for i
in range(1, len(loop1
)):
1148 if i
>= len(loop2
) - 1:
1150 lines
.append([loop1
[i
], loop2
[-1]])
1153 lines
.append([loop1
[i
], loop2
[i
]])
1155 # shortest edge algorithm
1156 else: # mode == 'shortest'
1157 lines
.append([loop1
[0], loop2
[0]])
1159 for i
in range(len(loop1
) -1):
1160 if prev_vert2
== len(loop2
) - 1 and not loop2_circular
:
1161 # force triangles, reached end of loop2
1163 elif prev_vert2
== len(loop2
) - 1 and loop2_circular
:
1164 # at end of loop2, but circular, so check with first vert
1165 tri
, quad
= [(bm
.verts
[loop1
[i
+1]].co
-
1166 bm
.verts
[loop2
[j
]].co
).length
1167 for j
in [prev_vert2
, 0]]
1169 elif len(loop1
) - 1 - i
== len(loop2
) - 1 - prev_vert2
and \
1171 # force quads, otherwise won't make it to end of loop2
1174 # calculate if tri or quad gives shortest edge
1175 tri
, quad
= [(bm
.verts
[loop1
[i
+1]].co
-
1176 bm
.verts
[loop2
[j
]].co
).length
1177 for j
in range(prev_vert2
, prev_vert2
+2)]
1181 lines
.append([loop1
[i
+1], loop2
[prev_vert2
]])
1182 if circle_full
== 2:
1185 elif not circle_full
:
1186 lines
.append([loop1
[i
+1], loop2
[prev_vert2
+1]])
1188 # quad to first vertex of loop2
1190 lines
.append([loop1
[i
+1], loop2
[0]])
1194 # final face for circular loops
1195 if loop1_circular
and loop2_circular
:
1196 lines
.append([loop1
[0], loop2
[0]])
1201 # calculate number of segments needed
1202 def bridge_calculate_segments(bm
, lines
, loops
, segments
):
1203 # return if amount of segments is set by user
1208 average_edge_length
= [(bm
.verts
[vertex
].co
- \
1209 bm
.verts
[loop
[0][i
+1]].co
).length
for loop
in loops
for \
1210 i
, vertex
in enumerate(loop
[0][:-1])]
1211 # closing edges of circular loops
1212 average_edge_length
+= [(bm
.verts
[loop
[0][-1]].co
- \
1213 bm
.verts
[loop
[0][0]].co
).length
for loop
in loops
if loop
[1]]
1216 average_edge_length
= sum(average_edge_length
) / len(average_edge_length
)
1217 average_bridge_length
= sum([(bm
.verts
[v1
].co
- \
1218 bm
.verts
[v2
].co
).length
for v1
, v2
in lines
]) / len(lines
)
1220 segments
= max(1, round(average_bridge_length
/ average_edge_length
))
1225 # return dictionary with vertex index as key, and the normal vector as value
1226 def bridge_calculate_virtual_vertex_normals(bm
, lines
, loops
, edge_faces
,
1228 if not edge_faces
: # interpolation isn't set to cubic
1231 # pity reduce() isn't one of the basic functions in python anymore
1232 def average_vector_dictionary(dic
):
1233 for key
, vectors
in dic
.items():
1234 #if type(vectors) == type([]) and len(vectors) > 1:
1235 if len(vectors
) > 1:
1236 average
= mathutils
.Vector()
1237 for vector
in vectors
:
1239 average
/= len(vectors
)
1240 dic
[key
] = [average
]
1243 # get all edges of the loop
1244 edges
= [[edgekey_to_edge
[tuple(sorted([loops
[j
][0][i
],
1245 loops
[j
][0][i
+1]]))] for i
in range(len(loops
[j
][0])-1)] for \
1247 edges
= edges
[0] + edges
[1]
1249 if loops
[j
][1]: # circular
1250 edges
.append(edgekey_to_edge
[tuple(sorted([loops
[j
][0][0],
1251 loops
[j
][0][-1]]))])
1254 calculation based on face topology (assign edge-normals to vertices)
1256 edge_normal = face_normal x edge_vector
1257 vertex_normal = average(edge_normals)
1259 vertex_normals
= dict([(vertex
, []) for vertex
in loops
[0][0]+loops
[1][0]])
1261 faces
= edge_faces
[edgekey(edge
)] # valid faces connected to edge
1264 # get edge coordinates
1265 v1
, v2
= [bm
.verts
[edgekey(edge
)[i
]].co
for i
in [0,1]]
1266 edge_vector
= v1
- v2
1267 if edge_vector
.length
< 1e-4:
1268 # zero-length edge, vertices at same location
1270 edge_center
= (v1
+ v2
) / 2
1272 # average face coordinates, if connected to more than 1 valid face
1274 face_normal
= mathutils
.Vector()
1275 face_center
= mathutils
.Vector()
1277 face_normal
+= face
.normal
1278 face_center
+= face
.calc_center_median()
1279 face_normal
/= len(faces
)
1280 face_center
/= len(faces
)
1282 face_normal
= faces
[0].normal
1283 face_center
= faces
[0].calc_center_median()
1284 if face_normal
.length
< 1e-4:
1285 # faces with a surface of 0 have no face normal
1288 # calculate virtual edge normal
1289 edge_normal
= edge_vector
.cross(face_normal
)
1290 edge_normal
.length
= 0.01
1291 if (face_center
- (edge_center
+ edge_normal
)).length
> \
1292 (face_center
- (edge_center
- edge_normal
)).length
:
1293 # make normal face the correct way
1294 edge_normal
.negate()
1295 edge_normal
.normalize()
1296 # add virtual edge normal as entry for both vertices it connects
1297 for vertex
in edgekey(edge
):
1298 vertex_normals
[vertex
].append(edge_normal
)
1301 calculation based on connection with other loop (vertex focused method)
1302 - used for vertices that aren't connected to any valid faces
1304 plane_normal = edge_vector x connection_vector
1305 vertex_normal = plane_normal x edge_vector
1307 vertices
= [vertex
for vertex
, normal
in vertex_normals
.items() if not \
1311 # edge vectors connected to vertices
1312 edge_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1314 for v
in edgekey(edge
):
1315 if v
in edge_vectors
:
1316 edge_vector
= bm
.verts
[edgekey(edge
)[0]].co
- \
1317 bm
.verts
[edgekey(edge
)[1]].co
1318 if edge_vector
.length
< 1e-4:
1319 # zero-length edge, vertices at same location
1321 edge_vectors
[v
].append(edge_vector
)
1323 # connection vectors between vertices of both loops
1324 connection_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1325 connections
= dict([[vertex
, []] for vertex
in vertices
])
1326 for v1
, v2
in lines
:
1327 if v1
in connection_vectors
or v2
in connection_vectors
:
1328 new_vector
= bm
.verts
[v1
].co
- bm
.verts
[v2
].co
1329 if new_vector
.length
< 1e-4:
1330 # zero-length connection vector,
1331 # vertices in different loops at same location
1333 if v1
in connection_vectors
:
1334 connection_vectors
[v1
].append(new_vector
)
1335 connections
[v1
].append(v2
)
1336 if v2
in connection_vectors
:
1337 connection_vectors
[v2
].append(new_vector
)
1338 connections
[v2
].append(v1
)
1339 connection_vectors
= average_vector_dictionary(connection_vectors
)
1340 connection_vectors
= dict([[vertex
, vector
[0]] if vector
else \
1341 [vertex
, []] for vertex
, vector
in connection_vectors
.items()])
1343 for vertex
, values
in edge_vectors
.items():
1344 # vertex normal doesn't matter, just assign a random vector to it
1345 if not connection_vectors
[vertex
]:
1346 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1349 # calculate to what location the vertex is connected,
1350 # used to determine what way to flip the normal
1351 connected_center
= mathutils
.Vector()
1352 for v
in connections
[vertex
]:
1353 connected_center
+= bm
.verts
[v
].co
1354 if len(connections
[vertex
]) > 1:
1355 connected_center
/= len(connections
[vertex
])
1356 if len(connections
[vertex
]) == 0:
1357 # shouldn't be possible, but better safe than sorry
1358 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1361 # can't do proper calculations, because of zero-length vector
1363 if (connected_center
- (bm
.verts
[vertex
].co
+ \
1364 connection_vectors
[vertex
])).length
< (connected_center
- \
1365 (bm
.verts
[vertex
].co
- connection_vectors
[vertex
])).\
1367 connection_vectors
[vertex
].negate()
1368 vertex_normals
[vertex
] = [connection_vectors
[vertex
].\
1372 # calculate vertex normals using edge-vectors,
1373 # connection-vectors and the derived plane normal
1374 for edge_vector
in values
:
1375 plane_normal
= edge_vector
.cross(connection_vectors
[vertex
])
1376 vertex_normal
= edge_vector
.cross(plane_normal
)
1377 vertex_normal
.length
= 0.1
1378 if (connected_center
- (bm
.verts
[vertex
].co
+ \
1379 vertex_normal
)).length
< (connected_center
- \
1380 (bm
.verts
[vertex
].co
- vertex_normal
)).length
:
1381 # make normal face the correct way
1382 vertex_normal
.negate()
1383 vertex_normal
.normalize()
1384 vertex_normals
[vertex
].append(vertex_normal
)
1386 # average virtual vertex normals, based on all edges it's connected to
1387 vertex_normals
= average_vector_dictionary(vertex_normals
)
1388 vertex_normals
= dict([[vertex
, vector
[0]] for vertex
, vector
in \
1389 vertex_normals
.items()])
1391 return(vertex_normals
)
1394 # add vertices to mesh
1395 def bridge_create_vertices(bm
, vertices
):
1396 for i
in range(len(vertices
)):
1397 bm
.verts
.new(vertices
[i
])
1401 def bridge_create_faces(object, bm
, faces
, twist
):
1402 # have the normal point the correct way
1404 [face
.reverse() for face
in faces
]
1405 faces
= [face
[2:]+face
[:2] if face
[0]==face
[1] else face
for \
1408 # eekadoodle prevention
1409 for i
in range(len(faces
)):
1410 if not faces
[i
][-1]:
1411 if faces
[i
][0] == faces
[i
][-1]:
1412 faces
[i
] = [faces
[i
][1], faces
[i
][2], faces
[i
][3], faces
[i
][1]]
1414 faces
[i
] = [faces
[i
][-1]] + faces
[i
][:-1]
1415 # result of converting from pre-bmesh period
1416 if faces
[i
][-1] == faces
[i
][-2]:
1417 faces
[i
] = faces
[i
][:-1]
1420 for i
in range(len(faces
)):
1421 new_faces
.append(bm
.faces
.new([bm
.verts
[v
] for v
in faces
[i
]]))
1423 object.data
.update(calc_edges
=True) # calc_edges prevents memory-corruption
1428 # calculate input loops
1429 def bridge_get_input(bm
):
1430 # create list of internal edges, which should be skipped
1431 eks_of_selected_faces
= [item
for sublist
in [face_edgekeys(face
) for \
1432 face
in bm
.faces
if face
.select
and not face
.hide
] for item
in sublist
]
1434 for ek
in eks_of_selected_faces
:
1435 if ek
in edge_count
:
1439 internal_edges
= [ek
for ek
in edge_count
if edge_count
[ek
] > 1]
1441 # sort correct edges into loops
1442 selected_edges
= [edgekey(edge
) for edge
in bm
.edges
if edge
.select \
1443 and not edge
.hide
and edgekey(edge
) not in internal_edges
]
1444 loops
= get_connected_selections(selected_edges
)
1449 # return values needed by the bridge operator
1450 def bridge_initialise(bm
, interpolation
):
1451 if interpolation
== 'cubic':
1452 # dict with edge-key as key and list of connected valid faces as value
1453 face_blacklist
= [face
.index
for face
in bm
.faces
if face
.select
or \
1455 edge_faces
= dict([[edgekey(edge
), []] for edge
in bm
.edges
if not \
1457 for face
in bm
.faces
:
1458 if face
.index
in face_blacklist
:
1460 for key
in face_edgekeys(face
):
1461 edge_faces
[key
].append(face
)
1462 # dictionary with the edge-key as key and edge as value
1463 edgekey_to_edge
= dict([[edgekey(edge
), edge
] for edge
in \
1464 bm
.edges
if edge
.select
and not edge
.hide
])
1467 edgekey_to_edge
= False
1469 # selected faces input
1470 old_selected_faces
= [face
.index
for face
in bm
.faces
if face
.select \
1473 # find out if faces created by bridging should be smoothed
1476 if sum([face
.smooth
for face
in bm
.faces
])/len(bm
.faces
) \
1480 return(edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
)
1483 # return a string with the input method
1484 def bridge_input_method(loft
, loft_loop
):
1488 method
= "Loft loop"
1490 method
= "Loft no-loop"
1497 # match up loops in pairs, used for multi-input bridging
1498 def bridge_match_loops(bm
, loops
):
1499 # calculate average loop normals and centers
1502 for vertices
, circular
in loops
:
1503 normal
= mathutils
.Vector()
1504 center
= mathutils
.Vector()
1505 for vertex
in vertices
:
1506 normal
+= bm
.verts
[vertex
].normal
1507 center
+= bm
.verts
[vertex
].co
1508 normals
.append(normal
/ len(vertices
) / 10)
1509 centers
.append(center
/ len(vertices
))
1511 # possible matches if loop normals are faced towards the center
1513 matches
= dict([[i
, []] for i
in range(len(loops
))])
1515 for i
in range(len(loops
) + 1):
1516 for j
in range(i
+1, len(loops
)):
1517 if (centers
[i
] - centers
[j
]).length
> (centers
[i
] - (centers
[j
] \
1518 + normals
[j
])).length
and (centers
[j
] - centers
[i
]).length
> \
1519 (centers
[j
] - (centers
[i
] + normals
[i
])).length
:
1521 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1522 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1523 # if no loops face each other, just make matches between all the loops
1524 if matches_amount
== 0:
1525 for i
in range(len(loops
) + 1):
1526 for j
in range(i
+1, len(loops
)):
1527 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1528 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1529 for key
, value
in matches
.items():
1532 # matches based on distance between centers and number of vertices in loops
1534 for loop_index
in range(len(loops
)):
1535 if loop_index
in new_order
:
1537 loop_matches
= matches
[loop_index
]
1538 if not loop_matches
:
1540 shortest_distance
= loop_matches
[0][0]
1541 shortest_distance
*= 1.1
1542 loop_matches
= [[abs(len(loops
[loop_index
][0]) - \
1543 len(loops
[loop
[2]][0])), loop
[0], loop
[1], loop
[2]] for loop
in \
1544 loop_matches
if loop
[0] < shortest_distance
]
1546 for match
in loop_matches
:
1547 if match
[3] not in new_order
:
1548 new_order
+= [loop_index
, match
[3]]
1551 # reorder loops based on matches
1552 if len(new_order
) >= 2:
1553 loops
= [loops
[i
] for i
in new_order
]
1558 # remove old_selected_faces
1559 def bridge_remove_internal_faces(bm
, old_selected_faces
):
1560 # collect bmesh faces and internal bmesh edges
1561 remove_faces
= [bm
.faces
[face
] for face
in old_selected_faces
]
1562 edges
= collections
.Counter([edge
.index
for face
in remove_faces
for \
1563 edge
in face
.edges
])
1564 remove_edges
= [bm
.edges
[edge
] for edge
in edges
if edges
[edge
] > 1]
1566 # remove internal faces and edges
1567 for face
in remove_faces
:
1568 bm
.faces
.remove(face
)
1569 for edge
in remove_edges
:
1570 bm
.edges
.remove(edge
)
1573 # update list of internal faces that are flagged for removal
1574 def bridge_save_unused_faces(bm
, old_selected_faces
, loops
):
1575 # key: vertex index, value: lists of selected faces using it
1576 vertex_to_face
= dict([[i
, []] for i
in range(len(bm
.verts
))])
1577 [[vertex_to_face
[vertex
.index
].append(face
) for vertex
in \
1578 bm
.faces
[face
].verts
] for face
in old_selected_faces
]
1580 # group selected faces that are connected
1583 for face
in old_selected_faces
:
1584 if face
in grouped_faces
:
1586 grouped_faces
.append(face
)
1590 grow_face
= new_faces
[0]
1591 for vertex
in bm
.faces
[grow_face
].verts
:
1592 vertex_face_group
= [face
for face
in vertex_to_face
[\
1593 vertex
.index
] if face
not in grouped_faces
]
1594 new_faces
+= vertex_face_group
1595 grouped_faces
+= vertex_face_group
1596 group
+= vertex_face_group
1598 groups
.append(group
)
1600 # key: vertex index, value: True/False (is it in a loop that is used)
1601 used_vertices
= dict([[i
, 0] for i
in range(len(bm
.verts
))])
1603 for vertex
in loop
[0]:
1604 used_vertices
[vertex
] = True
1606 # check if group is bridged, if not remove faces from internal faces list
1607 for group
in groups
:
1612 for vertex
in bm
.faces
[face
].verts
:
1613 if used_vertices
[vertex
.index
]:
1618 old_selected_faces
.remove(face
)
1621 # add the newly created faces to the selection
1622 def bridge_select_new_faces(new_faces
, smooth
):
1623 for face
in new_faces
:
1624 face
.select_set(True)
1625 face
.smooth
= smooth
1628 # sort loops, so they are connected in the correct order when lofting
1629 def bridge_sort_loops(bm
, loops
, loft_loop
):
1630 # simplify loops to single points, and prepare for pathfinding
1631 x
, y
, z
= [[sum([bm
.verts
[i
].co
[j
] for i
in loop
[0]]) / \
1632 len(loop
[0]) for loop
in loops
] for j
in range(3)]
1633 nodes
= [mathutils
.Vector((x
[i
], y
[i
], z
[i
])) for i
in range(len(loops
))]
1636 open = [i
for i
in range(1, len(loops
))]
1638 # connect node to path, that is shortest to active_node
1639 while len(open) > 0:
1640 distances
= [(nodes
[active_node
] - nodes
[i
]).length
for i
in open]
1641 active_node
= open[distances
.index(min(distances
))]
1642 open.remove(active_node
)
1643 path
.append([active_node
, min(distances
)])
1644 # check if we didn't start in the middle of the path
1645 for i
in range(2, len(path
)):
1646 if (nodes
[path
[i
][0]]-nodes
[0]).length
< path
[i
][1]:
1649 path
= path
[:-i
] + temp
1653 loops
= [loops
[i
[0]] for i
in path
]
1654 # if requested, duplicate first loop at last position, so loft can loop
1656 loops
= loops
+ [loops
[0]]
1661 # remapping old indices to new position in list
1662 def bridge_update_old_selection(bm
, old_selected_faces
):
1663 #old_indices = old_selected_faces[:]
1664 #old_selected_faces = []
1665 #for i, face in enumerate(bm.faces):
1666 # if face.index in old_indices:
1667 # old_selected_faces.append(i)
1669 old_selected_faces
= [i
for i
, face
in enumerate(bm
.faces
) if face
.index \
1670 in old_selected_faces
]
1672 return(old_selected_faces
)
1675 ##########################################
1676 ####### Circle functions #################
1677 ##########################################
1679 # convert 3d coordinates to 2d coordinates on plane
1680 def circle_3d_to_2d(bm_mod
, loop
, com
, normal
):
1681 # project vertices onto the plane
1682 verts
= [bm_mod
.verts
[v
] for v
in loop
[0]]
1683 verts_projected
= [[v
.co
- (v
.co
- com
).dot(normal
) * normal
, v
.index
]
1686 # calculate two vectors (p and q) along the plane
1687 m
= mathutils
.Vector((normal
[0] + 1.0, normal
[1], normal
[2]))
1688 p
= m
- (m
.dot(normal
) * normal
)
1690 m
= mathutils
.Vector((normal
[0], normal
[1] + 1.0, normal
[2]))
1691 p
= m
- (m
.dot(normal
) * normal
)
1694 # change to 2d coordinates using perpendicular projection
1696 for loc
, vert
in verts_projected
:
1698 x
= p
.dot(vloc
) / p
.dot(p
)
1699 y
= q
.dot(vloc
) / q
.dot(q
)
1700 locs_2d
.append([x
, y
, vert
])
1702 return(locs_2d
, p
, q
)
1705 # calculate a best-fit circle to the 2d locations on the plane
1706 def circle_calculate_best_fit(locs_2d
):
1712 # calculate center and radius (non-linear least squares solution)
1713 for iter in range(500):
1717 d
= (v
[0]**2-2.0*x0
*v
[0]+v
[1]**2-2.0*y0
*v
[1]+x0
**2+y0
**2)**0.5
1718 jmat
.append([(x0
-v
[0])/d
, (y0
-v
[1])/d
, -1.0])
1719 k
.append(-(((v
[0]-x0
)**2+(v
[1]-y0
)**2)**0.5-r
))
1720 jmat2
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1724 k2
= mathutils
.Vector((0.0, 0.0, 0.0))
1725 for i
in range(len(jmat
)):
1726 k2
+= mathutils
.Vector(jmat
[i
])*k
[i
]
1727 jmat2
[0][0] += jmat
[i
][0]**2
1728 jmat2
[1][0] += jmat
[i
][0]*jmat
[i
][1]
1729 jmat2
[2][0] += jmat
[i
][0]*jmat
[i
][2]
1730 jmat2
[1][1] += jmat
[i
][1]**2
1731 jmat2
[2][1] += jmat
[i
][1]*jmat
[i
][2]
1732 jmat2
[2][2] += jmat
[i
][2]**2
1733 jmat2
[0][1] = jmat2
[1][0]
1734 jmat2
[0][2] = jmat2
[2][0]
1735 jmat2
[1][2] = jmat2
[2][1]
1740 dx0
, dy0
, dr
= jmat2
* k2
1744 # stop iterating if we're close enough to optimal solution
1745 if abs(dx0
)<1e-6 and abs(dy0
)<1e-6 and abs(dr
)<1e-6:
1748 # return center of circle and radius
1752 # calculate circle so no vertices have to be moved away from the center
1753 def circle_calculate_min_fit(locs_2d
):
1755 x0
= (min([i
[0] for i
in locs_2d
])+max([i
[0] for i
in locs_2d
]))/2.0
1756 y0
= (min([i
[1] for i
in locs_2d
])+max([i
[1] for i
in locs_2d
]))/2.0
1757 center
= mathutils
.Vector([x0
, y0
])
1759 r
= min([(mathutils
.Vector([i
[0], i
[1]])-center
).length
for i
in locs_2d
])
1761 # return center of circle and radius
1765 # calculate the new locations of the vertices that need to be moved
1766 def circle_calculate_verts(flatten
, bm_mod
, locs_2d
, com
, p
, q
, normal
):
1767 # changing 2d coordinates back to 3d coordinates
1770 locs_3d
.append([loc
[2], loc
[0]*p
+ loc
[1]*q
+ com
])
1772 if flatten
: # flat circle
1775 else: # project the locations on the existing mesh
1776 vert_edges
= dict_vert_edges(bm_mod
)
1777 vert_faces
= dict_vert_faces(bm_mod
)
1778 faces
= [f
for f
in bm_mod
.faces
if not f
.hide
]
1779 rays
= [normal
, -normal
]
1783 if bm_mod
.verts
[loc
[0]].co
== loc
[1]: # vertex hasn't moved
1786 dif
= normal
.angle(loc
[1]-bm_mod
.verts
[loc
[0]].co
)
1787 if -1e-6 < dif
< 1e-6 or math
.pi
-1e-6 < dif
< math
.pi
+1e-6:
1788 # original location is already along projection normal
1789 projection
= bm_mod
.verts
[loc
[0]].co
1791 # quick search through adjacent faces
1792 for face
in vert_faces
[loc
[0]]:
1793 verts
= [v
.co
for v
in bm_mod
.faces
[face
].verts
]
1794 if len(verts
) == 3: # triangle
1798 v1
, v2
, v3
, v4
= verts
[:4]
1800 intersect
= mathutils
.geometry
.\
1801 intersect_ray_tri(v1
, v2
, v3
, ray
, loc
[1])
1803 projection
= intersect
1806 intersect
= mathutils
.geometry
.\
1807 intersect_ray_tri(v1
, v3
, v4
, ray
, loc
[1])
1809 projection
= intersect
1814 # check if projection is on adjacent edges
1815 for edgekey
in vert_edges
[loc
[0]]:
1816 line1
= bm_mod
.verts
[edgekey
[0]].co
1817 line2
= bm_mod
.verts
[edgekey
[1]].co
1818 intersect
, dist
= mathutils
.geometry
.intersect_point_line(\
1819 loc
[1], line1
, line2
)
1820 if 1e-6 < dist
< 1 - 1e-6:
1821 projection
= intersect
1824 # full search through the entire mesh
1827 verts
= [v
.co
for v
in face
.verts
]
1828 if len(verts
) == 3: # triangle
1832 v1
, v2
, v3
, v4
= verts
[:4]
1834 intersect
= mathutils
.geometry
.intersect_ray_tri(\
1835 v1
, v2
, v3
, ray
, loc
[1])
1837 hits
.append([(loc
[1] - intersect
).length
,
1841 intersect
= mathutils
.geometry
.intersect_ray_tri(\
1842 v1
, v3
, v4
, ray
, loc
[1])
1844 hits
.append([(loc
[1] - intersect
).length
,
1848 # if more than 1 hit with mesh, closest hit is new loc
1850 projection
= hits
[0][1]
1852 # nothing to project on, remain at flat location
1854 new_locs
.append([loc
[0], projection
])
1856 # return new positions of projected circle
1860 # check loops and only return valid ones
1861 def circle_check_loops(single_loops
, loops
, mapping
, bm_mod
):
1862 valid_single_loops
= {}
1864 for i
, [loop
, circular
] in enumerate(loops
):
1865 # loop needs to have at least 3 vertices
1868 # loop needs at least 1 vertex in the original, non-mirrored mesh
1872 if mapping
[vert
] > -1:
1877 # loop has to be non-collinear
1879 loc0
= mathutils
.Vector(bm_mod
.verts
[loop
[0]].co
[:])
1880 loc1
= mathutils
.Vector(bm_mod
.verts
[loop
[1]].co
[:])
1882 locn
= mathutils
.Vector(bm_mod
.verts
[v
].co
[:])
1883 if loc0
== loc1
or loc1
== locn
:
1889 if -1e-6 < d1
.angle(d2
, 0) < 1e-6:
1897 # passed all tests, loop is valid
1898 valid_loops
.append([loop
, circular
])
1899 valid_single_loops
[len(valid_loops
)-1] = single_loops
[i
]
1901 return(valid_single_loops
, valid_loops
)
1904 # calculate the location of single input vertices that need to be flattened
1905 def circle_flatten_singles(bm_mod
, com
, p
, q
, normal
, single_loop
):
1907 for vert
in single_loop
:
1908 loc
= mathutils
.Vector(bm_mod
.verts
[vert
].co
[:])
1909 new_locs
.append([vert
, loc
- (loc
-com
).dot(normal
)*normal
])
1914 # calculate input loops
1915 def circle_get_input(object, bm
, scene
):
1916 # get mesh with modifiers applied
1917 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
1919 # create list of edge-keys based on selection state
1921 for face
in bm
.faces
:
1922 if face
.select
and not face
.hide
:
1926 # get selected, non-hidden , non-internal edge-keys
1927 eks_selected
= [key
for keys
in [face_edgekeys(face
) for face
in \
1928 bm_mod
.faces
if face
.select
and not face
.hide
] for key
in keys
]
1930 for ek
in eks_selected
:
1931 if ek
in edge_count
:
1935 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select \
1936 and not edge
.hide
and edge_count
.get(edgekey(edge
), 1)==1]
1938 # no faces, so no internal edges either
1939 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select \
1942 # add edge-keys around single vertices
1943 verts_connected
= dict([[vert
, 1] for edge
in [edge
for edge
in \
1944 bm_mod
.edges
if edge
.select
and not edge
.hide
] for vert
in \
1946 single_vertices
= [vert
.index
for vert
in bm_mod
.verts
if \
1947 vert
.select
and not vert
.hide
and not \
1948 verts_connected
.get(vert
.index
, False)]
1950 if single_vertices
and len(bm
.faces
)>0:
1951 vert_to_single
= dict([[v
.index
, []] for v
in bm_mod
.verts \
1953 for face
in [face
for face
in bm_mod
.faces
if not face
.select \
1955 for vert
in face
.verts
:
1957 if vert
in single_vertices
:
1958 for ek
in face_edgekeys(face
):
1960 edge_keys
.append(ek
)
1961 if vert
not in vert_to_single
[ek
[0]]:
1962 vert_to_single
[ek
[0]].append(vert
)
1963 if vert
not in vert_to_single
[ek
[1]]:
1964 vert_to_single
[ek
[1]].append(vert
)
1967 # sort edge-keys into loops
1968 loops
= get_connected_selections(edge_keys
)
1970 # find out to which loops the single vertices belong
1971 single_loops
= dict([[i
, []] for i
in range(len(loops
))])
1972 if single_vertices
and len(bm
.faces
)>0:
1973 for i
, [loop
, circular
] in enumerate(loops
):
1975 if vert_to_single
[vert
]:
1976 for single
in vert_to_single
[vert
]:
1977 if single
not in single_loops
[i
]:
1978 single_loops
[i
].append(single
)
1980 return(derived
, bm_mod
, single_vertices
, single_loops
, loops
)
1983 # recalculate positions based on the influence of the circle shape
1984 def circle_influence_locs(locs_2d
, new_locs_2d
, influence
):
1985 for i
in range(len(locs_2d
)):
1986 oldx
, oldy
, j
= locs_2d
[i
]
1987 newx
, newy
, k
= new_locs_2d
[i
]
1988 altx
= newx
*(influence
/100)+ oldx
*((100-influence
)/100)
1989 alty
= newy
*(influence
/100)+ oldy
*((100-influence
)/100)
1990 locs_2d
[i
] = [altx
, alty
, j
]
1995 # project 2d locations on circle, respecting distance relations between verts
1996 def circle_project_non_regular(locs_2d
, x0
, y0
, r
):
1997 for i
in range(len(locs_2d
)):
1998 x
, y
, j
= locs_2d
[i
]
1999 loc
= mathutils
.Vector([x
-x0
, y
-y0
])
2001 locs_2d
[i
] = [loc
[0], loc
[1], j
]
2006 # project 2d locations on circle, with equal distance between all vertices
2007 def circle_project_regular(locs_2d
, x0
, y0
, r
):
2008 # find offset angle and circling direction
2009 x
, y
, i
= locs_2d
[0]
2010 loc
= mathutils
.Vector([x
-x0
, y
-y0
])
2012 offset_angle
= loc
.angle(mathutils
.Vector([1.0, 0.0]), 0.0)
2013 loca
= mathutils
.Vector([x
-x0
, y
-y0
, 0.0])
2016 x
, y
, j
= locs_2d
[1]
2017 locb
= mathutils
.Vector([x
-x0
, y
-y0
, 0.0])
2018 if loca
.cross(locb
)[2] >= 0:
2022 # distribute vertices along the circle
2023 for i
in range(len(locs_2d
)):
2024 t
= offset_angle
+ ccw
* (i
/ len(locs_2d
) * 2 * math
.pi
)
2027 locs_2d
[i
] = [x
, y
, locs_2d
[i
][2]]
2032 # shift loop, so the first vertex is closest to the center
2033 def circle_shift_loop(bm_mod
, loop
, com
):
2034 verts
, circular
= loop
2035 distances
= [[(bm_mod
.verts
[vert
].co
- com
).length
, i
] \
2036 for i
, vert
in enumerate(verts
)]
2038 shift
= distances
[0][1]
2039 loop
= [verts
[shift
:] + verts
[:shift
], circular
]
2044 ##########################################
2045 ####### Curve functions ##################
2046 ##########################################
2048 # create lists with knots and points, all correctly sorted
2049 def curve_calculate_knots(loop
, verts_selected
):
2050 knots
= [v
for v
in loop
[0] if v
in verts_selected
]
2052 # circular loop, potential for weird splines
2054 offset
= int(len(loop
[0]) / 4)
2057 kpos
.append(loop
[0].index(k
))
2059 for i
in range(len(kpos
) - 1):
2060 kdif
.append(kpos
[i
+1] - kpos
[i
])
2061 kdif
.append(len(loop
[0]) - kpos
[-1] + kpos
[0])
2065 kadd
.append([kdif
.index(k
), True])
2066 # next 2 lines are optional, they insert
2067 # an extra control point in small gaps
2069 # kadd.append([kdif.index(k), False])
2072 for k
in kadd
: # extra knots to be added
2073 if k
[1]: # big gap (break circular spline)
2074 kpos
= loop
[0].index(knots
[k
[0]]) + offset
2075 if kpos
> len(loop
[0]) - 1:
2076 kpos
-= len(loop
[0])
2077 kins
.append([knots
[k
[0]], loop
[0][kpos
]])
2079 if kpos2
> len(knots
)-1:
2081 kpos2
= loop
[0].index(knots
[kpos2
]) - offset
2083 kpos2
+= len(loop
[0])
2084 kins
.append([loop
[0][kpos
], loop
[0][kpos2
]])
2085 krot
= loop
[0][kpos2
]
2086 else: # small gap (keep circular spline)
2087 k1
= loop
[0].index(knots
[k
[0]])
2089 if k2
> len(knots
)-1:
2091 k2
= loop
[0].index(knots
[k2
])
2093 dif
= len(loop
[0]) - 1 - k1
+ k2
2096 kn
= k1
+ int(dif
/2)
2097 if kn
> len(loop
[0]) - 1:
2099 kins
.append([loop
[0][k1
], loop
[0][kn
]])
2100 for j
in kins
: # insert new knots
2101 knots
.insert(knots
.index(j
[0]) + 1, j
[1])
2102 if not krot
: # circular loop
2103 knots
.append(knots
[0])
2104 points
= loop
[0][loop
[0].index(knots
[0]):]
2105 points
+= loop
[0][0:loop
[0].index(knots
[0]) + 1]
2106 else: # non-circular loop (broken by script)
2107 krot
= knots
.index(krot
)
2108 knots
= knots
[krot
:] + knots
[0:krot
]
2109 if loop
[0].index(knots
[0]) > loop
[0].index(knots
[-1]):
2110 points
= loop
[0][loop
[0].index(knots
[0]):]
2111 points
+= loop
[0][0:loop
[0].index(knots
[-1])+1]
2113 points
= loop
[0][loop
[0].index(knots
[0]):\
2114 loop
[0].index(knots
[-1]) + 1]
2115 # non-circular loop, add first and last point as knots
2117 if loop
[0][0] not in knots
:
2118 knots
.insert(0, loop
[0][0])
2119 if loop
[0][-1] not in knots
:
2120 knots
.append(loop
[0][-1])
2122 return(knots
, points
)
2125 # calculate relative positions compared to first knot
2126 def curve_calculate_t(bm_mod
, knots
, points
, pknots
, regular
, circular
):
2133 loc
= pknots
[knots
.index(p
)] # use projected knot location
2135 loc
= mathutils
.Vector(bm_mod
.verts
[p
].co
[:])
2138 len_total
+= (loc
-loc_prev
).length
2139 tpoints
.append(len_total
)
2144 tknots
.append(tpoints
[points
.index(p
)])
2146 tknots
[-1] = tpoints
[-1]
2150 tpoints_average
= tpoints
[-1] / (len(tpoints
) - 1)
2151 for i
in range(1, len(tpoints
) - 1):
2152 tpoints
[i
] = i
* tpoints_average
2153 for i
in range(len(knots
)):
2154 tknots
[i
] = tpoints
[points
.index(knots
[i
])]
2156 tknots
[-1] = tpoints
[-1]
2158 return(tknots
, tpoints
)
2161 # change the location of non-selected points to their place on the spline
2162 def curve_calculate_vertices(bm_mod
, knots
, tknots
, points
, tpoints
, splines
,
2163 interpolation
, restriction
):
2170 m
= tpoints
[points
.index(p
)]
2178 if n
> len(splines
) - 1:
2179 n
= len(splines
) - 1
2183 if interpolation
== 'cubic':
2184 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
2185 x
= ax
+ bx
*(m
-tx
) + cx
*(m
-tx
)**2 + dx
*(m
-tx
)**3
2186 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
2187 y
= ay
+ by
*(m
-ty
) + cy
*(m
-ty
)**2 + dy
*(m
-ty
)**3
2188 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
2189 z
= az
+ bz
*(m
-tz
) + cz
*(m
-tz
)**2 + dz
*(m
-tz
)**3
2190 newloc
= mathutils
.Vector([x
,y
,z
])
2191 else: # interpolation == 'linear'
2192 a
, d
, t
, u
= splines
[n
]
2193 newloc
= ((m
-t
)/u
)*d
+ a
2195 if restriction
!= 'none': # vertex movement is restricted
2197 else: # set the vertex to its new location
2198 move
.append([p
, newloc
])
2200 if restriction
!= 'none': # vertex movement is restricted
2205 move
.append([p
, bm_mod
.verts
[p
].co
])
2207 oldloc
= bm_mod
.verts
[p
].co
2208 normal
= bm_mod
.verts
[p
].normal
2209 dloc
= newloc
- oldloc
2210 if dloc
.length
< 1e-6:
2211 move
.append([p
, newloc
])
2212 elif restriction
== 'extrude': # only extrusions
2213 if dloc
.angle(normal
, 0) < 0.5 * math
.pi
+ 1e-6:
2214 move
.append([p
, newloc
])
2215 else: # restriction == 'indent' only indentations
2216 if dloc
.angle(normal
) > 0.5 * math
.pi
- 1e-6:
2217 move
.append([p
, newloc
])
2222 # trim loops to part between first and last selected vertices (including)
2223 def curve_cut_boundaries(bm_mod
, loops
):
2225 for loop
, circular
in loops
:
2228 cut_loops
.append([loop
, circular
])
2230 selected
= [bm_mod
.verts
[v
].select
for v
in loop
]
2231 first
= selected
.index(True)
2233 last
= -selected
.index(True)
2235 cut_loops
.append([loop
[first
:], circular
])
2237 cut_loops
.append([loop
[first
:last
], circular
])
2242 # calculate input loops
2243 def curve_get_input(object, bm
, boundaries
, scene
):
2244 # get mesh with modifiers applied
2245 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
2247 # vertices that still need a loop to run through it
2248 verts_unsorted
= [v
.index
for v
in bm_mod
.verts
if \
2249 v
.select
and not v
.hide
]
2250 # necessary dictionaries
2251 vert_edges
= dict_vert_edges(bm_mod
)
2252 edge_faces
= dict_edge_faces(bm_mod
)
2255 # find loops through each selected vertex
2256 while len(verts_unsorted
) > 0:
2257 loops
= curve_vertex_loops(bm_mod
, verts_unsorted
[0], vert_edges
,
2259 verts_unsorted
.pop(0)
2261 # check if loop is fully selected
2262 search_perpendicular
= False
2264 for loop
, circular
in loops
:
2266 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2267 if len(selected
) < 2:
2268 # only one selected vertex on loop, don't use
2271 elif len(selected
) == len(loop
):
2272 search_perpendicular
= loop
2274 # entire loop is selected, find perpendicular loops
2275 if search_perpendicular
:
2277 if vert
in verts_unsorted
:
2278 verts_unsorted
.remove(vert
)
2279 perp_loops
= curve_perpendicular_loops(bm_mod
, loop
,
2280 vert_edges
, edge_faces
)
2281 for perp_loop
in perp_loops
:
2282 correct_loops
.append(perp_loop
)
2285 for loop
, circular
in loops
:
2286 correct_loops
.append([loop
, circular
])
2290 correct_loops
= curve_cut_boundaries(bm_mod
, correct_loops
)
2292 return(derived
, bm_mod
, correct_loops
)
2295 # return all loops that are perpendicular to the given one
2296 def curve_perpendicular_loops(bm_mod
, start_loop
, vert_edges
, edge_faces
):
2297 # find perpendicular loops
2299 for start_vert
in start_loop
:
2300 loops
= curve_vertex_loops(bm_mod
, start_vert
, vert_edges
,
2302 for loop
, circular
in loops
:
2303 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2304 if len(selected
) == len(loop
):
2307 perp_loops
.append([loop
, circular
, loop
.index(start_vert
)])
2309 # trim loops to same lengths
2310 shortest
= [[len(loop
[0]), i
] for i
, loop
in enumerate(perp_loops
)\
2313 # all loops are circular, not trimming
2314 return([[loop
[0], loop
[1]] for loop
in perp_loops
])
2316 shortest
= min(shortest
)
2317 shortest_start
= perp_loops
[shortest
[1]][2]
2318 before_start
= shortest_start
2319 after_start
= shortest
[0] - shortest_start
- 1
2320 bigger_before
= before_start
> after_start
2322 for loop
in perp_loops
:
2323 # have the loop face the same direction as the shortest one
2325 if loop
[2] < len(loop
[0]) / 2:
2327 loop
[2] = len(loop
[0]) - loop
[2] - 1
2329 if loop
[2] > len(loop
[0]) / 2:
2331 loop
[2] = len(loop
[0]) - loop
[2] - 1
2332 # circular loops can shift, to prevent wrong trimming
2334 shift
= shortest_start
- loop
[2]
2335 if loop
[2] + shift
> 0 and loop
[2] + shift
< len(loop
[0]):
2336 loop
[0] = loop
[0][-shift
:] + loop
[0][:-shift
]
2339 loop
[2] += len(loop
[0])
2340 elif loop
[2] > len(loop
[0]) -1:
2341 loop
[2] -= len(loop
[0])
2343 start
= max(0, loop
[2] - before_start
)
2344 end
= min(len(loop
[0]), loop
[2] + after_start
+ 1)
2345 trimmed_loops
.append([loop
[0][start
:end
], False])
2347 return(trimmed_loops
)
2350 # project knots on non-selected geometry
2351 def curve_project_knots(bm_mod
, verts_selected
, knots
, points
, circular
):
2352 # function to project vertex on edge
2353 def project(v1
, v2
, v3
):
2354 # v1 and v2 are part of a line
2355 # v3 is projected onto it
2361 if circular
: # project all knots
2365 else: # first and last knot shouldn't be projected
2368 pknots
= [mathutils
.Vector(bm_mod
.verts
[knots
[0]].co
[:])]
2369 for knot
in knots
[start
:end
]:
2370 if knot
in verts_selected
:
2371 knot_left
= knot_right
= False
2372 for i
in range(points
.index(knot
)-1, -1*len(points
), -1):
2373 if points
[i
] not in knots
:
2374 knot_left
= points
[i
]
2376 for i
in range(points
.index(knot
)+1, 2*len(points
)):
2377 if i
> len(points
) - 1:
2379 if points
[i
] not in knots
:
2380 knot_right
= points
[i
]
2382 if knot_left
and knot_right
and knot_left
!= knot_right
:
2383 knot_left
= mathutils
.Vector(\
2384 bm_mod
.verts
[knot_left
].co
[:])
2385 knot_right
= mathutils
.Vector(\
2386 bm_mod
.verts
[knot_right
].co
[:])
2387 knot
= mathutils
.Vector(bm_mod
.verts
[knot
].co
[:])
2388 pknots
.append(project(knot_left
, knot_right
, knot
))
2390 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2391 else: # knot isn't selected, so shouldn't be changed
2392 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2394 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knots
[-1]].co
[:]))
2399 # find all loops through a given vertex
2400 def curve_vertex_loops(bm_mod
, start_vert
, vert_edges
, edge_faces
):
2404 for edge
in vert_edges
[start_vert
]:
2405 if edge
in edges_used
:
2410 active_faces
= edge_faces
[edge
]
2415 new_edges
= vert_edges
[new_vert
]
2416 loop
.append(new_vert
)
2418 edges_used
.append(tuple(sorted([loop
[-1], loop
[-2]])))
2419 if len(new_edges
) < 3 or len(new_edges
) > 4:
2424 for new_edge
in new_edges
:
2425 if new_edge
in edges_used
:
2428 for new_face
in edge_faces
[new_edge
]:
2429 if new_face
in active_faces
:
2434 # found correct new edge
2435 active_faces
= edge_faces
[new_edge
]
2441 if new_vert
== loop
[0]:
2449 loops
.append([loop
, circular
])
2454 ##########################################
2455 ####### Flatten functions ################
2456 ##########################################
2458 # sort input into loops
2459 def flatten_get_input(bm
):
2460 vert_verts
= dict_vert_verts([edgekey(edge
) for edge
in bm
.edges \
2461 if edge
.select
and not edge
.hide
])
2462 verts
= [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
2464 # no connected verts, consider all selected verts as a single input
2466 return([[verts
, False]])
2469 while len(verts
) > 0:
2473 if loop
[-1] in vert_verts
:
2474 to_grow
= vert_verts
[loop
[-1]]
2478 while len(to_grow
) > 0:
2479 new_vert
= to_grow
[0]
2481 if new_vert
in loop
:
2483 loop
.append(new_vert
)
2484 verts
.remove(new_vert
)
2485 to_grow
+= vert_verts
[new_vert
]
2487 loops
.append([loop
, False])
2492 # calculate position of vertex projections on plane
2493 def flatten_project(bm
, loop
, com
, normal
):
2494 verts
= [bm
.verts
[v
] for v
in loop
[0]]
2495 verts_projected
= [[v
.index
, mathutils
.Vector(v
.co
[:]) - \
2496 (mathutils
.Vector(v
.co
[:])-com
).dot(normal
)*normal
] for v
in verts
]
2498 return(verts_projected
)
2501 ##########################################
2502 ####### Gstretch functions ###############
2503 ##########################################
2505 # fake stroke class, used to create custom strokes if no GP data is found
2506 class gstretch_fake_stroke():
2507 def __init__(self
, points
):
2508 self
.points
= [gstretch_fake_stroke_point(p
) for p
in points
]
2511 # fake stroke point class, used in fake strokes
2512 class gstretch_fake_stroke_point():
2513 def __init__(self
, loc
):
2517 # flips loops, if necessary, to obtain maximum alignment to stroke
2518 def gstretch_align_pairs(ls_pairs
, object, bm_mod
, method
):
2519 # returns total distance between all verts in loop and corresponding stroke
2520 def distance_loop_stroke(loop
, stroke
, object, bm_mod
, method
):
2521 stroke_lengths_cache
= False
2522 loop_length
= len(loop
[0])
2525 if method
!= 'regular':
2526 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2528 for i
, v_index
in enumerate(loop
[0]):
2529 if method
== 'regular':
2530 relative_distance
= i
/ (loop_length
- 1)
2532 relative_distance
= relative_lengths
[i
]
2534 loc1
= object.matrix_world
* bm_mod
.verts
[v_index
].co
2535 loc2
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2536 relative_distance
, stroke_lengths_cache
)
2537 total_distance
+= (loc2
- loc1
).length
2539 return(total_distance
)
2542 for (loop
, stroke
) in ls_pairs
:
2543 total_dist
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2546 total_dist_rev
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2548 if total_dist_rev
> total_dist
:
2554 # calculate vertex positions on stroke
2555 def gstretch_calculate_verts(loop
, stroke
, object, bm_mod
, method
):
2557 stroke_lengths_cache
= False
2558 loop_length
= len(loop
[0])
2559 matrix_inverse
= object.matrix_world
.inverted()
2561 # return intersection of line with stroke, or None
2562 def intersect_line_stroke(vec1
, vec2
, stroke
):
2563 for i
, p
in enumerate(stroke
.points
[1:]):
2564 intersections
= mathutils
.geometry
.intersect_line_line(vec1
, vec2
,
2565 p
.co
, stroke
.points
[i
].co
)
2566 if intersections
and \
2567 (intersections
[0] - intersections
[1]).length
< 1e-2:
2568 x
, dist
= mathutils
.geometry
.intersect_point_line(
2569 intersections
[0], p
.co
, stroke
.points
[i
].co
)
2571 return(intersections
[0])
2574 if method
== 'project':
2575 projection_vectors
= []
2576 vert_edges
= dict_vert_edges(bm_mod
)
2578 for v_index
in loop
[0]:
2580 for ek
in vert_edges
[v_index
]:
2582 v1
= bm_mod
.verts
[v1
]
2583 v2
= bm_mod
.verts
[v2
]
2584 if v1
.select
+ v2
.select
== 1 and not v1
.hide
and not v2
.hide
:
2585 vec1
= object.matrix_world
* v1
.co
2586 vec2
= object.matrix_world
* v2
.co
2587 intersection
= intersect_line_stroke(vec1
, vec2
, stroke
)
2590 if not intersection
:
2591 v
= bm_mod
.verts
[v_index
]
2592 intersection
= intersect_line_stroke(v
.co
, v
.co
+ v
.normal
,
2595 move
.append([v_index
, matrix_inverse
* intersection
])
2598 if method
== 'irregular':
2599 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2601 for i
, v_index
in enumerate(loop
[0]):
2602 if method
== 'regular':
2603 relative_distance
= i
/ (loop_length
- 1)
2604 else: # method == 'irregular'
2605 relative_distance
= relative_lengths
[i
]
2606 loc
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2607 relative_distance
, stroke_lengths_cache
)
2608 loc
= matrix_inverse
* loc
2609 move
.append([v_index
, loc
])
2614 # create new vertices, based on GP strokes
2615 def gstretch_create_verts(object, bm_mod
, strokes
, method
, conversion
,
2616 conversion_distance
, conversion_max
, conversion_min
, conversion_vertices
):
2619 mat_world
= object.matrix_world
.inverted()
2620 singles
= gstretch_match_single_verts(bm_mod
, strokes
, mat_world
)
2622 for stroke
in strokes
:
2623 stroke_verts
.append([stroke
, []])
2625 if conversion
== 'vertices':
2626 min_end_point
= conversion_vertices
2627 end_point
= conversion_vertices
2628 elif conversion
== 'limit_vertices':
2629 min_end_point
= conversion_min
2630 end_point
= conversion_max
2632 end_point
= len(stroke
.points
)
2633 # creation of new vertices at fixed user-defined distances
2634 if conversion
== 'distance':
2636 prev_point
= stroke
.points
[0]
2637 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* \
2640 limit
= conversion_distance
2641 for point
in stroke
.points
:
2642 new_distance
= distance
+ (point
.co
- prev_point
.co
).length
2644 while new_distance
> limit
:
2645 to_cover
= limit
- distance
+ (limit
* iteration
)
2646 new_loc
= prev_point
.co
+ to_cover
* \
2647 (point
.co
- prev_point
.co
).normalized()
2648 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* \
2650 new_distance
-= limit
2652 distance
= new_distance
2654 # creation of new vertices for other methods
2656 # add vertices at stroke points
2657 for point
in stroke
.points
[:end_point
]:
2658 stroke_verts
[-1][1].append(bm_mod
.verts
.new(\
2659 mat_world
* point
.co
))
2660 # add more vertices, beyond the points that are available
2661 if min_end_point
> min(len(stroke
.points
), end_point
):
2662 for i
in range(min_end_point
-
2663 (min(len(stroke
.points
), end_point
))):
2664 stroke_verts
[-1][1].append(bm_mod
.verts
.new(\
2665 mat_world
* point
.co
))
2666 # force even spreading of points, so they are placed on stroke
2668 bm_mod
.verts
.index_update()
2669 for stroke
, verts_seq
in stroke_verts
:
2670 if len(verts_seq
) < 2:
2672 # spread vertices evenly over the stroke
2673 if method
== 'regular':
2674 loop
= [[vert
.index
for vert
in verts_seq
], False]
2675 move
+= gstretch_calculate_verts(loop
, stroke
, object, bm_mod
,
2678 for i
, vert
in enumerate(verts_seq
):
2680 bm_mod
.edges
.new((verts_seq
[i
-1], verts_seq
[i
]))
2682 # connect single vertices to the closest stroke
2684 for vert
, m_stroke
, point
in singles
:
2685 if m_stroke
!= stroke
:
2687 bm_mod
.edges
.new((vert
, verts_seq
[point
]))
2689 bmesh
.update_edit_mesh(object.data
)
2694 # erases the grease pencil stroke
2695 def gstretch_erase_stroke(stroke
, context
):
2696 # change 3d coordinate into a stroke-point
2697 def sp(loc
, context
):
2701 'location': (0, 0, 0),
2702 'mouse': (view3d_utils
.location_3d_to_region_2d(\
2703 context
.region
, context
.space_data
.region_3d
, loc
)),
2709 if type(stroke
) != bpy
.types
.GPencilStroke
:
2710 # fake stroke, there is nothing to delete
2713 erase_stroke
= [sp(p
.co
, context
) for p
in stroke
.points
]
2715 erase_stroke
[0]['is_start'] = True
2716 bpy
.ops
.gpencil
.draw(mode
='ERASER', stroke
=erase_stroke
)
2719 # get point on stroke, given by relative distance (0.0 - 1.0)
2720 def gstretch_eval_stroke(stroke
, distance
, stroke_lengths_cache
=False):
2721 # use cache if available
2722 if not stroke_lengths_cache
:
2724 for i
, p
in enumerate(stroke
.points
[1:]):
2725 lengths
.append((p
.co
- stroke
.points
[i
].co
).length
+ \
2727 total_length
= max(lengths
[-1], 1e-7)
2728 stroke_lengths_cache
= [length
/ total_length
for length
in
2730 stroke_lengths
= stroke_lengths_cache
[:]
2732 if distance
in stroke_lengths
:
2733 loc
= stroke
.points
[stroke_lengths
.index(distance
)].co
2734 elif distance
> stroke_lengths
[-1]:
2735 # should be impossible, but better safe than sorry
2736 loc
= stroke
.points
[-1].co
2738 stroke_lengths
.append(distance
)
2739 stroke_lengths
.sort()
2740 stroke_index
= stroke_lengths
.index(distance
)
2741 interval_length
= stroke_lengths
[stroke_index
+1] - \
2742 stroke_lengths
[stroke_index
-1]
2743 distance_relative
= (distance
- stroke_lengths
[stroke_index
-1]) / \
2745 interval_vector
= stroke
.points
[stroke_index
].co
- \
2746 stroke
.points
[stroke_index
-1].co
2747 loc
= stroke
.points
[stroke_index
-1].co
+ \
2748 distance_relative
* interval_vector
2750 return(loc
, stroke_lengths_cache
)
2753 # create fake grease pencil strokes for the active object
2754 def gstretch_get_fake_strokes(object, bm_mod
, loops
):
2757 p1
= object.matrix_world
* bm_mod
.verts
[loop
[0][0]].co
2758 p2
= object.matrix_world
* bm_mod
.verts
[loop
[0][-1]].co
2759 strokes
.append(gstretch_fake_stroke([p1
, p2
]))
2764 # get grease pencil strokes for the active object
2765 def gstretch_get_strokes(object):
2766 gp
= object.grease_pencil
2769 layer
= gp
.layers
.active
2772 frame
= layer
.active_frame
2775 strokes
= frame
.strokes
2776 if len(strokes
) < 1:
2782 # returns a list with loop-stroke pairs
2783 def gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
):
2784 if not loops
or not strokes
:
2787 # calculate loop centers
2790 center
= mathutils
.Vector()
2791 for v_index
in loop
[0]:
2792 center
+= bm_mod
.verts
[v_index
].co
2793 center
/= len(loop
[0])
2794 center
= object.matrix_world
* center
2795 loop_centers
.append([center
, loop
])
2797 # calculate stroke centers
2799 for stroke
in strokes
:
2800 center
= mathutils
.Vector()
2801 for p
in stroke
.points
:
2803 center
/= len(stroke
.points
)
2804 stroke_centers
.append([center
, stroke
, 0])
2806 # match, first by stroke use count, then by distance
2808 for lc
in loop_centers
:
2810 for i
, sc
in enumerate(stroke_centers
):
2811 distances
.append([sc
[2], (lc
[0] - sc
[0]).length
, i
])
2813 best_stroke
= distances
[0][2]
2814 ls_pairs
.append([lc
[1], stroke_centers
[best_stroke
][1]])
2815 stroke_centers
[best_stroke
][2] += 1 # increase stroke use count
2820 # match single selected vertices to the closest stroke endpoint
2821 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2822 def gstretch_match_single_verts(bm_mod
, strokes
, mat_world
):
2823 # calculate stroke endpoints in object space
2825 for stroke
in strokes
:
2826 endpoints
.append((mat_world
* stroke
.points
[0].co
, stroke
, 0))
2827 endpoints
.append((mat_world
* stroke
.points
[-1].co
, stroke
, -1))
2830 # find single vertices (not connected to other selected verts)
2831 for vert
in bm_mod
.verts
:
2835 for edge
in vert
.link_edges
:
2836 if edge
.other_vert(vert
).select
:
2841 # calculate distances from vertex to endpoints
2842 distance
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2843 endpoint_index
) for endpoint_index
, (loc
, stroke
, stroke_point
) in
2844 enumerate(endpoints
)]
2846 distances
.append(distance
[0])
2848 # create matches, based on shortest distance first
2852 singles
.append((distances
[0][1], distances
[0][2], distances
[0][3]))
2853 endpoints
.pop(distances
[0][4])
2856 for (i
, vert
, j
, k
, l
) in distances
:
2857 distance_new
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2858 endpoint_index
) for endpoint_index
, (loc
, stroke
,
2859 stroke_point
) in enumerate(endpoints
)]
2861 distances_new
.append(distance_new
[0])
2862 distances
= distances_new
2867 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2868 def gstretch_relative_lengths(loop
, bm_mod
):
2870 for i
, v_index
in enumerate(loop
[0][1:]):
2871 lengths
.append((bm_mod
.verts
[v_index
].co
- \
2872 bm_mod
.verts
[loop
[0][i
]].co
).length
+ lengths
[-1])
2873 total_length
= max(lengths
[-1], 1e-7)
2874 relative_lengths
= [length
/ total_length
for length
in
2877 return(relative_lengths
)
2880 # convert cache-stored strokes into usable (fake) GP strokes
2881 def gstretch_safe_to_true_strokes(safe_strokes
):
2883 for safe_stroke
in safe_strokes
:
2884 strokes
.append(gstretch_fake_stroke(safe_stroke
))
2889 # convert a GP stroke into a list of points which can be stored in cache
2890 def gstretch_true_to_safe_strokes(strokes
):
2892 for stroke
in strokes
:
2893 safe_strokes
.append([p
.co
.copy() for p
in stroke
.points
])
2895 return(safe_strokes
)
2898 # force consistency in GUI, max value can never be lower than min value
2899 def gstretch_update_max(self
, context
):
2900 # called from operator settings (after execution)
2901 if 'conversion_min' in self
.keys():
2902 if self
.conversion_min
> self
.conversion_max
:
2903 self
.conversion_max
= self
.conversion_min
2904 # called from toolbar
2906 lt
= context
.window_manager
.looptools
2907 if lt
.gstretch_conversion_min
> lt
.gstretch_conversion_max
:
2908 lt
.gstretch_conversion_max
= lt
.gstretch_conversion_min
2911 # force consistency in GUI, min value can never be higher than max value
2912 def gstretch_update_min(self
, context
):
2913 # called from operator settings (after execution)
2914 if 'conversion_max' in self
.keys():
2915 if self
.conversion_max
< self
.conversion_min
:
2916 self
.conversion_min
= self
.conversion_max
2917 # called from toolbar
2919 lt
= context
.window_manager
.looptools
2920 if lt
.gstretch_conversion_max
< lt
.gstretch_conversion_min
:
2921 lt
.gstretch_conversion_min
= lt
.gstretch_conversion_max
2924 ##########################################
2925 ####### Relax functions ##################
2926 ##########################################
2928 # create lists with knots and points, all correctly sorted
2929 def relax_calculate_knots(loops
):
2932 for loop
, circular
in loops
:
2936 if len(loop
)%2 == 1: # odd
2937 extend
= [False, True, 0, 1, 0, 1]
2939 extend
= [True, False, 0, 1, 1, 2]
2941 if len(loop
)%2 == 1: # odd
2942 extend
= [False, False, 0, 1, 1, 2]
2944 extend
= [False, False, 0, 1, 1, 2]
2947 loop
= [loop
[-1]] + loop
+ [loop
[0]]
2948 for i
in range(extend
[2+2*j
], len(loop
), 2):
2949 knots
[j
].append(loop
[i
])
2950 for i
in range(extend
[3+2*j
], len(loop
), 2):
2951 if loop
[i
] == loop
[-1] and not circular
:
2953 if len(points
[j
]) == 0:
2954 points
[j
].append(loop
[i
])
2955 elif loop
[i
] != points
[j
][0]:
2956 points
[j
].append(loop
[i
])
2958 if knots
[j
][0] != knots
[j
][-1]:
2959 knots
[j
].append(knots
[j
][0])
2960 if len(points
[1]) == 0:
2966 all_points
.append(p
)
2968 return(all_knots
, all_points
)
2971 # calculate relative positions compared to first knot
2972 def relax_calculate_t(bm_mod
, knots
, points
, regular
):
2975 for i
in range(len(knots
)):
2976 amount
= len(knots
[i
]) + len(points
[i
])
2978 for j
in range(amount
):
2980 mix
.append([True, knots
[i
][round(j
/2)]])
2982 mix
.append([True, knots
[i
][-1]])
2984 mix
.append([False, points
[i
][int(j
/2)]])
2990 loc
= mathutils
.Vector(bm_mod
.verts
[m
[1]].co
[:])
2993 len_total
+= (loc
- loc_prev
).length
2995 tknots
.append(len_total
)
2997 tpoints
.append(len_total
)
3001 for p
in range(len(points
[i
])):
3002 tpoints
.append((tknots
[p
] + tknots
[p
+1]) / 2)
3003 all_tknots
.append(tknots
)
3004 all_tpoints
.append(tpoints
)
3006 return(all_tknots
, all_tpoints
)
3009 # change the location of the points to their place on the spline
3010 def relax_calculate_verts(bm_mod
, interpolation
, tknots
, knots
, tpoints
,
3014 for i
in range(len(knots
)):
3016 m
= tpoints
[i
][points
[i
].index(p
)]
3018 n
= tknots
[i
].index(m
)
3024 if n
> len(splines
[i
]) - 1:
3025 n
= len(splines
[i
]) - 1
3029 if interpolation
== 'cubic':
3030 ax
, bx
, cx
, dx
, tx
= splines
[i
][n
][0]
3031 x
= ax
+ bx
*(m
-tx
) + cx
*(m
-tx
)**2 + dx
*(m
-tx
)**3
3032 ay
, by
, cy
, dy
, ty
= splines
[i
][n
][1]
3033 y
= ay
+ by
*(m
-ty
) + cy
*(m
-ty
)**2 + dy
*(m
-ty
)**3
3034 az
, bz
, cz
, dz
, tz
= splines
[i
][n
][2]
3035 z
= az
+ bz
*(m
-tz
) + cz
*(m
-tz
)**2 + dz
*(m
-tz
)**3
3036 change
.append([p
, mathutils
.Vector([x
,y
,z
])])
3037 else: # interpolation == 'linear'
3038 a
, d
, t
, u
= splines
[i
][n
]
3041 change
.append([p
, ((m
-t
)/u
)*d
+ a
])
3043 move
.append([c
[0], (bm_mod
.verts
[c
[0]].co
+ c
[1]) / 2])
3048 ##########################################
3049 ####### Space functions ##################
3050 ##########################################
3052 # calculate relative positions compared to first knot
3053 def space_calculate_t(bm_mod
, knots
):
3058 loc
= mathutils
.Vector(bm_mod
.verts
[k
].co
[:])
3061 len_total
+= (loc
- loc_prev
).length
3062 tknots
.append(len_total
)
3065 t_per_segment
= len_total
/ (amount
- 1)
3066 tpoints
= [i
* t_per_segment
for i
in range(amount
)]
3068 return(tknots
, tpoints
)
3071 # change the location of the points to their place on the spline
3072 def space_calculate_verts(bm_mod
, interpolation
, tknots
, tpoints
, points
,
3076 m
= tpoints
[points
.index(p
)]
3084 if n
> len(splines
) - 1:
3085 n
= len(splines
) - 1
3089 if interpolation
== 'cubic':
3090 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
3091 x
= ax
+ bx
*(m
-tx
) + cx
*(m
-tx
)**2 + dx
*(m
-tx
)**3
3092 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
3093 y
= ay
+ by
*(m
-ty
) + cy
*(m
-ty
)**2 + dy
*(m
-ty
)**3
3094 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
3095 z
= az
+ bz
*(m
-tz
) + cz
*(m
-tz
)**2 + dz
*(m
-tz
)**3
3096 move
.append([p
, mathutils
.Vector([x
,y
,z
])])
3097 else: # interpolation == 'linear'
3098 a
, d
, t
, u
= splines
[n
]
3099 move
.append([p
, ((m
-t
)/u
)*d
+ a
])
3104 ##########################################
3105 ####### Operators ########################
3106 ##########################################
3109 class Bridge(bpy
.types
.Operator
):
3110 bl_idname
= 'mesh.looptools_bridge'
3111 bl_label
= "Bridge / Loft"
3112 bl_description
= "Bridge two, or loft several, loops of vertices"
3113 bl_options
= {'REGISTER', 'UNDO'}
3115 cubic_strength
= bpy
.props
.FloatProperty(name
= "Strength",
3116 description
= "Higher strength results in more fluid curves",
3120 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation mode",
3121 items
= (('cubic', "Cubic", "Gives curved results"),
3122 ('linear', "Linear", "Basic, fast, straight interpolation")),
3123 description
= "Interpolation mode: algorithm used when creating "\
3126 loft
= bpy
.props
.BoolProperty(name
= "Loft",
3127 description
= "Loft multiple loops, instead of considering them as "\
3128 "a multi-input for bridging",
3130 loft_loop
= bpy
.props
.BoolProperty(name
= "Loop",
3131 description
= "Connect the first and the last loop with each other",
3133 min_width
= bpy
.props
.IntProperty(name
= "Minimum width",
3134 description
= "Segments with an edge smaller than this are merged "\
3135 "(compared to base edge)",
3139 subtype
= 'PERCENTAGE')
3140 mode
= bpy
.props
.EnumProperty(name
= "Mode",
3141 items
= (('basic', "Basic", "Fast algorithm"), ('shortest',
3142 "Shortest edge", "Slower algorithm with better vertex matching")),
3143 description
= "Algorithm used for bridging",
3144 default
= 'shortest')
3145 remove_faces
= bpy
.props
.BoolProperty(name
= "Remove faces",
3146 description
= "Remove faces that are internal after bridging",
3148 reverse
= bpy
.props
.BoolProperty(name
= "Reverse",
3149 description
= "Manually override the direction in which the loops "\
3150 "are bridged. Only use if the tool gives the wrong " \
3153 segments
= bpy
.props
.IntProperty(name
= "Segments",
3154 description
= "Number of segments used to bridge the gap "\
3159 twist
= bpy
.props
.IntProperty(name
= "Twist",
3160 description
= "Twist what vertices are connected to each other",
3164 def poll(cls
, context
):
3165 ob
= context
.active_object
3166 return (ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3168 def draw(self
, context
):
3169 layout
= self
.layout
3170 #layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3173 col_top
= layout
.column(align
=True)
3174 row
= col_top
.row(align
=True)
3175 col_left
= row
.column(align
=True)
3176 col_right
= row
.column(align
=True)
3177 col_right
.active
= self
.segments
!= 1
3178 col_left
.prop(self
, "segments")
3179 col_right
.prop(self
, "min_width", text
="")
3181 bottom_left
= col_left
.row()
3182 bottom_left
.active
= self
.segments
!= 1
3183 bottom_left
.prop(self
, "interpolation", text
="")
3184 bottom_right
= col_right
.row()
3185 bottom_right
.active
= self
.interpolation
== 'cubic'
3186 bottom_right
.prop(self
, "cubic_strength")
3187 # boolean properties
3188 col_top
.prop(self
, "remove_faces")
3190 col_top
.prop(self
, "loft_loop")
3192 # override properties
3194 row
= layout
.row(align
= True)
3195 row
.prop(self
, "twist")
3196 row
.prop(self
, "reverse")
3198 def invoke(self
, context
, event
):
3199 # load custom settings
3200 context
.window_manager
.looptools
.bridge_loft
= self
.loft
3202 return self
.execute(context
)
3204 def execute(self
, context
):
3206 global_undo
, object, bm
= initialise()
3207 edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
= \
3208 bridge_initialise(bm
, self
.interpolation
)
3209 settings_write(self
)
3211 # check cache to see if we can save time
3212 input_method
= bridge_input_method(self
.loft
, self
.loft_loop
)
3213 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Bridge",
3214 object, bm
, input_method
, False)
3217 loops
= bridge_get_input(bm
)
3219 # reorder loops if there are more than 2
3222 loops
= bridge_sort_loops(bm
, loops
, self
.loft_loop
)
3224 loops
= bridge_match_loops(bm
, loops
)
3226 # saving cache for faster execution next time
3228 cache_write("Bridge", object, bm
, input_method
, False, False,
3229 loops
, False, False)
3232 # calculate new geometry
3235 max_vert_index
= len(bm
.verts
)-1
3236 for i
in range(1, len(loops
)):
3237 if not self
.loft
and i
%2 == 0:
3239 lines
= bridge_calculate_lines(bm
, loops
[i
-1:i
+1],
3240 self
.mode
, self
.twist
, self
.reverse
)
3241 vertex_normals
= bridge_calculate_virtual_vertex_normals(bm
,
3242 lines
, loops
[i
-1:i
+1], edge_faces
, edgekey_to_edge
)
3243 segments
= bridge_calculate_segments(bm
, lines
,
3244 loops
[i
-1:i
+1], self
.segments
)
3245 new_verts
, new_faces
, max_vert_index
= \
3246 bridge_calculate_geometry(bm
, lines
, vertex_normals
,
3247 segments
, self
.interpolation
, self
.cubic_strength
,
3248 self
.min_width
, max_vert_index
)
3250 vertices
+= new_verts
3253 # make sure faces in loops that aren't used, aren't removed
3254 if self
.remove_faces
and old_selected_faces
:
3255 bridge_save_unused_faces(bm
, old_selected_faces
, loops
)
3258 bridge_create_vertices(bm
, vertices
)
3261 new_faces
= bridge_create_faces(object, bm
, faces
, self
.twist
)
3262 old_selected_faces
= [i
for i
, face
in enumerate(bm
.faces
) \
3263 if face
.index
in old_selected_faces
] # updating list
3264 bridge_select_new_faces(new_faces
, smooth
)
3265 # edge-data could have changed, can't use cache next run
3266 if faces
and not vertices
:
3267 cache_delete("Bridge")
3268 # delete internal faces
3269 if self
.remove_faces
and old_selected_faces
:
3270 bridge_remove_internal_faces(bm
, old_selected_faces
)
3271 # make sure normals are facing outside
3272 bmesh
.update_edit_mesh(object.data
, tessface
=False,
3274 bpy
.ops
.mesh
.normals_make_consistent()
3277 terminate(global_undo
)
3283 class Circle(bpy
.types
.Operator
):
3284 bl_idname
= "mesh.looptools_circle"
3286 bl_description
= "Move selected vertices into a circle shape"
3287 bl_options
= {'REGISTER', 'UNDO'}
3289 custom_radius
= bpy
.props
.BoolProperty(name
= "Radius",
3290 description
= "Force a custom radius",
3292 fit
= bpy
.props
.EnumProperty(name
= "Method",
3293 items
= (("best", "Best fit", "Non-linear least squares"),
3294 ("inside", "Fit inside","Only move vertices towards the center")),
3295 description
= "Method used for fitting a circle to the vertices",
3297 flatten
= bpy
.props
.BoolProperty(name
= "Flatten",
3298 description
= "Flatten the circle, instead of projecting it on the " \
3301 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3302 description
= "Force of the tool",
3307 subtype
= 'PERCENTAGE')
3308 lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
3309 description
= "Lock editing of the x-coordinate",
3311 lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
3312 description
= "Lock editing of the y-coordinate",
3314 lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
3315 description
= "Lock editing of the z-coordinate",
3317 radius
= bpy
.props
.FloatProperty(name
= "Radius",
3318 description
= "Custom radius for circle",
3322 regular
= bpy
.props
.BoolProperty(name
= "Regular",
3323 description
= "Distribute vertices at constant distances along the " \
3328 def poll(cls
, context
):
3329 ob
= context
.active_object
3330 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3332 def draw(self
, context
):
3333 layout
= self
.layout
3334 col
= layout
.column()
3336 col
.prop(self
, "fit")
3339 col
.prop(self
, "flatten")
3340 row
= col
.row(align
=True)
3341 row
.prop(self
, "custom_radius")
3342 row_right
= row
.row(align
=True)
3343 row_right
.active
= self
.custom_radius
3344 row_right
.prop(self
, "radius", text
="")
3345 col
.prop(self
, "regular")
3348 col_move
= col
.column(align
=True)
3349 row
= col_move
.row(align
=True)
3351 row
.prop(self
, "lock_x", text
= "X", icon
='LOCKED')
3353 row
.prop(self
, "lock_x", text
= "X", icon
='UNLOCKED')
3355 row
.prop(self
, "lock_y", text
= "Y", icon
='LOCKED')
3357 row
.prop(self
, "lock_y", text
= "Y", icon
='UNLOCKED')
3359 row
.prop(self
, "lock_z", text
= "Z", icon
='LOCKED')
3361 row
.prop(self
, "lock_z", text
= "Z", icon
='UNLOCKED')
3362 col_move
.prop(self
, "influence")
3364 def invoke(self
, context
, event
):
3365 # load custom settings
3367 return self
.execute(context
)
3369 def execute(self
, context
):
3371 global_undo
, object, bm
= initialise()
3372 settings_write(self
)
3373 # check cache to see if we can save time
3374 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Circle",
3375 object, bm
, False, False)
3377 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3380 derived
, bm_mod
, single_vertices
, single_loops
, loops
= \
3381 circle_get_input(object, bm
, context
.scene
)
3382 mapping
= get_mapping(derived
, bm
, bm_mod
, single_vertices
,
3384 single_loops
, loops
= circle_check_loops(single_loops
, loops
,
3387 # saving cache for faster execution next time
3389 cache_write("Circle", object, bm
, False, False, single_loops
,
3390 loops
, derived
, mapping
)
3393 for i
, loop
in enumerate(loops
):
3394 # best fitting flat plane
3395 com
, normal
= calculate_plane(bm_mod
, loop
)
3396 # if circular, shift loop so we get a good starting vertex
3398 loop
= circle_shift_loop(bm_mod
, loop
, com
)
3399 # flatten vertices on plane
3400 locs_2d
, p
, q
= circle_3d_to_2d(bm_mod
, loop
, com
, normal
)
3402 if self
.fit
== 'best':
3403 x0
, y0
, r
= circle_calculate_best_fit(locs_2d
)
3404 else: # self.fit == 'inside'
3405 x0
, y0
, r
= circle_calculate_min_fit(locs_2d
)
3407 if self
.custom_radius
:
3408 r
= self
.radius
/ p
.length
3409 # calculate positions on circle
3411 new_locs_2d
= circle_project_regular(locs_2d
[:], x0
, y0
, r
)
3413 new_locs_2d
= circle_project_non_regular(locs_2d
[:], x0
, y0
, r
)
3414 # take influence into account
3415 locs_2d
= circle_influence_locs(locs_2d
, new_locs_2d
,
3417 # calculate 3d positions of the created 2d input
3418 move
.append(circle_calculate_verts(self
.flatten
, bm_mod
,
3419 locs_2d
, com
, p
, q
, normal
))
3420 # flatten single input vertices on plane defined by loop
3421 if self
.flatten
and single_loops
:
3422 move
.append(circle_flatten_singles(bm_mod
, com
, p
, q
,
3423 normal
, single_loops
[i
]))
3425 # move vertices to new locations
3426 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3427 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3430 move_verts(object, bm
, mapping
, move
, lock
, -1)
3435 terminate(global_undo
)
3441 class Curve(bpy
.types
.Operator
):
3442 bl_idname
= "mesh.looptools_curve"
3444 bl_description
= "Turn a loop into a smooth curve"
3445 bl_options
= {'REGISTER', 'UNDO'}
3447 boundaries
= bpy
.props
.BoolProperty(name
= "Boundaries",
3448 description
= "Limit the tool to work within the boundaries of the "\
3449 "selected vertices",
3451 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3452 description
= "Force of the tool",
3457 subtype
= 'PERCENTAGE')
3458 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
3459 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3460 ("linear", "Linear", "Simple and fast linear algorithm")),
3461 description
= "Algorithm used for interpolation",
3463 lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
3464 description
= "Lock editing of the x-coordinate",
3466 lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
3467 description
= "Lock editing of the y-coordinate",
3469 lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
3470 description
= "Lock editing of the z-coordinate",
3472 regular
= bpy
.props
.BoolProperty(name
= "Regular",
3473 description
= "Distribute vertices at constant distances along the" \
3476 restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
3477 items
= (("none", "None", "No restrictions on vertex movement"),
3478 ("extrude", "Extrude only","Only allow extrusions (no "\
3480 ("indent", "Indent only", "Only allow indentation (no "\
3482 description
= "Restrictions on how the vertices can be moved",
3486 def poll(cls
, context
):
3487 ob
= context
.active_object
3488 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3490 def draw(self
, context
):
3491 layout
= self
.layout
3492 col
= layout
.column()
3494 col
.prop(self
, "interpolation")
3495 col
.prop(self
, "restriction")
3496 col
.prop(self
, "boundaries")
3497 col
.prop(self
, "regular")
3500 col_move
= col
.column(align
=True)
3501 row
= col_move
.row(align
=True)
3503 row
.prop(self
, "lock_x", text
= "X", icon
='LOCKED')
3505 row
.prop(self
, "lock_x", text
= "X", icon
='UNLOCKED')
3507 row
.prop(self
, "lock_y", text
= "Y", icon
='LOCKED')
3509 row
.prop(self
, "lock_y", text
= "Y", icon
='UNLOCKED')
3511 row
.prop(self
, "lock_z", text
= "Z", icon
='LOCKED')
3513 row
.prop(self
, "lock_z", text
= "Z", icon
='UNLOCKED')
3514 col_move
.prop(self
, "influence")
3516 def invoke(self
, context
, event
):
3517 # load custom settings
3519 return self
.execute(context
)
3521 def execute(self
, context
):
3523 global_undo
, object, bm
= initialise()
3524 settings_write(self
)
3525 # check cache to see if we can save time
3526 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Curve",
3527 object, bm
, False, self
.boundaries
)
3529 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3532 derived
, bm_mod
, loops
= curve_get_input(object, bm
,
3533 self
.boundaries
, context
.scene
)
3534 mapping
= get_mapping(derived
, bm
, bm_mod
, False, True, loops
)
3535 loops
= check_loops(loops
, mapping
, bm_mod
)
3536 verts_selected
= [v
.index
for v
in bm_mod
.verts
if v
.select \
3539 # saving cache for faster execution next time
3541 cache_write("Curve", object, bm
, False, self
.boundaries
, False,
3542 loops
, derived
, mapping
)
3546 knots
, points
= curve_calculate_knots(loop
, verts_selected
)
3547 pknots
= curve_project_knots(bm_mod
, verts_selected
, knots
,
3549 tknots
, tpoints
= curve_calculate_t(bm_mod
, knots
, points
,
3550 pknots
, self
.regular
, loop
[1])
3551 splines
= calculate_splines(self
.interpolation
, bm_mod
,
3553 move
.append(curve_calculate_vertices(bm_mod
, knots
, tknots
,
3554 points
, tpoints
, splines
, self
.interpolation
,
3557 # move vertices to new locations
3558 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3559 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3562 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
3567 terminate(global_undo
)
3573 class Flatten(bpy
.types
.Operator
):
3574 bl_idname
= "mesh.looptools_flatten"
3575 bl_label
= "Flatten"
3576 bl_description
= "Flatten vertices on a best-fitting plane"
3577 bl_options
= {'REGISTER', 'UNDO'}
3579 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3580 description
= "Force of the tool",
3585 subtype
= 'PERCENTAGE')
3586 lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
3587 description
= "Lock editing of the x-coordinate",
3589 lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
3590 description
= "Lock editing of the y-coordinate",
3592 lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
3593 description
= "Lock editing of the z-coordinate",
3595 plane
= bpy
.props
.EnumProperty(name
= "Plane",
3596 items
= (("best_fit", "Best fit", "Calculate a best fitting plane"),
3597 ("normal", "Normal", "Derive plane from averaging vertex "\
3599 ("view", "View", "Flatten on a plane perpendicular to the "\
3601 description
= "Plane on which vertices are flattened",
3602 default
= 'best_fit')
3603 restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
3604 items
= (("none", "None", "No restrictions on vertex movement"),
3605 ("bounding_box", "Bounding box", "Vertices are restricted to "\
3606 "movement inside the bounding box of the selection")),
3607 description
= "Restrictions on how the vertices can be moved",
3611 def poll(cls
, context
):
3612 ob
= context
.active_object
3613 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3615 def draw(self
, context
):
3616 layout
= self
.layout
3617 col
= layout
.column()
3619 col
.prop(self
, "plane")
3620 #col.prop(self, "restriction")
3623 col_move
= col
.column(align
=True)
3624 row
= col_move
.row(align
=True)
3626 row
.prop(self
, "lock_x", text
= "X", icon
='LOCKED')
3628 row
.prop(self
, "lock_x", text
= "X", icon
='UNLOCKED')
3630 row
.prop(self
, "lock_y", text
= "Y", icon
='LOCKED')
3632 row
.prop(self
, "lock_y", text
= "Y", icon
='UNLOCKED')
3634 row
.prop(self
, "lock_z", text
= "Z", icon
='LOCKED')
3636 row
.prop(self
, "lock_z", text
= "Z", icon
='UNLOCKED')
3637 col_move
.prop(self
, "influence")
3639 def invoke(self
, context
, event
):
3640 # load custom settings
3642 return self
.execute(context
)
3644 def execute(self
, context
):
3646 global_undo
, object, bm
= initialise()
3647 settings_write(self
)
3648 # check cache to see if we can save time
3649 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Flatten",
3650 object, bm
, False, False)
3652 # order input into virtual loops
3653 loops
= flatten_get_input(bm
)
3654 loops
= check_loops(loops
, mapping
, bm
)
3656 # saving cache for faster execution next time
3658 cache_write("Flatten", object, bm
, False, False, False, loops
,
3663 # calculate plane and position of vertices on them
3664 com
, normal
= calculate_plane(bm
, loop
, method
=self
.plane
,
3666 to_move
= flatten_project(bm
, loop
, com
, normal
)
3667 if self
.restriction
== 'none':
3668 move
.append(to_move
)
3670 move
.append(to_move
)
3672 # move vertices to new locations
3673 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3674 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3677 move_verts(object, bm
, False, move
, lock
, self
.influence
)
3680 terminate(global_undo
)
3686 class GStretch(bpy
.types
.Operator
):
3687 bl_idname
= "mesh.looptools_gstretch"
3688 bl_label
= "Gstretch"
3689 bl_description
= "Stretch selected vertices to Grease Pencil stroke"
3690 bl_options
= {'REGISTER', 'UNDO'}
3692 conversion
= bpy
.props
.EnumProperty(name
= "Conversion",
3693 items
= (("distance", "Distance", "Set the distance between vertices "\
3694 "of the converted grease pencil stroke"),
3695 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "\
3696 "number of vertices that converted GP strokes will have"),
3697 ("vertices", "Exact vertices", "Set the exact number of vertices "\
3698 "that converted grease pencil strokes will have. Short strokes "\
3699 "with few points may contain less vertices than this number."),
3700 ("none", "No simplification", "Convert each grease pencil point "\
3702 description
= "If grease pencil strokes are converted to geometry, "\
3703 "use this simplification method",
3704 default
= 'limit_vertices')
3705 conversion_distance
= bpy
.props
.FloatProperty(name
= "Distance",
3706 description
= "Absolute distance between vertices along the converted "\
3707 "grease pencil stroke",
3712 conversion_max
= bpy
.props
.IntProperty(name
= "Max Vertices",
3713 description
= "Maximum number of vertices grease pencil strokes will "\
3714 "have, when they are converted to geomtery",
3718 update
= gstretch_update_min
)
3719 conversion_min
= bpy
.props
.IntProperty(name
= "Min Vertices",
3720 description
= "Minimum number of vertices grease pencil strokes will "\
3721 "have, when they are converted to geomtery",
3725 update
= gstretch_update_max
)
3726 conversion_vertices
= bpy
.props
.IntProperty(name
= "Vertices",
3727 description
= "Number of vertices grease pencil strokes will "\
3728 "have, when they are converted to geometry. If strokes have less "\
3729 "points than required, the 'Spread evenly' method is used",
3733 delete_strokes
= bpy
.props
.BoolProperty(name
="Delete strokes",
3734 description
= "Remove Grease Pencil strokes if they have been used "\
3735 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
3737 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3738 description
= "Force of the tool",
3743 subtype
= 'PERCENTAGE')
3744 lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
3745 description
= "Lock editing of the x-coordinate",
3747 lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
3748 description
= "Lock editing of the y-coordinate",
3750 lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
3751 description
= "Lock editing of the z-coordinate",
3753 method
= bpy
.props
.EnumProperty(name
= "Method",
3754 items
= (("project", "Project", "Project vertices onto the stroke, "\
3755 "using vertex normals and connected edges"),
3756 ("irregular", "Spread", "Distribute vertices along the full "\
3757 "stroke, retaining relative distances between the vertices"),
3758 ("regular", "Spread evenly", "Distribute vertices at regular "\
3759 "distances along the full stroke")),
3760 description
= "Method of distributing the vertices over the Grease "\
3762 default
= 'regular')
3765 def poll(cls
, context
):
3766 ob
= context
.active_object
3767 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3769 def draw(self
, context
):
3770 layout
= self
.layout
3771 col
= layout
.column()
3773 col
.prop(self
, "method")
3774 col
.prop(self
, "delete_strokes")
3777 col_conv
= col
.column(align
=True)
3778 col_conv
.prop(self
, "conversion", text
="")
3779 if self
.conversion
== 'distance':
3780 col_conv
.prop(self
, "conversion_distance")
3781 elif self
.conversion
== 'limit_vertices':
3782 row
= col_conv
.row(align
=True)
3783 row
.prop(self
, "conversion_min", text
="Min")
3784 row
.prop(self
, "conversion_max", text
="Max")
3785 elif self
.conversion
== 'vertices':
3786 col_conv
.prop(self
, "conversion_vertices")
3789 col_move
= col
.column(align
=True)
3790 row
= col_move
.row(align
=True)
3792 row
.prop(self
, "lock_x", text
= "X", icon
='LOCKED')
3794 row
.prop(self
, "lock_x", text
= "X", icon
='UNLOCKED')
3796 row
.prop(self
, "lock_y", text
= "Y", icon
='LOCKED')
3798 row
.prop(self
, "lock_y", text
= "Y", icon
='UNLOCKED')
3800 row
.prop(self
, "lock_z", text
= "Z", icon
='LOCKED')
3802 row
.prop(self
, "lock_z", text
= "Z", icon
='UNLOCKED')
3803 col_move
.prop(self
, "influence")
3805 def invoke(self
, context
, event
):
3806 # flush cached strokes
3807 if 'Gstretch' in looptools_cache
:
3808 looptools_cache
['Gstretch']['single_loops'] = []
3809 # load custom settings
3811 return self
.execute(context
)
3813 def execute(self
, context
):
3815 global_undo
, object, bm
= initialise()
3816 settings_write(self
)
3818 # check cache to see if we can save time
3819 cached
, safe_strokes
, loops
, derived
, mapping
= cache_read("Gstretch",
3820 object, bm
, False, False)
3822 straightening
= False
3824 strokes
= gstretch_safe_to_true_strokes(safe_strokes
)
3825 # cached strokes were flushed (see operator's invoke function)
3826 elif object.grease_pencil
:
3827 strokes
= gstretch_get_strokes(object)
3829 # straightening function (no GP) -> loops ignore modifiers
3830 straightening
= True
3833 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
3834 if not straightening
:
3835 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3837 # get loops and strokes
3838 if object.grease_pencil
:
3840 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
3841 context
.scene
, input='selected')
3842 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
3843 loops
= check_loops(loops
, mapping
, bm_mod
)
3845 strokes
= gstretch_get_strokes(object)
3847 # straightening function (no GP) -> loops ignore modifiers
3851 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if \
3852 edge
.select
and not edge
.hide
]
3853 loops
= get_connected_selections(edge_keys
)
3854 loops
= check_loops(loops
, mapping
, bm_mod
)
3855 # create fake strokes
3856 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
3858 # saving cache for faster execution next time
3861 safe_strokes
= gstretch_true_to_safe_strokes(strokes
)
3864 cache_write("Gstretch", object, bm
, False, False,
3865 safe_strokes
, loops
, derived
, mapping
)
3867 # pair loops and strokes
3868 ls_pairs
= gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
)
3869 ls_pairs
= gstretch_align_pairs(ls_pairs
, object, bm_mod
, self
.method
)
3873 # no selected geometry, convert GP to verts
3875 move
.append(gstretch_create_verts(object, bm
, strokes
,
3876 self
.method
, self
.conversion
, self
.conversion_distance
,
3877 self
.conversion_max
, self
.conversion_min
,
3878 self
.conversion_vertices
))
3879 for stroke
in strokes
:
3880 gstretch_erase_stroke(stroke
, context
)
3882 for (loop
, stroke
) in ls_pairs
:
3883 move
.append(gstretch_calculate_verts(loop
, stroke
, object,
3884 bm_mod
, self
.method
))
3885 if self
.delete_strokes
:
3886 if type(stroke
) != bpy
.types
.GPencilStroke
:
3887 # in case of cached fake stroke, get the real one
3888 if object.grease_pencil
:
3889 strokes
= gstretch_get_strokes(object)
3890 if loops
and strokes
:
3891 ls_pairs
= gstretch_match_loops_strokes(loops
,
3892 strokes
, object, bm_mod
)
3893 ls_pairs
= gstretch_align_pairs(ls_pairs
,
3894 object, bm_mod
, self
.method
)
3895 for (l
, s
) in ls_pairs
:
3899 gstretch_erase_stroke(stroke
, context
)
3901 # move vertices to new locations
3902 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3903 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3906 bmesh
.update_edit_mesh(object.data
, tessface
=True, destructive
=True)
3907 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
3912 terminate(global_undo
)
3918 class Relax(bpy
.types
.Operator
):
3919 bl_idname
= "mesh.looptools_relax"
3921 bl_description
= "Relax the loop, so it is smoother"
3922 bl_options
= {'REGISTER', 'UNDO'}
3924 input = bpy
.props
.EnumProperty(name
= "Input",
3925 items
= (("all", "Parallel (all)", "Also use non-selected "\
3926 "parallel loops as input"),
3927 ("selected", "Selection","Only use selected vertices as input")),
3928 description
= "Loops that are relaxed",
3929 default
= 'selected')
3930 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
3931 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3932 ("linear", "Linear", "Simple and fast linear algorithm")),
3933 description
= "Algorithm used for interpolation",
3935 iterations
= bpy
.props
.EnumProperty(name
= "Iterations",
3936 items
= (("1", "1", "One"),
3937 ("3", "3", "Three"),
3939 ("10", "10", "Ten"),
3940 ("25", "25", "Twenty-five")),
3941 description
= "Number of times the loop is relaxed",
3943 regular
= bpy
.props
.BoolProperty(name
= "Regular",
3944 description
= "Distribute vertices at constant distances along the" \
3949 def poll(cls
, context
):
3950 ob
= context
.active_object
3951 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3953 def draw(self
, context
):
3954 layout
= self
.layout
3955 col
= layout
.column()
3957 col
.prop(self
, "interpolation")
3958 col
.prop(self
, "input")
3959 col
.prop(self
, "iterations")
3960 col
.prop(self
, "regular")
3962 def invoke(self
, context
, event
):
3963 # load custom settings
3965 return self
.execute(context
)
3967 def execute(self
, context
):
3969 global_undo
, object, bm
= initialise()
3970 settings_write(self
)
3971 # check cache to see if we can save time
3972 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Relax",
3973 object, bm
, self
.input, False)
3975 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3978 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
3979 context
.scene
, self
.input)
3980 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
3981 loops
= check_loops(loops
, mapping
, bm_mod
)
3982 knots
, points
= relax_calculate_knots(loops
)
3984 # saving cache for faster execution next time
3986 cache_write("Relax", object, bm
, self
.input, False, False, loops
,
3989 for iteration
in range(int(self
.iterations
)):
3990 # calculate splines and new positions
3991 tknots
, tpoints
= relax_calculate_t(bm_mod
, knots
, points
,
3994 for i
in range(len(knots
)):
3995 splines
.append(calculate_splines(self
.interpolation
, bm_mod
,
3996 tknots
[i
], knots
[i
]))
3997 move
= [relax_calculate_verts(bm_mod
, self
.interpolation
,
3998 tknots
, knots
, tpoints
, points
, splines
)]
3999 move_verts(object, bm
, mapping
, move
, False, -1)
4004 terminate(global_undo
)
4010 class Space(bpy
.types
.Operator
):
4011 bl_idname
= "mesh.looptools_space"
4013 bl_description
= "Space the vertices in a regular distrubtion on the loop"
4014 bl_options
= {'REGISTER', 'UNDO'}
4016 influence
= bpy
.props
.FloatProperty(name
= "Influence",
4017 description
= "Force of the tool",
4022 subtype
= 'PERCENTAGE')
4023 input = bpy
.props
.EnumProperty(name
= "Input",
4024 items
= (("all", "Parallel (all)", "Also use non-selected "\
4025 "parallel loops as input"),
4026 ("selected", "Selection","Only use selected vertices as input")),
4027 description
= "Loops that are spaced",
4028 default
= 'selected')
4029 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4030 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4031 ("linear", "Linear", "Vertices are projected on existing edges")),
4032 description
= "Algorithm used for interpolation",
4034 lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4035 description
= "Lock editing of the x-coordinate",
4037 lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4038 description
= "Lock editing of the y-coordinate",
4040 lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4041 description
= "Lock editing of the z-coordinate",
4045 def poll(cls
, context
):
4046 ob
= context
.active_object
4047 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4049 def draw(self
, context
):
4050 layout
= self
.layout
4051 col
= layout
.column()
4053 col
.prop(self
, "interpolation")
4054 col
.prop(self
, "input")
4057 col_move
= col
.column(align
=True)
4058 row
= col_move
.row(align
=True)
4060 row
.prop(self
, "lock_x", text
= "X", icon
='LOCKED')
4062 row
.prop(self
, "lock_x", text
= "X", icon
='UNLOCKED')
4064 row
.prop(self
, "lock_y", text
= "Y", icon
='LOCKED')
4066 row
.prop(self
, "lock_y", text
= "Y", icon
='UNLOCKED')
4068 row
.prop(self
, "lock_z", text
= "Z", icon
='LOCKED')
4070 row
.prop(self
, "lock_z", text
= "Z", icon
='UNLOCKED')
4071 col_move
.prop(self
, "influence")
4073 def invoke(self
, context
, event
):
4074 # load custom settings
4076 return self
.execute(context
)
4078 def execute(self
, context
):
4080 global_undo
, object, bm
= initialise()
4081 settings_write(self
)
4082 # check cache to see if we can save time
4083 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Space",
4084 object, bm
, self
.input, False)
4086 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
4089 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
4090 context
.scene
, self
.input)
4091 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4092 loops
= check_loops(loops
, mapping
, bm_mod
)
4094 # saving cache for faster execution next time
4096 cache_write("Space", object, bm
, self
.input, False, False, loops
,
4101 # calculate splines and new positions
4102 if loop
[1]: # circular
4103 loop
[0].append(loop
[0][0])
4104 tknots
, tpoints
= space_calculate_t(bm_mod
, loop
[0][:])
4105 splines
= calculate_splines(self
.interpolation
, bm_mod
,
4107 move
.append(space_calculate_verts(bm_mod
, self
.interpolation
,
4108 tknots
, tpoints
, loop
[0][:-1], splines
))
4109 # move vertices to new locations
4110 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4111 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4114 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4119 terminate(global_undo
)
4124 ##########################################
4125 ####### GUI and registration #############
4126 ##########################################
4128 # menu containing all tools
4129 class VIEW3D_MT_edit_mesh_looptools(bpy
.types
.Menu
):
4130 bl_label
= "LoopTools"
4132 def draw(self
, context
):
4133 layout
= self
.layout
4135 layout
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4136 layout
.operator("mesh.looptools_circle")
4137 layout
.operator("mesh.looptools_curve")
4138 layout
.operator("mesh.looptools_flatten")
4139 layout
.operator("mesh.looptools_gstretch")
4140 layout
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4141 layout
.operator("mesh.looptools_relax")
4142 layout
.operator("mesh.looptools_space")
4145 # panel containing all tools
4146 class VIEW3D_PT_tools_looptools(bpy
.types
.Panel
):
4147 bl_space_type
= 'VIEW_3D'
4148 bl_region_type
= 'TOOLS'
4149 bl_category
= 'Tools'
4150 bl_context
= "mesh_edit"
4151 bl_label
= "LoopTools"
4152 bl_options
= {'DEFAULT_CLOSED'}
4154 def draw(self
, context
):
4155 layout
= self
.layout
4156 col
= layout
.column(align
=True)
4157 lt
= context
.window_manager
.looptools
4159 # bridge - first line
4160 split
= col
.split(percentage
=0.15, align
=True)
4161 if lt
.display_bridge
:
4162 split
.prop(lt
, "display_bridge", text
="", icon
='DOWNARROW_HLT')
4164 split
.prop(lt
, "display_bridge", text
="", icon
='RIGHTARROW')
4165 split
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4167 if lt
.display_bridge
:
4168 box
= col
.column(align
=True).box().column()
4169 #box.prop(self, "mode")
4172 col_top
= box
.column(align
=True)
4173 row
= col_top
.row(align
=True)
4174 col_left
= row
.column(align
=True)
4175 col_right
= row
.column(align
=True)
4176 col_right
.active
= lt
.bridge_segments
!= 1
4177 col_left
.prop(lt
, "bridge_segments")
4178 col_right
.prop(lt
, "bridge_min_width", text
="")
4180 bottom_left
= col_left
.row()
4181 bottom_left
.active
= lt
.bridge_segments
!= 1
4182 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4183 bottom_right
= col_right
.row()
4184 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4185 bottom_right
.prop(lt
, "bridge_cubic_strength")
4186 # boolean properties
4187 col_top
.prop(lt
, "bridge_remove_faces")
4189 # override properties
4191 row
= box
.row(align
= True)
4192 row
.prop(lt
, "bridge_twist")
4193 row
.prop(lt
, "bridge_reverse")
4195 # circle - first line
4196 split
= col
.split(percentage
=0.15, align
=True)
4197 if lt
.display_circle
:
4198 split
.prop(lt
, "display_circle", text
="", icon
='DOWNARROW_HLT')
4200 split
.prop(lt
, "display_circle", text
="", icon
='RIGHTARROW')
4201 split
.operator("mesh.looptools_circle")
4203 if lt
.display_circle
:
4204 box
= col
.column(align
=True).box().column()
4205 box
.prop(lt
, "circle_fit")
4208 box
.prop(lt
, "circle_flatten")
4209 row
= box
.row(align
=True)
4210 row
.prop(lt
, "circle_custom_radius")
4211 row_right
= row
.row(align
=True)
4212 row_right
.active
= lt
.circle_custom_radius
4213 row_right
.prop(lt
, "circle_radius", text
="")
4214 box
.prop(lt
, "circle_regular")
4217 col_move
= box
.column(align
=True)
4218 row
= col_move
.row(align
=True)
4219 if lt
.circle_lock_x
:
4220 row
.prop(lt
, "circle_lock_x", text
= "X", icon
='LOCKED')
4222 row
.prop(lt
, "circle_lock_x", text
= "X", icon
='UNLOCKED')
4223 if lt
.circle_lock_y
:
4224 row
.prop(lt
, "circle_lock_y", text
= "Y", icon
='LOCKED')
4226 row
.prop(lt
, "circle_lock_y", text
= "Y", icon
='UNLOCKED')
4227 if lt
.circle_lock_z
:
4228 row
.prop(lt
, "circle_lock_z", text
= "Z", icon
='LOCKED')
4230 row
.prop(lt
, "circle_lock_z", text
= "Z", icon
='UNLOCKED')
4231 col_move
.prop(lt
, "circle_influence")
4233 # curve - first line
4234 split
= col
.split(percentage
=0.15, align
=True)
4235 if lt
.display_curve
:
4236 split
.prop(lt
, "display_curve", text
="", icon
='DOWNARROW_HLT')
4238 split
.prop(lt
, "display_curve", text
="", icon
='RIGHTARROW')
4239 split
.operator("mesh.looptools_curve")
4241 if lt
.display_curve
:
4242 box
= col
.column(align
=True).box().column()
4243 box
.prop(lt
, "curve_interpolation")
4244 box
.prop(lt
, "curve_restriction")
4245 box
.prop(lt
, "curve_boundaries")
4246 box
.prop(lt
, "curve_regular")
4249 col_move
= box
.column(align
=True)
4250 row
= col_move
.row(align
=True)
4252 row
.prop(lt
, "curve_lock_x", text
= "X", icon
='LOCKED')
4254 row
.prop(lt
, "curve_lock_x", text
= "X", icon
='UNLOCKED')
4256 row
.prop(lt
, "curve_lock_y", text
= "Y", icon
='LOCKED')
4258 row
.prop(lt
, "curve_lock_y", text
= "Y", icon
='UNLOCKED')
4260 row
.prop(lt
, "curve_lock_z", text
= "Z", icon
='LOCKED')
4262 row
.prop(lt
, "curve_lock_z", text
= "Z", icon
='UNLOCKED')
4263 col_move
.prop(lt
, "curve_influence")
4265 # flatten - first line
4266 split
= col
.split(percentage
=0.15, align
=True)
4267 if lt
.display_flatten
:
4268 split
.prop(lt
, "display_flatten", text
="", icon
='DOWNARROW_HLT')
4270 split
.prop(lt
, "display_flatten", text
="", icon
='RIGHTARROW')
4271 split
.operator("mesh.looptools_flatten")
4272 # flatten - settings
4273 if lt
.display_flatten
:
4274 box
= col
.column(align
=True).box().column()
4275 box
.prop(lt
, "flatten_plane")
4276 #box.prop(lt, "flatten_restriction")
4279 col_move
= box
.column(align
=True)
4280 row
= col_move
.row(align
=True)
4281 if lt
.flatten_lock_x
:
4282 row
.prop(lt
, "flatten_lock_x", text
= "X", icon
='LOCKED')
4284 row
.prop(lt
, "flatten_lock_x", text
= "X", icon
='UNLOCKED')
4285 if lt
.flatten_lock_y
:
4286 row
.prop(lt
, "flatten_lock_y", text
= "Y", icon
='LOCKED')
4288 row
.prop(lt
, "flatten_lock_y", text
= "Y", icon
='UNLOCKED')
4289 if lt
.flatten_lock_z
:
4290 row
.prop(lt
, "flatten_lock_z", text
= "Z", icon
='LOCKED')
4292 row
.prop(lt
, "flatten_lock_z", text
= "Z", icon
='UNLOCKED')
4293 col_move
.prop(lt
, "flatten_influence")
4295 # gstretch - first line
4296 split
= col
.split(percentage
=0.15, align
=True)
4297 if lt
.display_gstretch
:
4298 split
.prop(lt
, "display_gstretch", text
="", icon
='DOWNARROW_HLT')
4300 split
.prop(lt
, "display_gstretch", text
="", icon
='RIGHTARROW')
4301 split
.operator("mesh.looptools_gstretch")
4303 if lt
.display_gstretch
:
4304 box
= col
.column(align
=True).box().column()
4305 box
.prop(lt
, "gstretch_method")
4306 box
.prop(lt
, "gstretch_delete_strokes")
4309 col_conv
= box
.column(align
=True)
4310 col_conv
.prop(lt
, "gstretch_conversion", text
="")
4311 if lt
.gstretch_conversion
== 'distance':
4312 col_conv
.prop(lt
, "gstretch_conversion_distance")
4313 elif lt
.gstretch_conversion
== 'limit_vertices':
4314 row
= col_conv
.row(align
=True)
4315 row
.prop(lt
, "gstretch_conversion_min", text
="Min")
4316 row
.prop(lt
, "gstretch_conversion_max", text
="Max")
4317 elif lt
.gstretch_conversion
== 'vertices':
4318 col_conv
.prop(lt
, "gstretch_conversion_vertices")
4321 col_move
= box
.column(align
=True)
4322 row
= col_move
.row(align
=True)
4323 if lt
.gstretch_lock_x
:
4324 row
.prop(lt
, "gstretch_lock_x", text
= "X", icon
='LOCKED')
4326 row
.prop(lt
, "gstretch_lock_x", text
= "X", icon
='UNLOCKED')
4327 if lt
.gstretch_lock_y
:
4328 row
.prop(lt
, "gstretch_lock_y", text
= "Y", icon
='LOCKED')
4330 row
.prop(lt
, "gstretch_lock_y", text
= "Y", icon
='UNLOCKED')
4331 if lt
.gstretch_lock_z
:
4332 row
.prop(lt
, "gstretch_lock_z", text
= "Z", icon
='LOCKED')
4334 row
.prop(lt
, "gstretch_lock_z", text
= "Z", icon
='UNLOCKED')
4335 col_move
.prop(lt
, "gstretch_influence")
4338 split
= col
.split(percentage
=0.15, align
=True)
4340 split
.prop(lt
, "display_loft", text
="", icon
='DOWNARROW_HLT')
4342 split
.prop(lt
, "display_loft", text
="", icon
='RIGHTARROW')
4343 split
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4346 box
= col
.column(align
=True).box().column()
4347 #box.prop(self, "mode")
4350 col_top
= box
.column(align
=True)
4351 row
= col_top
.row(align
=True)
4352 col_left
= row
.column(align
=True)
4353 col_right
= row
.column(align
=True)
4354 col_right
.active
= lt
.bridge_segments
!= 1
4355 col_left
.prop(lt
, "bridge_segments")
4356 col_right
.prop(lt
, "bridge_min_width", text
="")
4358 bottom_left
= col_left
.row()
4359 bottom_left
.active
= lt
.bridge_segments
!= 1
4360 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4361 bottom_right
= col_right
.row()
4362 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4363 bottom_right
.prop(lt
, "bridge_cubic_strength")
4364 # boolean properties
4365 col_top
.prop(lt
, "bridge_remove_faces")
4366 col_top
.prop(lt
, "bridge_loft_loop")
4368 # override properties
4370 row
= box
.row(align
= True)
4371 row
.prop(lt
, "bridge_twist")
4372 row
.prop(lt
, "bridge_reverse")
4374 # relax - first line
4375 split
= col
.split(percentage
=0.15, align
=True)
4376 if lt
.display_relax
:
4377 split
.prop(lt
, "display_relax", text
="", icon
='DOWNARROW_HLT')
4379 split
.prop(lt
, "display_relax", text
="", icon
='RIGHTARROW')
4380 split
.operator("mesh.looptools_relax")
4382 if lt
.display_relax
:
4383 box
= col
.column(align
=True).box().column()
4384 box
.prop(lt
, "relax_interpolation")
4385 box
.prop(lt
, "relax_input")
4386 box
.prop(lt
, "relax_iterations")
4387 box
.prop(lt
, "relax_regular")
4389 # space - first line
4390 split
= col
.split(percentage
=0.15, align
=True)
4391 if lt
.display_space
:
4392 split
.prop(lt
, "display_space", text
="", icon
='DOWNARROW_HLT')
4394 split
.prop(lt
, "display_space", text
="", icon
='RIGHTARROW')
4395 split
.operator("mesh.looptools_space")
4397 if lt
.display_space
:
4398 box
= col
.column(align
=True).box().column()
4399 box
.prop(lt
, "space_interpolation")
4400 box
.prop(lt
, "space_input")
4403 col_move
= box
.column(align
=True)
4404 row
= col_move
.row(align
=True)
4406 row
.prop(lt
, "space_lock_x", text
= "X", icon
='LOCKED')
4408 row
.prop(lt
, "space_lock_x", text
= "X", icon
='UNLOCKED')
4410 row
.prop(lt
, "space_lock_y", text
= "Y", icon
='LOCKED')
4412 row
.prop(lt
, "space_lock_y", text
= "Y", icon
='UNLOCKED')
4414 row
.prop(lt
, "space_lock_z", text
= "Z", icon
='LOCKED')
4416 row
.prop(lt
, "space_lock_z", text
= "Z", icon
='UNLOCKED')
4417 col_move
.prop(lt
, "space_influence")
4420 # property group containing all properties for the gui in the panel
4421 class LoopToolsProps(bpy
.types
.PropertyGroup
):
4423 Fake module like class
4424 bpy.context.window_manager.looptools
4427 # general display properties
4428 display_bridge
= bpy
.props
.BoolProperty(name
= "Bridge settings",
4429 description
= "Display settings of the Bridge tool",
4431 display_circle
= bpy
.props
.BoolProperty(name
= "Circle settings",
4432 description
= "Display settings of the Circle tool",
4434 display_curve
= bpy
.props
.BoolProperty(name
= "Curve settings",
4435 description
= "Display settings of the Curve tool",
4437 display_flatten
= bpy
.props
.BoolProperty(name
= "Flatten settings",
4438 description
= "Display settings of the Flatten tool",
4440 display_gstretch
= bpy
.props
.BoolProperty(name
= "Gstretch settings",
4441 description
= "Display settings of the Gstretch tool",
4443 display_loft
= bpy
.props
.BoolProperty(name
= "Loft settings",
4444 description
= "Display settings of the Loft tool",
4446 display_relax
= bpy
.props
.BoolProperty(name
= "Relax settings",
4447 description
= "Display settings of the Relax tool",
4449 display_space
= bpy
.props
.BoolProperty(name
= "Space settings",
4450 description
= "Display settings of the Space tool",
4454 bridge_cubic_strength
= bpy
.props
.FloatProperty(name
= "Strength",
4455 description
= "Higher strength results in more fluid curves",
4459 bridge_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation mode",
4460 items
= (('cubic', "Cubic", "Gives curved results"),
4461 ('linear', "Linear", "Basic, fast, straight interpolation")),
4462 description
= "Interpolation mode: algorithm used when creating "\
4465 bridge_loft
= bpy
.props
.BoolProperty(name
= "Loft",
4466 description
= "Loft multiple loops, instead of considering them as "\
4467 "a multi-input for bridging",
4469 bridge_loft_loop
= bpy
.props
.BoolProperty(name
= "Loop",
4470 description
= "Connect the first and the last loop with each other",
4472 bridge_min_width
= bpy
.props
.IntProperty(name
= "Minimum width",
4473 description
= "Segments with an edge smaller than this are merged "\
4474 "(compared to base edge)",
4478 subtype
= 'PERCENTAGE')
4479 bridge_mode
= bpy
.props
.EnumProperty(name
= "Mode",
4480 items
= (('basic', "Basic", "Fast algorithm"),
4481 ('shortest', "Shortest edge", "Slower algorithm with " \
4482 "better vertex matching")),
4483 description
= "Algorithm used for bridging",
4484 default
= 'shortest')
4485 bridge_remove_faces
= bpy
.props
.BoolProperty(name
= "Remove faces",
4486 description
= "Remove faces that are internal after bridging",
4488 bridge_reverse
= bpy
.props
.BoolProperty(name
= "Reverse",
4489 description
= "Manually override the direction in which the loops "\
4490 "are bridged. Only use if the tool gives the wrong " \
4493 bridge_segments
= bpy
.props
.IntProperty(name
= "Segments",
4494 description
= "Number of segments used to bridge the gap "\
4499 bridge_twist
= bpy
.props
.IntProperty(name
= "Twist",
4500 description
= "Twist what vertices are connected to each other",
4504 circle_custom_radius
= bpy
.props
.BoolProperty(name
= "Radius",
4505 description
= "Force a custom radius",
4507 circle_fit
= bpy
.props
.EnumProperty(name
= "Method",
4508 items
= (("best", "Best fit", "Non-linear least squares"),
4509 ("inside", "Fit inside","Only move vertices towards the center")),
4510 description
= "Method used for fitting a circle to the vertices",
4512 circle_flatten
= bpy
.props
.BoolProperty(name
= "Flatten",
4513 description
= "Flatten the circle, instead of projecting it on the " \
4516 circle_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4517 description
= "Force of the tool",
4522 subtype
= 'PERCENTAGE')
4523 circle_lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4524 description
= "Lock editing of the x-coordinate",
4526 circle_lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4527 description
= "Lock editing of the y-coordinate",
4529 circle_lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4530 description
= "Lock editing of the z-coordinate",
4532 circle_radius
= bpy
.props
.FloatProperty(name
= "Radius",
4533 description
= "Custom radius for circle",
4537 circle_regular
= bpy
.props
.BoolProperty(name
= "Regular",
4538 description
= "Distribute vertices at constant distances along the " \
4543 curve_boundaries
= bpy
.props
.BoolProperty(name
= "Boundaries",
4544 description
= "Limit the tool to work within the boundaries of the "\
4545 "selected vertices",
4547 curve_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4548 description
= "Force of the tool",
4553 subtype
= 'PERCENTAGE')
4554 curve_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4555 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4556 ("linear", "Linear", "Simple and fast linear algorithm")),
4557 description
= "Algorithm used for interpolation",
4559 curve_lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4560 description
= "Lock editing of the x-coordinate",
4562 curve_lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4563 description
= "Lock editing of the y-coordinate",
4565 curve_lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4566 description
= "Lock editing of the z-coordinate",
4568 curve_regular
= bpy
.props
.BoolProperty(name
= "Regular",
4569 description
= "Distribute vertices at constant distances along the " \
4572 curve_restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
4573 items
= (("none", "None", "No restrictions on vertex movement"),
4574 ("extrude", "Extrude only","Only allow extrusions (no "\
4576 ("indent", "Indent only", "Only allow indentation (no "\
4578 description
= "Restrictions on how the vertices can be moved",
4581 # flatten properties
4582 flatten_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4583 description
= "Force of the tool",
4588 subtype
= 'PERCENTAGE')
4589 flatten_lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4590 description
= "Lock editing of the x-coordinate",
4592 flatten_lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4593 description
= "Lock editing of the y-coordinate",
4595 flatten_lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4596 description
= "Lock editing of the z-coordinate",
4598 flatten_plane
= bpy
.props
.EnumProperty(name
= "Plane",
4599 items
= (("best_fit", "Best fit", "Calculate a best fitting plane"),
4600 ("normal", "Normal", "Derive plane from averaging vertex "\
4602 ("view", "View", "Flatten on a plane perpendicular to the "\
4604 description
= "Plane on which vertices are flattened",
4605 default
= 'best_fit')
4606 flatten_restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
4607 items
= (("none", "None", "No restrictions on vertex movement"),
4608 ("bounding_box", "Bounding box", "Vertices are restricted to "\
4609 "movement inside the bounding box of the selection")),
4610 description
= "Restrictions on how the vertices can be moved",
4613 # gstretch properties
4614 gstretch_conversion
= bpy
.props
.EnumProperty(name
= "Conversion",
4615 items
= (("distance", "Distance", "Set the distance between vertices "\
4616 "of the converted grease pencil stroke"),
4617 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "\
4618 "number of vertices that converted GP strokes will have"),
4619 ("vertices", "Exact vertices", "Set the exact number of vertices "\
4620 "that converted grease pencil strokes will have. Short strokes "\
4621 "with few points may contain less vertices than this number."),
4622 ("none", "No simplification", "Convert each grease pencil point "\
4624 description
= "If grease pencil strokes are converted to geometry, "\
4625 "use this simplification method",
4626 default
= 'limit_vertices')
4627 gstretch_conversion_distance
= bpy
.props
.FloatProperty(name
= "Distance",
4628 description
= "Absolute distance between vertices along the converted "\
4629 "grease pencil stroke",
4634 gstretch_conversion_max
= bpy
.props
.IntProperty(name
= "Max Vertices",
4635 description
= "Maximum number of vertices grease pencil strokes will "\
4636 "have, when they are converted to geomtery",
4640 update
= gstretch_update_min
)
4641 gstretch_conversion_min
= bpy
.props
.IntProperty(name
= "Min Vertices",
4642 description
= "Minimum number of vertices grease pencil strokes will "\
4643 "have, when they are converted to geomtery",
4647 update
= gstretch_update_max
)
4648 gstretch_conversion_vertices
= bpy
.props
.IntProperty(name
= "Vertices",
4649 description
= "Number of vertices grease pencil strokes will "\
4650 "have, when they are converted to geometry. If strokes have less "\
4651 "points than required, the 'Spread evenly' method is used",
4655 gstretch_delete_strokes
= bpy
.props
.BoolProperty(name
="Delete strokes",
4656 description
= "Remove Grease Pencil strokes if they have been used "\
4657 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
4659 gstretch_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4660 description
= "Force of the tool",
4665 subtype
= 'PERCENTAGE')
4666 gstretch_lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4667 description
= "Lock editing of the x-coordinate",
4669 gstretch_lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4670 description
= "Lock editing of the y-coordinate",
4672 gstretch_lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4673 description
= "Lock editing of the z-coordinate",
4675 gstretch_method
= bpy
.props
.EnumProperty(name
= "Method",
4676 items
= (("project", "Project", "Project vertices onto the stroke, "\
4677 "using vertex normals and connected edges"),
4678 ("irregular", "Spread", "Distribute vertices along the full "\
4679 "stroke, retaining relative distances between the vertices"),
4680 ("regular", "Spread evenly", "Distribute vertices at regular "\
4681 "distances along the full stroke")),
4682 description
= "Method of distributing the vertices over the Grease "\
4684 default
= 'regular')
4687 relax_input
= bpy
.props
.EnumProperty(name
= "Input",
4688 items
= (("all", "Parallel (all)", "Also use non-selected "\
4689 "parallel loops as input"),
4690 ("selected", "Selection","Only use selected vertices as input")),
4691 description
= "Loops that are relaxed",
4692 default
= 'selected')
4693 relax_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4694 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4695 ("linear", "Linear", "Simple and fast linear algorithm")),
4696 description
= "Algorithm used for interpolation",
4698 relax_iterations
= bpy
.props
.EnumProperty(name
= "Iterations",
4699 items
= (("1", "1", "One"),
4700 ("3", "3", "Three"),
4702 ("10", "10", "Ten"),
4703 ("25", "25", "Twenty-five")),
4704 description
= "Number of times the loop is relaxed",
4706 relax_regular
= bpy
.props
.BoolProperty(name
= "Regular",
4707 description
= "Distribute vertices at constant distances along the" \
4712 space_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4713 description
= "Force of the tool",
4718 subtype
= 'PERCENTAGE')
4719 space_input
= bpy
.props
.EnumProperty(name
= "Input",
4720 items
= (("all", "Parallel (all)", "Also use non-selected "\
4721 "parallel loops as input"),
4722 ("selected", "Selection","Only use selected vertices as input")),
4723 description
= "Loops that are spaced",
4724 default
= 'selected')
4725 space_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4726 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4727 ("linear", "Linear", "Vertices are projected on existing edges")),
4728 description
= "Algorithm used for interpolation",
4730 space_lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4731 description
= "Lock editing of the x-coordinate",
4733 space_lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4734 description
= "Lock editing of the y-coordinate",
4736 space_lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4737 description
= "Lock editing of the z-coordinate",
4741 # draw function for integration in menus
4742 def menu_func(self
, context
):
4743 self
.layout
.menu("VIEW3D_MT_edit_mesh_looptools")
4744 self
.layout
.separator()
4747 # define classes for registration
4748 classes
= [VIEW3D_MT_edit_mesh_looptools
,
4749 VIEW3D_PT_tools_looptools
,
4760 # registering and menu integration
4763 bpy
.utils
.register_class(c
)
4764 bpy
.types
.VIEW3D_MT_edit_mesh_specials
.prepend(menu_func
)
4765 bpy
.types
.WindowManager
.looptools
= bpy
.props
.PointerProperty(\
4766 type = LoopToolsProps
)
4769 # unregistering and removing menus
4772 bpy
.utils
.unregister_class(c
)
4773 bpy
.types
.VIEW3D_MT_edit_mesh_specials
.remove(menu_func
)
4775 del bpy
.types
.WindowManager
.looptools
4780 if __name__
== "__main__":