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, 69, 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",
29 "tracker_url": "https://developer.blender.org/T26189",
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
, influence
):
746 for index
, loc
in loop
:
748 if mapping
[index
] == -1:
751 index
= mapping
[index
]
753 bm
.verts
[index
].co
= loc
*(influence
/100) + \
754 bm
.verts
[index
].co
*((100-influence
)/100)
756 bm
.verts
[index
].co
= loc
761 # load custom tool settings
762 def settings_load(self
):
763 lt
= bpy
.context
.window_manager
.looptools
764 tool
= self
.name
.split()[0].lower()
765 keys
= self
.as_keywords().keys()
767 setattr(self
, key
, getattr(lt
, tool
+ "_" + key
))
770 # store custom tool settings
771 def settings_write(self
):
772 lt
= bpy
.context
.window_manager
.looptools
773 tool
= self
.name
.split()[0].lower()
774 keys
= self
.as_keywords().keys()
776 setattr(lt
, tool
+ "_" + key
, getattr(self
, key
))
779 # clean up and set settings back to original state
780 def terminate(global_undo
):
781 # update editmesh cached data
782 obj
= bpy
.context
.active_object
783 if obj
.mode
== 'EDIT':
784 bmesh
.update_edit_mesh(obj
.data
, tessface
=True, destructive
=True)
786 bpy
.context
.user_preferences
.edit
.use_global_undo
= global_undo
789 ##########################################
790 ####### Bridge functions #################
791 ##########################################
793 # calculate a cubic spline through the middle section of 4 given coordinates
794 def bridge_calculate_cubic_spline(bm
, coordinates
):
800 for i
in coordinates
:
801 a
.append(float(i
[j
]))
804 h
.append(x
[i
+1]-x
[i
])
807 q
.append(3.0/h
[i
]*(a
[i
+1]-a
[i
])-3.0/h
[i
-1]*(a
[i
]-a
[i
-1]))
812 l
.append(2.0*(x
[i
+1]-x
[i
-1])-h
[i
-1]*u
[i
-1])
814 z
.append((q
[i
]-h
[i
-1]*z
[i
-1])/l
[i
])
817 b
= [False for i
in range(3)]
818 c
= [False for i
in range(4)]
819 d
= [False for i
in range(3)]
821 for i
in range(2,-1,-1):
822 c
[i
] = z
[i
]-u
[i
]*c
[i
+1]
823 b
[i
] = (a
[i
+1]-a
[i
])/h
[i
]-h
[i
]*(c
[i
+1]+2.0*c
[i
])/3.0
824 d
[i
] = (c
[i
+1]-c
[i
])/(3.0*h
[i
])
826 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
827 spline
= [result
[1], result
[4], result
[7]]
832 # return a list with new vertex location vectors, a list with face vertex
833 # integers, and the highest vertex integer in the virtual mesh
834 def bridge_calculate_geometry(bm
, lines
, vertex_normals
, segments
,
835 interpolation
, cubic_strength
, min_width
, max_vert_index
):
839 # calculate location based on interpolation method
840 def get_location(line
, segment
, splines
):
841 v1
= bm
.verts
[lines
[line
][0]].co
842 v2
= bm
.verts
[lines
[line
][1]].co
843 if interpolation
== 'linear':
844 return v1
+ (segment
/segments
) * (v2
-v1
)
845 else: # interpolation == 'cubic'
846 m
= (segment
/segments
)
847 ax
,bx
,cx
,dx
,tx
= splines
[line
][0]
848 x
= ax
+bx
*m
+cx
*m
**2+dx
*m
**3
849 ay
,by
,cy
,dy
,ty
= splines
[line
][1]
850 y
= ay
+by
*m
+cy
*m
**2+dy
*m
**3
851 az
,bz
,cz
,dz
,tz
= splines
[line
][2]
852 z
= az
+bz
*m
+cz
*m
**2+dz
*m
**3
853 return mathutils
.Vector((x
, y
, z
))
855 # no interpolation needed
857 for i
, line
in enumerate(lines
):
859 faces
.append([line
[0], lines
[i
+1][0], lines
[i
+1][1], line
[1]])
860 # more than 1 segment, interpolate
862 # calculate splines (if necessary) once, so no recalculations needed
863 if interpolation
== 'cubic':
866 v1
= bm
.verts
[line
[0]].co
867 v2
= bm
.verts
[line
[1]].co
868 size
= (v2
-v1
).length
* cubic_strength
869 splines
.append(bridge_calculate_cubic_spline(bm
,
870 [v1
+size
*vertex_normals
[line
[0]], v1
, v2
,
871 v2
+size
*vertex_normals
[line
[1]]]))
875 # create starting situation
876 virtual_width
= [(bm
.verts
[lines
[i
][0]].co
-
877 bm
.verts
[lines
[i
+1][0]].co
).length
for i
878 in range(len(lines
)-1)]
879 new_verts
= [get_location(0, seg
, splines
) for seg
in range(1,
881 first_line_indices
= [i
for i
in range(max_vert_index
+1,
882 max_vert_index
+segments
)]
884 prev_verts
= new_verts
[:] # vertex locations of verts on previous line
885 prev_vert_indices
= first_line_indices
[:]
886 max_vert_index
+= segments
- 1 # highest vertex index in virtual mesh
887 next_verts
= [] # vertex locations of verts on current line
888 next_vert_indices
= []
890 for i
, line
in enumerate(lines
):
895 for seg
in range(1, segments
):
896 loc1
= prev_verts
[seg
-1]
897 loc2
= get_location(i
+1, seg
, splines
)
898 if (loc1
-loc2
).length
< (min_width
/100)*virtual_width
[i
] \
899 and line
[1]==lines
[i
+1][1]:
900 # triangle, no new vertex
901 faces
.append([v1
, v2
, prev_vert_indices
[seg
-1],
902 prev_vert_indices
[seg
-1]])
903 next_verts
+= prev_verts
[seg
-1:]
904 next_vert_indices
+= prev_vert_indices
[seg
-1:]
908 if i
== len(lines
)-2 and lines
[0] == lines
[-1]:
909 # quad with first line, no new vertex
910 faces
.append([v1
, v2
, first_line_indices
[seg
-1],
911 prev_vert_indices
[seg
-1]])
912 v2
= first_line_indices
[seg
-1]
913 v1
= prev_vert_indices
[seg
-1]
915 # quad, add new vertex
917 faces
.append([v1
, v2
, max_vert_index
,
918 prev_vert_indices
[seg
-1]])
920 v1
= prev_vert_indices
[seg
-1]
921 new_verts
.append(loc2
)
922 next_verts
.append(loc2
)
923 next_vert_indices
.append(max_vert_index
)
925 faces
.append([v1
, v2
, lines
[i
+1][1], line
[1]])
927 prev_verts
= next_verts
[:]
928 prev_vert_indices
= next_vert_indices
[:]
930 next_vert_indices
= []
932 return(new_verts
, faces
, max_vert_index
)
935 # calculate lines (list of lists, vertex indices) that are used for bridging
936 def bridge_calculate_lines(bm
, loops
, mode
, twist
, reverse
):
938 loop1
, loop2
= [i
[0] for i
in loops
]
939 loop1_circular
, loop2_circular
= [i
[1] for i
in loops
]
940 circular
= loop1_circular
or loop2_circular
943 # calculate loop centers
945 for loop
in [loop1
, loop2
]:
946 center
= mathutils
.Vector()
948 center
+= bm
.verts
[vertex
].co
950 centers
.append(center
)
951 for i
, loop
in enumerate([loop1
, loop2
]):
953 if bm
.verts
[vertex
].co
== centers
[i
]:
954 # prevent zero-length vectors in angle comparisons
955 centers
[i
] += mathutils
.Vector((0.01, 0, 0))
957 center1
, center2
= centers
959 # calculate the normals of the virtual planes that the loops are on
961 normal_plurity
= False
962 for i
, loop
in enumerate([loop1
, loop2
]):
964 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
968 for loc
in [bm
.verts
[vertex
].co
for vertex
in loop
]:
969 mat
[0][0] += (loc
[0]-x
)**2
970 mat
[1][0] += (loc
[0]-x
)*(loc
[1]-y
)
971 mat
[2][0] += (loc
[0]-x
)*(loc
[2]-z
)
972 mat
[0][1] += (loc
[1]-y
)*(loc
[0]-x
)
973 mat
[1][1] += (loc
[1]-y
)**2
974 mat
[2][1] += (loc
[1]-y
)*(loc
[2]-z
)
975 mat
[0][2] += (loc
[2]-z
)*(loc
[0]-x
)
976 mat
[1][2] += (loc
[2]-z
)*(loc
[1]-y
)
977 mat
[2][2] += (loc
[2]-z
)**2
980 if sum(mat
[0]) < 1e-6 or sum(mat
[1]) < 1e-6 or sum(mat
[2]) < 1e-6:
981 normal_plurity
= True
986 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
987 elif sum(mat
[1]) == 0:
988 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
989 elif sum(mat
[2]) == 0:
990 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
992 # warning! this is different from .normalize()
995 vec
= mathutils
.Vector((1.0, 1.0, 1.0))
996 vec2
= (mat
* vec
)/(mat
* vec
).length
997 while vec
!= vec2
and iter<itermax
:
1001 if vec2
.length
!= 0:
1003 if vec2
.length
== 0:
1004 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
1006 normals
.append(normal
)
1007 # have plane normals face in the same direction (maximum angle: 90 degrees)
1008 if ((center1
+ normals
[0]) - center2
).length
< \
1009 ((center1
- normals
[0]) - center2
).length
:
1011 if ((center2
+ normals
[1]) - center1
).length
> \
1012 ((center2
- normals
[1]) - center1
).length
:
1015 # rotation matrix, representing the difference between the plane normals
1016 axis
= normals
[0].cross(normals
[1])
1017 axis
= mathutils
.Vector([loc
if abs(loc
) > 1e-8 else 0 for loc
in axis
])
1018 if axis
.angle(mathutils
.Vector((0, 0, 1)), 0) > 1.5707964:
1020 angle
= normals
[0].dot(normals
[1])
1021 rotation_matrix
= mathutils
.Matrix
.Rotation(angle
, 4, axis
)
1023 # if circular, rotate loops so they are aligned
1025 # make sure loop1 is the circular one (or both are circular)
1026 if loop2_circular
and not loop1_circular
:
1027 loop1_circular
, loop2_circular
= True, False
1028 loop1
, loop2
= loop2
, loop1
1030 # match start vertex of loop1 with loop2
1031 target_vector
= bm
.verts
[loop2
[0]].co
- center2
1032 dif_angles
= [[(rotation_matrix
* (bm
.verts
[vertex
].co
- center1
)
1033 ).angle(target_vector
, 0), False, i
] for
1034 i
, vertex
in enumerate(loop1
)]
1036 if len(loop1
) != len(loop2
):
1037 angle_limit
= dif_angles
[0][0] * 1.2 # 20% margin
1038 dif_angles
= [[(bm
.verts
[loop2
[0]].co
- \
1039 bm
.verts
[loop1
[index
]].co
).length
, angle
, index
] for \
1040 angle
, distance
, index
in dif_angles
if angle
<= angle_limit
]
1042 loop1
= loop1
[dif_angles
[0][2]:] + loop1
[:dif_angles
[0][2]]
1044 # have both loops face the same way
1045 if normal_plurity
and not circular
:
1046 second_to_first
, second_to_second
, second_to_last
= \
1047 [(bm
.verts
[loop1
[1]].co
- center1
).\
1048 angle(bm
.verts
[loop2
[i
]].co
- center2
) for i
in [0, 1, -1]]
1049 last_to_first
, last_to_second
= [(bm
.verts
[loop1
[-1]].co
- \
1050 center1
).angle(bm
.verts
[loop2
[i
]].co
- center2
) for \
1052 if (min(last_to_first
, last_to_second
)*1.1 < min(second_to_first
, \
1053 second_to_second
)) or (loop2_circular
and second_to_last
*1.1 < \
1054 min(second_to_first
, second_to_second
)):
1057 loop1
= [loop1
[-1]] + loop1
[:-1]
1059 angle
= (bm
.verts
[loop1
[0]].co
- center1
).\
1060 cross(bm
.verts
[loop1
[1]].co
- center1
).angle(normals
[0], 0)
1061 target_angle
= (bm
.verts
[loop2
[0]].co
- center2
).\
1062 cross(bm
.verts
[loop2
[1]].co
- center2
).angle(normals
[1], 0)
1063 limit
= 1.5707964 # 0.5*pi, 90 degrees
1064 if not ((angle
> limit
and target_angle
> limit
) or \
1065 (angle
< limit
and target_angle
< limit
)):
1068 loop1
= [loop1
[-1]] + loop1
[:-1]
1069 elif normals
[0].angle(normals
[1]) > limit
:
1072 loop1
= [loop1
[-1]] + loop1
[:-1]
1074 # both loops have the same length
1075 if len(loop1
) == len(loop2
):
1078 if abs(twist
) < len(loop1
):
1079 loop1
= loop1
[twist
:]+loop1
[:twist
]
1083 lines
.append([loop1
[0], loop2
[0]])
1084 for i
in range(1, len(loop1
)):
1085 lines
.append([loop1
[i
], loop2
[i
]])
1087 # loops of different lengths
1089 # make loop1 longest loop
1090 if len(loop2
) > len(loop1
):
1091 loop1
, loop2
= loop2
, loop1
1092 loop1_circular
, loop2_circular
= loop2_circular
, loop1_circular
1096 if abs(twist
) < len(loop1
):
1097 loop1
= loop1
[twist
:]+loop1
[:twist
]
1101 # shortest angle difference doesn't always give correct start vertex
1102 if loop1_circular
and not loop2_circular
:
1105 if len(loop1
) - shifting
< len(loop2
):
1108 to_last
, to_first
= [(rotation_matrix
*
1109 (bm
.verts
[loop1
[-1]].co
- center1
)).angle((bm
.\
1110 verts
[loop2
[i
]].co
- center2
), 0) for i
in [-1, 0]]
1111 if to_first
< to_last
:
1112 loop1
= [loop1
[-1]] + loop1
[:-1]
1118 # basic shortest side first
1120 lines
.append([loop1
[0], loop2
[0]])
1121 for i
in range(1, len(loop1
)):
1122 if i
>= len(loop2
) - 1:
1124 lines
.append([loop1
[i
], loop2
[-1]])
1127 lines
.append([loop1
[i
], loop2
[i
]])
1129 # shortest edge algorithm
1130 else: # mode == 'shortest'
1131 lines
.append([loop1
[0], loop2
[0]])
1133 for i
in range(len(loop1
) -1):
1134 if prev_vert2
== len(loop2
) - 1 and not loop2_circular
:
1135 # force triangles, reached end of loop2
1137 elif prev_vert2
== len(loop2
) - 1 and loop2_circular
:
1138 # at end of loop2, but circular, so check with first vert
1139 tri
, quad
= [(bm
.verts
[loop1
[i
+1]].co
-
1140 bm
.verts
[loop2
[j
]].co
).length
1141 for j
in [prev_vert2
, 0]]
1144 elif len(loop1
) - 1 - i
== len(loop2
) - 1 - prev_vert2
and \
1146 # force quads, otherwise won't make it to end of loop2
1149 # calculate if tri or quad gives shortest edge
1150 tri
, quad
= [(bm
.verts
[loop1
[i
+1]].co
-
1151 bm
.verts
[loop2
[j
]].co
).length
1152 for j
in range(prev_vert2
, prev_vert2
+2)]
1156 lines
.append([loop1
[i
+1], loop2
[prev_vert2
]])
1157 if circle_full
== 2:
1160 elif not circle_full
:
1161 lines
.append([loop1
[i
+1], loop2
[prev_vert2
+1]])
1163 # quad to first vertex of loop2
1165 lines
.append([loop1
[i
+1], loop2
[0]])
1169 # final face for circular loops
1170 if loop1_circular
and loop2_circular
:
1171 lines
.append([loop1
[0], loop2
[0]])
1176 # calculate number of segments needed
1177 def bridge_calculate_segments(bm
, lines
, loops
, segments
):
1178 # return if amount of segments is set by user
1183 average_edge_length
= [(bm
.verts
[vertex
].co
- \
1184 bm
.verts
[loop
[0][i
+1]].co
).length
for loop
in loops
for \
1185 i
, vertex
in enumerate(loop
[0][:-1])]
1186 # closing edges of circular loops
1187 average_edge_length
+= [(bm
.verts
[loop
[0][-1]].co
- \
1188 bm
.verts
[loop
[0][0]].co
).length
for loop
in loops
if loop
[1]]
1191 average_edge_length
= sum(average_edge_length
) / len(average_edge_length
)
1192 average_bridge_length
= sum([(bm
.verts
[v1
].co
- \
1193 bm
.verts
[v2
].co
).length
for v1
, v2
in lines
]) / len(lines
)
1195 segments
= max(1, round(average_bridge_length
/ average_edge_length
))
1200 # return dictionary with vertex index as key, and the normal vector as value
1201 def bridge_calculate_virtual_vertex_normals(bm
, lines
, loops
, edge_faces
,
1203 if not edge_faces
: # interpolation isn't set to cubic
1206 # pity reduce() isn't one of the basic functions in python anymore
1207 def average_vector_dictionary(dic
):
1208 for key
, vectors
in dic
.items():
1209 #if type(vectors) == type([]) and len(vectors) > 1:
1210 if len(vectors
) > 1:
1211 average
= mathutils
.Vector()
1212 for vector
in vectors
:
1214 average
/= len(vectors
)
1215 dic
[key
] = [average
]
1218 # get all edges of the loop
1219 edges
= [[edgekey_to_edge
[tuple(sorted([loops
[j
][0][i
],
1220 loops
[j
][0][i
+1]]))] for i
in range(len(loops
[j
][0])-1)] for \
1222 edges
= edges
[0] + edges
[1]
1224 if loops
[j
][1]: # circular
1225 edges
.append(edgekey_to_edge
[tuple(sorted([loops
[j
][0][0],
1226 loops
[j
][0][-1]]))])
1229 calculation based on face topology (assign edge-normals to vertices)
1231 edge_normal = face_normal x edge_vector
1232 vertex_normal = average(edge_normals)
1234 vertex_normals
= dict([(vertex
, []) for vertex
in loops
[0][0]+loops
[1][0]])
1236 faces
= edge_faces
[edgekey(edge
)] # valid faces connected to edge
1239 # get edge coordinates
1240 v1
, v2
= [bm
.verts
[edgekey(edge
)[i
]].co
for i
in [0,1]]
1241 edge_vector
= v1
- v2
1242 if edge_vector
.length
< 1e-4:
1243 # zero-length edge, vertices at same location
1245 edge_center
= (v1
+ v2
) / 2
1247 # average face coordinates, if connected to more than 1 valid face
1249 face_normal
= mathutils
.Vector()
1250 face_center
= mathutils
.Vector()
1252 face_normal
+= face
.normal
1253 face_center
+= face
.calc_center_median()
1254 face_normal
/= len(faces
)
1255 face_center
/= len(faces
)
1257 face_normal
= faces
[0].normal
1258 face_center
= faces
[0].calc_center_median()
1259 if face_normal
.length
< 1e-4:
1260 # faces with a surface of 0 have no face normal
1263 # calculate virtual edge normal
1264 edge_normal
= edge_vector
.cross(face_normal
)
1265 edge_normal
.length
= 0.01
1266 if (face_center
- (edge_center
+ edge_normal
)).length
> \
1267 (face_center
- (edge_center
- edge_normal
)).length
:
1268 # make normal face the correct way
1269 edge_normal
.negate()
1270 edge_normal
.normalize()
1271 # add virtual edge normal as entry for both vertices it connects
1272 for vertex
in edgekey(edge
):
1273 vertex_normals
[vertex
].append(edge_normal
)
1276 calculation based on connection with other loop (vertex focused method)
1277 - used for vertices that aren't connected to any valid faces
1279 plane_normal = edge_vector x connection_vector
1280 vertex_normal = plane_normal x edge_vector
1282 vertices
= [vertex
for vertex
, normal
in vertex_normals
.items() if not \
1286 # edge vectors connected to vertices
1287 edge_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1289 for v
in edgekey(edge
):
1290 if v
in edge_vectors
:
1291 edge_vector
= bm
.verts
[edgekey(edge
)[0]].co
- \
1292 bm
.verts
[edgekey(edge
)[1]].co
1293 if edge_vector
.length
< 1e-4:
1294 # zero-length edge, vertices at same location
1296 edge_vectors
[v
].append(edge_vector
)
1298 # connection vectors between vertices of both loops
1299 connection_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1300 connections
= dict([[vertex
, []] for vertex
in vertices
])
1301 for v1
, v2
in lines
:
1302 if v1
in connection_vectors
or v2
in connection_vectors
:
1303 new_vector
= bm
.verts
[v1
].co
- bm
.verts
[v2
].co
1304 if new_vector
.length
< 1e-4:
1305 # zero-length connection vector,
1306 # vertices in different loops at same location
1308 if v1
in connection_vectors
:
1309 connection_vectors
[v1
].append(new_vector
)
1310 connections
[v1
].append(v2
)
1311 if v2
in connection_vectors
:
1312 connection_vectors
[v2
].append(new_vector
)
1313 connections
[v2
].append(v1
)
1314 connection_vectors
= average_vector_dictionary(connection_vectors
)
1315 connection_vectors
= dict([[vertex
, vector
[0]] if vector
else \
1316 [vertex
, []] for vertex
, vector
in connection_vectors
.items()])
1318 for vertex
, values
in edge_vectors
.items():
1319 # vertex normal doesn't matter, just assign a random vector to it
1320 if not connection_vectors
[vertex
]:
1321 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1324 # calculate to what location the vertex is connected,
1325 # used to determine what way to flip the normal
1326 connected_center
= mathutils
.Vector()
1327 for v
in connections
[vertex
]:
1328 connected_center
+= bm
.verts
[v
].co
1329 if len(connections
[vertex
]) > 1:
1330 connected_center
/= len(connections
[vertex
])
1331 if len(connections
[vertex
]) == 0:
1332 # shouldn't be possible, but better safe than sorry
1333 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1336 # can't do proper calculations, because of zero-length vector
1338 if (connected_center
- (bm
.verts
[vertex
].co
+ \
1339 connection_vectors
[vertex
])).length
< (connected_center
- \
1340 (bm
.verts
[vertex
].co
- connection_vectors
[vertex
])).\
1342 connection_vectors
[vertex
].negate()
1343 vertex_normals
[vertex
] = [connection_vectors
[vertex
].\
1347 # calculate vertex normals using edge-vectors,
1348 # connection-vectors and the derived plane normal
1349 for edge_vector
in values
:
1350 plane_normal
= edge_vector
.cross(connection_vectors
[vertex
])
1351 vertex_normal
= edge_vector
.cross(plane_normal
)
1352 vertex_normal
.length
= 0.1
1353 if (connected_center
- (bm
.verts
[vertex
].co
+ \
1354 vertex_normal
)).length
< (connected_center
- \
1355 (bm
.verts
[vertex
].co
- vertex_normal
)).length
:
1356 # make normal face the correct way
1357 vertex_normal
.negate()
1358 vertex_normal
.normalize()
1359 vertex_normals
[vertex
].append(vertex_normal
)
1361 # average virtual vertex normals, based on all edges it's connected to
1362 vertex_normals
= average_vector_dictionary(vertex_normals
)
1363 vertex_normals
= dict([[vertex
, vector
[0]] for vertex
, vector
in \
1364 vertex_normals
.items()])
1366 return(vertex_normals
)
1369 # add vertices to mesh
1370 def bridge_create_vertices(bm
, vertices
):
1371 for i
in range(len(vertices
)):
1372 bm
.verts
.new(vertices
[i
])
1376 def bridge_create_faces(object, bm
, faces
, twist
):
1377 # have the normal point the correct way
1379 [face
.reverse() for face
in faces
]
1380 faces
= [face
[2:]+face
[:2] if face
[0]==face
[1] else face
for \
1383 # eekadoodle prevention
1384 for i
in range(len(faces
)):
1385 if not faces
[i
][-1]:
1386 if faces
[i
][0] == faces
[i
][-1]:
1387 faces
[i
] = [faces
[i
][1], faces
[i
][2], faces
[i
][3], faces
[i
][1]]
1389 faces
[i
] = [faces
[i
][-1]] + faces
[i
][:-1]
1390 # result of converting from pre-bmesh period
1391 if faces
[i
][-1] == faces
[i
][-2]:
1392 faces
[i
] = faces
[i
][:-1]
1395 for i
in range(len(faces
)):
1396 new_faces
.append(bm
.faces
.new([bm
.verts
[v
] for v
in faces
[i
]]))
1398 object.data
.update(calc_edges
=True) # calc_edges prevents memory-corruption
1403 # calculate input loops
1404 def bridge_get_input(bm
):
1405 # create list of internal edges, which should be skipped
1406 eks_of_selected_faces
= [item
for sublist
in [face_edgekeys(face
) for \
1407 face
in bm
.faces
if face
.select
and not face
.hide
] for item
in sublist
]
1409 for ek
in eks_of_selected_faces
:
1410 if ek
in edge_count
:
1414 internal_edges
= [ek
for ek
in edge_count
if edge_count
[ek
] > 1]
1416 # sort correct edges into loops
1417 selected_edges
= [edgekey(edge
) for edge
in bm
.edges
if edge
.select \
1418 and not edge
.hide
and edgekey(edge
) not in internal_edges
]
1419 loops
= get_connected_selections(selected_edges
)
1424 # return values needed by the bridge operator
1425 def bridge_initialise(bm
, interpolation
):
1426 if interpolation
== 'cubic':
1427 # dict with edge-key as key and list of connected valid faces as value
1428 face_blacklist
= [face
.index
for face
in bm
.faces
if face
.select
or \
1430 edge_faces
= dict([[edgekey(edge
), []] for edge
in bm
.edges
if not \
1432 for face
in bm
.faces
:
1433 if face
.index
in face_blacklist
:
1435 for key
in face_edgekeys(face
):
1436 edge_faces
[key
].append(face
)
1437 # dictionary with the edge-key as key and edge as value
1438 edgekey_to_edge
= dict([[edgekey(edge
), edge
] for edge
in \
1439 bm
.edges
if edge
.select
and not edge
.hide
])
1442 edgekey_to_edge
= False
1444 # selected faces input
1445 old_selected_faces
= [face
.index
for face
in bm
.faces
if face
.select \
1448 # find out if faces created by bridging should be smoothed
1451 if sum([face
.smooth
for face
in bm
.faces
])/len(bm
.faces
) \
1455 return(edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
)
1458 # return a string with the input method
1459 def bridge_input_method(loft
, loft_loop
):
1463 method
= "Loft loop"
1465 method
= "Loft no-loop"
1472 # match up loops in pairs, used for multi-input bridging
1473 def bridge_match_loops(bm
, loops
):
1474 # calculate average loop normals and centers
1477 for vertices
, circular
in loops
:
1478 normal
= mathutils
.Vector()
1479 center
= mathutils
.Vector()
1480 for vertex
in vertices
:
1481 normal
+= bm
.verts
[vertex
].normal
1482 center
+= bm
.verts
[vertex
].co
1483 normals
.append(normal
/ len(vertices
) / 10)
1484 centers
.append(center
/ len(vertices
))
1486 # possible matches if loop normals are faced towards the center
1488 matches
= dict([[i
, []] for i
in range(len(loops
))])
1490 for i
in range(len(loops
) + 1):
1491 for j
in range(i
+1, len(loops
)):
1492 if (centers
[i
] - centers
[j
]).length
> (centers
[i
] - (centers
[j
] \
1493 + normals
[j
])).length
and (centers
[j
] - centers
[i
]).length
> \
1494 (centers
[j
] - (centers
[i
] + normals
[i
])).length
:
1496 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1497 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1498 # if no loops face each other, just make matches between all the loops
1499 if matches_amount
== 0:
1500 for i
in range(len(loops
) + 1):
1501 for j
in range(i
+1, len(loops
)):
1502 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1503 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1504 for key
, value
in matches
.items():
1507 # matches based on distance between centers and number of vertices in loops
1509 for loop_index
in range(len(loops
)):
1510 if loop_index
in new_order
:
1512 loop_matches
= matches
[loop_index
]
1513 if not loop_matches
:
1515 shortest_distance
= loop_matches
[0][0]
1516 shortest_distance
*= 1.1
1517 loop_matches
= [[abs(len(loops
[loop_index
][0]) - \
1518 len(loops
[loop
[2]][0])), loop
[0], loop
[1], loop
[2]] for loop
in \
1519 loop_matches
if loop
[0] < shortest_distance
]
1521 for match
in loop_matches
:
1522 if match
[3] not in new_order
:
1523 new_order
+= [loop_index
, match
[3]]
1526 # reorder loops based on matches
1527 if len(new_order
) >= 2:
1528 loops
= [loops
[i
] for i
in new_order
]
1533 # remove old_selected_faces
1534 def bridge_remove_internal_faces(bm
, old_selected_faces
):
1535 # collect bmesh faces and internal bmesh edges
1536 remove_faces
= [bm
.faces
[face
] for face
in old_selected_faces
]
1537 edges
= collections
.Counter([edge
.index
for face
in remove_faces
for \
1538 edge
in face
.edges
])
1539 remove_edges
= [bm
.edges
[edge
] for edge
in edges
if edges
[edge
] > 1]
1541 # remove internal faces and edges
1542 for face
in remove_faces
:
1543 bm
.faces
.remove(face
)
1544 for edge
in remove_edges
:
1545 bm
.edges
.remove(edge
)
1548 # update list of internal faces that are flagged for removal
1549 def bridge_save_unused_faces(bm
, old_selected_faces
, loops
):
1550 # key: vertex index, value: lists of selected faces using it
1551 vertex_to_face
= dict([[i
, []] for i
in range(len(bm
.verts
))])
1552 [[vertex_to_face
[vertex
.index
].append(face
) for vertex
in \
1553 bm
.faces
[face
].verts
] for face
in old_selected_faces
]
1555 # group selected faces that are connected
1558 for face
in old_selected_faces
:
1559 if face
in grouped_faces
:
1561 grouped_faces
.append(face
)
1565 grow_face
= new_faces
[0]
1566 for vertex
in bm
.faces
[grow_face
].verts
:
1567 vertex_face_group
= [face
for face
in vertex_to_face
[\
1568 vertex
.index
] if face
not in grouped_faces
]
1569 new_faces
+= vertex_face_group
1570 grouped_faces
+= vertex_face_group
1571 group
+= vertex_face_group
1573 groups
.append(group
)
1575 # key: vertex index, value: True/False (is it in a loop that is used)
1576 used_vertices
= dict([[i
, 0] for i
in range(len(bm
.verts
))])
1578 for vertex
in loop
[0]:
1579 used_vertices
[vertex
] = True
1581 # check if group is bridged, if not remove faces from internal faces list
1582 for group
in groups
:
1587 for vertex
in bm
.faces
[face
].verts
:
1588 if used_vertices
[vertex
.index
]:
1593 old_selected_faces
.remove(face
)
1596 # add the newly created faces to the selection
1597 def bridge_select_new_faces(new_faces
, smooth
):
1598 for face
in new_faces
:
1599 face
.select_set(True)
1600 face
.smooth
= smooth
1603 # sort loops, so they are connected in the correct order when lofting
1604 def bridge_sort_loops(bm
, loops
, loft_loop
):
1605 # simplify loops to single points, and prepare for pathfinding
1606 x
, y
, z
= [[sum([bm
.verts
[i
].co
[j
] for i
in loop
[0]]) / \
1607 len(loop
[0]) for loop
in loops
] for j
in range(3)]
1608 nodes
= [mathutils
.Vector((x
[i
], y
[i
], z
[i
])) for i
in range(len(loops
))]
1611 open = [i
for i
in range(1, len(loops
))]
1613 # connect node to path, that is shortest to active_node
1614 while len(open) > 0:
1615 distances
= [(nodes
[active_node
] - nodes
[i
]).length
for i
in open]
1616 active_node
= open[distances
.index(min(distances
))]
1617 open.remove(active_node
)
1618 path
.append([active_node
, min(distances
)])
1619 # check if we didn't start in the middle of the path
1620 for i
in range(2, len(path
)):
1621 if (nodes
[path
[i
][0]]-nodes
[0]).length
< path
[i
][1]:
1624 path
= path
[:-i
] + temp
1628 loops
= [loops
[i
[0]] for i
in path
]
1629 # if requested, duplicate first loop at last position, so loft can loop
1631 loops
= loops
+ [loops
[0]]
1636 # remapping old indices to new position in list
1637 def bridge_update_old_selection(bm
, old_selected_faces
):
1638 #old_indices = old_selected_faces[:]
1639 #old_selected_faces = []
1640 #for i, face in enumerate(bm.faces):
1641 # if face.index in old_indices:
1642 # old_selected_faces.append(i)
1644 old_selected_faces
= [i
for i
, face
in enumerate(bm
.faces
) if face
.index \
1645 in old_selected_faces
]
1647 return(old_selected_faces
)
1650 ##########################################
1651 ####### Circle functions #################
1652 ##########################################
1654 # convert 3d coordinates to 2d coordinates on plane
1655 def circle_3d_to_2d(bm_mod
, loop
, com
, normal
):
1656 # project vertices onto the plane
1657 verts
= [bm_mod
.verts
[v
] for v
in loop
[0]]
1658 verts_projected
= [[v
.co
- (v
.co
- com
).dot(normal
) * normal
, v
.index
]
1661 # calculate two vectors (p and q) along the plane
1662 m
= mathutils
.Vector((normal
[0] + 1.0, normal
[1], normal
[2]))
1663 p
= m
- (m
.dot(normal
) * normal
)
1665 m
= mathutils
.Vector((normal
[0], normal
[1] + 1.0, normal
[2]))
1666 p
= m
- (m
.dot(normal
) * normal
)
1669 # change to 2d coordinates using perpendicular projection
1671 for loc
, vert
in verts_projected
:
1673 x
= p
.dot(vloc
) / p
.dot(p
)
1674 y
= q
.dot(vloc
) / q
.dot(q
)
1675 locs_2d
.append([x
, y
, vert
])
1677 return(locs_2d
, p
, q
)
1680 # calculate a best-fit circle to the 2d locations on the plane
1681 def circle_calculate_best_fit(locs_2d
):
1687 # calculate center and radius (non-linear least squares solution)
1688 for iter in range(500):
1692 d
= (v
[0]**2-2.0*x0
*v
[0]+v
[1]**2-2.0*y0
*v
[1]+x0
**2+y0
**2)**0.5
1693 jmat
.append([(x0
-v
[0])/d
, (y0
-v
[1])/d
, -1.0])
1694 k
.append(-(((v
[0]-x0
)**2+(v
[1]-y0
)**2)**0.5-r
))
1695 jmat2
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1699 k2
= mathutils
.Vector((0.0, 0.0, 0.0))
1700 for i
in range(len(jmat
)):
1701 k2
+= mathutils
.Vector(jmat
[i
])*k
[i
]
1702 jmat2
[0][0] += jmat
[i
][0]**2
1703 jmat2
[1][0] += jmat
[i
][0]*jmat
[i
][1]
1704 jmat2
[2][0] += jmat
[i
][0]*jmat
[i
][2]
1705 jmat2
[1][1] += jmat
[i
][1]**2
1706 jmat2
[2][1] += jmat
[i
][1]*jmat
[i
][2]
1707 jmat2
[2][2] += jmat
[i
][2]**2
1708 jmat2
[0][1] = jmat2
[1][0]
1709 jmat2
[0][2] = jmat2
[2][0]
1710 jmat2
[1][2] = jmat2
[2][1]
1715 dx0
, dy0
, dr
= jmat2
* k2
1719 # stop iterating if we're close enough to optimal solution
1720 if abs(dx0
)<1e-6 and abs(dy0
)<1e-6 and abs(dr
)<1e-6:
1723 # return center of circle and radius
1727 # calculate circle so no vertices have to be moved away from the center
1728 def circle_calculate_min_fit(locs_2d
):
1730 x0
= (min([i
[0] for i
in locs_2d
])+max([i
[0] for i
in locs_2d
]))/2.0
1731 y0
= (min([i
[1] for i
in locs_2d
])+max([i
[1] for i
in locs_2d
]))/2.0
1732 center
= mathutils
.Vector([x0
, y0
])
1734 r
= min([(mathutils
.Vector([i
[0], i
[1]])-center
).length
for i
in locs_2d
])
1736 # return center of circle and radius
1740 # calculate the new locations of the vertices that need to be moved
1741 def circle_calculate_verts(flatten
, bm_mod
, locs_2d
, com
, p
, q
, normal
):
1742 # changing 2d coordinates back to 3d coordinates
1745 locs_3d
.append([loc
[2], loc
[0]*p
+ loc
[1]*q
+ com
])
1747 if flatten
: # flat circle
1750 else: # project the locations on the existing mesh
1751 vert_edges
= dict_vert_edges(bm_mod
)
1752 vert_faces
= dict_vert_faces(bm_mod
)
1753 faces
= [f
for f
in bm_mod
.faces
if not f
.hide
]
1754 rays
= [normal
, -normal
]
1758 if bm_mod
.verts
[loc
[0]].co
== loc
[1]: # vertex hasn't moved
1761 dif
= normal
.angle(loc
[1]-bm_mod
.verts
[loc
[0]].co
)
1762 if -1e-6 < dif
< 1e-6 or math
.pi
-1e-6 < dif
< math
.pi
+1e-6:
1763 # original location is already along projection normal
1764 projection
= bm_mod
.verts
[loc
[0]].co
1766 # quick search through adjacent faces
1767 for face
in vert_faces
[loc
[0]]:
1768 verts
= [v
.co
for v
in bm_mod
.faces
[face
].verts
]
1769 if len(verts
) == 3: # triangle
1773 v1
, v2
, v3
, v4
= verts
[:4]
1775 intersect
= mathutils
.geometry
.\
1776 intersect_ray_tri(v1
, v2
, v3
, ray
, loc
[1])
1778 projection
= intersect
1781 intersect
= mathutils
.geometry
.\
1782 intersect_ray_tri(v1
, v3
, v4
, ray
, loc
[1])
1784 projection
= intersect
1789 # check if projection is on adjacent edges
1790 for edgekey
in vert_edges
[loc
[0]]:
1791 line1
= bm_mod
.verts
[edgekey
[0]].co
1792 line2
= bm_mod
.verts
[edgekey
[1]].co
1793 intersect
, dist
= mathutils
.geometry
.intersect_point_line(\
1794 loc
[1], line1
, line2
)
1795 if 1e-6 < dist
< 1 - 1e-6:
1796 projection
= intersect
1799 # full search through the entire mesh
1802 verts
= [v
.co
for v
in face
.verts
]
1803 if len(verts
) == 3: # triangle
1807 v1
, v2
, v3
, v4
= verts
[:4]
1809 intersect
= mathutils
.geometry
.intersect_ray_tri(\
1810 v1
, v2
, v3
, ray
, loc
[1])
1812 hits
.append([(loc
[1] - intersect
).length
,
1816 intersect
= mathutils
.geometry
.intersect_ray_tri(\
1817 v1
, v3
, v4
, ray
, loc
[1])
1819 hits
.append([(loc
[1] - intersect
).length
,
1823 # if more than 1 hit with mesh, closest hit is new loc
1825 projection
= hits
[0][1]
1827 # nothing to project on, remain at flat location
1829 new_locs
.append([loc
[0], projection
])
1831 # return new positions of projected circle
1835 # check loops and only return valid ones
1836 def circle_check_loops(single_loops
, loops
, mapping
, bm_mod
):
1837 valid_single_loops
= {}
1839 for i
, [loop
, circular
] in enumerate(loops
):
1840 # loop needs to have at least 3 vertices
1843 # loop needs at least 1 vertex in the original, non-mirrored mesh
1847 if mapping
[vert
] > -1:
1852 # loop has to be non-collinear
1854 loc0
= mathutils
.Vector(bm_mod
.verts
[loop
[0]].co
[:])
1855 loc1
= mathutils
.Vector(bm_mod
.verts
[loop
[1]].co
[:])
1857 locn
= mathutils
.Vector(bm_mod
.verts
[v
].co
[:])
1858 if loc0
== loc1
or loc1
== locn
:
1864 if -1e-6 < d1
.angle(d2
, 0) < 1e-6:
1872 # passed all tests, loop is valid
1873 valid_loops
.append([loop
, circular
])
1874 valid_single_loops
[len(valid_loops
)-1] = single_loops
[i
]
1876 return(valid_single_loops
, valid_loops
)
1879 # calculate the location of single input vertices that need to be flattened
1880 def circle_flatten_singles(bm_mod
, com
, p
, q
, normal
, single_loop
):
1882 for vert
in single_loop
:
1883 loc
= mathutils
.Vector(bm_mod
.verts
[vert
].co
[:])
1884 new_locs
.append([vert
, loc
- (loc
-com
).dot(normal
)*normal
])
1889 # calculate input loops
1890 def circle_get_input(object, bm
, scene
):
1891 # get mesh with modifiers applied
1892 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
1894 # create list of edge-keys based on selection state
1896 for face
in bm
.faces
:
1897 if face
.select
and not face
.hide
:
1901 # get selected, non-hidden , non-internal edge-keys
1902 eks_selected
= [key
for keys
in [face_edgekeys(face
) for face
in \
1903 bm_mod
.faces
if face
.select
and not face
.hide
] for key
in keys
]
1905 for ek
in eks_selected
:
1906 if ek
in edge_count
:
1910 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select \
1911 and not edge
.hide
and edge_count
.get(edgekey(edge
), 1)==1]
1913 # no faces, so no internal edges either
1914 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select \
1917 # add edge-keys around single vertices
1918 verts_connected
= dict([[vert
, 1] for edge
in [edge
for edge
in \
1919 bm_mod
.edges
if edge
.select
and not edge
.hide
] for vert
in \
1921 single_vertices
= [vert
.index
for vert
in bm_mod
.verts
if \
1922 vert
.select
and not vert
.hide
and not \
1923 verts_connected
.get(vert
.index
, False)]
1925 if single_vertices
and len(bm
.faces
)>0:
1926 vert_to_single
= dict([[v
.index
, []] for v
in bm_mod
.verts \
1928 for face
in [face
for face
in bm_mod
.faces
if not face
.select \
1930 for vert
in face
.verts
:
1932 if vert
in single_vertices
:
1933 for ek
in face_edgekeys(face
):
1935 edge_keys
.append(ek
)
1936 if vert
not in vert_to_single
[ek
[0]]:
1937 vert_to_single
[ek
[0]].append(vert
)
1938 if vert
not in vert_to_single
[ek
[1]]:
1939 vert_to_single
[ek
[1]].append(vert
)
1942 # sort edge-keys into loops
1943 loops
= get_connected_selections(edge_keys
)
1945 # find out to which loops the single vertices belong
1946 single_loops
= dict([[i
, []] for i
in range(len(loops
))])
1947 if single_vertices
and len(bm
.faces
)>0:
1948 for i
, [loop
, circular
] in enumerate(loops
):
1950 if vert_to_single
[vert
]:
1951 for single
in vert_to_single
[vert
]:
1952 if single
not in single_loops
[i
]:
1953 single_loops
[i
].append(single
)
1955 return(derived
, bm_mod
, single_vertices
, single_loops
, loops
)
1958 # recalculate positions based on the influence of the circle shape
1959 def circle_influence_locs(locs_2d
, new_locs_2d
, influence
):
1960 for i
in range(len(locs_2d
)):
1961 oldx
, oldy
, j
= locs_2d
[i
]
1962 newx
, newy
, k
= new_locs_2d
[i
]
1963 altx
= newx
*(influence
/100)+ oldx
*((100-influence
)/100)
1964 alty
= newy
*(influence
/100)+ oldy
*((100-influence
)/100)
1965 locs_2d
[i
] = [altx
, alty
, j
]
1970 # project 2d locations on circle, respecting distance relations between verts
1971 def circle_project_non_regular(locs_2d
, x0
, y0
, r
):
1972 for i
in range(len(locs_2d
)):
1973 x
, y
, j
= locs_2d
[i
]
1974 loc
= mathutils
.Vector([x
-x0
, y
-y0
])
1976 locs_2d
[i
] = [loc
[0], loc
[1], j
]
1981 # project 2d locations on circle, with equal distance between all vertices
1982 def circle_project_regular(locs_2d
, x0
, y0
, r
):
1983 # find offset angle and circling direction
1984 x
, y
, i
= locs_2d
[0]
1985 loc
= mathutils
.Vector([x
-x0
, y
-y0
])
1987 offset_angle
= loc
.angle(mathutils
.Vector([1.0, 0.0]), 0.0)
1988 loca
= mathutils
.Vector([x
-x0
, y
-y0
, 0.0])
1991 x
, y
, j
= locs_2d
[1]
1992 locb
= mathutils
.Vector([x
-x0
, y
-y0
, 0.0])
1993 if loca
.cross(locb
)[2] >= 0:
1997 # distribute vertices along the circle
1998 for i
in range(len(locs_2d
)):
1999 t
= offset_angle
+ ccw
* (i
/ len(locs_2d
) * 2 * math
.pi
)
2002 locs_2d
[i
] = [x
, y
, locs_2d
[i
][2]]
2007 # shift loop, so the first vertex is closest to the center
2008 def circle_shift_loop(bm_mod
, loop
, com
):
2009 verts
, circular
= loop
2010 distances
= [[(bm_mod
.verts
[vert
].co
- com
).length
, i
] \
2011 for i
, vert
in enumerate(verts
)]
2013 shift
= distances
[0][1]
2014 loop
= [verts
[shift
:] + verts
[:shift
], circular
]
2019 ##########################################
2020 ####### Curve functions ##################
2021 ##########################################
2023 # create lists with knots and points, all correctly sorted
2024 def curve_calculate_knots(loop
, verts_selected
):
2025 knots
= [v
for v
in loop
[0] if v
in verts_selected
]
2027 # circular loop, potential for weird splines
2029 offset
= int(len(loop
[0]) / 4)
2032 kpos
.append(loop
[0].index(k
))
2034 for i
in range(len(kpos
) - 1):
2035 kdif
.append(kpos
[i
+1] - kpos
[i
])
2036 kdif
.append(len(loop
[0]) - kpos
[-1] + kpos
[0])
2040 kadd
.append([kdif
.index(k
), True])
2041 # next 2 lines are optional, they insert
2042 # an extra control point in small gaps
2044 # kadd.append([kdif.index(k), False])
2047 for k
in kadd
: # extra knots to be added
2048 if k
[1]: # big gap (break circular spline)
2049 kpos
= loop
[0].index(knots
[k
[0]]) + offset
2050 if kpos
> len(loop
[0]) - 1:
2051 kpos
-= len(loop
[0])
2052 kins
.append([knots
[k
[0]], loop
[0][kpos
]])
2054 if kpos2
> len(knots
)-1:
2056 kpos2
= loop
[0].index(knots
[kpos2
]) - offset
2058 kpos2
+= len(loop
[0])
2059 kins
.append([loop
[0][kpos
], loop
[0][kpos2
]])
2060 krot
= loop
[0][kpos2
]
2061 else: # small gap (keep circular spline)
2062 k1
= loop
[0].index(knots
[k
[0]])
2064 if k2
> len(knots
)-1:
2066 k2
= loop
[0].index(knots
[k2
])
2068 dif
= len(loop
[0]) - 1 - k1
+ k2
2071 kn
= k1
+ int(dif
/2)
2072 if kn
> len(loop
[0]) - 1:
2074 kins
.append([loop
[0][k1
], loop
[0][kn
]])
2075 for j
in kins
: # insert new knots
2076 knots
.insert(knots
.index(j
[0]) + 1, j
[1])
2077 if not krot
: # circular loop
2078 knots
.append(knots
[0])
2079 points
= loop
[0][loop
[0].index(knots
[0]):]
2080 points
+= loop
[0][0:loop
[0].index(knots
[0]) + 1]
2081 else: # non-circular loop (broken by script)
2082 krot
= knots
.index(krot
)
2083 knots
= knots
[krot
:] + knots
[0:krot
]
2084 if loop
[0].index(knots
[0]) > loop
[0].index(knots
[-1]):
2085 points
= loop
[0][loop
[0].index(knots
[0]):]
2086 points
+= loop
[0][0:loop
[0].index(knots
[-1])+1]
2088 points
= loop
[0][loop
[0].index(knots
[0]):\
2089 loop
[0].index(knots
[-1]) + 1]
2090 # non-circular loop, add first and last point as knots
2092 if loop
[0][0] not in knots
:
2093 knots
.insert(0, loop
[0][0])
2094 if loop
[0][-1] not in knots
:
2095 knots
.append(loop
[0][-1])
2097 return(knots
, points
)
2100 # calculate relative positions compared to first knot
2101 def curve_calculate_t(bm_mod
, knots
, points
, pknots
, regular
, circular
):
2108 loc
= pknots
[knots
.index(p
)] # use projected knot location
2110 loc
= mathutils
.Vector(bm_mod
.verts
[p
].co
[:])
2113 len_total
+= (loc
-loc_prev
).length
2114 tpoints
.append(len_total
)
2119 tknots
.append(tpoints
[points
.index(p
)])
2121 tknots
[-1] = tpoints
[-1]
2125 tpoints_average
= tpoints
[-1] / (len(tpoints
) - 1)
2126 for i
in range(1, len(tpoints
) - 1):
2127 tpoints
[i
] = i
* tpoints_average
2128 for i
in range(len(knots
)):
2129 tknots
[i
] = tpoints
[points
.index(knots
[i
])]
2131 tknots
[-1] = tpoints
[-1]
2134 return(tknots
, tpoints
)
2137 # change the location of non-selected points to their place on the spline
2138 def curve_calculate_vertices(bm_mod
, knots
, tknots
, points
, tpoints
, splines
,
2139 interpolation
, restriction
):
2146 m
= tpoints
[points
.index(p
)]
2154 if n
> len(splines
) - 1:
2155 n
= len(splines
) - 1
2159 if interpolation
== 'cubic':
2160 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
2161 x
= ax
+ bx
*(m
-tx
) + cx
*(m
-tx
)**2 + dx
*(m
-tx
)**3
2162 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
2163 y
= ay
+ by
*(m
-ty
) + cy
*(m
-ty
)**2 + dy
*(m
-ty
)**3
2164 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
2165 z
= az
+ bz
*(m
-tz
) + cz
*(m
-tz
)**2 + dz
*(m
-tz
)**3
2166 newloc
= mathutils
.Vector([x
,y
,z
])
2167 else: # interpolation == 'linear'
2168 a
, d
, t
, u
= splines
[n
]
2169 newloc
= ((m
-t
)/u
)*d
+ a
2171 if restriction
!= 'none': # vertex movement is restricted
2173 else: # set the vertex to its new location
2174 move
.append([p
, newloc
])
2176 if restriction
!= 'none': # vertex movement is restricted
2181 move
.append([p
, bm_mod
.verts
[p
].co
])
2183 oldloc
= bm_mod
.verts
[p
].co
2184 normal
= bm_mod
.verts
[p
].normal
2185 dloc
= newloc
- oldloc
2186 if dloc
.length
< 1e-6:
2187 move
.append([p
, newloc
])
2188 elif restriction
== 'extrude': # only extrusions
2189 if dloc
.angle(normal
, 0) < 0.5 * math
.pi
+ 1e-6:
2190 move
.append([p
, newloc
])
2191 else: # restriction == 'indent' only indentations
2192 if dloc
.angle(normal
) > 0.5 * math
.pi
- 1e-6:
2193 move
.append([p
, newloc
])
2198 # trim loops to part between first and last selected vertices (including)
2199 def curve_cut_boundaries(bm_mod
, loops
):
2201 for loop
, circular
in loops
:
2204 cut_loops
.append([loop
, circular
])
2206 selected
= [bm_mod
.verts
[v
].select
for v
in loop
]
2207 first
= selected
.index(True)
2209 last
= -selected
.index(True)
2211 cut_loops
.append([loop
[first
:], circular
])
2213 cut_loops
.append([loop
[first
:last
], circular
])
2218 # calculate input loops
2219 def curve_get_input(object, bm
, boundaries
, scene
):
2220 # get mesh with modifiers applied
2221 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
2223 # vertices that still need a loop to run through it
2224 verts_unsorted
= [v
.index
for v
in bm_mod
.verts
if \
2225 v
.select
and not v
.hide
]
2226 # necessary dictionaries
2227 vert_edges
= dict_vert_edges(bm_mod
)
2228 edge_faces
= dict_edge_faces(bm_mod
)
2231 # find loops through each selected vertex
2232 while len(verts_unsorted
) > 0:
2233 loops
= curve_vertex_loops(bm_mod
, verts_unsorted
[0], vert_edges
,
2235 verts_unsorted
.pop(0)
2237 # check if loop is fully selected
2238 search_perpendicular
= False
2240 for loop
, circular
in loops
:
2242 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2243 if len(selected
) < 2:
2244 # only one selected vertex on loop, don't use
2247 elif len(selected
) == len(loop
):
2248 search_perpendicular
= loop
2250 # entire loop is selected, find perpendicular loops
2251 if search_perpendicular
:
2253 if vert
in verts_unsorted
:
2254 verts_unsorted
.remove(vert
)
2255 perp_loops
= curve_perpendicular_loops(bm_mod
, loop
,
2256 vert_edges
, edge_faces
)
2257 for perp_loop
in perp_loops
:
2258 correct_loops
.append(perp_loop
)
2261 for loop
, circular
in loops
:
2262 correct_loops
.append([loop
, circular
])
2266 correct_loops
= curve_cut_boundaries(bm_mod
, correct_loops
)
2268 return(derived
, bm_mod
, correct_loops
)
2271 # return all loops that are perpendicular to the given one
2272 def curve_perpendicular_loops(bm_mod
, start_loop
, vert_edges
, edge_faces
):
2273 # find perpendicular loops
2275 for start_vert
in start_loop
:
2276 loops
= curve_vertex_loops(bm_mod
, start_vert
, vert_edges
,
2278 for loop
, circular
in loops
:
2279 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2280 if len(selected
) == len(loop
):
2283 perp_loops
.append([loop
, circular
, loop
.index(start_vert
)])
2285 # trim loops to same lengths
2286 shortest
= [[len(loop
[0]), i
] for i
, loop
in enumerate(perp_loops
)\
2289 # all loops are circular, not trimming
2290 return([[loop
[0], loop
[1]] for loop
in perp_loops
])
2292 shortest
= min(shortest
)
2293 shortest_start
= perp_loops
[shortest
[1]][2]
2294 before_start
= shortest_start
2295 after_start
= shortest
[0] - shortest_start
- 1
2296 bigger_before
= before_start
> after_start
2298 for loop
in perp_loops
:
2299 # have the loop face the same direction as the shortest one
2301 if loop
[2] < len(loop
[0]) / 2:
2303 loop
[2] = len(loop
[0]) - loop
[2] - 1
2305 if loop
[2] > len(loop
[0]) / 2:
2307 loop
[2] = len(loop
[0]) - loop
[2] - 1
2308 # circular loops can shift, to prevent wrong trimming
2310 shift
= shortest_start
- loop
[2]
2311 if loop
[2] + shift
> 0 and loop
[2] + shift
< len(loop
[0]):
2312 loop
[0] = loop
[0][-shift
:] + loop
[0][:-shift
]
2315 loop
[2] += len(loop
[0])
2316 elif loop
[2] > len(loop
[0]) -1:
2317 loop
[2] -= len(loop
[0])
2319 start
= max(0, loop
[2] - before_start
)
2320 end
= min(len(loop
[0]), loop
[2] + after_start
+ 1)
2321 trimmed_loops
.append([loop
[0][start
:end
], False])
2323 return(trimmed_loops
)
2326 # project knots on non-selected geometry
2327 def curve_project_knots(bm_mod
, verts_selected
, knots
, points
, circular
):
2328 # function to project vertex on edge
2329 def project(v1
, v2
, v3
):
2330 # v1 and v2 are part of a line
2331 # v3 is projected onto it
2337 if circular
: # project all knots
2341 else: # first and last knot shouldn't be projected
2344 pknots
= [mathutils
.Vector(bm_mod
.verts
[knots
[0]].co
[:])]
2345 for knot
in knots
[start
:end
]:
2346 if knot
in verts_selected
:
2347 knot_left
= knot_right
= False
2348 for i
in range(points
.index(knot
)-1, -1*len(points
), -1):
2349 if points
[i
] not in knots
:
2350 knot_left
= points
[i
]
2352 for i
in range(points
.index(knot
)+1, 2*len(points
)):
2353 if i
> len(points
) - 1:
2355 if points
[i
] not in knots
:
2356 knot_right
= points
[i
]
2358 if knot_left
and knot_right
and knot_left
!= knot_right
:
2359 knot_left
= mathutils
.Vector(\
2360 bm_mod
.verts
[knot_left
].co
[:])
2361 knot_right
= mathutils
.Vector(\
2362 bm_mod
.verts
[knot_right
].co
[:])
2363 knot
= mathutils
.Vector(bm_mod
.verts
[knot
].co
[:])
2364 pknots
.append(project(knot_left
, knot_right
, knot
))
2366 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2367 else: # knot isn't selected, so shouldn't be changed
2368 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2370 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knots
[-1]].co
[:]))
2375 # find all loops through a given vertex
2376 def curve_vertex_loops(bm_mod
, start_vert
, vert_edges
, edge_faces
):
2380 for edge
in vert_edges
[start_vert
]:
2381 if edge
in edges_used
:
2386 active_faces
= edge_faces
[edge
]
2391 new_edges
= vert_edges
[new_vert
]
2392 loop
.append(new_vert
)
2394 edges_used
.append(tuple(sorted([loop
[-1], loop
[-2]])))
2395 if len(new_edges
) < 3 or len(new_edges
) > 4:
2400 for new_edge
in new_edges
:
2401 if new_edge
in edges_used
:
2404 for new_face
in edge_faces
[new_edge
]:
2405 if new_face
in active_faces
:
2410 # found correct new edge
2411 active_faces
= edge_faces
[new_edge
]
2417 if new_vert
== loop
[0]:
2425 loops
.append([loop
, circular
])
2430 ##########################################
2431 ####### Flatten functions ################
2432 ##########################################
2434 # sort input into loops
2435 def flatten_get_input(bm
):
2436 vert_verts
= dict_vert_verts([edgekey(edge
) for edge
in bm
.edges \
2437 if edge
.select
and not edge
.hide
])
2438 verts
= [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
2440 # no connected verts, consider all selected verts as a single input
2442 return([[verts
, False]])
2445 while len(verts
) > 0:
2449 if loop
[-1] in vert_verts
:
2450 to_grow
= vert_verts
[loop
[-1]]
2454 while len(to_grow
) > 0:
2455 new_vert
= to_grow
[0]
2457 if new_vert
in loop
:
2459 loop
.append(new_vert
)
2460 verts
.remove(new_vert
)
2461 to_grow
+= vert_verts
[new_vert
]
2463 loops
.append([loop
, False])
2468 # calculate position of vertex projections on plane
2469 def flatten_project(bm
, loop
, com
, normal
):
2470 verts
= [bm
.verts
[v
] for v
in loop
[0]]
2471 verts_projected
= [[v
.index
, mathutils
.Vector(v
.co
[:]) - \
2472 (mathutils
.Vector(v
.co
[:])-com
).dot(normal
)*normal
] for v
in verts
]
2474 return(verts_projected
)
2477 ##########################################
2478 ####### Gstretch functions ###############
2479 ##########################################
2481 # fake stroke class, used to create custom strokes if no GP data is found
2482 class gstretch_fake_stroke():
2483 def __init__(self
, points
):
2484 self
.points
= [gstretch_fake_stroke_point(p
) for p
in points
]
2487 # fake stroke point class, used in fake strokes
2488 class gstretch_fake_stroke_point():
2489 def __init__(self
, loc
):
2493 # flips loops, if necessary, to obtain maximum alignment to stroke
2494 def gstretch_align_pairs(ls_pairs
, object, bm_mod
, method
):
2495 # returns total distance between all verts in loop and corresponding stroke
2496 def distance_loop_stroke(loop
, stroke
, object, bm_mod
, method
):
2497 stroke_lengths_cache
= False
2498 loop_length
= len(loop
[0])
2501 if method
!= 'regular':
2502 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2504 for i
, v_index
in enumerate(loop
[0]):
2505 if method
== 'regular':
2506 relative_distance
= i
/ (loop_length
- 1)
2508 relative_distance
= relative_lengths
[i
]
2510 loc1
= object.matrix_world
* bm_mod
.verts
[v_index
].co
2511 loc2
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2512 relative_distance
, stroke_lengths_cache
)
2513 total_distance
+= (loc2
- loc1
).length
2515 return(total_distance
)
2518 for (loop
, stroke
) in ls_pairs
:
2519 distance_loop_stroke
2520 total_dist
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2523 total_dist_rev
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2525 if total_dist_rev
> total_dist
:
2531 # calculate vertex positions on stroke
2532 def gstretch_calculate_verts(loop
, stroke
, object, bm_mod
, method
):
2534 stroke_lengths_cache
= False
2535 loop_length
= len(loop
[0])
2536 matrix_inverse
= object.matrix_world
.inverted()
2538 # return intersection of line with stroke, or None
2539 def intersect_line_stroke(vec1
, vec2
, stroke
):
2540 for i
, p
in enumerate(stroke
.points
[1:]):
2541 intersections
= mathutils
.geometry
.intersect_line_line(vec1
, vec2
,
2542 p
.co
, stroke
.points
[i
].co
)
2543 if intersections
and \
2544 (intersections
[0] - intersections
[1]).length
< 1e-2:
2545 x
, dist
= mathutils
.geometry
.intersect_point_line(
2546 intersections
[0], p
.co
, stroke
.points
[i
].co
)
2548 return(intersections
[0])
2551 if method
== 'project':
2552 projection_vectors
= []
2553 vert_edges
= dict_vert_edges(bm_mod
)
2555 for v_index
in loop
[0]:
2556 for ek
in vert_edges
[v_index
]:
2558 v1
= bm_mod
.verts
[v1
]
2559 v2
= bm_mod
.verts
[v2
]
2560 if v1
.select
+ v2
.select
== 1 and not v1
.hide
and not v2
.hide
:
2561 vec1
= object.matrix_world
* v1
.co
2562 vec2
= object.matrix_world
* v2
.co
2563 intersection
= intersect_line_stroke(vec1
, vec2
, stroke
)
2566 if not intersection
:
2567 v
= bm_mod
.verts
[v_index
]
2568 intersection
= intersect_line_stroke(v
.co
, v
.co
+ v
.normal
,
2571 move
.append([v_index
, matrix_inverse
* intersection
])
2574 if method
== 'irregular':
2575 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2577 for i
, v_index
in enumerate(loop
[0]):
2578 if method
== 'regular':
2579 relative_distance
= i
/ (loop_length
- 1)
2580 else: # method == 'irregular'
2581 relative_distance
= relative_lengths
[i
]
2582 loc
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2583 relative_distance
, stroke_lengths_cache
)
2584 loc
= matrix_inverse
* loc
2585 move
.append([v_index
, loc
])
2590 # create new vertices, based on GP strokes
2591 def gstretch_create_verts(object, bm_mod
, strokes
, method
, conversion
,
2592 conversion_distance
, conversion_max
, conversion_min
, conversion_vertices
):
2595 mat_world
= object.matrix_world
.inverted()
2596 singles
= gstretch_match_single_verts(bm_mod
, strokes
, mat_world
)
2598 for stroke
in strokes
:
2599 stroke_verts
.append([stroke
, []])
2601 if conversion
== 'vertices':
2602 min_end_point
= conversion_vertices
2603 end_point
= conversion_vertices
2604 elif conversion
== 'limit_vertices':
2605 min_end_point
= conversion_min
2606 end_point
= conversion_max
2608 end_point
= len(stroke
.points
)
2609 # creation of new vertices at fixed user-defined distances
2610 if conversion
== 'distance':
2612 prev_point
= stroke
.points
[0]
2613 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* \
2616 limit
= conversion_distance
2617 for point
in stroke
.points
:
2618 new_distance
= distance
+ (point
.co
- prev_point
.co
).length
2620 while new_distance
> limit
:
2621 to_cover
= limit
- distance
+ (limit
* iteration
)
2622 new_loc
= prev_point
.co
+ to_cover
* \
2623 (point
.co
- prev_point
.co
).normalized()
2624 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* \
2626 new_distance
-= limit
2628 distance
= new_distance
2630 # creation of new vertices for other methods
2632 # add vertices at stroke points
2633 for point
in stroke
.points
[:end_point
]:
2634 stroke_verts
[-1][1].append(bm_mod
.verts
.new(\
2635 mat_world
* point
.co
))
2636 # add more vertices, beyond the points that are available
2637 if min_end_point
> min(len(stroke
.points
), end_point
):
2638 for i
in range(min_end_point
-
2639 (min(len(stroke
.points
), end_point
))):
2640 stroke_verts
[-1][1].append(bm_mod
.verts
.new(\
2641 mat_world
* point
.co
))
2642 # force even spreading of points, so they are placed on stroke
2644 bm_mod
.verts
.index_update()
2645 for stroke
, verts_seq
in stroke_verts
:
2646 if len(verts_seq
) < 2:
2648 # spread vertices evenly over the stroke
2649 if method
== 'regular':
2650 loop
= [[vert
.index
for vert
in verts_seq
], False]
2651 move
+= gstretch_calculate_verts(loop
, stroke
, object, bm_mod
,
2654 for i
, vert
in enumerate(verts_seq
):
2656 bm_mod
.edges
.new((verts_seq
[i
-1], verts_seq
[i
]))
2658 # connect single vertices to the closest stroke
2660 for vert
, m_stroke
, point
in singles
:
2661 if m_stroke
!= stroke
:
2663 bm_mod
.edges
.new((vert
, verts_seq
[point
]))
2665 bmesh
.update_edit_mesh(object.data
)
2670 # erases the grease pencil stroke
2671 def gstretch_erase_stroke(stroke
, context
):
2672 # change 3d coordinate into a stroke-point
2673 def sp(loc
, context
):
2677 'location': (0, 0, 0),
2678 'mouse': (view3d_utils
.location_3d_to_region_2d(\
2679 context
.region
, context
.space_data
.region_3d
, loc
)),
2684 if type(stroke
) != bpy
.types
.GPencilStroke
:
2685 # fake stroke, there is nothing to delete
2688 erase_stroke
= [sp(p
.co
, context
) for p
in stroke
.points
]
2690 erase_stroke
[0]['is_start'] = True
2691 bpy
.ops
.gpencil
.draw(mode
='ERASER', stroke
=erase_stroke
)
2694 # get point on stroke, given by relative distance (0.0 - 1.0)
2695 def gstretch_eval_stroke(stroke
, distance
, stroke_lengths_cache
=False):
2696 # use cache if available
2697 if not stroke_lengths_cache
:
2699 for i
, p
in enumerate(stroke
.points
[1:]):
2700 lengths
.append((p
.co
- stroke
.points
[i
].co
).length
+ \
2702 total_length
= max(lengths
[-1], 1e-7)
2703 stroke_lengths_cache
= [length
/ total_length
for length
in
2705 stroke_lengths
= stroke_lengths_cache
[:]
2707 if distance
in stroke_lengths
:
2708 loc
= stroke
.points
[stroke_lengths
.index(distance
)].co
2709 elif distance
> stroke_lengths
[-1]:
2710 # should be impossible, but better safe than sorry
2711 loc
= stroke
.points
[-1].co
2713 stroke_lengths
.append(distance
)
2714 stroke_lengths
.sort()
2715 stroke_index
= stroke_lengths
.index(distance
)
2716 interval_length
= stroke_lengths
[stroke_index
+1] - \
2717 stroke_lengths
[stroke_index
-1]
2718 distance_relative
= (distance
- stroke_lengths
[stroke_index
-1]) / \
2720 interval_vector
= stroke
.points
[stroke_index
].co
- \
2721 stroke
.points
[stroke_index
-1].co
2722 loc
= stroke
.points
[stroke_index
-1].co
+ \
2723 distance_relative
* interval_vector
2725 return(loc
, stroke_lengths_cache
)
2728 # create fake grease pencil strokes for the active object
2729 def gstretch_get_fake_strokes(object, bm_mod
, loops
):
2732 p1
= object.matrix_world
* bm_mod
.verts
[loop
[0][0]].co
2733 p2
= object.matrix_world
* bm_mod
.verts
[loop
[0][-1]].co
2734 strokes
.append(gstretch_fake_stroke([p1
, p2
]))
2739 # get grease pencil strokes for the active object
2740 def gstretch_get_strokes(object):
2741 gp
= object.grease_pencil
2744 layer
= gp
.layers
.active
2747 frame
= layer
.active_frame
2750 strokes
= frame
.strokes
2751 if len(strokes
) < 1:
2757 # returns a list with loop-stroke pairs
2758 def gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
):
2759 if not loops
or not strokes
:
2762 # calculate loop centers
2765 center
= mathutils
.Vector()
2766 for v_index
in loop
[0]:
2767 center
+= bm_mod
.verts
[v_index
].co
2768 center
/= len(loop
[0])
2769 center
= object.matrix_world
* center
2770 loop_centers
.append([center
, loop
])
2772 # calculate stroke centers
2774 for stroke
in strokes
:
2775 center
= mathutils
.Vector()
2776 for p
in stroke
.points
:
2778 center
/= len(stroke
.points
)
2779 stroke_centers
.append([center
, stroke
, 0])
2781 # match, first by stroke use count, then by distance
2783 for lc
in loop_centers
:
2785 for i
, sc
in enumerate(stroke_centers
):
2786 distances
.append([sc
[2], (lc
[0] - sc
[0]).length
, i
])
2788 best_stroke
= distances
[0][2]
2789 ls_pairs
.append([lc
[1], stroke_centers
[best_stroke
][1]])
2790 stroke_centers
[best_stroke
][2] += 1 # increase stroke use count
2795 # match single selected vertices to the closest stroke endpoint
2796 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2797 def gstretch_match_single_verts(bm_mod
, strokes
, mat_world
):
2798 # calculate stroke endpoints in object space
2800 for stroke
in strokes
:
2801 endpoints
.append((mat_world
* stroke
.points
[0].co
, stroke
, 0))
2802 endpoints
.append((mat_world
* stroke
.points
[-1].co
, stroke
, -1))
2805 # find single vertices (not connected to other selected verts)
2806 for vert
in bm_mod
.verts
:
2810 for edge
in vert
.link_edges
:
2811 if edge
.other_vert(vert
).select
:
2816 # calculate distances from vertex to endpoints
2817 distance
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2818 endpoint_index
) for endpoint_index
, (loc
, stroke
, stroke_point
) in
2819 enumerate(endpoints
)]
2821 distances
.append(distance
[0])
2823 # create matches, based on shortest distance first
2827 singles
.append((distances
[0][1], distances
[0][2], distances
[0][3]))
2828 endpoints
.pop(distances
[0][4])
2831 for (i
, vert
, j
, k
, l
) in distances
:
2832 distance_new
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2833 endpoint_index
) for endpoint_index
, (loc
, stroke
,
2834 stroke_point
) in enumerate(endpoints
)]
2836 distances_new
.append(distance_new
[0])
2837 distances
= distances_new
2842 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2843 def gstretch_relative_lengths(loop
, bm_mod
):
2845 for i
, v_index
in enumerate(loop
[0][1:]):
2846 lengths
.append((bm_mod
.verts
[v_index
].co
- \
2847 bm_mod
.verts
[loop
[0][i
]].co
).length
+ lengths
[-1])
2848 total_length
= max(lengths
[-1], 1e-7)
2849 relative_lengths
= [length
/ total_length
for length
in
2852 return(relative_lengths
)
2855 # convert cache-stored strokes into usable (fake) GP strokes
2856 def gstretch_safe_to_true_strokes(safe_strokes
):
2858 for safe_stroke
in safe_strokes
:
2859 strokes
.append(gstretch_fake_stroke(safe_stroke
))
2864 # convert a GP stroke into a list of points which can be stored in cache
2865 def gstretch_true_to_safe_strokes(strokes
):
2867 for stroke
in strokes
:
2868 safe_strokes
.append([p
.co
.copy() for p
in stroke
.points
])
2870 return(safe_strokes
)
2873 # force consistency in GUI, max value can never be lower than min value
2874 def gstretch_update_max(self
, context
):
2875 # called from operator settings (after execution)
2876 if 'conversion_min' in self
.keys():
2877 if self
.conversion_min
> self
.conversion_max
:
2878 self
.conversion_max
= self
.conversion_min
2879 # called from toolbar
2881 lt
= context
.window_manager
.looptools
2882 if lt
.gstretch_conversion_min
> lt
.gstretch_conversion_max
:
2883 lt
.gstretch_conversion_max
= lt
.gstretch_conversion_min
2886 # force consistency in GUI, min value can never be higher than max value
2887 def gstretch_update_min(self
, context
):
2888 # called from operator settings (after execution)
2889 if 'conversion_max' in self
.keys():
2890 if self
.conversion_max
< self
.conversion_min
:
2891 self
.conversion_min
= self
.conversion_max
2892 # called from toolbar
2894 lt
= context
.window_manager
.looptools
2895 if lt
.gstretch_conversion_max
< lt
.gstretch_conversion_min
:
2896 lt
.gstretch_conversion_min
= lt
.gstretch_conversion_max
2899 ##########################################
2900 ####### Relax functions ##################
2901 ##########################################
2903 # create lists with knots and points, all correctly sorted
2904 def relax_calculate_knots(loops
):
2907 for loop
, circular
in loops
:
2911 if len(loop
)%2 == 1: # odd
2912 extend
= [False, True, 0, 1, 0, 1]
2914 extend
= [True, False, 0, 1, 1, 2]
2916 if len(loop
)%2 == 1: # odd
2917 extend
= [False, False, 0, 1, 1, 2]
2919 extend
= [False, False, 0, 1, 1, 2]
2922 loop
= [loop
[-1]] + loop
+ [loop
[0]]
2923 for i
in range(extend
[2+2*j
], len(loop
), 2):
2924 knots
[j
].append(loop
[i
])
2925 for i
in range(extend
[3+2*j
], len(loop
), 2):
2926 if loop
[i
] == loop
[-1] and not circular
:
2928 if len(points
[j
]) == 0:
2929 points
[j
].append(loop
[i
])
2930 elif loop
[i
] != points
[j
][0]:
2931 points
[j
].append(loop
[i
])
2933 if knots
[j
][0] != knots
[j
][-1]:
2934 knots
[j
].append(knots
[j
][0])
2935 if len(points
[1]) == 0:
2941 all_points
.append(p
)
2943 return(all_knots
, all_points
)
2946 # calculate relative positions compared to first knot
2947 def relax_calculate_t(bm_mod
, knots
, points
, regular
):
2950 for i
in range(len(knots
)):
2951 amount
= len(knots
[i
]) + len(points
[i
])
2953 for j
in range(amount
):
2955 mix
.append([True, knots
[i
][round(j
/2)]])
2957 mix
.append([True, knots
[i
][-1]])
2959 mix
.append([False, points
[i
][int(j
/2)]])
2965 loc
= mathutils
.Vector(bm_mod
.verts
[m
[1]].co
[:])
2968 len_total
+= (loc
- loc_prev
).length
2970 tknots
.append(len_total
)
2972 tpoints
.append(len_total
)
2976 for p
in range(len(points
[i
])):
2977 tpoints
.append((tknots
[p
] + tknots
[p
+1]) / 2)
2978 all_tknots
.append(tknots
)
2979 all_tpoints
.append(tpoints
)
2981 return(all_tknots
, all_tpoints
)
2984 # change the location of the points to their place on the spline
2985 def relax_calculate_verts(bm_mod
, interpolation
, tknots
, knots
, tpoints
,
2989 for i
in range(len(knots
)):
2991 m
= tpoints
[i
][points
[i
].index(p
)]
2993 n
= tknots
[i
].index(m
)
2999 if n
> len(splines
[i
]) - 1:
3000 n
= len(splines
[i
]) - 1
3004 if interpolation
== 'cubic':
3005 ax
, bx
, cx
, dx
, tx
= splines
[i
][n
][0]
3006 x
= ax
+ bx
*(m
-tx
) + cx
*(m
-tx
)**2 + dx
*(m
-tx
)**3
3007 ay
, by
, cy
, dy
, ty
= splines
[i
][n
][1]
3008 y
= ay
+ by
*(m
-ty
) + cy
*(m
-ty
)**2 + dy
*(m
-ty
)**3
3009 az
, bz
, cz
, dz
, tz
= splines
[i
][n
][2]
3010 z
= az
+ bz
*(m
-tz
) + cz
*(m
-tz
)**2 + dz
*(m
-tz
)**3
3011 change
.append([p
, mathutils
.Vector([x
,y
,z
])])
3012 else: # interpolation == 'linear'
3013 a
, d
, t
, u
= splines
[i
][n
]
3016 change
.append([p
, ((m
-t
)/u
)*d
+ a
])
3018 move
.append([c
[0], (bm_mod
.verts
[c
[0]].co
+ c
[1]) / 2])
3023 ##########################################
3024 ####### Space functions ##################
3025 ##########################################
3027 # calculate relative positions compared to first knot
3028 def space_calculate_t(bm_mod
, knots
):
3033 loc
= mathutils
.Vector(bm_mod
.verts
[k
].co
[:])
3036 len_total
+= (loc
- loc_prev
).length
3037 tknots
.append(len_total
)
3040 t_per_segment
= len_total
/ (amount
- 1)
3041 tpoints
= [i
* t_per_segment
for i
in range(amount
)]
3043 return(tknots
, tpoints
)
3046 # change the location of the points to their place on the spline
3047 def space_calculate_verts(bm_mod
, interpolation
, tknots
, tpoints
, points
,
3051 m
= tpoints
[points
.index(p
)]
3059 if n
> len(splines
) - 1:
3060 n
= len(splines
) - 1
3064 if interpolation
== 'cubic':
3065 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
3066 x
= ax
+ bx
*(m
-tx
) + cx
*(m
-tx
)**2 + dx
*(m
-tx
)**3
3067 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
3068 y
= ay
+ by
*(m
-ty
) + cy
*(m
-ty
)**2 + dy
*(m
-ty
)**3
3069 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
3070 z
= az
+ bz
*(m
-tz
) + cz
*(m
-tz
)**2 + dz
*(m
-tz
)**3
3071 move
.append([p
, mathutils
.Vector([x
,y
,z
])])
3072 else: # interpolation == 'linear'
3073 a
, d
, t
, u
= splines
[n
]
3074 move
.append([p
, ((m
-t
)/u
)*d
+ a
])
3079 ##########################################
3080 ####### Operators ########################
3081 ##########################################
3084 class Bridge(bpy
.types
.Operator
):
3085 bl_idname
= 'mesh.looptools_bridge'
3086 bl_label
= "Bridge / Loft"
3087 bl_description
= "Bridge two, or loft several, loops of vertices"
3088 bl_options
= {'REGISTER', 'UNDO'}
3090 cubic_strength
= bpy
.props
.FloatProperty(name
= "Strength",
3091 description
= "Higher strength results in more fluid curves",
3095 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation mode",
3096 items
= (('cubic', "Cubic", "Gives curved results"),
3097 ('linear', "Linear", "Basic, fast, straight interpolation")),
3098 description
= "Interpolation mode: algorithm used when creating "\
3101 loft
= bpy
.props
.BoolProperty(name
= "Loft",
3102 description
= "Loft multiple loops, instead of considering them as "\
3103 "a multi-input for bridging",
3105 loft_loop
= bpy
.props
.BoolProperty(name
= "Loop",
3106 description
= "Connect the first and the last loop with each other",
3108 min_width
= bpy
.props
.IntProperty(name
= "Minimum width",
3109 description
= "Segments with an edge smaller than this are merged "\
3110 "(compared to base edge)",
3114 subtype
= 'PERCENTAGE')
3115 mode
= bpy
.props
.EnumProperty(name
= "Mode",
3116 items
= (('basic', "Basic", "Fast algorithm"), ('shortest',
3117 "Shortest edge", "Slower algorithm with better vertex matching")),
3118 description
= "Algorithm used for bridging",
3119 default
= 'shortest')
3120 remove_faces
= bpy
.props
.BoolProperty(name
= "Remove faces",
3121 description
= "Remove faces that are internal after bridging",
3123 reverse
= bpy
.props
.BoolProperty(name
= "Reverse",
3124 description
= "Manually override the direction in which the loops "\
3125 "are bridged. Only use if the tool gives the wrong " \
3128 segments
= bpy
.props
.IntProperty(name
= "Segments",
3129 description
= "Number of segments used to bridge the gap "\
3134 twist
= bpy
.props
.IntProperty(name
= "Twist",
3135 description
= "Twist what vertices are connected to each other",
3139 def poll(cls
, context
):
3140 ob
= context
.active_object
3141 return (ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3143 def draw(self
, context
):
3144 layout
= self
.layout
3145 #layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3148 col_top
= layout
.column(align
=True)
3149 row
= col_top
.row(align
=True)
3150 col_left
= row
.column(align
=True)
3151 col_right
= row
.column(align
=True)
3152 col_right
.active
= self
.segments
!= 1
3153 col_left
.prop(self
, "segments")
3154 col_right
.prop(self
, "min_width", text
="")
3156 bottom_left
= col_left
.row()
3157 bottom_left
.active
= self
.segments
!= 1
3158 bottom_left
.prop(self
, "interpolation", text
="")
3159 bottom_right
= col_right
.row()
3160 bottom_right
.active
= self
.interpolation
== 'cubic'
3161 bottom_right
.prop(self
, "cubic_strength")
3162 # boolean properties
3163 col_top
.prop(self
, "remove_faces")
3165 col_top
.prop(self
, "loft_loop")
3167 # override properties
3169 row
= layout
.row(align
= True)
3170 row
.prop(self
, "twist")
3171 row
.prop(self
, "reverse")
3173 def invoke(self
, context
, event
):
3174 # load custom settings
3175 context
.window_manager
.looptools
.bridge_loft
= self
.loft
3177 return self
.execute(context
)
3179 def execute(self
, context
):
3181 global_undo
, object, bm
= initialise()
3182 edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
= \
3183 bridge_initialise(bm
, self
.interpolation
)
3184 settings_write(self
)
3186 # check cache to see if we can save time
3187 input_method
= bridge_input_method(self
.loft
, self
.loft_loop
)
3188 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Bridge",
3189 object, bm
, input_method
, False)
3192 loops
= bridge_get_input(bm
)
3194 # reorder loops if there are more than 2
3197 loops
= bridge_sort_loops(bm
, loops
, self
.loft_loop
)
3199 loops
= bridge_match_loops(bm
, loops
)
3201 # saving cache for faster execution next time
3203 cache_write("Bridge", object, bm
, input_method
, False, False,
3204 loops
, False, False)
3207 # calculate new geometry
3210 max_vert_index
= len(bm
.verts
)-1
3211 for i
in range(1, len(loops
)):
3212 if not self
.loft
and i
%2 == 0:
3214 lines
= bridge_calculate_lines(bm
, loops
[i
-1:i
+1],
3215 self
.mode
, self
.twist
, self
.reverse
)
3216 vertex_normals
= bridge_calculate_virtual_vertex_normals(bm
,
3217 lines
, loops
[i
-1:i
+1], edge_faces
, edgekey_to_edge
)
3218 segments
= bridge_calculate_segments(bm
, lines
,
3219 loops
[i
-1:i
+1], self
.segments
)
3220 new_verts
, new_faces
, max_vert_index
= \
3221 bridge_calculate_geometry(bm
, lines
, vertex_normals
,
3222 segments
, self
.interpolation
, self
.cubic_strength
,
3223 self
.min_width
, max_vert_index
)
3225 vertices
+= new_verts
3228 # make sure faces in loops that aren't used, aren't removed
3229 if self
.remove_faces
and old_selected_faces
:
3230 bridge_save_unused_faces(bm
, old_selected_faces
, loops
)
3233 bridge_create_vertices(bm
, vertices
)
3236 new_faces
= bridge_create_faces(object, bm
, faces
, self
.twist
)
3237 old_selected_faces
= [i
for i
, face
in enumerate(bm
.faces
) \
3238 if face
.index
in old_selected_faces
] # updating list
3239 bridge_select_new_faces(new_faces
, smooth
)
3240 # edge-data could have changed, can't use cache next run
3241 if faces
and not vertices
:
3242 cache_delete("Bridge")
3243 # delete internal faces
3244 if self
.remove_faces
and old_selected_faces
:
3245 bridge_remove_internal_faces(bm
, old_selected_faces
)
3246 # make sure normals are facing outside
3247 bmesh
.update_edit_mesh(object.data
, tessface
=False,
3249 bpy
.ops
.mesh
.normals_make_consistent()
3252 terminate(global_undo
)
3258 class Circle(bpy
.types
.Operator
):
3259 bl_idname
= "mesh.looptools_circle"
3261 bl_description
= "Move selected vertices into a circle shape"
3262 bl_options
= {'REGISTER', 'UNDO'}
3264 custom_radius
= bpy
.props
.BoolProperty(name
= "Radius",
3265 description
= "Force a custom radius",
3267 fit
= bpy
.props
.EnumProperty(name
= "Method",
3268 items
= (("best", "Best fit", "Non-linear least squares"),
3269 ("inside", "Fit inside","Only move vertices towards the center")),
3270 description
= "Method used for fitting a circle to the vertices",
3272 flatten
= bpy
.props
.BoolProperty(name
= "Flatten",
3273 description
= "Flatten the circle, instead of projecting it on the " \
3276 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3277 description
= "Force of the tool",
3282 subtype
= 'PERCENTAGE')
3283 radius
= bpy
.props
.FloatProperty(name
= "Radius",
3284 description
= "Custom radius for circle",
3288 regular
= bpy
.props
.BoolProperty(name
= "Regular",
3289 description
= "Distribute vertices at constant distances along the " \
3294 def poll(cls
, context
):
3295 ob
= context
.active_object
3296 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3298 def draw(self
, context
):
3299 layout
= self
.layout
3300 col
= layout
.column()
3302 col
.prop(self
, "fit")
3305 col
.prop(self
, "flatten")
3306 row
= col
.row(align
=True)
3307 row
.prop(self
, "custom_radius")
3308 row_right
= row
.row(align
=True)
3309 row_right
.active
= self
.custom_radius
3310 row_right
.prop(self
, "radius", text
="")
3311 col
.prop(self
, "regular")
3314 col
.prop(self
, "influence")
3316 def invoke(self
, context
, event
):
3317 # load custom settings
3319 return self
.execute(context
)
3321 def execute(self
, context
):
3323 global_undo
, object, bm
= initialise()
3324 settings_write(self
)
3325 # check cache to see if we can save time
3326 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Circle",
3327 object, bm
, False, False)
3329 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3332 derived
, bm_mod
, single_vertices
, single_loops
, loops
= \
3333 circle_get_input(object, bm
, context
.scene
)
3334 mapping
= get_mapping(derived
, bm
, bm_mod
, single_vertices
,
3336 single_loops
, loops
= circle_check_loops(single_loops
, loops
,
3339 # saving cache for faster execution next time
3341 cache_write("Circle", object, bm
, False, False, single_loops
,
3342 loops
, derived
, mapping
)
3345 for i
, loop
in enumerate(loops
):
3346 # best fitting flat plane
3347 com
, normal
= calculate_plane(bm_mod
, loop
)
3348 # if circular, shift loop so we get a good starting vertex
3350 loop
= circle_shift_loop(bm_mod
, loop
, com
)
3351 # flatten vertices on plane
3352 locs_2d
, p
, q
= circle_3d_to_2d(bm_mod
, loop
, com
, normal
)
3354 if self
.fit
== 'best':
3355 x0
, y0
, r
= circle_calculate_best_fit(locs_2d
)
3356 else: # self.fit == 'inside'
3357 x0
, y0
, r
= circle_calculate_min_fit(locs_2d
)
3359 if self
.custom_radius
:
3360 r
= self
.radius
/ p
.length
3361 # calculate positions on circle
3363 new_locs_2d
= circle_project_regular(locs_2d
[:], x0
, y0
, r
)
3365 new_locs_2d
= circle_project_non_regular(locs_2d
[:], x0
, y0
, r
)
3366 # take influence into account
3367 locs_2d
= circle_influence_locs(locs_2d
, new_locs_2d
,
3369 # calculate 3d positions of the created 2d input
3370 move
.append(circle_calculate_verts(self
.flatten
, bm_mod
,
3371 locs_2d
, com
, p
, q
, normal
))
3372 # flatten single input vertices on plane defined by loop
3373 if self
.flatten
and single_loops
:
3374 move
.append(circle_flatten_singles(bm_mod
, com
, p
, q
,
3375 normal
, single_loops
[i
]))
3377 # move vertices to new locations
3378 move_verts(object, bm
, mapping
, move
, -1)
3383 terminate(global_undo
)
3389 class Curve(bpy
.types
.Operator
):
3390 bl_idname
= "mesh.looptools_curve"
3392 bl_description
= "Turn a loop into a smooth curve"
3393 bl_options
= {'REGISTER', 'UNDO'}
3395 boundaries
= bpy
.props
.BoolProperty(name
= "Boundaries",
3396 description
= "Limit the tool to work within the boundaries of the "\
3397 "selected vertices",
3399 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3400 description
= "Force of the tool",
3405 subtype
= 'PERCENTAGE')
3406 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
3407 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3408 ("linear", "Linear", "Simple and fast linear algorithm")),
3409 description
= "Algorithm used for interpolation",
3411 regular
= bpy
.props
.BoolProperty(name
= "Regular",
3412 description
= "Distribute vertices at constant distances along the" \
3415 restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
3416 items
= (("none", "None", "No restrictions on vertex movement"),
3417 ("extrude", "Extrude only","Only allow extrusions (no "\
3419 ("indent", "Indent only", "Only allow indentation (no "\
3421 description
= "Restrictions on how the vertices can be moved",
3425 def poll(cls
, context
):
3426 ob
= context
.active_object
3427 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3429 def draw(self
, context
):
3430 layout
= self
.layout
3431 col
= layout
.column()
3433 col
.prop(self
, "interpolation")
3434 col
.prop(self
, "restriction")
3435 col
.prop(self
, "boundaries")
3436 col
.prop(self
, "regular")
3439 col
.prop(self
, "influence")
3441 def invoke(self
, context
, event
):
3442 # load custom settings
3444 return self
.execute(context
)
3446 def execute(self
, context
):
3448 global_undo
, object, bm
= initialise()
3449 settings_write(self
)
3450 # check cache to see if we can save time
3451 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Curve",
3452 object, bm
, False, self
.boundaries
)
3454 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3457 derived
, bm_mod
, loops
= curve_get_input(object, bm
,
3458 self
.boundaries
, context
.scene
)
3459 mapping
= get_mapping(derived
, bm
, bm_mod
, False, True, loops
)
3460 loops
= check_loops(loops
, mapping
, bm_mod
)
3461 verts_selected
= [v
.index
for v
in bm_mod
.verts
if v
.select \
3464 # saving cache for faster execution next time
3466 cache_write("Curve", object, bm
, False, self
.boundaries
, False,
3467 loops
, derived
, mapping
)
3471 knots
, points
= curve_calculate_knots(loop
, verts_selected
)
3472 pknots
= curve_project_knots(bm_mod
, verts_selected
, knots
,
3474 tknots
, tpoints
= curve_calculate_t(bm_mod
, knots
, points
,
3475 pknots
, self
.regular
, loop
[1])
3476 splines
= calculate_splines(self
.interpolation
, bm_mod
,
3478 move
.append(curve_calculate_vertices(bm_mod
, knots
, tknots
,
3479 points
, tpoints
, splines
, self
.interpolation
,
3482 # move vertices to new locations
3483 move_verts(object, bm
, mapping
, move
, self
.influence
)
3488 terminate(global_undo
)
3494 class Flatten(bpy
.types
.Operator
):
3495 bl_idname
= "mesh.looptools_flatten"
3496 bl_label
= "Flatten"
3497 bl_description
= "Flatten vertices on a best-fitting plane"
3498 bl_options
= {'REGISTER', 'UNDO'}
3500 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3501 description
= "Force of the tool",
3506 subtype
= 'PERCENTAGE')
3507 plane
= bpy
.props
.EnumProperty(name
= "Plane",
3508 items
= (("best_fit", "Best fit", "Calculate a best fitting plane"),
3509 ("normal", "Normal", "Derive plane from averaging vertex "\
3511 ("view", "View", "Flatten on a plane perpendicular to the "\
3513 description
= "Plane on which vertices are flattened",
3514 default
= 'best_fit')
3515 restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
3516 items
= (("none", "None", "No restrictions on vertex movement"),
3517 ("bounding_box", "Bounding box", "Vertices are restricted to "\
3518 "movement inside the bounding box of the selection")),
3519 description
= "Restrictions on how the vertices can be moved",
3523 def poll(cls
, context
):
3524 ob
= context
.active_object
3525 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3527 def draw(self
, context
):
3528 layout
= self
.layout
3529 col
= layout
.column()
3531 col
.prop(self
, "plane")
3532 #col.prop(self, "restriction")
3535 col
.prop(self
, "influence")
3537 def invoke(self
, context
, event
):
3538 # load custom settings
3540 return self
.execute(context
)
3542 def execute(self
, context
):
3544 global_undo
, object, bm
= initialise()
3545 settings_write(self
)
3546 # check cache to see if we can save time
3547 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Flatten",
3548 object, bm
, False, False)
3550 # order input into virtual loops
3551 loops
= flatten_get_input(bm
)
3552 loops
= check_loops(loops
, mapping
, bm
)
3554 # saving cache for faster execution next time
3556 cache_write("Flatten", object, bm
, False, False, False, loops
,
3561 # calculate plane and position of vertices on them
3562 com
, normal
= calculate_plane(bm
, loop
, method
=self
.plane
,
3564 to_move
= flatten_project(bm
, loop
, com
, normal
)
3565 if self
.restriction
== 'none':
3566 move
.append(to_move
)
3568 move
.append(to_move
)
3569 move_verts(object, bm
, False, move
, self
.influence
)
3572 terminate(global_undo
)
3578 class GStretch(bpy
.types
.Operator
):
3579 bl_idname
= "mesh.looptools_gstretch"
3580 bl_label
= "Gstretch"
3581 bl_description
= "Stretch selected vertices to Grease Pencil stroke"
3582 bl_options
= {'REGISTER', 'UNDO'}
3584 conversion
= bpy
.props
.EnumProperty(name
= "Conversion",
3585 items
= (("distance", "Distance", "Set the distance between vertices "\
3586 "of the converted grease pencil stroke"),
3587 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "\
3588 "number of vertices that converted GP strokes will have"),
3589 ("vertices", "Exact vertices", "Set the exact number of vertices "\
3590 "that converted grease pencil strokes will have. Short strokes "\
3591 "with few points may contain less vertices than this number."),
3592 ("none", "No simplification", "Convert each grease pencil point "\
3594 description
= "If grease pencil strokes are converted to geometry, "\
3595 "use this simplification method",
3596 default
= 'limit_vertices')
3597 conversion_distance
= bpy
.props
.FloatProperty(name
= "Distance",
3598 description
= "Absolute distance between vertices along the converted "\
3599 "grease pencil stroke",
3604 conversion_max
= bpy
.props
.IntProperty(name
= "Max Vertices",
3605 description
= "Maximum number of vertices grease pencil strokes will "\
3606 "have, when they are converted to geomtery",
3610 update
= gstretch_update_min
)
3611 conversion_min
= bpy
.props
.IntProperty(name
= "Min Vertices",
3612 description
= "Minimum number of vertices grease pencil strokes will "\
3613 "have, when they are converted to geomtery",
3617 update
= gstretch_update_max
)
3618 conversion_vertices
= bpy
.props
.IntProperty(name
= "Vertices",
3619 description
= "Number of vertices grease pencil strokes will "\
3620 "have, when they are converted to geometry. If strokes have less "\
3621 "points than required, the 'Spread evenly' method is used.",
3625 delete_strokes
= bpy
.props
.BoolProperty(name
="Delete strokes",
3626 description
= "Remove Grease Pencil strokes if they have been used "\
3627 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
3629 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3630 description
= "Force of the tool",
3635 subtype
= 'PERCENTAGE')
3636 method
= bpy
.props
.EnumProperty(name
= "Method",
3637 items
= (("project", "Project", "Project vertices onto the stroke, "\
3638 "using vertex normals and connected edges"),
3639 ("irregular", "Spread", "Distribute vertices along the full "\
3640 "stroke, retaining relative distances between the vertices"),
3641 ("regular", "Spread evenly", "Distribute vertices at regular "\
3642 "distances along the full stroke")),
3643 description
= "Method of distributing the vertices over the Grease "\
3645 default
= 'regular')
3648 def poll(cls
, context
):
3649 ob
= context
.active_object
3650 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3652 def draw(self
, context
):
3653 layout
= self
.layout
3654 col
= layout
.column()
3656 col
.prop(self
, "method")
3657 col
.prop(self
, "delete_strokes")
3660 col_conv
= col
.column(align
=True)
3661 col_conv
.prop(self
, "conversion", text
="")
3662 if self
.conversion
== 'distance':
3663 col_conv
.prop(self
, "conversion_distance")
3664 elif self
.conversion
== 'limit_vertices':
3665 row
= col_conv
.row(align
=True)
3666 row
.prop(self
, "conversion_min", text
="Min")
3667 row
.prop(self
, "conversion_max", text
="Max")
3668 elif self
.conversion
== 'vertices':
3669 col_conv
.prop(self
, "conversion_vertices")
3672 col
.prop(self
, "influence")
3674 def invoke(self
, context
, event
):
3675 # flush cached strokes
3676 if 'Gstretch' in looptools_cache
:
3677 looptools_cache
['Gstretch']['single_loops'] = []
3678 # load custom settings
3680 return self
.execute(context
)
3682 def execute(self
, context
):
3684 global_undo
, object, bm
= initialise()
3685 settings_write(self
)
3687 # check cache to see if we can save time
3688 cached
, safe_strokes
, loops
, derived
, mapping
= cache_read("Gstretch",
3689 object, bm
, False, False)
3692 strokes
= gstretch_safe_to_true_strokes(safe_strokes
)
3693 # cached strokes were flushed (see operator's invoke function)
3694 elif object.grease_pencil
:
3695 strokes
= gstretch_get_strokes(object)
3697 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3698 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
3699 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3702 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
3703 context
.scene
, input='selected')
3704 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
3705 loops
= check_loops(loops
, mapping
, bm_mod
)
3707 if object.grease_pencil
:
3708 strokes
= gstretch_get_strokes(object)
3710 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
3712 # saving cache for faster execution next time
3715 safe_strokes
= gstretch_true_to_safe_strokes(strokes
)
3718 cache_write("Gstretch", object, bm
, False, False,
3719 safe_strokes
, loops
, derived
, mapping
)
3721 # pair loops and strokes
3722 ls_pairs
= gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
)
3723 ls_pairs
= gstretch_align_pairs(ls_pairs
, object, bm_mod
, self
.method
)
3727 # no selected geometry, convert GP to verts
3729 move
.append(gstretch_create_verts(object, bm
, strokes
,
3730 self
.method
, self
.conversion
, self
.conversion_distance
,
3731 self
.conversion_max
, self
.conversion_min
,
3732 self
.conversion_vertices
))
3733 for stroke
in strokes
:
3734 gstretch_erase_stroke(stroke
, context
)
3736 for (loop
, stroke
) in ls_pairs
:
3737 move
.append(gstretch_calculate_verts(loop
, stroke
, object,
3738 bm_mod
, self
.method
))
3739 if self
.delete_strokes
:
3740 if type(stroke
) != bpy
.types
.GPencilStroke
:
3741 # in case of cached fake stroke, get the real one
3742 if object.grease_pencil
:
3743 strokes
= gstretch_get_strokes(object)
3744 ls_pairs
= gstretch_match_loops_strokes(loops
,
3745 strokes
, object, bm_mod
)
3746 ls_pairs
= gstretch_align_pairs(ls_pairs
, object,
3747 bm_mod
, self
.method
)
3748 for (l
, s
) in ls_pairs
:
3752 gstretch_erase_stroke(stroke
, context
)
3754 # move vertices to new locations
3755 bmesh
.update_edit_mesh(object.data
, tessface
=True,
3757 move_verts(object, bm
, mapping
, move
, self
.influence
)
3762 terminate(global_undo
)
3768 class Relax(bpy
.types
.Operator
):
3769 bl_idname
= "mesh.looptools_relax"
3771 bl_description
= "Relax the loop, so it is smoother"
3772 bl_options
= {'REGISTER', 'UNDO'}
3774 input = bpy
.props
.EnumProperty(name
= "Input",
3775 items
= (("all", "Parallel (all)", "Also use non-selected "\
3776 "parallel loops as input"),
3777 ("selected", "Selection","Only use selected vertices as input")),
3778 description
= "Loops that are relaxed",
3779 default
= 'selected')
3780 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
3781 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3782 ("linear", "Linear", "Simple and fast linear algorithm")),
3783 description
= "Algorithm used for interpolation",
3785 iterations
= bpy
.props
.EnumProperty(name
= "Iterations",
3786 items
= (("1", "1", "One"),
3787 ("3", "3", "Three"),
3789 ("10", "10", "Ten"),
3790 ("25", "25", "Twenty-five")),
3791 description
= "Number of times the loop is relaxed",
3793 regular
= bpy
.props
.BoolProperty(name
= "Regular",
3794 description
= "Distribute vertices at constant distances along the" \
3799 def poll(cls
, context
):
3800 ob
= context
.active_object
3801 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3803 def draw(self
, context
):
3804 layout
= self
.layout
3805 col
= layout
.column()
3807 col
.prop(self
, "interpolation")
3808 col
.prop(self
, "input")
3809 col
.prop(self
, "iterations")
3810 col
.prop(self
, "regular")
3812 def invoke(self
, context
, event
):
3813 # load custom settings
3815 return self
.execute(context
)
3817 def execute(self
, context
):
3819 global_undo
, object, bm
= initialise()
3820 settings_write(self
)
3821 # check cache to see if we can save time
3822 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Relax",
3823 object, bm
, self
.input, False)
3825 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3828 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
3829 context
.scene
, self
.input)
3830 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
3831 loops
= check_loops(loops
, mapping
, bm_mod
)
3832 knots
, points
= relax_calculate_knots(loops
)
3834 # saving cache for faster execution next time
3836 cache_write("Relax", object, bm
, self
.input, False, False, loops
,
3839 for iteration
in range(int(self
.iterations
)):
3840 # calculate splines and new positions
3841 tknots
, tpoints
= relax_calculate_t(bm_mod
, knots
, points
,
3844 for i
in range(len(knots
)):
3845 splines
.append(calculate_splines(self
.interpolation
, bm_mod
,
3846 tknots
[i
], knots
[i
]))
3847 move
= [relax_calculate_verts(bm_mod
, self
.interpolation
,
3848 tknots
, knots
, tpoints
, points
, splines
)]
3849 move_verts(object, bm
, mapping
, move
, -1)
3854 terminate(global_undo
)
3860 class Space(bpy
.types
.Operator
):
3861 bl_idname
= "mesh.looptools_space"
3863 bl_description
= "Space the vertices in a regular distrubtion on the loop"
3864 bl_options
= {'REGISTER', 'UNDO'}
3866 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3867 description
= "Force of the tool",
3872 subtype
= 'PERCENTAGE')
3873 input = bpy
.props
.EnumProperty(name
= "Input",
3874 items
= (("all", "Parallel (all)", "Also use non-selected "\
3875 "parallel loops as input"),
3876 ("selected", "Selection","Only use selected vertices as input")),
3877 description
= "Loops that are spaced",
3878 default
= 'selected')
3879 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
3880 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3881 ("linear", "Linear", "Vertices are projected on existing edges")),
3882 description
= "Algorithm used for interpolation",
3886 def poll(cls
, context
):
3887 ob
= context
.active_object
3888 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3890 def draw(self
, context
):
3891 layout
= self
.layout
3892 col
= layout
.column()
3894 col
.prop(self
, "interpolation")
3895 col
.prop(self
, "input")
3898 col
.prop(self
, "influence")
3900 def invoke(self
, context
, event
):
3901 # load custom settings
3903 return self
.execute(context
)
3905 def execute(self
, context
):
3907 global_undo
, object, bm
= initialise()
3908 settings_write(self
)
3909 # check cache to see if we can save time
3910 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Space",
3911 object, bm
, self
.input, False)
3913 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3916 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
3917 context
.scene
, self
.input)
3918 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
3919 loops
= check_loops(loops
, mapping
, bm_mod
)
3921 # saving cache for faster execution next time
3923 cache_write("Space", object, bm
, self
.input, False, False, loops
,
3928 # calculate splines and new positions
3929 if loop
[1]: # circular
3930 loop
[0].append(loop
[0][0])
3931 tknots
, tpoints
= space_calculate_t(bm_mod
, loop
[0][:])
3932 splines
= calculate_splines(self
.interpolation
, bm_mod
,
3934 move
.append(space_calculate_verts(bm_mod
, self
.interpolation
,
3935 tknots
, tpoints
, loop
[0][:-1], splines
))
3936 # move vertices to new locations
3937 move_verts(object, bm
, mapping
, move
, self
.influence
)
3942 terminate(global_undo
)
3947 ##########################################
3948 ####### GUI and registration #############
3949 ##########################################
3951 # menu containing all tools
3952 class VIEW3D_MT_edit_mesh_looptools(bpy
.types
.Menu
):
3953 bl_label
= "LoopTools"
3955 def draw(self
, context
):
3956 layout
= self
.layout
3958 layout
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
3959 layout
.operator("mesh.looptools_circle")
3960 layout
.operator("mesh.looptools_curve")
3961 layout
.operator("mesh.looptools_flatten")
3962 layout
.operator("mesh.looptools_gstretch")
3963 layout
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
3964 layout
.operator("mesh.looptools_relax")
3965 layout
.operator("mesh.looptools_space")
3968 # panel containing all tools
3969 class VIEW3D_PT_tools_looptools(bpy
.types
.Panel
):
3970 bl_space_type
= 'VIEW_3D'
3971 bl_region_type
= 'TOOLS'
3972 bl_category
= 'Tools'
3973 bl_context
= "mesh_edit"
3974 bl_label
= "LoopTools"
3975 bl_options
= {'DEFAULT_CLOSED'}
3977 def draw(self
, context
):
3978 layout
= self
.layout
3979 col
= layout
.column(align
=True)
3980 lt
= context
.window_manager
.looptools
3982 # bridge - first line
3983 split
= col
.split(percentage
=0.15, align
=True)
3984 if lt
.display_bridge
:
3985 split
.prop(lt
, "display_bridge", text
="", icon
='DOWNARROW_HLT')
3987 split
.prop(lt
, "display_bridge", text
="", icon
='RIGHTARROW')
3988 split
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
3990 if lt
.display_bridge
:
3991 box
= col
.column(align
=True).box().column()
3992 #box.prop(self, "mode")
3995 col_top
= box
.column(align
=True)
3996 row
= col_top
.row(align
=True)
3997 col_left
= row
.column(align
=True)
3998 col_right
= row
.column(align
=True)
3999 col_right
.active
= lt
.bridge_segments
!= 1
4000 col_left
.prop(lt
, "bridge_segments")
4001 col_right
.prop(lt
, "bridge_min_width", text
="")
4003 bottom_left
= col_left
.row()
4004 bottom_left
.active
= lt
.bridge_segments
!= 1
4005 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4006 bottom_right
= col_right
.row()
4007 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4008 bottom_right
.prop(lt
, "bridge_cubic_strength")
4009 # boolean properties
4010 col_top
.prop(lt
, "bridge_remove_faces")
4012 # override properties
4014 row
= box
.row(align
= True)
4015 row
.prop(lt
, "bridge_twist")
4016 row
.prop(lt
, "bridge_reverse")
4018 # circle - first line
4019 split
= col
.split(percentage
=0.15, align
=True)
4020 if lt
.display_circle
:
4021 split
.prop(lt
, "display_circle", text
="", icon
='DOWNARROW_HLT')
4023 split
.prop(lt
, "display_circle", text
="", icon
='RIGHTARROW')
4024 split
.operator("mesh.looptools_circle")
4026 if lt
.display_circle
:
4027 box
= col
.column(align
=True).box().column()
4028 box
.prop(lt
, "circle_fit")
4031 box
.prop(lt
, "circle_flatten")
4032 row
= box
.row(align
=True)
4033 row
.prop(lt
, "circle_custom_radius")
4034 row_right
= row
.row(align
=True)
4035 row_right
.active
= lt
.circle_custom_radius
4036 row_right
.prop(lt
, "circle_radius", text
="")
4037 box
.prop(lt
, "circle_regular")
4040 box
.prop(lt
, "circle_influence")
4042 # curve - first line
4043 split
= col
.split(percentage
=0.15, align
=True)
4044 if lt
.display_curve
:
4045 split
.prop(lt
, "display_curve", text
="", icon
='DOWNARROW_HLT')
4047 split
.prop(lt
, "display_curve", text
="", icon
='RIGHTARROW')
4048 split
.operator("mesh.looptools_curve")
4050 if lt
.display_curve
:
4051 box
= col
.column(align
=True).box().column()
4052 box
.prop(lt
, "curve_interpolation")
4053 box
.prop(lt
, "curve_restriction")
4054 box
.prop(lt
, "curve_boundaries")
4055 box
.prop(lt
, "curve_regular")
4058 box
.prop(lt
, "curve_influence")
4060 # flatten - first line
4061 split
= col
.split(percentage
=0.15, align
=True)
4062 if lt
.display_flatten
:
4063 split
.prop(lt
, "display_flatten", text
="", icon
='DOWNARROW_HLT')
4065 split
.prop(lt
, "display_flatten", text
="", icon
='RIGHTARROW')
4066 split
.operator("mesh.looptools_flatten")
4067 # flatten - settings
4068 if lt
.display_flatten
:
4069 box
= col
.column(align
=True).box().column()
4070 box
.prop(lt
, "flatten_plane")
4071 #box.prop(lt, "flatten_restriction")
4074 box
.prop(lt
, "flatten_influence")
4076 # gstretch - first line
4077 split
= col
.split(percentage
=0.15, align
=True)
4078 if lt
.display_gstretch
:
4079 split
.prop(lt
, "display_gstretch", text
="", icon
='DOWNARROW_HLT')
4081 split
.prop(lt
, "display_gstretch", text
="", icon
='RIGHTARROW')
4082 split
.operator("mesh.looptools_gstretch")
4084 if lt
.display_gstretch
:
4085 box
= col
.column(align
=True).box().column()
4086 box
.prop(lt
, "gstretch_method")
4087 box
.prop(lt
, "gstretch_delete_strokes")
4090 col_conv
= box
.column(align
=True)
4091 col_conv
.prop(lt
, "gstretch_conversion", text
="")
4092 if lt
.gstretch_conversion
== 'distance':
4093 col_conv
.prop(lt
, "gstretch_conversion_distance")
4094 elif lt
.gstretch_conversion
== 'limit_vertices':
4095 row
= col_conv
.row(align
=True)
4096 row
.prop(lt
, "gstretch_conversion_min", text
="Min")
4097 row
.prop(lt
, "gstretch_conversion_max", text
="Max")
4098 elif lt
.gstretch_conversion
== 'vertices':
4099 col_conv
.prop(lt
, "gstretch_conversion_vertices")
4102 box
.prop(lt
, "gstretch_influence")
4105 split
= col
.split(percentage
=0.15, align
=True)
4107 split
.prop(lt
, "display_loft", text
="", icon
='DOWNARROW_HLT')
4109 split
.prop(lt
, "display_loft", text
="", icon
='RIGHTARROW')
4110 split
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4113 box
= col
.column(align
=True).box().column()
4114 #box.prop(self, "mode")
4117 col_top
= box
.column(align
=True)
4118 row
= col_top
.row(align
=True)
4119 col_left
= row
.column(align
=True)
4120 col_right
= row
.column(align
=True)
4121 col_right
.active
= lt
.bridge_segments
!= 1
4122 col_left
.prop(lt
, "bridge_segments")
4123 col_right
.prop(lt
, "bridge_min_width", text
="")
4125 bottom_left
= col_left
.row()
4126 bottom_left
.active
= lt
.bridge_segments
!= 1
4127 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4128 bottom_right
= col_right
.row()
4129 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4130 bottom_right
.prop(lt
, "bridge_cubic_strength")
4131 # boolean properties
4132 col_top
.prop(lt
, "bridge_remove_faces")
4133 col_top
.prop(lt
, "bridge_loft_loop")
4135 # override properties
4137 row
= box
.row(align
= True)
4138 row
.prop(lt
, "bridge_twist")
4139 row
.prop(lt
, "bridge_reverse")
4141 # relax - first line
4142 split
= col
.split(percentage
=0.15, align
=True)
4143 if lt
.display_relax
:
4144 split
.prop(lt
, "display_relax", text
="", icon
='DOWNARROW_HLT')
4146 split
.prop(lt
, "display_relax", text
="", icon
='RIGHTARROW')
4147 split
.operator("mesh.looptools_relax")
4149 if lt
.display_relax
:
4150 box
= col
.column(align
=True).box().column()
4151 box
.prop(lt
, "relax_interpolation")
4152 box
.prop(lt
, "relax_input")
4153 box
.prop(lt
, "relax_iterations")
4154 box
.prop(lt
, "relax_regular")
4156 # space - first line
4157 split
= col
.split(percentage
=0.15, align
=True)
4158 if lt
.display_space
:
4159 split
.prop(lt
, "display_space", text
="", icon
='DOWNARROW_HLT')
4161 split
.prop(lt
, "display_space", text
="", icon
='RIGHTARROW')
4162 split
.operator("mesh.looptools_space")
4164 if lt
.display_space
:
4165 box
= col
.column(align
=True).box().column()
4166 box
.prop(lt
, "space_interpolation")
4167 box
.prop(lt
, "space_input")
4170 box
.prop(lt
, "space_influence")
4173 # property group containing all properties for the gui in the panel
4174 class LoopToolsProps(bpy
.types
.PropertyGroup
):
4176 Fake module like class
4177 bpy.context.window_manager.looptools
4180 # general display properties
4181 display_bridge
= bpy
.props
.BoolProperty(name
= "Bridge settings",
4182 description
= "Display settings of the Bridge tool",
4184 display_circle
= bpy
.props
.BoolProperty(name
= "Circle settings",
4185 description
= "Display settings of the Circle tool",
4187 display_curve
= bpy
.props
.BoolProperty(name
= "Curve settings",
4188 description
= "Display settings of the Curve tool",
4190 display_flatten
= bpy
.props
.BoolProperty(name
= "Flatten settings",
4191 description
= "Display settings of the Flatten tool",
4193 display_gstretch
= bpy
.props
.BoolProperty(name
= "Gstretch settings",
4194 description
= "Display settings of the Gstretch tool",
4196 display_loft
= bpy
.props
.BoolProperty(name
= "Loft settings",
4197 description
= "Display settings of the Loft tool",
4199 display_relax
= bpy
.props
.BoolProperty(name
= "Relax settings",
4200 description
= "Display settings of the Relax tool",
4202 display_space
= bpy
.props
.BoolProperty(name
= "Space settings",
4203 description
= "Display settings of the Space tool",
4207 bridge_cubic_strength
= bpy
.props
.FloatProperty(name
= "Strength",
4208 description
= "Higher strength results in more fluid curves",
4212 bridge_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation mode",
4213 items
= (('cubic', "Cubic", "Gives curved results"),
4214 ('linear', "Linear", "Basic, fast, straight interpolation")),
4215 description
= "Interpolation mode: algorithm used when creating "\
4218 bridge_loft
= bpy
.props
.BoolProperty(name
= "Loft",
4219 description
= "Loft multiple loops, instead of considering them as "\
4220 "a multi-input for bridging",
4222 bridge_loft_loop
= bpy
.props
.BoolProperty(name
= "Loop",
4223 description
= "Connect the first and the last loop with each other",
4225 bridge_min_width
= bpy
.props
.IntProperty(name
= "Minimum width",
4226 description
= "Segments with an edge smaller than this are merged "\
4227 "(compared to base edge)",
4231 subtype
= 'PERCENTAGE')
4232 bridge_mode
= bpy
.props
.EnumProperty(name
= "Mode",
4233 items
= (('basic', "Basic", "Fast algorithm"),
4234 ('shortest', "Shortest edge", "Slower algorithm with " \
4235 "better vertex matching")),
4236 description
= "Algorithm used for bridging",
4237 default
= 'shortest')
4238 bridge_remove_faces
= bpy
.props
.BoolProperty(name
= "Remove faces",
4239 description
= "Remove faces that are internal after bridging",
4241 bridge_reverse
= bpy
.props
.BoolProperty(name
= "Reverse",
4242 description
= "Manually override the direction in which the loops "\
4243 "are bridged. Only use if the tool gives the wrong " \
4246 bridge_segments
= bpy
.props
.IntProperty(name
= "Segments",
4247 description
= "Number of segments used to bridge the gap "\
4252 bridge_twist
= bpy
.props
.IntProperty(name
= "Twist",
4253 description
= "Twist what vertices are connected to each other",
4257 circle_custom_radius
= bpy
.props
.BoolProperty(name
= "Radius",
4258 description
= "Force a custom radius",
4260 circle_fit
= bpy
.props
.EnumProperty(name
= "Method",
4261 items
= (("best", "Best fit", "Non-linear least squares"),
4262 ("inside", "Fit inside","Only move vertices towards the center")),
4263 description
= "Method used for fitting a circle to the vertices",
4265 circle_flatten
= bpy
.props
.BoolProperty(name
= "Flatten",
4266 description
= "Flatten the circle, instead of projecting it on the " \
4269 circle_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4270 description
= "Force of the tool",
4275 subtype
= 'PERCENTAGE')
4276 circle_radius
= bpy
.props
.FloatProperty(name
= "Radius",
4277 description
= "Custom radius for circle",
4281 circle_regular
= bpy
.props
.BoolProperty(name
= "Regular",
4282 description
= "Distribute vertices at constant distances along the " \
4287 curve_boundaries
= bpy
.props
.BoolProperty(name
= "Boundaries",
4288 description
= "Limit the tool to work within the boundaries of the "\
4289 "selected vertices",
4291 curve_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4292 description
= "Force of the tool",
4297 subtype
= 'PERCENTAGE')
4298 curve_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4299 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4300 ("linear", "Linear", "Simple and fast linear algorithm")),
4301 description
= "Algorithm used for interpolation",
4303 curve_regular
= bpy
.props
.BoolProperty(name
= "Regular",
4304 description
= "Distribute vertices at constant distances along the " \
4307 curve_restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
4308 items
= (("none", "None", "No restrictions on vertex movement"),
4309 ("extrude", "Extrude only","Only allow extrusions (no "\
4311 ("indent", "Indent only", "Only allow indentation (no "\
4313 description
= "Restrictions on how the vertices can be moved",
4316 # flatten properties
4317 flatten_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4318 description
= "Force of the tool",
4323 subtype
= 'PERCENTAGE')
4324 flatten_plane
= bpy
.props
.EnumProperty(name
= "Plane",
4325 items
= (("best_fit", "Best fit", "Calculate a best fitting plane"),
4326 ("normal", "Normal", "Derive plane from averaging vertex "\
4328 ("view", "View", "Flatten on a plane perpendicular to the "\
4330 description
= "Plane on which vertices are flattened",
4331 default
= 'best_fit')
4332 flatten_restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
4333 items
= (("none", "None", "No restrictions on vertex movement"),
4334 ("bounding_box", "Bounding box", "Vertices are restricted to "\
4335 "movement inside the bounding box of the selection")),
4336 description
= "Restrictions on how the vertices can be moved",
4339 # gstretch properties
4340 gstretch_conversion
= bpy
.props
.EnumProperty(name
= "Conversion",
4341 items
= (("distance", "Distance", "Set the distance between vertices "\
4342 "of the converted grease pencil stroke"),
4343 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "\
4344 "number of vertices that converted GP strokes will have"),
4345 ("vertices", "Exact vertices", "Set the exact number of vertices "\
4346 "that converted grease pencil strokes will have. Short strokes "\
4347 "with few points may contain less vertices than this number."),
4348 ("none", "No simplification", "Convert each grease pencil point "\
4350 description
= "If grease pencil strokes are converted to geometry, "\
4351 "use this simplification method",
4352 default
= 'limit_vertices')
4353 gstretch_conversion_distance
= bpy
.props
.FloatProperty(name
= "Distance",
4354 description
= "Absolute distance between vertices along the converted "\
4355 "grease pencil stroke",
4360 gstretch_conversion_max
= bpy
.props
.IntProperty(name
= "Max Vertices",
4361 description
= "Maximum number of vertices grease pencil strokes will "\
4362 "have, when they are converted to geomtery",
4366 update
= gstretch_update_min
)
4367 gstretch_conversion_min
= bpy
.props
.IntProperty(name
= "Min Vertices",
4368 description
= "Minimum number of vertices grease pencil strokes will "\
4369 "have, when they are converted to geomtery",
4373 update
= gstretch_update_max
)
4374 gstretch_conversion_vertices
= bpy
.props
.IntProperty(name
= "Vertices",
4375 description
= "Number of vertices grease pencil strokes will "\
4376 "have, when they are converted to geometry. If strokes have less "\
4377 "points than required, the 'Spread evenly' method is used.",
4381 gstretch_delete_strokes
= bpy
.props
.BoolProperty(name
="Delete strokes",
4382 description
= "Remove Grease Pencil strokes if they have been used "\
4383 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
4385 gstretch_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4386 description
= "Force of the tool",
4391 subtype
= 'PERCENTAGE')
4392 gstretch_method
= bpy
.props
.EnumProperty(name
= "Method",
4393 items
= (("project", "Project", "Project vertices onto the stroke, "\
4394 "using vertex normals and connected edges"),
4395 ("irregular", "Spread", "Distribute vertices along the full "\
4396 "stroke, retaining relative distances between the vertices"),
4397 ("regular", "Spread evenly", "Distribute vertices at regular "\
4398 "distances along the full stroke")),
4399 description
= "Method of distributing the vertices over the Grease "\
4401 default
= 'regular')
4404 relax_input
= bpy
.props
.EnumProperty(name
= "Input",
4405 items
= (("all", "Parallel (all)", "Also use non-selected "\
4406 "parallel loops as input"),
4407 ("selected", "Selection","Only use selected vertices as input")),
4408 description
= "Loops that are relaxed",
4409 default
= 'selected')
4410 relax_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4411 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4412 ("linear", "Linear", "Simple and fast linear algorithm")),
4413 description
= "Algorithm used for interpolation",
4415 relax_iterations
= bpy
.props
.EnumProperty(name
= "Iterations",
4416 items
= (("1", "1", "One"),
4417 ("3", "3", "Three"),
4419 ("10", "10", "Ten"),
4420 ("25", "25", "Twenty-five")),
4421 description
= "Number of times the loop is relaxed",
4423 relax_regular
= bpy
.props
.BoolProperty(name
= "Regular",
4424 description
= "Distribute vertices at constant distances along the" \
4429 space_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4430 description
= "Force of the tool",
4435 subtype
= 'PERCENTAGE')
4436 space_input
= bpy
.props
.EnumProperty(name
= "Input",
4437 items
= (("all", "Parallel (all)", "Also use non-selected "\
4438 "parallel loops as input"),
4439 ("selected", "Selection","Only use selected vertices as input")),
4440 description
= "Loops that are spaced",
4441 default
= 'selected')
4442 space_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4443 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4444 ("linear", "Linear", "Vertices are projected on existing edges")),
4445 description
= "Algorithm used for interpolation",
4449 # draw function for integration in menus
4450 def menu_func(self
, context
):
4451 self
.layout
.menu("VIEW3D_MT_edit_mesh_looptools")
4452 self
.layout
.separator()
4455 # define classes for registration
4456 classes
= [VIEW3D_MT_edit_mesh_looptools
,
4457 VIEW3D_PT_tools_looptools
,
4468 # registering and menu integration
4471 bpy
.utils
.register_class(c
)
4472 bpy
.types
.VIEW3D_MT_edit_mesh_specials
.prepend(menu_func
)
4473 bpy
.types
.WindowManager
.looptools
= bpy
.props
.PointerProperty(\
4474 type = LoopToolsProps
)
4477 # unregistering and removing menus
4480 bpy
.utils
.unregister_class(c
)
4481 bpy
.types
.VIEW3D_MT_edit_mesh_specials
.remove(menu_func
)
4483 del bpy
.types
.WindowManager
.looptools
4488 if __name__
== "__main__":