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, 72, 2),
24 "location": "View3D > Toolbar and View3D > Specials (W-key)",
26 "description": "Mesh modelling toolkit. Several tools to aid modelling",
27 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
28 "Scripts/Modeling/LoopTools",
38 from bpy_extras
import view3d_utils
41 ##########################################
42 ####### General functions ################
43 ##########################################
46 # used by all tools to improve speed on reruns
50 def get_grease_pencil(object, context
):
51 gp
= object.grease_pencil
53 gp
= context
.scene
.grease_pencil
57 # force a full recalculation next time
58 def cache_delete(tool
):
59 if tool
in looptools_cache
:
60 del looptools_cache
[tool
]
63 # check cache for stored information
64 def cache_read(tool
, object, bm
, input_method
, boundaries
):
65 # current tool not cached yet
66 if tool
not in looptools_cache
:
67 return(False, False, False, False, False)
68 # check if selected object didn't change
69 if object.name
!= looptools_cache
[tool
]["object"]:
70 return(False, False, False, False, False)
71 # check if input didn't change
72 if input_method
!= looptools_cache
[tool
]["input_method"]:
73 return(False, False, False, False, False)
74 if boundaries
!= looptools_cache
[tool
]["boundaries"]:
75 return(False, False, False, False, False)
76 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport \
77 and mod
.type == 'MIRROR']
78 if modifiers
!= looptools_cache
[tool
]["modifiers"]:
79 return(False, False, False, False, False)
80 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
81 if input != looptools_cache
[tool
]["input"]:
82 return(False, False, False, False, False)
84 single_loops
= looptools_cache
[tool
]["single_loops"]
85 loops
= looptools_cache
[tool
]["loops"]
86 derived
= looptools_cache
[tool
]["derived"]
87 mapping
= looptools_cache
[tool
]["mapping"]
89 return(True, single_loops
, loops
, derived
, mapping
)
92 # store information in the cache
93 def cache_write(tool
, object, bm
, input_method
, boundaries
, single_loops
,
94 loops
, derived
, mapping
):
95 # clear cache of current tool
96 if tool
in looptools_cache
:
97 del looptools_cache
[tool
]
98 # prepare values to be saved to cache
99 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
100 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport \
101 and mod
.type == 'MIRROR']
103 looptools_cache
[tool
] = {"input": input, "object": object.name
,
104 "input_method": input_method
, "boundaries": boundaries
,
105 "single_loops": single_loops
, "loops": loops
,
106 "derived": derived
, "mapping": mapping
, "modifiers": modifiers
}
109 # calculates natural cubic splines through all given knots
110 def calculate_cubic_splines(bm_mod
, tknots
, knots
):
111 # hack for circular loops
112 if knots
[0] == knots
[-1] and len(knots
) > 1:
115 for k
in range(-1, -5, -1):
116 if k
- 1 < -len(knots
):
118 k_new1
.append(knots
[k
-1])
121 if k
+ 1 > len(knots
) - 1:
123 k_new2
.append(knots
[k
+1])
130 for t
in range(-1, -5, -1):
131 if t
- 1 < -len(tknots
):
133 total1
+= tknots
[t
] - tknots
[t
-1]
134 t_new1
.append(tknots
[0] - total1
)
138 if t
+ 1 > len(tknots
) - 1:
140 total2
+= tknots
[t
+1] - tknots
[t
]
141 t_new2
.append(tknots
[-1] + total2
)
154 locs
= [bm_mod
.verts
[k
].co
[:] for k
in knots
]
162 if x
[i
+1] - x
[i
] == 0:
165 h
.append(x
[i
+1] - x
[i
])
167 for i
in range(1, n
-1):
168 q
.append(3/h
[i
]*(a
[i
+1]-a
[i
]) - 3/h
[i
-1]*(a
[i
]-a
[i
-1]))
172 for i
in range(1, n
-1):
173 l
.append(2*(x
[i
+1]-x
[i
-1]) - h
[i
-1]*u
[i
-1])
176 u
.append(h
[i
] / l
[i
])
177 z
.append((q
[i
] - h
[i
-1] * z
[i
-1]) / l
[i
])
180 b
= [False for i
in range(n
-1)]
181 c
= [False for i
in range(n
)]
182 d
= [False for i
in range(n
-1)]
184 for i
in range(n
-2, -1, -1):
185 c
[i
] = z
[i
] - u
[i
]*c
[i
+1]
186 b
[i
] = (a
[i
+1]-a
[i
])/h
[i
] - h
[i
]*(c
[i
+1]+2*c
[i
])/3
187 d
[i
] = (c
[i
+1]-c
[i
]) / (3*h
[i
])
189 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
191 for i
in range(len(knots
)-1):
192 splines
.append([result
[i
], result
[i
+n
-1], result
[i
+(n
-1)*2]])
193 if circular
: # cleaning up after hack
195 tknots
= tknots
[4:-4]
200 # calculates linear splines through all given knots
201 def calculate_linear_splines(bm_mod
, tknots
, knots
):
203 for i
in range(len(knots
)-1):
204 a
= bm_mod
.verts
[knots
[i
]].co
205 b
= bm_mod
.verts
[knots
[i
+1]].co
209 splines
.append([a
, d
, t
, u
]) # [locStart, locDif, tStart, tDif]
214 # calculate a best-fit plane to the given vertices
215 def calculate_plane(bm_mod
, loop
, method
="best_fit", object=False):
216 # getting the vertex locations
217 locs
= [bm_mod
.verts
[v
].co
.copy() for v
in loop
[0]]
219 # calculating the center of masss
220 com
= mathutils
.Vector()
226 if method
== 'best_fit':
227 # creating the covariance matrix
228 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
233 mat
[0][0] += (loc
[0]-x
)**2
234 mat
[1][0] += (loc
[0]-x
)*(loc
[1]-y
)
235 mat
[2][0] += (loc
[0]-x
)*(loc
[2]-z
)
236 mat
[0][1] += (loc
[1]-y
)*(loc
[0]-x
)
237 mat
[1][1] += (loc
[1]-y
)**2
238 mat
[2][1] += (loc
[1]-y
)*(loc
[2]-z
)
239 mat
[0][2] += (loc
[2]-z
)*(loc
[0]-x
)
240 mat
[1][2] += (loc
[2]-z
)*(loc
[1]-y
)
241 mat
[2][2] += (loc
[2]-z
)**2
243 # calculating the normal to the plane
246 mat
= matrix_invert(mat
)
249 if math
.fabs(sum(mat
[0])) < math
.fabs(sum(mat
[1])):
250 if math
.fabs(sum(mat
[0])) < math
.fabs(sum(mat
[2])):
252 elif math
.fabs(sum(mat
[1])) < math
.fabs(sum(mat
[2])):
255 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
257 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
259 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
261 # warning! this is different from .normalize()
264 vec
= mathutils
.Vector((1.0, 1.0, 1.0))
265 vec2
= (mat
* vec
)/(mat
* vec
).length
266 while vec
!= vec2
and iter<itermax
:
273 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
276 elif method
== 'normal':
277 # averaging the vertex normals
278 v_normals
= [bm_mod
.verts
[v
].normal
for v
in loop
[0]]
279 normal
= mathutils
.Vector()
280 for v_normal
in v_normals
:
282 normal
/= len(v_normals
)
285 elif method
== 'view':
286 # calculate view normal
287 rotation
= bpy
.context
.space_data
.region_3d
.view_matrix
.to_3x3().\
289 normal
= rotation
* mathutils
.Vector((0.0, 0.0, 1.0))
291 normal
= object.matrix_world
.inverted().to_euler().to_matrix() * \
297 # calculate splines based on given interpolation method (controller function)
298 def calculate_splines(interpolation
, bm_mod
, tknots
, knots
):
299 if interpolation
== 'cubic':
300 splines
= calculate_cubic_splines(bm_mod
, tknots
, knots
[:])
301 else: # interpolations == 'linear'
302 splines
= calculate_linear_splines(bm_mod
, tknots
, knots
[:])
307 # check loops and only return valid ones
308 def check_loops(loops
, mapping
, bm_mod
):
310 for loop
, circular
in loops
:
311 # loop needs to have at least 3 vertices
314 # loop needs at least 1 vertex in the original, non-mirrored mesh
318 if mapping
[vert
] > -1:
323 # vertices can not all be at the same location
325 for i
in range(len(loop
) - 1):
326 if (bm_mod
.verts
[loop
[i
]].co
- \
327 bm_mod
.verts
[loop
[i
+1]].co
).length
> 1e-6:
332 # passed all tests, loop is valid
333 valid_loops
.append([loop
, circular
])
338 # input: bmesh, output: dict with the edge-key as key and face-index as value
339 def dict_edge_faces(bm
):
340 edge_faces
= dict([[edgekey(edge
), []] for edge
in bm
.edges
if \
342 for face
in bm
.faces
:
345 for key
in face_edgekeys(face
):
346 edge_faces
[key
].append(face
.index
)
351 # input: bmesh (edge-faces optional), output: dict with face-face connections
352 def dict_face_faces(bm
, edge_faces
=False):
354 edge_faces
= dict_edge_faces(bm
)
356 connected_faces
= dict([[face
.index
, []] for face
in bm
.faces
if \
358 for face
in bm
.faces
:
361 for edge_key
in face_edgekeys(face
):
362 for connected_face
in edge_faces
[edge_key
]:
363 if connected_face
== face
.index
:
365 connected_faces
[face
.index
].append(connected_face
)
367 return(connected_faces
)
370 # input: bmesh, output: dict with the vert index as key and edge-keys as value
371 def dict_vert_edges(bm
):
372 vert_edges
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
373 for edge
in bm
.edges
:
378 vert_edges
[vert
].append(ek
)
383 # input: bmesh, output: dict with the vert index as key and face index as value
384 def dict_vert_faces(bm
):
385 vert_faces
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
386 for face
in bm
.faces
:
388 for vert
in face
.verts
:
389 vert_faces
[vert
.index
].append(face
.index
)
394 # input: list of edge-keys, output: dictionary with vertex-vertex connections
395 def dict_vert_verts(edge_keys
):
396 # create connection data
400 if ek
[i
] in vert_verts
:
401 vert_verts
[ek
[i
]].append(ek
[1-i
])
403 vert_verts
[ek
[i
]] = [ek
[1-i
]]
408 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
410 return(tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])))
413 # returns the edgekeys of a bmesh face
414 def face_edgekeys(face
):
415 return([tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])) for \
419 # calculate input loops
420 def get_connected_input(object, bm
, scene
, input):
421 # get mesh with modifiers applied
422 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
424 # calculate selected loops
425 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if \
426 edge
.select
and not edge
.hide
]
427 loops
= get_connected_selections(edge_keys
)
429 # if only selected loops are needed, we're done
430 if input == 'selected':
431 return(derived
, bm_mod
, loops
)
432 # elif input == 'all':
433 loops
= get_parallel_loops(bm_mod
, loops
)
435 return(derived
, bm_mod
, loops
)
438 # sorts all edge-keys into a list of loops
439 def get_connected_selections(edge_keys
):
440 # create connection data
441 vert_verts
= dict_vert_verts(edge_keys
)
443 # find loops consisting of connected selected edges
445 while len(vert_verts
) > 0:
446 loop
= [iter(vert_verts
.keys()).__next
__()]
452 # no more connection data for current vertex
453 if loop
[-1] not in vert_verts
:
461 for i
, next_vert
in enumerate(vert_verts
[loop
[-1]]):
462 if next_vert
not in loop
:
463 vert_verts
[loop
[-1]].pop(i
)
464 if len(vert_verts
[loop
[-1]]) == 0:
465 del vert_verts
[loop
[-1]]
466 # remove connection both ways
467 if next_vert
in vert_verts
:
468 if len(vert_verts
[next_vert
]) == 1:
469 del vert_verts
[next_vert
]
471 vert_verts
[next_vert
].remove(loop
[-1])
472 loop
.append(next_vert
)
476 # found one end of the loop, continue with next
480 # found both ends of the loop, stop growing
484 # check if loop is circular
485 if loop
[0] in vert_verts
:
486 if loop
[-1] in vert_verts
[loop
[0]]:
488 if len(vert_verts
[loop
[0]]) == 1:
489 del vert_verts
[loop
[0]]
491 vert_verts
[loop
[0]].remove(loop
[-1])
492 if len(vert_verts
[loop
[-1]]) == 1:
493 del vert_verts
[loop
[-1]]
495 vert_verts
[loop
[-1]].remove(loop
[0])
509 # get the derived mesh data, if there is a mirror modifier
510 def get_derived_bmesh(object, bm
, scene
):
511 # check for mirror modifiers
512 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
514 # disable other modifiers
515 show_viewport
= [mod
.name
for mod
in object.modifiers
if \
517 for mod
in object.modifiers
:
518 if mod
.type != 'MIRROR':
519 mod
.show_viewport
= False
522 mesh_mod
= object.to_mesh(scene
, True, 'PREVIEW')
523 bm_mod
.from_mesh(mesh_mod
)
524 bpy
.context
.blend_data
.meshes
.remove(mesh_mod
)
525 # re-enable other modifiers
526 for mod_name
in show_viewport
:
527 object.modifiers
[mod_name
].show_viewport
= True
528 # no mirror modifiers, so no derived mesh necessary
533 bm_mod
.verts
.ensure_lookup_table()
534 bm_mod
.edges
.ensure_lookup_table()
535 bm_mod
.faces
.ensure_lookup_table()
537 return(derived
, bm_mod
)
540 # return a mapping of derived indices to indices
541 def get_mapping(derived
, bm
, bm_mod
, single_vertices
, full_search
, loops
):
546 verts
= [v
for v
in bm
.verts
if not v
.hide
]
548 verts
= [v
for v
in bm
.verts
if v
.select
and not v
.hide
]
550 # non-selected vertices around single vertices also need to be mapped
552 mapping
= dict([[vert
, -1] for vert
in single_vertices
])
553 verts_mod
= [bm_mod
.verts
[vert
] for vert
in single_vertices
]
555 for v_mod
in verts_mod
:
556 if (v
.co
- v_mod
.co
).length
< 1e-6:
557 mapping
[v_mod
.index
] = v
.index
559 real_singles
= [v_real
for v_real
in mapping
.values() if v_real
>-1]
561 verts_indices
= [vert
.index
for vert
in verts
]
562 for face
in [face
for face
in bm
.faces
if not face
.select \
564 for vert
in face
.verts
:
565 if vert
.index
in real_singles
:
567 if not v
.index
in verts_indices
:
572 # create mapping of derived indices to indices
573 mapping
= dict([[vert
, -1] for loop
in loops
for vert
in loop
[0]])
575 for single
in single_vertices
:
577 verts_mod
= [bm_mod
.verts
[i
] for i
in mapping
.keys()]
579 for v_mod
in verts_mod
:
580 if (v
.co
- v_mod
.co
).length
< 1e-6:
581 mapping
[v_mod
.index
] = v
.index
582 verts_mod
.remove(v_mod
)
588 # calculate the determinant of a matrix
589 def matrix_determinant(m
):
590 determinant
= m
[0][0] * m
[1][1] * m
[2][2] + m
[0][1] * m
[1][2] * m
[2][0] \
591 + m
[0][2] * m
[1][0] * m
[2][1] - m
[0][2] * m
[1][1] * m
[2][0] \
592 - m
[0][1] * m
[1][0] * m
[2][2] - m
[0][0] * m
[1][2] * m
[2][1]
597 # custom matrix inversion, to provide higher precision than the built-in one
598 def matrix_invert(m
):
599 r
= mathutils
.Matrix((
600 (m
[1][1]*m
[2][2] - m
[1][2]*m
[2][1], m
[0][2]*m
[2][1] - m
[0][1]*m
[2][2],
601 m
[0][1]*m
[1][2] - m
[0][2]*m
[1][1]),
602 (m
[1][2]*m
[2][0] - m
[1][0]*m
[2][2], m
[0][0]*m
[2][2] - m
[0][2]*m
[2][0],
603 m
[0][2]*m
[1][0] - m
[0][0]*m
[1][2]),
604 (m
[1][0]*m
[2][1] - m
[1][1]*m
[2][0], m
[0][1]*m
[2][0] - m
[0][0]*m
[2][1],
605 m
[0][0]*m
[1][1] - m
[0][1]*m
[1][0])))
607 return (r
* (1 / matrix_determinant(m
)))
610 # returns a list of all loops parallel to the input, input included
611 def get_parallel_loops(bm_mod
, loops
):
612 # get required dictionaries
613 edge_faces
= dict_edge_faces(bm_mod
)
614 connected_faces
= dict_face_faces(bm_mod
, edge_faces
)
615 # turn vertex loops into edge loops
618 edgeloop
= [[sorted([loop
[0][i
], loop
[0][i
+1]]) for i
in \
619 range(len(loop
[0])-1)], loop
[1]]
620 if loop
[1]: # circular
621 edgeloop
[0].append(sorted([loop
[0][-1], loop
[0][0]]))
622 edgeloops
.append(edgeloop
[:])
623 # variables to keep track while iterating
627 for loop
in edgeloops
:
628 # initialise with original loop
629 all_edgeloops
.append(loop
[0])
633 if edge
[0] not in verts_used
:
634 verts_used
.append(edge
[0])
635 if edge
[1] not in verts_used
:
636 verts_used
.append(edge
[1])
638 # find parallel loops
639 while len(newloops
) > 0:
642 for i
in newloops
[-1]:
644 forbidden_side
= False
645 if not i
in edge_faces
:
646 # weird input with branches
649 for face
in edge_faces
[i
]:
650 if len(side_a
) == 0 and forbidden_side
!= "a":
656 elif side_a
[-1] in connected_faces
[face
] and \
657 forbidden_side
!= "a":
663 if len(side_b
) == 0 and forbidden_side
!= "b":
669 elif side_b
[-1] in connected_faces
[face
] and \
670 forbidden_side
!= "b":
678 # weird input with branches
691 for key
in face_edgekeys(bm_mod
.faces
[fi
]):
692 if key
[0] not in verts_used
and key
[1] not in \
694 extraloop
.append(key
)
697 for key
in extraloop
:
699 if new_vert
not in verts_used
:
700 verts_used
.append(new_vert
)
701 newloops
.append(extraloop
)
702 all_edgeloops
.append(extraloop
)
704 # input contains branches, only return selected loop
708 # change edgeloops into normal loops
710 for edgeloop
in all_edgeloops
:
712 # grow loop by comparing vertices between consecutive edge-keys
713 for i
in range(len(edgeloop
)-1):
714 for vert
in range(2):
715 if edgeloop
[i
][vert
] in edgeloop
[i
+1]:
716 loop
.append(edgeloop
[i
][vert
])
719 # add starting vertex
720 for vert
in range(2):
721 if edgeloop
[0][vert
] != loop
[0]:
722 loop
= [edgeloop
[0][vert
]] + loop
725 for vert
in range(2):
726 if edgeloop
[-1][vert
] != loop
[-1]:
727 loop
.append(edgeloop
[-1][vert
])
729 # check if loop is circular
730 if loop
[0] == loop
[-1]:
735 loops
.append([loop
, circular
])
740 # gather initial data
742 global_undo
= bpy
.context
.user_preferences
.edit
.use_global_undo
743 bpy
.context
.user_preferences
.edit
.use_global_undo
= False
744 object = bpy
.context
.active_object
745 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
746 # ensure that selection is synced for the derived mesh
747 bpy
.ops
.object.mode_set(mode
='OBJECT')
748 bpy
.ops
.object.mode_set(mode
='EDIT')
749 bm
= bmesh
.from_edit_mesh(object.data
)
751 bm
.verts
.ensure_lookup_table()
752 bm
.edges
.ensure_lookup_table()
753 bm
.faces
.ensure_lookup_table()
755 return(global_undo
, object, bm
)
758 # move the vertices to their new locations
759 def move_verts(object, bm
, mapping
, move
, lock
, influence
):
761 lock_x
, lock_y
, lock_z
= lock
762 orientation
= bpy
.context
.space_data
.transform_orientation
763 custom
= bpy
.context
.space_data
.current_orientation
765 mat
= custom
.matrix
.to_4x4().inverted() * object.matrix_world
.copy()
766 elif orientation
== 'LOCAL':
767 mat
= mathutils
.Matrix
.Identity(4)
768 elif orientation
== 'VIEW':
769 mat
= bpy
.context
.region_data
.view_matrix
.copy() * \
770 object.matrix_world
.copy()
771 else: # orientation == 'GLOBAL'
772 mat
= object.matrix_world
.copy()
773 mat_inv
= mat
.inverted()
776 for index
, loc
in loop
:
778 if mapping
[index
] == -1:
781 index
= mapping
[index
]
783 delta
= (loc
- bm
.verts
[index
].co
) * mat_inv
791 loc
= bm
.verts
[index
].co
+ delta
795 new_loc
= loc
*(influence
/100) + \
796 bm
.verts
[index
].co
*((100-influence
)/100)
797 bm
.verts
[index
].co
= new_loc
801 bm
.verts
.ensure_lookup_table()
802 bm
.edges
.ensure_lookup_table()
803 bm
.faces
.ensure_lookup_table()
806 # load custom tool settings
807 def settings_load(self
):
808 lt
= bpy
.context
.window_manager
.looptools
809 tool
= self
.name
.split()[0].lower()
810 keys
= self
.as_keywords().keys()
812 setattr(self
, key
, getattr(lt
, tool
+ "_" + key
))
815 # store custom tool settings
816 def settings_write(self
):
817 lt
= bpy
.context
.window_manager
.looptools
818 tool
= self
.name
.split()[0].lower()
819 keys
= self
.as_keywords().keys()
821 setattr(lt
, tool
+ "_" + key
, getattr(self
, key
))
824 # clean up and set settings back to original state
825 def terminate(global_undo
):
826 # update editmesh cached data
827 obj
= bpy
.context
.active_object
828 if obj
.mode
== 'EDIT':
829 bmesh
.update_edit_mesh(obj
.data
, tessface
=True, destructive
=True)
831 bpy
.context
.user_preferences
.edit
.use_global_undo
= global_undo
834 ##########################################
835 ####### Bridge functions #################
836 ##########################################
838 # calculate a cubic spline through the middle section of 4 given coordinates
839 def bridge_calculate_cubic_spline(bm
, coordinates
):
845 for i
in coordinates
:
846 a
.append(float(i
[j
]))
849 h
.append(x
[i
+1]-x
[i
])
852 q
.append(3.0/h
[i
]*(a
[i
+1]-a
[i
])-3.0/h
[i
-1]*(a
[i
]-a
[i
-1]))
857 l
.append(2.0*(x
[i
+1]-x
[i
-1])-h
[i
-1]*u
[i
-1])
859 z
.append((q
[i
]-h
[i
-1]*z
[i
-1])/l
[i
])
862 b
= [False for i
in range(3)]
863 c
= [False for i
in range(4)]
864 d
= [False for i
in range(3)]
866 for i
in range(2,-1,-1):
867 c
[i
] = z
[i
]-u
[i
]*c
[i
+1]
868 b
[i
] = (a
[i
+1]-a
[i
])/h
[i
]-h
[i
]*(c
[i
+1]+2.0*c
[i
])/3.0
869 d
[i
] = (c
[i
+1]-c
[i
])/(3.0*h
[i
])
871 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
872 spline
= [result
[1], result
[4], result
[7]]
877 # return a list with new vertex location vectors, a list with face vertex
878 # integers, and the highest vertex integer in the virtual mesh
879 def bridge_calculate_geometry(bm
, lines
, vertex_normals
, segments
,
880 interpolation
, cubic_strength
, min_width
, max_vert_index
):
884 # calculate location based on interpolation method
885 def get_location(line
, segment
, splines
):
886 v1
= bm
.verts
[lines
[line
][0]].co
887 v2
= bm
.verts
[lines
[line
][1]].co
888 if interpolation
== 'linear':
889 return v1
+ (segment
/segments
) * (v2
-v1
)
890 else: # interpolation == 'cubic'
891 m
= (segment
/segments
)
892 ax
,bx
,cx
,dx
,tx
= splines
[line
][0]
893 x
= ax
+bx
*m
+cx
*m
**2+dx
*m
**3
894 ay
,by
,cy
,dy
,ty
= splines
[line
][1]
895 y
= ay
+by
*m
+cy
*m
**2+dy
*m
**3
896 az
,bz
,cz
,dz
,tz
= splines
[line
][2]
897 z
= az
+bz
*m
+cz
*m
**2+dz
*m
**3
898 return mathutils
.Vector((x
, y
, z
))
900 # no interpolation needed
902 for i
, line
in enumerate(lines
):
904 faces
.append([line
[0], lines
[i
+1][0], lines
[i
+1][1], line
[1]])
905 # more than 1 segment, interpolate
907 # calculate splines (if necessary) once, so no recalculations needed
908 if interpolation
== 'cubic':
911 v1
= bm
.verts
[line
[0]].co
912 v2
= bm
.verts
[line
[1]].co
913 size
= (v2
-v1
).length
* cubic_strength
914 splines
.append(bridge_calculate_cubic_spline(bm
,
915 [v1
+size
*vertex_normals
[line
[0]], v1
, v2
,
916 v2
+size
*vertex_normals
[line
[1]]]))
920 # create starting situation
921 virtual_width
= [(bm
.verts
[lines
[i
][0]].co
-
922 bm
.verts
[lines
[i
+1][0]].co
).length
for i
923 in range(len(lines
)-1)]
924 new_verts
= [get_location(0, seg
, splines
) for seg
in range(1,
926 first_line_indices
= [i
for i
in range(max_vert_index
+1,
927 max_vert_index
+segments
)]
929 prev_verts
= new_verts
[:] # vertex locations of verts on previous line
930 prev_vert_indices
= first_line_indices
[:]
931 max_vert_index
+= segments
- 1 # highest vertex index in virtual mesh
932 next_verts
= [] # vertex locations of verts on current line
933 next_vert_indices
= []
935 for i
, line
in enumerate(lines
):
940 for seg
in range(1, segments
):
941 loc1
= prev_verts
[seg
-1]
942 loc2
= get_location(i
+1, seg
, splines
)
943 if (loc1
-loc2
).length
< (min_width
/100)*virtual_width
[i
] \
944 and line
[1]==lines
[i
+1][1]:
945 # triangle, no new vertex
946 faces
.append([v1
, v2
, prev_vert_indices
[seg
-1],
947 prev_vert_indices
[seg
-1]])
948 next_verts
+= prev_verts
[seg
-1:]
949 next_vert_indices
+= prev_vert_indices
[seg
-1:]
953 if i
== len(lines
)-2 and lines
[0] == lines
[-1]:
954 # quad with first line, no new vertex
955 faces
.append([v1
, v2
, first_line_indices
[seg
-1],
956 prev_vert_indices
[seg
-1]])
957 v2
= first_line_indices
[seg
-1]
958 v1
= prev_vert_indices
[seg
-1]
960 # quad, add new vertex
962 faces
.append([v1
, v2
, max_vert_index
,
963 prev_vert_indices
[seg
-1]])
965 v1
= prev_vert_indices
[seg
-1]
966 new_verts
.append(loc2
)
967 next_verts
.append(loc2
)
968 next_vert_indices
.append(max_vert_index
)
970 faces
.append([v1
, v2
, lines
[i
+1][1], line
[1]])
972 prev_verts
= next_verts
[:]
973 prev_vert_indices
= next_vert_indices
[:]
975 next_vert_indices
= []
977 return(new_verts
, faces
, max_vert_index
)
980 # calculate lines (list of lists, vertex indices) that are used for bridging
981 def bridge_calculate_lines(bm
, loops
, mode
, twist
, reverse
):
983 loop1
, loop2
= [i
[0] for i
in loops
]
984 loop1_circular
, loop2_circular
= [i
[1] for i
in loops
]
985 circular
= loop1_circular
or loop2_circular
988 # calculate loop centers
990 for loop
in [loop1
, loop2
]:
991 center
= mathutils
.Vector()
993 center
+= bm
.verts
[vertex
].co
995 centers
.append(center
)
996 for i
, loop
in enumerate([loop1
, loop2
]):
998 if bm
.verts
[vertex
].co
== centers
[i
]:
999 # prevent zero-length vectors in angle comparisons
1000 centers
[i
] += mathutils
.Vector((0.01, 0, 0))
1002 center1
, center2
= centers
1004 # calculate the normals of the virtual planes that the loops are on
1006 normal_plurity
= False
1007 for i
, loop
in enumerate([loop1
, loop2
]):
1009 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1012 x
, y
, z
= centers
[i
]
1013 for loc
in [bm
.verts
[vertex
].co
for vertex
in loop
]:
1014 mat
[0][0] += (loc
[0]-x
)**2
1015 mat
[1][0] += (loc
[0]-x
)*(loc
[1]-y
)
1016 mat
[2][0] += (loc
[0]-x
)*(loc
[2]-z
)
1017 mat
[0][1] += (loc
[1]-y
)*(loc
[0]-x
)
1018 mat
[1][1] += (loc
[1]-y
)**2
1019 mat
[2][1] += (loc
[1]-y
)*(loc
[2]-z
)
1020 mat
[0][2] += (loc
[2]-z
)*(loc
[0]-x
)
1021 mat
[1][2] += (loc
[2]-z
)*(loc
[1]-y
)
1022 mat
[2][2] += (loc
[2]-z
)**2
1025 if sum(mat
[0]) < 1e-6 or sum(mat
[1]) < 1e-6 or sum(mat
[2]) < 1e-6:
1026 normal_plurity
= True
1030 if sum(mat
[0]) == 0:
1031 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
1032 elif sum(mat
[1]) == 0:
1033 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
1034 elif sum(mat
[2]) == 0:
1035 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
1037 # warning! this is different from .normalize()
1040 vec
= mathutils
.Vector((1.0, 1.0, 1.0))
1041 vec2
= (mat
* vec
)/(mat
* vec
).length
1042 while vec
!= vec2
and iter<itermax
:
1046 if vec2
.length
!= 0:
1048 if vec2
.length
== 0:
1049 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
1051 normals
.append(normal
)
1052 # have plane normals face in the same direction (maximum angle: 90 degrees)
1053 if ((center1
+ normals
[0]) - center2
).length
< \
1054 ((center1
- normals
[0]) - center2
).length
:
1056 if ((center2
+ normals
[1]) - center1
).length
> \
1057 ((center2
- normals
[1]) - center1
).length
:
1060 # rotation matrix, representing the difference between the plane normals
1061 axis
= normals
[0].cross(normals
[1])
1062 axis
= mathutils
.Vector([loc
if abs(loc
) > 1e-8 else 0 for loc
in axis
])
1063 if axis
.angle(mathutils
.Vector((0, 0, 1)), 0) > 1.5707964:
1065 angle
= normals
[0].dot(normals
[1])
1066 rotation_matrix
= mathutils
.Matrix
.Rotation(angle
, 4, axis
)
1068 # if circular, rotate loops so they are aligned
1070 # make sure loop1 is the circular one (or both are circular)
1071 if loop2_circular
and not loop1_circular
:
1072 loop1_circular
, loop2_circular
= True, False
1073 loop1
, loop2
= loop2
, loop1
1075 # match start vertex of loop1 with loop2
1076 target_vector
= bm
.verts
[loop2
[0]].co
- center2
1077 dif_angles
= [[(rotation_matrix
* (bm
.verts
[vertex
].co
- center1
)
1078 ).angle(target_vector
, 0), False, i
] for
1079 i
, vertex
in enumerate(loop1
)]
1081 if len(loop1
) != len(loop2
):
1082 angle_limit
= dif_angles
[0][0] * 1.2 # 20% margin
1083 dif_angles
= [[(bm
.verts
[loop2
[0]].co
- \
1084 bm
.verts
[loop1
[index
]].co
).length
, angle
, index
] for \
1085 angle
, distance
, index
in dif_angles
if angle
<= angle_limit
]
1087 loop1
= loop1
[dif_angles
[0][2]:] + loop1
[:dif_angles
[0][2]]
1089 # have both loops face the same way
1090 if normal_plurity
and not circular
:
1091 second_to_first
, second_to_second
, second_to_last
= \
1092 [(bm
.verts
[loop1
[1]].co
- center1
).\
1093 angle(bm
.verts
[loop2
[i
]].co
- center2
) for i
in [0, 1, -1]]
1094 last_to_first
, last_to_second
= [(bm
.verts
[loop1
[-1]].co
- \
1095 center1
).angle(bm
.verts
[loop2
[i
]].co
- center2
) for \
1097 if (min(last_to_first
, last_to_second
)*1.1 < min(second_to_first
, \
1098 second_to_second
)) or (loop2_circular
and second_to_last
*1.1 < \
1099 min(second_to_first
, second_to_second
)):
1102 loop1
= [loop1
[-1]] + loop1
[:-1]
1104 angle
= (bm
.verts
[loop1
[0]].co
- center1
).\
1105 cross(bm
.verts
[loop1
[1]].co
- center1
).angle(normals
[0], 0)
1106 target_angle
= (bm
.verts
[loop2
[0]].co
- center2
).\
1107 cross(bm
.verts
[loop2
[1]].co
- center2
).angle(normals
[1], 0)
1108 limit
= 1.5707964 # 0.5*pi, 90 degrees
1109 if not ((angle
> limit
and target_angle
> limit
) or \
1110 (angle
< limit
and target_angle
< limit
)):
1113 loop1
= [loop1
[-1]] + loop1
[:-1]
1114 elif normals
[0].angle(normals
[1]) > limit
:
1117 loop1
= [loop1
[-1]] + loop1
[:-1]
1119 # both loops have the same length
1120 if len(loop1
) == len(loop2
):
1123 if abs(twist
) < len(loop1
):
1124 loop1
= loop1
[twist
:]+loop1
[:twist
]
1128 lines
.append([loop1
[0], loop2
[0]])
1129 for i
in range(1, len(loop1
)):
1130 lines
.append([loop1
[i
], loop2
[i
]])
1132 # loops of different lengths
1134 # make loop1 longest loop
1135 if len(loop2
) > len(loop1
):
1136 loop1
, loop2
= loop2
, loop1
1137 loop1_circular
, loop2_circular
= loop2_circular
, loop1_circular
1141 if abs(twist
) < len(loop1
):
1142 loop1
= loop1
[twist
:]+loop1
[:twist
]
1146 # shortest angle difference doesn't always give correct start vertex
1147 if loop1_circular
and not loop2_circular
:
1150 if len(loop1
) - shifting
< len(loop2
):
1153 to_last
, to_first
= [(rotation_matrix
*
1154 (bm
.verts
[loop1
[-1]].co
- center1
)).angle((bm
.\
1155 verts
[loop2
[i
]].co
- center2
), 0) for i
in [-1, 0]]
1156 if to_first
< to_last
:
1157 loop1
= [loop1
[-1]] + loop1
[:-1]
1163 # basic shortest side first
1165 lines
.append([loop1
[0], loop2
[0]])
1166 for i
in range(1, len(loop1
)):
1167 if i
>= len(loop2
) - 1:
1169 lines
.append([loop1
[i
], loop2
[-1]])
1172 lines
.append([loop1
[i
], loop2
[i
]])
1174 # shortest edge algorithm
1175 else: # mode == 'shortest'
1176 lines
.append([loop1
[0], loop2
[0]])
1178 for i
in range(len(loop1
) -1):
1179 if prev_vert2
== len(loop2
) - 1 and not loop2_circular
:
1180 # force triangles, reached end of loop2
1182 elif prev_vert2
== len(loop2
) - 1 and loop2_circular
:
1183 # at end of loop2, but circular, so check with first vert
1184 tri
, quad
= [(bm
.verts
[loop1
[i
+1]].co
-
1185 bm
.verts
[loop2
[j
]].co
).length
1186 for j
in [prev_vert2
, 0]]
1188 elif len(loop1
) - 1 - i
== len(loop2
) - 1 - prev_vert2
and \
1190 # force quads, otherwise won't make it to end of loop2
1193 # calculate if tri or quad gives shortest edge
1194 tri
, quad
= [(bm
.verts
[loop1
[i
+1]].co
-
1195 bm
.verts
[loop2
[j
]].co
).length
1196 for j
in range(prev_vert2
, prev_vert2
+2)]
1200 lines
.append([loop1
[i
+1], loop2
[prev_vert2
]])
1201 if circle_full
== 2:
1204 elif not circle_full
:
1205 lines
.append([loop1
[i
+1], loop2
[prev_vert2
+1]])
1207 # quad to first vertex of loop2
1209 lines
.append([loop1
[i
+1], loop2
[0]])
1213 # final face for circular loops
1214 if loop1_circular
and loop2_circular
:
1215 lines
.append([loop1
[0], loop2
[0]])
1220 # calculate number of segments needed
1221 def bridge_calculate_segments(bm
, lines
, loops
, segments
):
1222 # return if amount of segments is set by user
1227 average_edge_length
= [(bm
.verts
[vertex
].co
- \
1228 bm
.verts
[loop
[0][i
+1]].co
).length
for loop
in loops
for \
1229 i
, vertex
in enumerate(loop
[0][:-1])]
1230 # closing edges of circular loops
1231 average_edge_length
+= [(bm
.verts
[loop
[0][-1]].co
- \
1232 bm
.verts
[loop
[0][0]].co
).length
for loop
in loops
if loop
[1]]
1235 average_edge_length
= sum(average_edge_length
) / len(average_edge_length
)
1236 average_bridge_length
= sum([(bm
.verts
[v1
].co
- \
1237 bm
.verts
[v2
].co
).length
for v1
, v2
in lines
]) / len(lines
)
1239 segments
= max(1, round(average_bridge_length
/ average_edge_length
))
1244 # return dictionary with vertex index as key, and the normal vector as value
1245 def bridge_calculate_virtual_vertex_normals(bm
, lines
, loops
, edge_faces
,
1247 if not edge_faces
: # interpolation isn't set to cubic
1250 # pity reduce() isn't one of the basic functions in python anymore
1251 def average_vector_dictionary(dic
):
1252 for key
, vectors
in dic
.items():
1253 #if type(vectors) == type([]) and len(vectors) > 1:
1254 if len(vectors
) > 1:
1255 average
= mathutils
.Vector()
1256 for vector
in vectors
:
1258 average
/= len(vectors
)
1259 dic
[key
] = [average
]
1262 # get all edges of the loop
1263 edges
= [[edgekey_to_edge
[tuple(sorted([loops
[j
][0][i
],
1264 loops
[j
][0][i
+1]]))] for i
in range(len(loops
[j
][0])-1)] for \
1266 edges
= edges
[0] + edges
[1]
1268 if loops
[j
][1]: # circular
1269 edges
.append(edgekey_to_edge
[tuple(sorted([loops
[j
][0][0],
1270 loops
[j
][0][-1]]))])
1273 calculation based on face topology (assign edge-normals to vertices)
1275 edge_normal = face_normal x edge_vector
1276 vertex_normal = average(edge_normals)
1278 vertex_normals
= dict([(vertex
, []) for vertex
in loops
[0][0]+loops
[1][0]])
1280 faces
= edge_faces
[edgekey(edge
)] # valid faces connected to edge
1283 # get edge coordinates
1284 v1
, v2
= [bm
.verts
[edgekey(edge
)[i
]].co
for i
in [0,1]]
1285 edge_vector
= v1
- v2
1286 if edge_vector
.length
< 1e-4:
1287 # zero-length edge, vertices at same location
1289 edge_center
= (v1
+ v2
) / 2
1291 # average face coordinates, if connected to more than 1 valid face
1293 face_normal
= mathutils
.Vector()
1294 face_center
= mathutils
.Vector()
1296 face_normal
+= face
.normal
1297 face_center
+= face
.calc_center_median()
1298 face_normal
/= len(faces
)
1299 face_center
/= len(faces
)
1301 face_normal
= faces
[0].normal
1302 face_center
= faces
[0].calc_center_median()
1303 if face_normal
.length
< 1e-4:
1304 # faces with a surface of 0 have no face normal
1307 # calculate virtual edge normal
1308 edge_normal
= edge_vector
.cross(face_normal
)
1309 edge_normal
.length
= 0.01
1310 if (face_center
- (edge_center
+ edge_normal
)).length
> \
1311 (face_center
- (edge_center
- edge_normal
)).length
:
1312 # make normal face the correct way
1313 edge_normal
.negate()
1314 edge_normal
.normalize()
1315 # add virtual edge normal as entry for both vertices it connects
1316 for vertex
in edgekey(edge
):
1317 vertex_normals
[vertex
].append(edge_normal
)
1320 calculation based on connection with other loop (vertex focused method)
1321 - used for vertices that aren't connected to any valid faces
1323 plane_normal = edge_vector x connection_vector
1324 vertex_normal = plane_normal x edge_vector
1326 vertices
= [vertex
for vertex
, normal
in vertex_normals
.items() if not \
1330 # edge vectors connected to vertices
1331 edge_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1333 for v
in edgekey(edge
):
1334 if v
in edge_vectors
:
1335 edge_vector
= bm
.verts
[edgekey(edge
)[0]].co
- \
1336 bm
.verts
[edgekey(edge
)[1]].co
1337 if edge_vector
.length
< 1e-4:
1338 # zero-length edge, vertices at same location
1340 edge_vectors
[v
].append(edge_vector
)
1342 # connection vectors between vertices of both loops
1343 connection_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1344 connections
= dict([[vertex
, []] for vertex
in vertices
])
1345 for v1
, v2
in lines
:
1346 if v1
in connection_vectors
or v2
in connection_vectors
:
1347 new_vector
= bm
.verts
[v1
].co
- bm
.verts
[v2
].co
1348 if new_vector
.length
< 1e-4:
1349 # zero-length connection vector,
1350 # vertices in different loops at same location
1352 if v1
in connection_vectors
:
1353 connection_vectors
[v1
].append(new_vector
)
1354 connections
[v1
].append(v2
)
1355 if v2
in connection_vectors
:
1356 connection_vectors
[v2
].append(new_vector
)
1357 connections
[v2
].append(v1
)
1358 connection_vectors
= average_vector_dictionary(connection_vectors
)
1359 connection_vectors
= dict([[vertex
, vector
[0]] if vector
else \
1360 [vertex
, []] for vertex
, vector
in connection_vectors
.items()])
1362 for vertex
, values
in edge_vectors
.items():
1363 # vertex normal doesn't matter, just assign a random vector to it
1364 if not connection_vectors
[vertex
]:
1365 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1368 # calculate to what location the vertex is connected,
1369 # used to determine what way to flip the normal
1370 connected_center
= mathutils
.Vector()
1371 for v
in connections
[vertex
]:
1372 connected_center
+= bm
.verts
[v
].co
1373 if len(connections
[vertex
]) > 1:
1374 connected_center
/= len(connections
[vertex
])
1375 if len(connections
[vertex
]) == 0:
1376 # shouldn't be possible, but better safe than sorry
1377 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1380 # can't do proper calculations, because of zero-length vector
1382 if (connected_center
- (bm
.verts
[vertex
].co
+ \
1383 connection_vectors
[vertex
])).length
< (connected_center
- \
1384 (bm
.verts
[vertex
].co
- connection_vectors
[vertex
])).\
1386 connection_vectors
[vertex
].negate()
1387 vertex_normals
[vertex
] = [connection_vectors
[vertex
].\
1391 # calculate vertex normals using edge-vectors,
1392 # connection-vectors and the derived plane normal
1393 for edge_vector
in values
:
1394 plane_normal
= edge_vector
.cross(connection_vectors
[vertex
])
1395 vertex_normal
= edge_vector
.cross(plane_normal
)
1396 vertex_normal
.length
= 0.1
1397 if (connected_center
- (bm
.verts
[vertex
].co
+ \
1398 vertex_normal
)).length
< (connected_center
- \
1399 (bm
.verts
[vertex
].co
- vertex_normal
)).length
:
1400 # make normal face the correct way
1401 vertex_normal
.negate()
1402 vertex_normal
.normalize()
1403 vertex_normals
[vertex
].append(vertex_normal
)
1405 # average virtual vertex normals, based on all edges it's connected to
1406 vertex_normals
= average_vector_dictionary(vertex_normals
)
1407 vertex_normals
= dict([[vertex
, vector
[0]] for vertex
, vector
in \
1408 vertex_normals
.items()])
1410 return(vertex_normals
)
1413 # add vertices to mesh
1414 def bridge_create_vertices(bm
, vertices
):
1415 for i
in range(len(vertices
)):
1416 bm
.verts
.new(vertices
[i
])
1417 bm
.verts
.ensure_lookup_table()
1421 def bridge_create_faces(object, bm
, faces
, twist
):
1422 # have the normal point the correct way
1424 [face
.reverse() for face
in faces
]
1425 faces
= [face
[2:]+face
[:2] if face
[0]==face
[1] else face
for \
1428 # eekadoodle prevention
1429 for i
in range(len(faces
)):
1430 if not faces
[i
][-1]:
1431 if faces
[i
][0] == faces
[i
][-1]:
1432 faces
[i
] = [faces
[i
][1], faces
[i
][2], faces
[i
][3], faces
[i
][1]]
1434 faces
[i
] = [faces
[i
][-1]] + faces
[i
][:-1]
1435 # result of converting from pre-bmesh period
1436 if faces
[i
][-1] == faces
[i
][-2]:
1437 faces
[i
] = faces
[i
][:-1]
1440 for i
in range(len(faces
)):
1441 new_faces
.append(bm
.faces
.new([bm
.verts
[v
] for v
in faces
[i
]]))
1443 object.data
.update(calc_edges
=True) # calc_edges prevents memory-corruption
1445 bm
.verts
.ensure_lookup_table()
1446 bm
.edges
.ensure_lookup_table()
1447 bm
.faces
.ensure_lookup_table()
1452 # calculate input loops
1453 def bridge_get_input(bm
):
1454 # create list of internal edges, which should be skipped
1455 eks_of_selected_faces
= [item
for sublist
in [face_edgekeys(face
) for \
1456 face
in bm
.faces
if face
.select
and not face
.hide
] for item
in sublist
]
1458 for ek
in eks_of_selected_faces
:
1459 if ek
in edge_count
:
1463 internal_edges
= [ek
for ek
in edge_count
if edge_count
[ek
] > 1]
1465 # sort correct edges into loops
1466 selected_edges
= [edgekey(edge
) for edge
in bm
.edges
if edge
.select \
1467 and not edge
.hide
and edgekey(edge
) not in internal_edges
]
1468 loops
= get_connected_selections(selected_edges
)
1473 # return values needed by the bridge operator
1474 def bridge_initialise(bm
, interpolation
):
1475 if interpolation
== 'cubic':
1476 # dict with edge-key as key and list of connected valid faces as value
1477 face_blacklist
= [face
.index
for face
in bm
.faces
if face
.select
or \
1479 edge_faces
= dict([[edgekey(edge
), []] for edge
in bm
.edges
if not \
1481 for face
in bm
.faces
:
1482 if face
.index
in face_blacklist
:
1484 for key
in face_edgekeys(face
):
1485 edge_faces
[key
].append(face
)
1486 # dictionary with the edge-key as key and edge as value
1487 edgekey_to_edge
= dict([[edgekey(edge
), edge
] for edge
in \
1488 bm
.edges
if edge
.select
and not edge
.hide
])
1491 edgekey_to_edge
= False
1493 # selected faces input
1494 old_selected_faces
= [face
.index
for face
in bm
.faces
if face
.select \
1497 # find out if faces created by bridging should be smoothed
1500 if sum([face
.smooth
for face
in bm
.faces
])/len(bm
.faces
) \
1504 return(edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
)
1507 # return a string with the input method
1508 def bridge_input_method(loft
, loft_loop
):
1512 method
= "Loft loop"
1514 method
= "Loft no-loop"
1521 # match up loops in pairs, used for multi-input bridging
1522 def bridge_match_loops(bm
, loops
):
1523 # calculate average loop normals and centers
1526 for vertices
, circular
in loops
:
1527 normal
= mathutils
.Vector()
1528 center
= mathutils
.Vector()
1529 for vertex
in vertices
:
1530 normal
+= bm
.verts
[vertex
].normal
1531 center
+= bm
.verts
[vertex
].co
1532 normals
.append(normal
/ len(vertices
) / 10)
1533 centers
.append(center
/ len(vertices
))
1535 # possible matches if loop normals are faced towards the center
1537 matches
= dict([[i
, []] for i
in range(len(loops
))])
1539 for i
in range(len(loops
) + 1):
1540 for j
in range(i
+1, len(loops
)):
1541 if (centers
[i
] - centers
[j
]).length
> (centers
[i
] - (centers
[j
] \
1542 + normals
[j
])).length
and (centers
[j
] - centers
[i
]).length
> \
1543 (centers
[j
] - (centers
[i
] + normals
[i
])).length
:
1545 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1546 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1547 # if no loops face each other, just make matches between all the loops
1548 if matches_amount
== 0:
1549 for i
in range(len(loops
) + 1):
1550 for j
in range(i
+1, len(loops
)):
1551 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1552 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1553 for key
, value
in matches
.items():
1556 # matches based on distance between centers and number of vertices in loops
1558 for loop_index
in range(len(loops
)):
1559 if loop_index
in new_order
:
1561 loop_matches
= matches
[loop_index
]
1562 if not loop_matches
:
1564 shortest_distance
= loop_matches
[0][0]
1565 shortest_distance
*= 1.1
1566 loop_matches
= [[abs(len(loops
[loop_index
][0]) - \
1567 len(loops
[loop
[2]][0])), loop
[0], loop
[1], loop
[2]] for loop
in \
1568 loop_matches
if loop
[0] < shortest_distance
]
1570 for match
in loop_matches
:
1571 if match
[3] not in new_order
:
1572 new_order
+= [loop_index
, match
[3]]
1575 # reorder loops based on matches
1576 if len(new_order
) >= 2:
1577 loops
= [loops
[i
] for i
in new_order
]
1582 # remove old_selected_faces
1583 def bridge_remove_internal_faces(bm
, old_selected_faces
):
1584 # collect bmesh faces and internal bmesh edges
1585 remove_faces
= [bm
.faces
[face
] for face
in old_selected_faces
]
1586 edges
= collections
.Counter([edge
.index
for face
in remove_faces
for \
1587 edge
in face
.edges
])
1588 remove_edges
= [bm
.edges
[edge
] for edge
in edges
if edges
[edge
] > 1]
1590 # remove internal faces and edges
1591 for face
in remove_faces
:
1592 bm
.faces
.remove(face
)
1593 for edge
in remove_edges
:
1594 bm
.edges
.remove(edge
)
1596 bm
.faces
.ensure_lookup_table()
1597 bm
.edges
.ensure_lookup_table()
1598 bm
.verts
.ensure_lookup_table()
1601 # update list of internal faces that are flagged for removal
1602 def bridge_save_unused_faces(bm
, old_selected_faces
, loops
):
1603 # key: vertex index, value: lists of selected faces using it
1605 vertex_to_face
= dict([[i
, []] for i
in range(len(bm
.verts
))])
1606 [[vertex_to_face
[vertex
.index
].append(face
) for vertex
in \
1607 bm
.faces
[face
].verts
] for face
in old_selected_faces
]
1609 # group selected faces that are connected
1612 for face
in old_selected_faces
:
1613 if face
in grouped_faces
:
1615 grouped_faces
.append(face
)
1619 grow_face
= new_faces
[0]
1620 for vertex
in bm
.faces
[grow_face
].verts
:
1621 vertex_face_group
= [face
for face
in vertex_to_face
[\
1622 vertex
.index
] if face
not in grouped_faces
]
1623 new_faces
+= vertex_face_group
1624 grouped_faces
+= vertex_face_group
1625 group
+= vertex_face_group
1627 groups
.append(group
)
1629 # key: vertex index, value: True/False (is it in a loop that is used)
1630 used_vertices
= dict([[i
, 0] for i
in range(len(bm
.verts
))])
1632 for vertex
in loop
[0]:
1633 used_vertices
[vertex
] = True
1635 # check if group is bridged, if not remove faces from internal faces list
1636 for group
in groups
:
1641 for vertex
in bm
.faces
[face
].verts
:
1642 if used_vertices
[vertex
.index
]:
1647 old_selected_faces
.remove(face
)
1650 # add the newly created faces to the selection
1651 def bridge_select_new_faces(new_faces
, smooth
):
1652 for face
in new_faces
:
1653 face
.select_set(True)
1654 face
.smooth
= smooth
1657 # sort loops, so they are connected in the correct order when lofting
1658 def bridge_sort_loops(bm
, loops
, loft_loop
):
1659 # simplify loops to single points, and prepare for pathfinding
1660 x
, y
, z
= [[sum([bm
.verts
[i
].co
[j
] for i
in loop
[0]]) / \
1661 len(loop
[0]) for loop
in loops
] for j
in range(3)]
1662 nodes
= [mathutils
.Vector((x
[i
], y
[i
], z
[i
])) for i
in range(len(loops
))]
1665 open = [i
for i
in range(1, len(loops
))]
1667 # connect node to path, that is shortest to active_node
1668 while len(open) > 0:
1669 distances
= [(nodes
[active_node
] - nodes
[i
]).length
for i
in open]
1670 active_node
= open[distances
.index(min(distances
))]
1671 open.remove(active_node
)
1672 path
.append([active_node
, min(distances
)])
1673 # check if we didn't start in the middle of the path
1674 for i
in range(2, len(path
)):
1675 if (nodes
[path
[i
][0]]-nodes
[0]).length
< path
[i
][1]:
1678 path
= path
[:-i
] + temp
1682 loops
= [loops
[i
[0]] for i
in path
]
1683 # if requested, duplicate first loop at last position, so loft can loop
1685 loops
= loops
+ [loops
[0]]
1690 # remapping old indices to new position in list
1691 def bridge_update_old_selection(bm
, old_selected_faces
):
1692 #old_indices = old_selected_faces[:]
1693 #old_selected_faces = []
1694 #for i, face in enumerate(bm.faces):
1695 # if face.index in old_indices:
1696 # old_selected_faces.append(i)
1698 old_selected_faces
= [i
for i
, face
in enumerate(bm
.faces
) if face
.index \
1699 in old_selected_faces
]
1701 return(old_selected_faces
)
1704 ##########################################
1705 ####### Circle functions #################
1706 ##########################################
1708 # convert 3d coordinates to 2d coordinates on plane
1709 def circle_3d_to_2d(bm_mod
, loop
, com
, normal
):
1710 # project vertices onto the plane
1711 verts
= [bm_mod
.verts
[v
] for v
in loop
[0]]
1712 verts_projected
= [[v
.co
- (v
.co
- com
).dot(normal
) * normal
, v
.index
]
1715 # calculate two vectors (p and q) along the plane
1716 m
= mathutils
.Vector((normal
[0] + 1.0, normal
[1], normal
[2]))
1717 p
= m
- (m
.dot(normal
) * normal
)
1719 m
= mathutils
.Vector((normal
[0], normal
[1] + 1.0, normal
[2]))
1720 p
= m
- (m
.dot(normal
) * normal
)
1723 # change to 2d coordinates using perpendicular projection
1725 for loc
, vert
in verts_projected
:
1727 x
= p
.dot(vloc
) / p
.dot(p
)
1728 y
= q
.dot(vloc
) / q
.dot(q
)
1729 locs_2d
.append([x
, y
, vert
])
1731 return(locs_2d
, p
, q
)
1734 # calculate a best-fit circle to the 2d locations on the plane
1735 def circle_calculate_best_fit(locs_2d
):
1741 # calculate center and radius (non-linear least squares solution)
1742 for iter in range(500):
1746 d
= (v
[0]**2-2.0*x0
*v
[0]+v
[1]**2-2.0*y0
*v
[1]+x0
**2+y0
**2)**0.5
1747 jmat
.append([(x0
-v
[0])/d
, (y0
-v
[1])/d
, -1.0])
1748 k
.append(-(((v
[0]-x0
)**2+(v
[1]-y0
)**2)**0.5-r
))
1749 jmat2
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1753 k2
= mathutils
.Vector((0.0, 0.0, 0.0))
1754 for i
in range(len(jmat
)):
1755 k2
+= mathutils
.Vector(jmat
[i
])*k
[i
]
1756 jmat2
[0][0] += jmat
[i
][0]**2
1757 jmat2
[1][0] += jmat
[i
][0]*jmat
[i
][1]
1758 jmat2
[2][0] += jmat
[i
][0]*jmat
[i
][2]
1759 jmat2
[1][1] += jmat
[i
][1]**2
1760 jmat2
[2][1] += jmat
[i
][1]*jmat
[i
][2]
1761 jmat2
[2][2] += jmat
[i
][2]**2
1762 jmat2
[0][1] = jmat2
[1][0]
1763 jmat2
[0][2] = jmat2
[2][0]
1764 jmat2
[1][2] = jmat2
[2][1]
1769 dx0
, dy0
, dr
= jmat2
* k2
1773 # stop iterating if we're close enough to optimal solution
1774 if abs(dx0
)<1e-6 and abs(dy0
)<1e-6 and abs(dr
)<1e-6:
1777 # return center of circle and radius
1781 # calculate circle so no vertices have to be moved away from the center
1782 def circle_calculate_min_fit(locs_2d
):
1784 x0
= (min([i
[0] for i
in locs_2d
])+max([i
[0] for i
in locs_2d
]))/2.0
1785 y0
= (min([i
[1] for i
in locs_2d
])+max([i
[1] for i
in locs_2d
]))/2.0
1786 center
= mathutils
.Vector([x0
, y0
])
1788 r
= min([(mathutils
.Vector([i
[0], i
[1]])-center
).length
for i
in locs_2d
])
1790 # return center of circle and radius
1794 # calculate the new locations of the vertices that need to be moved
1795 def circle_calculate_verts(flatten
, bm_mod
, locs_2d
, com
, p
, q
, normal
):
1796 # changing 2d coordinates back to 3d coordinates
1799 locs_3d
.append([loc
[2], loc
[0]*p
+ loc
[1]*q
+ com
])
1801 if flatten
: # flat circle
1804 else: # project the locations on the existing mesh
1805 vert_edges
= dict_vert_edges(bm_mod
)
1806 vert_faces
= dict_vert_faces(bm_mod
)
1807 faces
= [f
for f
in bm_mod
.faces
if not f
.hide
]
1808 rays
= [normal
, -normal
]
1812 if bm_mod
.verts
[loc
[0]].co
== loc
[1]: # vertex hasn't moved
1815 dif
= normal
.angle(loc
[1]-bm_mod
.verts
[loc
[0]].co
)
1816 if -1e-6 < dif
< 1e-6 or math
.pi
-1e-6 < dif
< math
.pi
+1e-6:
1817 # original location is already along projection normal
1818 projection
= bm_mod
.verts
[loc
[0]].co
1820 # quick search through adjacent faces
1821 for face
in vert_faces
[loc
[0]]:
1822 verts
= [v
.co
for v
in bm_mod
.faces
[face
].verts
]
1823 if len(verts
) == 3: # triangle
1827 v1
, v2
, v3
, v4
= verts
[:4]
1829 intersect
= mathutils
.geometry
.\
1830 intersect_ray_tri(v1
, v2
, v3
, ray
, loc
[1])
1832 projection
= intersect
1835 intersect
= mathutils
.geometry
.\
1836 intersect_ray_tri(v1
, v3
, v4
, ray
, loc
[1])
1838 projection
= intersect
1843 # check if projection is on adjacent edges
1844 for edgekey
in vert_edges
[loc
[0]]:
1845 line1
= bm_mod
.verts
[edgekey
[0]].co
1846 line2
= bm_mod
.verts
[edgekey
[1]].co
1847 intersect
, dist
= mathutils
.geometry
.intersect_point_line(\
1848 loc
[1], line1
, line2
)
1849 if 1e-6 < dist
< 1 - 1e-6:
1850 projection
= intersect
1853 # full search through the entire mesh
1856 verts
= [v
.co
for v
in face
.verts
]
1857 if len(verts
) == 3: # triangle
1861 v1
, v2
, v3
, v4
= verts
[:4]
1863 intersect
= mathutils
.geometry
.intersect_ray_tri(\
1864 v1
, v2
, v3
, ray
, loc
[1])
1866 hits
.append([(loc
[1] - intersect
).length
,
1870 intersect
= mathutils
.geometry
.intersect_ray_tri(\
1871 v1
, v3
, v4
, ray
, loc
[1])
1873 hits
.append([(loc
[1] - intersect
).length
,
1877 # if more than 1 hit with mesh, closest hit is new loc
1879 projection
= hits
[0][1]
1881 # nothing to project on, remain at flat location
1883 new_locs
.append([loc
[0], projection
])
1885 # return new positions of projected circle
1889 # check loops and only return valid ones
1890 def circle_check_loops(single_loops
, loops
, mapping
, bm_mod
):
1891 valid_single_loops
= {}
1893 for i
, [loop
, circular
] in enumerate(loops
):
1894 # loop needs to have at least 3 vertices
1897 # loop needs at least 1 vertex in the original, non-mirrored mesh
1901 if mapping
[vert
] > -1:
1906 # loop has to be non-collinear
1908 loc0
= mathutils
.Vector(bm_mod
.verts
[loop
[0]].co
[:])
1909 loc1
= mathutils
.Vector(bm_mod
.verts
[loop
[1]].co
[:])
1911 locn
= mathutils
.Vector(bm_mod
.verts
[v
].co
[:])
1912 if loc0
== loc1
or loc1
== locn
:
1918 if -1e-6 < d1
.angle(d2
, 0) < 1e-6:
1926 # passed all tests, loop is valid
1927 valid_loops
.append([loop
, circular
])
1928 valid_single_loops
[len(valid_loops
)-1] = single_loops
[i
]
1930 return(valid_single_loops
, valid_loops
)
1933 # calculate the location of single input vertices that need to be flattened
1934 def circle_flatten_singles(bm_mod
, com
, p
, q
, normal
, single_loop
):
1936 for vert
in single_loop
:
1937 loc
= mathutils
.Vector(bm_mod
.verts
[vert
].co
[:])
1938 new_locs
.append([vert
, loc
- (loc
-com
).dot(normal
)*normal
])
1943 # calculate input loops
1944 def circle_get_input(object, bm
, scene
):
1945 # get mesh with modifiers applied
1946 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
1948 # create list of edge-keys based on selection state
1950 for face
in bm
.faces
:
1951 if face
.select
and not face
.hide
:
1955 # get selected, non-hidden , non-internal edge-keys
1956 eks_selected
= [key
for keys
in [face_edgekeys(face
) for face
in \
1957 bm_mod
.faces
if face
.select
and not face
.hide
] for key
in keys
]
1959 for ek
in eks_selected
:
1960 if ek
in edge_count
:
1964 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select \
1965 and not edge
.hide
and edge_count
.get(edgekey(edge
), 1)==1]
1967 # no faces, so no internal edges either
1968 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select \
1971 # add edge-keys around single vertices
1972 verts_connected
= dict([[vert
, 1] for edge
in [edge
for edge
in \
1973 bm_mod
.edges
if edge
.select
and not edge
.hide
] for vert
in \
1975 single_vertices
= [vert
.index
for vert
in bm_mod
.verts
if \
1976 vert
.select
and not vert
.hide
and not \
1977 verts_connected
.get(vert
.index
, False)]
1979 if single_vertices
and len(bm
.faces
)>0:
1980 vert_to_single
= dict([[v
.index
, []] for v
in bm_mod
.verts \
1982 for face
in [face
for face
in bm_mod
.faces
if not face
.select \
1984 for vert
in face
.verts
:
1986 if vert
in single_vertices
:
1987 for ek
in face_edgekeys(face
):
1989 edge_keys
.append(ek
)
1990 if vert
not in vert_to_single
[ek
[0]]:
1991 vert_to_single
[ek
[0]].append(vert
)
1992 if vert
not in vert_to_single
[ek
[1]]:
1993 vert_to_single
[ek
[1]].append(vert
)
1996 # sort edge-keys into loops
1997 loops
= get_connected_selections(edge_keys
)
1999 # find out to which loops the single vertices belong
2000 single_loops
= dict([[i
, []] for i
in range(len(loops
))])
2001 if single_vertices
and len(bm
.faces
)>0:
2002 for i
, [loop
, circular
] in enumerate(loops
):
2004 if vert_to_single
[vert
]:
2005 for single
in vert_to_single
[vert
]:
2006 if single
not in single_loops
[i
]:
2007 single_loops
[i
].append(single
)
2009 return(derived
, bm_mod
, single_vertices
, single_loops
, loops
)
2012 # recalculate positions based on the influence of the circle shape
2013 def circle_influence_locs(locs_2d
, new_locs_2d
, influence
):
2014 for i
in range(len(locs_2d
)):
2015 oldx
, oldy
, j
= locs_2d
[i
]
2016 newx
, newy
, k
= new_locs_2d
[i
]
2017 altx
= newx
*(influence
/100)+ oldx
*((100-influence
)/100)
2018 alty
= newy
*(influence
/100)+ oldy
*((100-influence
)/100)
2019 locs_2d
[i
] = [altx
, alty
, j
]
2024 # project 2d locations on circle, respecting distance relations between verts
2025 def circle_project_non_regular(locs_2d
, x0
, y0
, r
):
2026 for i
in range(len(locs_2d
)):
2027 x
, y
, j
= locs_2d
[i
]
2028 loc
= mathutils
.Vector([x
-x0
, y
-y0
])
2030 locs_2d
[i
] = [loc
[0], loc
[1], j
]
2035 # project 2d locations on circle, with equal distance between all vertices
2036 def circle_project_regular(locs_2d
, x0
, y0
, r
):
2037 # find offset angle and circling direction
2038 x
, y
, i
= locs_2d
[0]
2039 loc
= mathutils
.Vector([x
-x0
, y
-y0
])
2041 offset_angle
= loc
.angle(mathutils
.Vector([1.0, 0.0]), 0.0)
2042 loca
= mathutils
.Vector([x
-x0
, y
-y0
, 0.0])
2045 x
, y
, j
= locs_2d
[1]
2046 locb
= mathutils
.Vector([x
-x0
, y
-y0
, 0.0])
2047 if loca
.cross(locb
)[2] >= 0:
2051 # distribute vertices along the circle
2052 for i
in range(len(locs_2d
)):
2053 t
= offset_angle
+ ccw
* (i
/ len(locs_2d
) * 2 * math
.pi
)
2056 locs_2d
[i
] = [x
, y
, locs_2d
[i
][2]]
2061 # shift loop, so the first vertex is closest to the center
2062 def circle_shift_loop(bm_mod
, loop
, com
):
2063 verts
, circular
= loop
2064 distances
= [[(bm_mod
.verts
[vert
].co
- com
).length
, i
] \
2065 for i
, vert
in enumerate(verts
)]
2067 shift
= distances
[0][1]
2068 loop
= [verts
[shift
:] + verts
[:shift
], circular
]
2073 ##########################################
2074 ####### Curve functions ##################
2075 ##########################################
2077 # create lists with knots and points, all correctly sorted
2078 def curve_calculate_knots(loop
, verts_selected
):
2079 knots
= [v
for v
in loop
[0] if v
in verts_selected
]
2081 # circular loop, potential for weird splines
2083 offset
= int(len(loop
[0]) / 4)
2086 kpos
.append(loop
[0].index(k
))
2088 for i
in range(len(kpos
) - 1):
2089 kdif
.append(kpos
[i
+1] - kpos
[i
])
2090 kdif
.append(len(loop
[0]) - kpos
[-1] + kpos
[0])
2094 kadd
.append([kdif
.index(k
), True])
2095 # next 2 lines are optional, they insert
2096 # an extra control point in small gaps
2098 # kadd.append([kdif.index(k), False])
2101 for k
in kadd
: # extra knots to be added
2102 if k
[1]: # big gap (break circular spline)
2103 kpos
= loop
[0].index(knots
[k
[0]]) + offset
2104 if kpos
> len(loop
[0]) - 1:
2105 kpos
-= len(loop
[0])
2106 kins
.append([knots
[k
[0]], loop
[0][kpos
]])
2108 if kpos2
> len(knots
)-1:
2110 kpos2
= loop
[0].index(knots
[kpos2
]) - offset
2112 kpos2
+= len(loop
[0])
2113 kins
.append([loop
[0][kpos
], loop
[0][kpos2
]])
2114 krot
= loop
[0][kpos2
]
2115 else: # small gap (keep circular spline)
2116 k1
= loop
[0].index(knots
[k
[0]])
2118 if k2
> len(knots
)-1:
2120 k2
= loop
[0].index(knots
[k2
])
2122 dif
= len(loop
[0]) - 1 - k1
+ k2
2125 kn
= k1
+ int(dif
/2)
2126 if kn
> len(loop
[0]) - 1:
2128 kins
.append([loop
[0][k1
], loop
[0][kn
]])
2129 for j
in kins
: # insert new knots
2130 knots
.insert(knots
.index(j
[0]) + 1, j
[1])
2131 if not krot
: # circular loop
2132 knots
.append(knots
[0])
2133 points
= loop
[0][loop
[0].index(knots
[0]):]
2134 points
+= loop
[0][0:loop
[0].index(knots
[0]) + 1]
2135 else: # non-circular loop (broken by script)
2136 krot
= knots
.index(krot
)
2137 knots
= knots
[krot
:] + knots
[0:krot
]
2138 if loop
[0].index(knots
[0]) > loop
[0].index(knots
[-1]):
2139 points
= loop
[0][loop
[0].index(knots
[0]):]
2140 points
+= loop
[0][0:loop
[0].index(knots
[-1])+1]
2142 points
= loop
[0][loop
[0].index(knots
[0]):\
2143 loop
[0].index(knots
[-1]) + 1]
2144 # non-circular loop, add first and last point as knots
2146 if loop
[0][0] not in knots
:
2147 knots
.insert(0, loop
[0][0])
2148 if loop
[0][-1] not in knots
:
2149 knots
.append(loop
[0][-1])
2151 return(knots
, points
)
2154 # calculate relative positions compared to first knot
2155 def curve_calculate_t(bm_mod
, knots
, points
, pknots
, regular
, circular
):
2162 loc
= pknots
[knots
.index(p
)] # use projected knot location
2164 loc
= mathutils
.Vector(bm_mod
.verts
[p
].co
[:])
2167 len_total
+= (loc
-loc_prev
).length
2168 tpoints
.append(len_total
)
2173 tknots
.append(tpoints
[points
.index(p
)])
2175 tknots
[-1] = tpoints
[-1]
2179 tpoints_average
= tpoints
[-1] / (len(tpoints
) - 1)
2180 for i
in range(1, len(tpoints
) - 1):
2181 tpoints
[i
] = i
* tpoints_average
2182 for i
in range(len(knots
)):
2183 tknots
[i
] = tpoints
[points
.index(knots
[i
])]
2185 tknots
[-1] = tpoints
[-1]
2187 return(tknots
, tpoints
)
2190 # change the location of non-selected points to their place on the spline
2191 def curve_calculate_vertices(bm_mod
, knots
, tknots
, points
, tpoints
, splines
,
2192 interpolation
, restriction
):
2199 m
= tpoints
[points
.index(p
)]
2207 if n
> len(splines
) - 1:
2208 n
= len(splines
) - 1
2212 if interpolation
== 'cubic':
2213 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
2214 x
= ax
+ bx
*(m
-tx
) + cx
*(m
-tx
)**2 + dx
*(m
-tx
)**3
2215 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
2216 y
= ay
+ by
*(m
-ty
) + cy
*(m
-ty
)**2 + dy
*(m
-ty
)**3
2217 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
2218 z
= az
+ bz
*(m
-tz
) + cz
*(m
-tz
)**2 + dz
*(m
-tz
)**3
2219 newloc
= mathutils
.Vector([x
,y
,z
])
2220 else: # interpolation == 'linear'
2221 a
, d
, t
, u
= splines
[n
]
2222 newloc
= ((m
-t
)/u
)*d
+ a
2224 if restriction
!= 'none': # vertex movement is restricted
2226 else: # set the vertex to its new location
2227 move
.append([p
, newloc
])
2229 if restriction
!= 'none': # vertex movement is restricted
2234 move
.append([p
, bm_mod
.verts
[p
].co
])
2236 oldloc
= bm_mod
.verts
[p
].co
2237 normal
= bm_mod
.verts
[p
].normal
2238 dloc
= newloc
- oldloc
2239 if dloc
.length
< 1e-6:
2240 move
.append([p
, newloc
])
2241 elif restriction
== 'extrude': # only extrusions
2242 if dloc
.angle(normal
, 0) < 0.5 * math
.pi
+ 1e-6:
2243 move
.append([p
, newloc
])
2244 else: # restriction == 'indent' only indentations
2245 if dloc
.angle(normal
) > 0.5 * math
.pi
- 1e-6:
2246 move
.append([p
, newloc
])
2251 # trim loops to part between first and last selected vertices (including)
2252 def curve_cut_boundaries(bm_mod
, loops
):
2254 for loop
, circular
in loops
:
2257 cut_loops
.append([loop
, circular
])
2259 selected
= [bm_mod
.verts
[v
].select
for v
in loop
]
2260 first
= selected
.index(True)
2262 last
= -selected
.index(True)
2264 cut_loops
.append([loop
[first
:], circular
])
2266 cut_loops
.append([loop
[first
:last
], circular
])
2271 # calculate input loops
2272 def curve_get_input(object, bm
, boundaries
, scene
):
2273 # get mesh with modifiers applied
2274 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
2276 # vertices that still need a loop to run through it
2277 verts_unsorted
= [v
.index
for v
in bm_mod
.verts
if \
2278 v
.select
and not v
.hide
]
2279 # necessary dictionaries
2280 vert_edges
= dict_vert_edges(bm_mod
)
2281 edge_faces
= dict_edge_faces(bm_mod
)
2283 # find loops through each selected vertex
2284 while len(verts_unsorted
) > 0:
2285 loops
= curve_vertex_loops(bm_mod
, verts_unsorted
[0], vert_edges
,
2287 verts_unsorted
.pop(0)
2289 # check if loop is fully selected
2290 search_perpendicular
= False
2292 for loop
, circular
in loops
:
2294 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2295 if len(selected
) < 2:
2296 # only one selected vertex on loop, don't use
2299 elif len(selected
) == len(loop
):
2300 search_perpendicular
= loop
2302 # entire loop is selected, find perpendicular loops
2303 if search_perpendicular
:
2305 if vert
in verts_unsorted
:
2306 verts_unsorted
.remove(vert
)
2307 perp_loops
= curve_perpendicular_loops(bm_mod
, loop
,
2308 vert_edges
, edge_faces
)
2309 for perp_loop
in perp_loops
:
2310 correct_loops
.append(perp_loop
)
2313 for loop
, circular
in loops
:
2314 correct_loops
.append([loop
, circular
])
2318 correct_loops
= curve_cut_boundaries(bm_mod
, correct_loops
)
2320 return(derived
, bm_mod
, correct_loops
)
2323 # return all loops that are perpendicular to the given one
2324 def curve_perpendicular_loops(bm_mod
, start_loop
, vert_edges
, edge_faces
):
2325 # find perpendicular loops
2327 for start_vert
in start_loop
:
2328 loops
= curve_vertex_loops(bm_mod
, start_vert
, vert_edges
,
2330 for loop
, circular
in loops
:
2331 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2332 if len(selected
) == len(loop
):
2335 perp_loops
.append([loop
, circular
, loop
.index(start_vert
)])
2337 # trim loops to same lengths
2338 shortest
= [[len(loop
[0]), i
] for i
, loop
in enumerate(perp_loops
)\
2341 # all loops are circular, not trimming
2342 return([[loop
[0], loop
[1]] for loop
in perp_loops
])
2344 shortest
= min(shortest
)
2345 shortest_start
= perp_loops
[shortest
[1]][2]
2346 before_start
= shortest_start
2347 after_start
= shortest
[0] - shortest_start
- 1
2348 bigger_before
= before_start
> after_start
2350 for loop
in perp_loops
:
2351 # have the loop face the same direction as the shortest one
2353 if loop
[2] < len(loop
[0]) / 2:
2355 loop
[2] = len(loop
[0]) - loop
[2] - 1
2357 if loop
[2] > len(loop
[0]) / 2:
2359 loop
[2] = len(loop
[0]) - loop
[2] - 1
2360 # circular loops can shift, to prevent wrong trimming
2362 shift
= shortest_start
- loop
[2]
2363 if loop
[2] + shift
> 0 and loop
[2] + shift
< len(loop
[0]):
2364 loop
[0] = loop
[0][-shift
:] + loop
[0][:-shift
]
2367 loop
[2] += len(loop
[0])
2368 elif loop
[2] > len(loop
[0]) -1:
2369 loop
[2] -= len(loop
[0])
2371 start
= max(0, loop
[2] - before_start
)
2372 end
= min(len(loop
[0]), loop
[2] + after_start
+ 1)
2373 trimmed_loops
.append([loop
[0][start
:end
], False])
2375 return(trimmed_loops
)
2378 # project knots on non-selected geometry
2379 def curve_project_knots(bm_mod
, verts_selected
, knots
, points
, circular
):
2380 # function to project vertex on edge
2381 def project(v1
, v2
, v3
):
2382 # v1 and v2 are part of a line
2383 # v3 is projected onto it
2389 if circular
: # project all knots
2393 else: # first and last knot shouldn't be projected
2396 pknots
= [mathutils
.Vector(bm_mod
.verts
[knots
[0]].co
[:])]
2397 for knot
in knots
[start
:end
]:
2398 if knot
in verts_selected
:
2399 knot_left
= knot_right
= False
2400 for i
in range(points
.index(knot
)-1, -1*len(points
), -1):
2401 if points
[i
] not in knots
:
2402 knot_left
= points
[i
]
2404 for i
in range(points
.index(knot
)+1, 2*len(points
)):
2405 if i
> len(points
) - 1:
2407 if points
[i
] not in knots
:
2408 knot_right
= points
[i
]
2410 if knot_left
and knot_right
and knot_left
!= knot_right
:
2411 knot_left
= mathutils
.Vector(\
2412 bm_mod
.verts
[knot_left
].co
[:])
2413 knot_right
= mathutils
.Vector(\
2414 bm_mod
.verts
[knot_right
].co
[:])
2415 knot
= mathutils
.Vector(bm_mod
.verts
[knot
].co
[:])
2416 pknots
.append(project(knot_left
, knot_right
, knot
))
2418 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2419 else: # knot isn't selected, so shouldn't be changed
2420 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2422 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knots
[-1]].co
[:]))
2427 # find all loops through a given vertex
2428 def curve_vertex_loops(bm_mod
, start_vert
, vert_edges
, edge_faces
):
2432 for edge
in vert_edges
[start_vert
]:
2433 if edge
in edges_used
:
2438 active_faces
= edge_faces
[edge
]
2443 new_edges
= vert_edges
[new_vert
]
2444 loop
.append(new_vert
)
2446 edges_used
.append(tuple(sorted([loop
[-1], loop
[-2]])))
2447 if len(new_edges
) < 3 or len(new_edges
) > 4:
2452 for new_edge
in new_edges
:
2453 if new_edge
in edges_used
:
2456 for new_face
in edge_faces
[new_edge
]:
2457 if new_face
in active_faces
:
2462 # found correct new edge
2463 active_faces
= edge_faces
[new_edge
]
2469 if new_vert
== loop
[0]:
2477 loops
.append([loop
, circular
])
2482 ##########################################
2483 ####### Flatten functions ################
2484 ##########################################
2486 # sort input into loops
2487 def flatten_get_input(bm
):
2488 vert_verts
= dict_vert_verts([edgekey(edge
) for edge
in bm
.edges \
2489 if edge
.select
and not edge
.hide
])
2490 verts
= [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
2492 # no connected verts, consider all selected verts as a single input
2494 return([[verts
, False]])
2497 while len(verts
) > 0:
2501 if loop
[-1] in vert_verts
:
2502 to_grow
= vert_verts
[loop
[-1]]
2506 while len(to_grow
) > 0:
2507 new_vert
= to_grow
[0]
2509 if new_vert
in loop
:
2511 loop
.append(new_vert
)
2512 verts
.remove(new_vert
)
2513 to_grow
+= vert_verts
[new_vert
]
2515 loops
.append([loop
, False])
2520 # calculate position of vertex projections on plane
2521 def flatten_project(bm
, loop
, com
, normal
):
2522 verts
= [bm
.verts
[v
] for v
in loop
[0]]
2523 verts_projected
= [[v
.index
, mathutils
.Vector(v
.co
[:]) - \
2524 (mathutils
.Vector(v
.co
[:])-com
).dot(normal
)*normal
] for v
in verts
]
2526 return(verts_projected
)
2529 ##########################################
2530 ####### Gstretch functions ###############
2531 ##########################################
2533 # fake stroke class, used to create custom strokes if no GP data is found
2534 class gstretch_fake_stroke():
2535 def __init__(self
, points
):
2536 self
.points
= [gstretch_fake_stroke_point(p
) for p
in points
]
2539 # fake stroke point class, used in fake strokes
2540 class gstretch_fake_stroke_point():
2541 def __init__(self
, loc
):
2545 # flips loops, if necessary, to obtain maximum alignment to stroke
2546 def gstretch_align_pairs(ls_pairs
, object, bm_mod
, method
):
2547 # returns total distance between all verts in loop and corresponding stroke
2548 def distance_loop_stroke(loop
, stroke
, object, bm_mod
, method
):
2549 stroke_lengths_cache
= False
2550 loop_length
= len(loop
[0])
2553 if method
!= 'regular':
2554 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2556 for i
, v_index
in enumerate(loop
[0]):
2557 if method
== 'regular':
2558 relative_distance
= i
/ (loop_length
- 1)
2560 relative_distance
= relative_lengths
[i
]
2562 loc1
= object.matrix_world
* bm_mod
.verts
[v_index
].co
2563 loc2
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2564 relative_distance
, stroke_lengths_cache
)
2565 total_distance
+= (loc2
- loc1
).length
2567 return(total_distance
)
2570 for (loop
, stroke
) in ls_pairs
:
2571 total_dist
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2574 total_dist_rev
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2576 if total_dist_rev
> total_dist
:
2582 # calculate vertex positions on stroke
2583 def gstretch_calculate_verts(loop
, stroke
, object, bm_mod
, method
):
2585 stroke_lengths_cache
= False
2586 loop_length
= len(loop
[0])
2587 matrix_inverse
= object.matrix_world
.inverted()
2589 # return intersection of line with stroke, or None
2590 def intersect_line_stroke(vec1
, vec2
, stroke
):
2591 for i
, p
in enumerate(stroke
.points
[1:]):
2592 intersections
= mathutils
.geometry
.intersect_line_line(vec1
, vec2
,
2593 p
.co
, stroke
.points
[i
].co
)
2594 if intersections
and \
2595 (intersections
[0] - intersections
[1]).length
< 1e-2:
2596 x
, dist
= mathutils
.geometry
.intersect_point_line(
2597 intersections
[0], p
.co
, stroke
.points
[i
].co
)
2599 return(intersections
[0])
2602 if method
== 'project':
2603 projection_vectors
= []
2604 vert_edges
= dict_vert_edges(bm_mod
)
2606 for v_index
in loop
[0]:
2608 for ek
in vert_edges
[v_index
]:
2610 v1
= bm_mod
.verts
[v1
]
2611 v2
= bm_mod
.verts
[v2
]
2612 if v1
.select
+ v2
.select
== 1 and not v1
.hide
and not v2
.hide
:
2613 vec1
= object.matrix_world
* v1
.co
2614 vec2
= object.matrix_world
* v2
.co
2615 intersection
= intersect_line_stroke(vec1
, vec2
, stroke
)
2618 if not intersection
:
2619 v
= bm_mod
.verts
[v_index
]
2620 intersection
= intersect_line_stroke(v
.co
, v
.co
+ v
.normal
,
2623 move
.append([v_index
, matrix_inverse
* intersection
])
2626 if method
== 'irregular':
2627 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2629 for i
, v_index
in enumerate(loop
[0]):
2630 if method
== 'regular':
2631 relative_distance
= i
/ (loop_length
- 1)
2632 else: # method == 'irregular'
2633 relative_distance
= relative_lengths
[i
]
2634 loc
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2635 relative_distance
, stroke_lengths_cache
)
2636 loc
= matrix_inverse
* loc
2637 move
.append([v_index
, loc
])
2642 # create new vertices, based on GP strokes
2643 def gstretch_create_verts(object, bm_mod
, strokes
, method
, conversion
,
2644 conversion_distance
, conversion_max
, conversion_min
, conversion_vertices
):
2647 mat_world
= object.matrix_world
.inverted()
2648 singles
= gstretch_match_single_verts(bm_mod
, strokes
, mat_world
)
2650 for stroke
in strokes
:
2651 stroke_verts
.append([stroke
, []])
2653 if conversion
== 'vertices':
2654 min_end_point
= conversion_vertices
2655 end_point
= conversion_vertices
2656 elif conversion
== 'limit_vertices':
2657 min_end_point
= conversion_min
2658 end_point
= conversion_max
2660 end_point
= len(stroke
.points
)
2661 # creation of new vertices at fixed user-defined distances
2662 if conversion
== 'distance':
2664 prev_point
= stroke
.points
[0]
2665 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* \
2668 limit
= conversion_distance
2669 for point
in stroke
.points
:
2670 new_distance
= distance
+ (point
.co
- prev_point
.co
).length
2672 while new_distance
> limit
:
2673 to_cover
= limit
- distance
+ (limit
* iteration
)
2674 new_loc
= prev_point
.co
+ to_cover
* \
2675 (point
.co
- prev_point
.co
).normalized()
2676 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* \
2678 new_distance
-= limit
2680 distance
= new_distance
2682 # creation of new vertices for other methods
2684 # add vertices at stroke points
2685 for point
in stroke
.points
[:end_point
]:
2686 stroke_verts
[-1][1].append(bm_mod
.verts
.new(\
2687 mat_world
* point
.co
))
2688 # add more vertices, beyond the points that are available
2689 if min_end_point
> min(len(stroke
.points
), end_point
):
2690 for i
in range(min_end_point
-
2691 (min(len(stroke
.points
), end_point
))):
2692 stroke_verts
[-1][1].append(bm_mod
.verts
.new(\
2693 mat_world
* point
.co
))
2694 # force even spreading of points, so they are placed on stroke
2696 bm_mod
.verts
.ensure_lookup_table()
2697 bm_mod
.verts
.index_update()
2698 for stroke
, verts_seq
in stroke_verts
:
2699 if len(verts_seq
) < 2:
2701 # spread vertices evenly over the stroke
2702 if method
== 'regular':
2703 loop
= [[vert
.index
for vert
in verts_seq
], False]
2704 move
+= gstretch_calculate_verts(loop
, stroke
, object, bm_mod
,
2707 for i
, vert
in enumerate(verts_seq
):
2709 bm_mod
.edges
.new((verts_seq
[i
-1], verts_seq
[i
]))
2711 # connect single vertices to the closest stroke
2713 for vert
, m_stroke
, point
in singles
:
2714 if m_stroke
!= stroke
:
2716 bm_mod
.edges
.new((vert
, verts_seq
[point
]))
2717 bm_mod
.edges
.ensure_lookup_table()
2718 bmesh
.update_edit_mesh(object.data
)
2723 # erases the grease pencil stroke
2724 def gstretch_erase_stroke(stroke
, context
):
2725 # change 3d coordinate into a stroke-point
2726 def sp(loc
, context
):
2730 'location': (0, 0, 0),
2731 'mouse': (view3d_utils
.location_3d_to_region_2d(\
2732 context
.region
, context
.space_data
.region_3d
, loc
)),
2738 if type(stroke
) != bpy
.types
.GPencilStroke
:
2739 # fake stroke, there is nothing to delete
2742 erase_stroke
= [sp(p
.co
, context
) for p
in stroke
.points
]
2744 erase_stroke
[0]['is_start'] = True
2745 bpy
.ops
.gpencil
.draw(mode
='ERASER', stroke
=erase_stroke
)
2748 # get point on stroke, given by relative distance (0.0 - 1.0)
2749 def gstretch_eval_stroke(stroke
, distance
, stroke_lengths_cache
=False):
2750 # use cache if available
2751 if not stroke_lengths_cache
:
2753 for i
, p
in enumerate(stroke
.points
[1:]):
2754 lengths
.append((p
.co
- stroke
.points
[i
].co
).length
+ \
2756 total_length
= max(lengths
[-1], 1e-7)
2757 stroke_lengths_cache
= [length
/ total_length
for length
in
2759 stroke_lengths
= stroke_lengths_cache
[:]
2761 if distance
in stroke_lengths
:
2762 loc
= stroke
.points
[stroke_lengths
.index(distance
)].co
2763 elif distance
> stroke_lengths
[-1]:
2764 # should be impossible, but better safe than sorry
2765 loc
= stroke
.points
[-1].co
2767 stroke_lengths
.append(distance
)
2768 stroke_lengths
.sort()
2769 stroke_index
= stroke_lengths
.index(distance
)
2770 interval_length
= stroke_lengths
[stroke_index
+1] - \
2771 stroke_lengths
[stroke_index
-1]
2772 distance_relative
= (distance
- stroke_lengths
[stroke_index
-1]) / \
2774 interval_vector
= stroke
.points
[stroke_index
].co
- \
2775 stroke
.points
[stroke_index
-1].co
2776 loc
= stroke
.points
[stroke_index
-1].co
+ \
2777 distance_relative
* interval_vector
2779 return(loc
, stroke_lengths_cache
)
2782 # create fake grease pencil strokes for the active object
2783 def gstretch_get_fake_strokes(object, bm_mod
, loops
):
2786 p1
= object.matrix_world
* bm_mod
.verts
[loop
[0][0]].co
2787 p2
= object.matrix_world
* bm_mod
.verts
[loop
[0][-1]].co
2788 strokes
.append(gstretch_fake_stroke([p1
, p2
]))
2793 # get grease pencil strokes for the active object
2794 def gstretch_get_strokes(object, context
):
2795 gp
= get_grease_pencil(object, context
)
2798 layer
= gp
.layers
.active
2801 frame
= layer
.active_frame
2804 strokes
= frame
.strokes
2805 if len(strokes
) < 1:
2811 # returns a list with loop-stroke pairs
2812 def gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
):
2813 if not loops
or not strokes
:
2816 # calculate loop centers
2818 bm_mod
.verts
.ensure_lookup_table()
2820 center
= mathutils
.Vector()
2821 for v_index
in loop
[0]:
2822 center
+= bm_mod
.verts
[v_index
].co
2823 center
/= len(loop
[0])
2824 center
= object.matrix_world
* center
2825 loop_centers
.append([center
, loop
])
2827 # calculate stroke centers
2829 for stroke
in strokes
:
2830 center
= mathutils
.Vector()
2831 for p
in stroke
.points
:
2833 center
/= len(stroke
.points
)
2834 stroke_centers
.append([center
, stroke
, 0])
2836 # match, first by stroke use count, then by distance
2838 for lc
in loop_centers
:
2840 for i
, sc
in enumerate(stroke_centers
):
2841 distances
.append([sc
[2], (lc
[0] - sc
[0]).length
, i
])
2843 best_stroke
= distances
[0][2]
2844 ls_pairs
.append([lc
[1], stroke_centers
[best_stroke
][1]])
2845 stroke_centers
[best_stroke
][2] += 1 # increase stroke use count
2850 # match single selected vertices to the closest stroke endpoint
2851 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2852 def gstretch_match_single_verts(bm_mod
, strokes
, mat_world
):
2853 # calculate stroke endpoints in object space
2855 for stroke
in strokes
:
2856 endpoints
.append((mat_world
* stroke
.points
[0].co
, stroke
, 0))
2857 endpoints
.append((mat_world
* stroke
.points
[-1].co
, stroke
, -1))
2860 # find single vertices (not connected to other selected verts)
2861 for vert
in bm_mod
.verts
:
2865 for edge
in vert
.link_edges
:
2866 if edge
.other_vert(vert
).select
:
2871 # calculate distances from vertex to endpoints
2872 distance
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2873 endpoint_index
) for endpoint_index
, (loc
, stroke
, stroke_point
) in
2874 enumerate(endpoints
)]
2876 distances
.append(distance
[0])
2878 # create matches, based on shortest distance first
2882 singles
.append((distances
[0][1], distances
[0][2], distances
[0][3]))
2883 endpoints
.pop(distances
[0][4])
2886 for (i
, vert
, j
, k
, l
) in distances
:
2887 distance_new
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2888 endpoint_index
) for endpoint_index
, (loc
, stroke
,
2889 stroke_point
) in enumerate(endpoints
)]
2891 distances_new
.append(distance_new
[0])
2892 distances
= distances_new
2897 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2898 def gstretch_relative_lengths(loop
, bm_mod
):
2900 for i
, v_index
in enumerate(loop
[0][1:]):
2901 lengths
.append((bm_mod
.verts
[v_index
].co
- \
2902 bm_mod
.verts
[loop
[0][i
]].co
).length
+ lengths
[-1])
2903 total_length
= max(lengths
[-1], 1e-7)
2904 relative_lengths
= [length
/ total_length
for length
in
2907 return(relative_lengths
)
2910 # convert cache-stored strokes into usable (fake) GP strokes
2911 def gstretch_safe_to_true_strokes(safe_strokes
):
2913 for safe_stroke
in safe_strokes
:
2914 strokes
.append(gstretch_fake_stroke(safe_stroke
))
2919 # convert a GP stroke into a list of points which can be stored in cache
2920 def gstretch_true_to_safe_strokes(strokes
):
2922 for stroke
in strokes
:
2923 safe_strokes
.append([p
.co
.copy() for p
in stroke
.points
])
2925 return(safe_strokes
)
2928 # force consistency in GUI, max value can never be lower than min value
2929 def gstretch_update_max(self
, context
):
2930 # called from operator settings (after execution)
2931 if 'conversion_min' in self
.keys():
2932 if self
.conversion_min
> self
.conversion_max
:
2933 self
.conversion_max
= self
.conversion_min
2934 # called from toolbar
2936 lt
= context
.window_manager
.looptools
2937 if lt
.gstretch_conversion_min
> lt
.gstretch_conversion_max
:
2938 lt
.gstretch_conversion_max
= lt
.gstretch_conversion_min
2941 # force consistency in GUI, min value can never be higher than max value
2942 def gstretch_update_min(self
, context
):
2943 # called from operator settings (after execution)
2944 if 'conversion_max' in self
.keys():
2945 if self
.conversion_max
< self
.conversion_min
:
2946 self
.conversion_min
= self
.conversion_max
2947 # called from toolbar
2949 lt
= context
.window_manager
.looptools
2950 if lt
.gstretch_conversion_max
< lt
.gstretch_conversion_min
:
2951 lt
.gstretch_conversion_min
= lt
.gstretch_conversion_max
2954 ##########################################
2955 ####### Relax functions ##################
2956 ##########################################
2958 # create lists with knots and points, all correctly sorted
2959 def relax_calculate_knots(loops
):
2962 for loop
, circular
in loops
:
2966 if len(loop
)%2 == 1: # odd
2967 extend
= [False, True, 0, 1, 0, 1]
2969 extend
= [True, False, 0, 1, 1, 2]
2971 if len(loop
)%2 == 1: # odd
2972 extend
= [False, False, 0, 1, 1, 2]
2974 extend
= [False, False, 0, 1, 1, 2]
2977 loop
= [loop
[-1]] + loop
+ [loop
[0]]
2978 for i
in range(extend
[2+2*j
], len(loop
), 2):
2979 knots
[j
].append(loop
[i
])
2980 for i
in range(extend
[3+2*j
], len(loop
), 2):
2981 if loop
[i
] == loop
[-1] and not circular
:
2983 if len(points
[j
]) == 0:
2984 points
[j
].append(loop
[i
])
2985 elif loop
[i
] != points
[j
][0]:
2986 points
[j
].append(loop
[i
])
2988 if knots
[j
][0] != knots
[j
][-1]:
2989 knots
[j
].append(knots
[j
][0])
2990 if len(points
[1]) == 0:
2996 all_points
.append(p
)
2998 return(all_knots
, all_points
)
3001 # calculate relative positions compared to first knot
3002 def relax_calculate_t(bm_mod
, knots
, points
, regular
):
3005 for i
in range(len(knots
)):
3006 amount
= len(knots
[i
]) + len(points
[i
])
3008 for j
in range(amount
):
3010 mix
.append([True, knots
[i
][round(j
/2)]])
3012 mix
.append([True, knots
[i
][-1]])
3014 mix
.append([False, points
[i
][int(j
/2)]])
3020 loc
= mathutils
.Vector(bm_mod
.verts
[m
[1]].co
[:])
3023 len_total
+= (loc
- loc_prev
).length
3025 tknots
.append(len_total
)
3027 tpoints
.append(len_total
)
3031 for p
in range(len(points
[i
])):
3032 tpoints
.append((tknots
[p
] + tknots
[p
+1]) / 2)
3033 all_tknots
.append(tknots
)
3034 all_tpoints
.append(tpoints
)
3036 return(all_tknots
, all_tpoints
)
3039 # change the location of the points to their place on the spline
3040 def relax_calculate_verts(bm_mod
, interpolation
, tknots
, knots
, tpoints
,
3044 for i
in range(len(knots
)):
3046 m
= tpoints
[i
][points
[i
].index(p
)]
3048 n
= tknots
[i
].index(m
)
3054 if n
> len(splines
[i
]) - 1:
3055 n
= len(splines
[i
]) - 1
3059 if interpolation
== 'cubic':
3060 ax
, bx
, cx
, dx
, tx
= splines
[i
][n
][0]
3061 x
= ax
+ bx
*(m
-tx
) + cx
*(m
-tx
)**2 + dx
*(m
-tx
)**3
3062 ay
, by
, cy
, dy
, ty
= splines
[i
][n
][1]
3063 y
= ay
+ by
*(m
-ty
) + cy
*(m
-ty
)**2 + dy
*(m
-ty
)**3
3064 az
, bz
, cz
, dz
, tz
= splines
[i
][n
][2]
3065 z
= az
+ bz
*(m
-tz
) + cz
*(m
-tz
)**2 + dz
*(m
-tz
)**3
3066 change
.append([p
, mathutils
.Vector([x
,y
,z
])])
3067 else: # interpolation == 'linear'
3068 a
, d
, t
, u
= splines
[i
][n
]
3071 change
.append([p
, ((m
-t
)/u
)*d
+ a
])
3073 move
.append([c
[0], (bm_mod
.verts
[c
[0]].co
+ c
[1]) / 2])
3078 ##########################################
3079 ####### Space functions ##################
3080 ##########################################
3082 # calculate relative positions compared to first knot
3083 def space_calculate_t(bm_mod
, knots
):
3088 loc
= mathutils
.Vector(bm_mod
.verts
[k
].co
[:])
3091 len_total
+= (loc
- loc_prev
).length
3092 tknots
.append(len_total
)
3095 t_per_segment
= len_total
/ (amount
- 1)
3096 tpoints
= [i
* t_per_segment
for i
in range(amount
)]
3098 return(tknots
, tpoints
)
3101 # change the location of the points to their place on the spline
3102 def space_calculate_verts(bm_mod
, interpolation
, tknots
, tpoints
, points
,
3106 m
= tpoints
[points
.index(p
)]
3114 if n
> len(splines
) - 1:
3115 n
= len(splines
) - 1
3119 if interpolation
== 'cubic':
3120 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
3121 x
= ax
+ bx
*(m
-tx
) + cx
*(m
-tx
)**2 + dx
*(m
-tx
)**3
3122 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
3123 y
= ay
+ by
*(m
-ty
) + cy
*(m
-ty
)**2 + dy
*(m
-ty
)**3
3124 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
3125 z
= az
+ bz
*(m
-tz
) + cz
*(m
-tz
)**2 + dz
*(m
-tz
)**3
3126 move
.append([p
, mathutils
.Vector([x
,y
,z
])])
3127 else: # interpolation == 'linear'
3128 a
, d
, t
, u
= splines
[n
]
3129 move
.append([p
, ((m
-t
)/u
)*d
+ a
])
3134 ##########################################
3135 ####### Operators ########################
3136 ##########################################
3139 class Bridge(bpy
.types
.Operator
):
3140 bl_idname
= 'mesh.looptools_bridge'
3141 bl_label
= "Bridge / Loft"
3142 bl_description
= "Bridge two, or loft several, loops of vertices"
3143 bl_options
= {'REGISTER', 'UNDO'}
3145 cubic_strength
= bpy
.props
.FloatProperty(name
= "Strength",
3146 description
= "Higher strength results in more fluid curves",
3150 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation mode",
3151 items
= (('cubic', "Cubic", "Gives curved results"),
3152 ('linear', "Linear", "Basic, fast, straight interpolation")),
3153 description
= "Interpolation mode: algorithm used when creating "\
3156 loft
= bpy
.props
.BoolProperty(name
= "Loft",
3157 description
= "Loft multiple loops, instead of considering them as "\
3158 "a multi-input for bridging",
3160 loft_loop
= bpy
.props
.BoolProperty(name
= "Loop",
3161 description
= "Connect the first and the last loop with each other",
3163 min_width
= bpy
.props
.IntProperty(name
= "Minimum width",
3164 description
= "Segments with an edge smaller than this are merged "\
3165 "(compared to base edge)",
3169 subtype
= 'PERCENTAGE')
3170 mode
= bpy
.props
.EnumProperty(name
= "Mode",
3171 items
= (('basic', "Basic", "Fast algorithm"), ('shortest',
3172 "Shortest edge", "Slower algorithm with better vertex matching")),
3173 description
= "Algorithm used for bridging",
3174 default
= 'shortest')
3175 remove_faces
= bpy
.props
.BoolProperty(name
= "Remove faces",
3176 description
= "Remove faces that are internal after bridging",
3178 reverse
= bpy
.props
.BoolProperty(name
= "Reverse",
3179 description
= "Manually override the direction in which the loops "\
3180 "are bridged. Only use if the tool gives the wrong " \
3183 segments
= bpy
.props
.IntProperty(name
= "Segments",
3184 description
= "Number of segments used to bridge the gap "\
3189 twist
= bpy
.props
.IntProperty(name
= "Twist",
3190 description
= "Twist what vertices are connected to each other",
3194 def poll(cls
, context
):
3195 ob
= context
.active_object
3196 return (ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3198 def draw(self
, context
):
3199 layout
= self
.layout
3200 #layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3203 col_top
= layout
.column(align
=True)
3204 row
= col_top
.row(align
=True)
3205 col_left
= row
.column(align
=True)
3206 col_right
= row
.column(align
=True)
3207 col_right
.active
= self
.segments
!= 1
3208 col_left
.prop(self
, "segments")
3209 col_right
.prop(self
, "min_width", text
="")
3211 bottom_left
= col_left
.row()
3212 bottom_left
.active
= self
.segments
!= 1
3213 bottom_left
.prop(self
, "interpolation", text
="")
3214 bottom_right
= col_right
.row()
3215 bottom_right
.active
= self
.interpolation
== 'cubic'
3216 bottom_right
.prop(self
, "cubic_strength")
3217 # boolean properties
3218 col_top
.prop(self
, "remove_faces")
3220 col_top
.prop(self
, "loft_loop")
3222 # override properties
3224 row
= layout
.row(align
= True)
3225 row
.prop(self
, "twist")
3226 row
.prop(self
, "reverse")
3228 def invoke(self
, context
, event
):
3229 # load custom settings
3230 context
.window_manager
.looptools
.bridge_loft
= self
.loft
3232 return self
.execute(context
)
3234 def execute(self
, context
):
3236 global_undo
, object, bm
= initialise()
3237 edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
= \
3238 bridge_initialise(bm
, self
.interpolation
)
3239 settings_write(self
)
3241 # check cache to see if we can save time
3242 input_method
= bridge_input_method(self
.loft
, self
.loft_loop
)
3243 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Bridge",
3244 object, bm
, input_method
, False)
3247 loops
= bridge_get_input(bm
)
3249 # reorder loops if there are more than 2
3252 loops
= bridge_sort_loops(bm
, loops
, self
.loft_loop
)
3254 loops
= bridge_match_loops(bm
, loops
)
3256 # saving cache for faster execution next time
3258 cache_write("Bridge", object, bm
, input_method
, False, False,
3259 loops
, False, False)
3262 # calculate new geometry
3265 max_vert_index
= len(bm
.verts
)-1
3266 for i
in range(1, len(loops
)):
3267 if not self
.loft
and i
%2 == 0:
3269 lines
= bridge_calculate_lines(bm
, loops
[i
-1:i
+1],
3270 self
.mode
, self
.twist
, self
.reverse
)
3271 vertex_normals
= bridge_calculate_virtual_vertex_normals(bm
,
3272 lines
, loops
[i
-1:i
+1], edge_faces
, edgekey_to_edge
)
3273 segments
= bridge_calculate_segments(bm
, lines
,
3274 loops
[i
-1:i
+1], self
.segments
)
3275 new_verts
, new_faces
, max_vert_index
= \
3276 bridge_calculate_geometry(bm
, lines
, vertex_normals
,
3277 segments
, self
.interpolation
, self
.cubic_strength
,
3278 self
.min_width
, max_vert_index
)
3280 vertices
+= new_verts
3283 # make sure faces in loops that aren't used, aren't removed
3284 if self
.remove_faces
and old_selected_faces
:
3285 bridge_save_unused_faces(bm
, old_selected_faces
, loops
)
3288 bridge_create_vertices(bm
, vertices
)
3291 new_faces
= bridge_create_faces(object, bm
, faces
, self
.twist
)
3292 old_selected_faces
= [i
for i
, face
in enumerate(bm
.faces
) \
3293 if face
.index
in old_selected_faces
] # updating list
3294 bridge_select_new_faces(new_faces
, smooth
)
3295 # edge-data could have changed, can't use cache next run
3296 if faces
and not vertices
:
3297 cache_delete("Bridge")
3298 # delete internal faces
3299 if self
.remove_faces
and old_selected_faces
:
3300 bridge_remove_internal_faces(bm
, old_selected_faces
)
3301 # make sure normals are facing outside
3302 bmesh
.update_edit_mesh(object.data
, tessface
=False,
3304 bpy
.ops
.mesh
.normals_make_consistent()
3307 terminate(global_undo
)
3313 class Circle(bpy
.types
.Operator
):
3314 bl_idname
= "mesh.looptools_circle"
3316 bl_description
= "Move selected vertices into a circle shape"
3317 bl_options
= {'REGISTER', 'UNDO'}
3319 custom_radius
= bpy
.props
.BoolProperty(name
= "Radius",
3320 description
= "Force a custom radius",
3322 fit
= bpy
.props
.EnumProperty(name
= "Method",
3323 items
= (("best", "Best fit", "Non-linear least squares"),
3324 ("inside", "Fit inside","Only move vertices towards the center")),
3325 description
= "Method used for fitting a circle to the vertices",
3327 flatten
= bpy
.props
.BoolProperty(name
= "Flatten",
3328 description
= "Flatten the circle, instead of projecting it on the " \
3331 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3332 description
= "Force of the tool",
3337 subtype
= 'PERCENTAGE')
3338 lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
3339 description
= "Lock editing of the x-coordinate",
3341 lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
3342 description
= "Lock editing of the y-coordinate",
3344 lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
3345 description
= "Lock editing of the z-coordinate",
3347 radius
= bpy
.props
.FloatProperty(name
= "Radius",
3348 description
= "Custom radius for circle",
3352 regular
= bpy
.props
.BoolProperty(name
= "Regular",
3353 description
= "Distribute vertices at constant distances along the " \
3358 def poll(cls
, context
):
3359 ob
= context
.active_object
3360 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3362 def draw(self
, context
):
3363 layout
= self
.layout
3364 col
= layout
.column()
3366 col
.prop(self
, "fit")
3369 col
.prop(self
, "flatten")
3370 row
= col
.row(align
=True)
3371 row
.prop(self
, "custom_radius")
3372 row_right
= row
.row(align
=True)
3373 row_right
.active
= self
.custom_radius
3374 row_right
.prop(self
, "radius", text
="")
3375 col
.prop(self
, "regular")
3378 col_move
= col
.column(align
=True)
3379 row
= col_move
.row(align
=True)
3381 row
.prop(self
, "lock_x", text
= "X", icon
='LOCKED')
3383 row
.prop(self
, "lock_x", text
= "X", icon
='UNLOCKED')
3385 row
.prop(self
, "lock_y", text
= "Y", icon
='LOCKED')
3387 row
.prop(self
, "lock_y", text
= "Y", icon
='UNLOCKED')
3389 row
.prop(self
, "lock_z", text
= "Z", icon
='LOCKED')
3391 row
.prop(self
, "lock_z", text
= "Z", icon
='UNLOCKED')
3392 col_move
.prop(self
, "influence")
3394 def invoke(self
, context
, event
):
3395 # load custom settings
3397 return self
.execute(context
)
3399 def execute(self
, context
):
3401 global_undo
, object, bm
= initialise()
3402 settings_write(self
)
3403 # check cache to see if we can save time
3404 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Circle",
3405 object, bm
, False, False)
3407 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3410 derived
, bm_mod
, single_vertices
, single_loops
, loops
= \
3411 circle_get_input(object, bm
, context
.scene
)
3412 mapping
= get_mapping(derived
, bm
, bm_mod
, single_vertices
,
3414 single_loops
, loops
= circle_check_loops(single_loops
, loops
,
3417 # saving cache for faster execution next time
3419 cache_write("Circle", object, bm
, False, False, single_loops
,
3420 loops
, derived
, mapping
)
3423 for i
, loop
in enumerate(loops
):
3424 # best fitting flat plane
3425 com
, normal
= calculate_plane(bm_mod
, loop
)
3426 # if circular, shift loop so we get a good starting vertex
3428 loop
= circle_shift_loop(bm_mod
, loop
, com
)
3429 # flatten vertices on plane
3430 locs_2d
, p
, q
= circle_3d_to_2d(bm_mod
, loop
, com
, normal
)
3432 if self
.fit
== 'best':
3433 x0
, y0
, r
= circle_calculate_best_fit(locs_2d
)
3434 else: # self.fit == 'inside'
3435 x0
, y0
, r
= circle_calculate_min_fit(locs_2d
)
3437 if self
.custom_radius
:
3438 r
= self
.radius
/ p
.length
3439 # calculate positions on circle
3441 new_locs_2d
= circle_project_regular(locs_2d
[:], x0
, y0
, r
)
3443 new_locs_2d
= circle_project_non_regular(locs_2d
[:], x0
, y0
, r
)
3444 # take influence into account
3445 locs_2d
= circle_influence_locs(locs_2d
, new_locs_2d
,
3447 # calculate 3d positions of the created 2d input
3448 move
.append(circle_calculate_verts(self
.flatten
, bm_mod
,
3449 locs_2d
, com
, p
, q
, normal
))
3450 # flatten single input vertices on plane defined by loop
3451 if self
.flatten
and single_loops
:
3452 move
.append(circle_flatten_singles(bm_mod
, com
, p
, q
,
3453 normal
, single_loops
[i
]))
3455 # move vertices to new locations
3456 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3457 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3460 move_verts(object, bm
, mapping
, move
, lock
, -1)
3465 terminate(global_undo
)
3471 class Curve(bpy
.types
.Operator
):
3472 bl_idname
= "mesh.looptools_curve"
3474 bl_description
= "Turn a loop into a smooth curve"
3475 bl_options
= {'REGISTER', 'UNDO'}
3477 boundaries
= bpy
.props
.BoolProperty(name
= "Boundaries",
3478 description
= "Limit the tool to work within the boundaries of the "\
3479 "selected vertices",
3481 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3482 description
= "Force of the tool",
3487 subtype
= 'PERCENTAGE')
3488 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
3489 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3490 ("linear", "Linear", "Simple and fast linear algorithm")),
3491 description
= "Algorithm used for interpolation",
3493 lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
3494 description
= "Lock editing of the x-coordinate",
3496 lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
3497 description
= "Lock editing of the y-coordinate",
3499 lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
3500 description
= "Lock editing of the z-coordinate",
3502 regular
= bpy
.props
.BoolProperty(name
= "Regular",
3503 description
= "Distribute vertices at constant distances along the" \
3506 restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
3507 items
= (("none", "None", "No restrictions on vertex movement"),
3508 ("extrude", "Extrude only","Only allow extrusions (no "\
3510 ("indent", "Indent only", "Only allow indentation (no "\
3512 description
= "Restrictions on how the vertices can be moved",
3516 def poll(cls
, context
):
3517 ob
= context
.active_object
3518 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3520 def draw(self
, context
):
3521 layout
= self
.layout
3522 col
= layout
.column()
3524 col
.prop(self
, "interpolation")
3525 col
.prop(self
, "restriction")
3526 col
.prop(self
, "boundaries")
3527 col
.prop(self
, "regular")
3530 col_move
= col
.column(align
=True)
3531 row
= col_move
.row(align
=True)
3533 row
.prop(self
, "lock_x", text
= "X", icon
='LOCKED')
3535 row
.prop(self
, "lock_x", text
= "X", icon
='UNLOCKED')
3537 row
.prop(self
, "lock_y", text
= "Y", icon
='LOCKED')
3539 row
.prop(self
, "lock_y", text
= "Y", icon
='UNLOCKED')
3541 row
.prop(self
, "lock_z", text
= "Z", icon
='LOCKED')
3543 row
.prop(self
, "lock_z", text
= "Z", icon
='UNLOCKED')
3544 col_move
.prop(self
, "influence")
3546 def invoke(self
, context
, event
):
3547 # load custom settings
3549 return self
.execute(context
)
3551 def execute(self
, context
):
3553 global_undo
, object, bm
= initialise()
3554 settings_write(self
)
3555 # check cache to see if we can save time
3556 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Curve",
3557 object, bm
, False, self
.boundaries
)
3559 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3562 derived
, bm_mod
, loops
= curve_get_input(object, bm
,
3563 self
.boundaries
, context
.scene
)
3564 mapping
= get_mapping(derived
, bm
, bm_mod
, False, True, loops
)
3565 loops
= check_loops(loops
, mapping
, bm_mod
)
3566 verts_selected
= [v
.index
for v
in bm_mod
.verts
if v
.select \
3569 # saving cache for faster execution next time
3571 cache_write("Curve", object, bm
, False, self
.boundaries
, False,
3572 loops
, derived
, mapping
)
3576 knots
, points
= curve_calculate_knots(loop
, verts_selected
)
3577 pknots
= curve_project_knots(bm_mod
, verts_selected
, knots
,
3579 tknots
, tpoints
= curve_calculate_t(bm_mod
, knots
, points
,
3580 pknots
, self
.regular
, loop
[1])
3581 splines
= calculate_splines(self
.interpolation
, bm_mod
,
3583 move
.append(curve_calculate_vertices(bm_mod
, knots
, tknots
,
3584 points
, tpoints
, splines
, self
.interpolation
,
3587 # move vertices to new locations
3588 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3589 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3592 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
3597 terminate(global_undo
)
3603 class Flatten(bpy
.types
.Operator
):
3604 bl_idname
= "mesh.looptools_flatten"
3605 bl_label
= "Flatten"
3606 bl_description
= "Flatten vertices on a best-fitting plane"
3607 bl_options
= {'REGISTER', 'UNDO'}
3609 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3610 description
= "Force of the tool",
3615 subtype
= 'PERCENTAGE')
3616 lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
3617 description
= "Lock editing of the x-coordinate",
3619 lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
3620 description
= "Lock editing of the y-coordinate",
3622 lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
3623 description
= "Lock editing of the z-coordinate",
3625 plane
= bpy
.props
.EnumProperty(name
= "Plane",
3626 items
= (("best_fit", "Best fit", "Calculate a best fitting plane"),
3627 ("normal", "Normal", "Derive plane from averaging vertex "\
3629 ("view", "View", "Flatten on a plane perpendicular to the "\
3631 description
= "Plane on which vertices are flattened",
3632 default
= 'best_fit')
3633 restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
3634 items
= (("none", "None", "No restrictions on vertex movement"),
3635 ("bounding_box", "Bounding box", "Vertices are restricted to "\
3636 "movement inside the bounding box of the selection")),
3637 description
= "Restrictions on how the vertices can be moved",
3641 def poll(cls
, context
):
3642 ob
= context
.active_object
3643 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3645 def draw(self
, context
):
3646 layout
= self
.layout
3647 col
= layout
.column()
3649 col
.prop(self
, "plane")
3650 #col.prop(self, "restriction")
3653 col_move
= col
.column(align
=True)
3654 row
= col_move
.row(align
=True)
3656 row
.prop(self
, "lock_x", text
= "X", icon
='LOCKED')
3658 row
.prop(self
, "lock_x", text
= "X", icon
='UNLOCKED')
3660 row
.prop(self
, "lock_y", text
= "Y", icon
='LOCKED')
3662 row
.prop(self
, "lock_y", text
= "Y", icon
='UNLOCKED')
3664 row
.prop(self
, "lock_z", text
= "Z", icon
='LOCKED')
3666 row
.prop(self
, "lock_z", text
= "Z", icon
='UNLOCKED')
3667 col_move
.prop(self
, "influence")
3669 def invoke(self
, context
, event
):
3670 # load custom settings
3672 return self
.execute(context
)
3674 def execute(self
, context
):
3676 global_undo
, object, bm
= initialise()
3677 settings_write(self
)
3678 # check cache to see if we can save time
3679 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Flatten",
3680 object, bm
, False, False)
3682 # order input into virtual loops
3683 loops
= flatten_get_input(bm
)
3684 loops
= check_loops(loops
, mapping
, bm
)
3686 # saving cache for faster execution next time
3688 cache_write("Flatten", object, bm
, False, False, False, loops
,
3693 # calculate plane and position of vertices on them
3694 com
, normal
= calculate_plane(bm
, loop
, method
=self
.plane
,
3696 to_move
= flatten_project(bm
, loop
, com
, normal
)
3697 if self
.restriction
== 'none':
3698 move
.append(to_move
)
3700 move
.append(to_move
)
3702 # move vertices to new locations
3703 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3704 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3707 move_verts(object, bm
, False, move
, lock
, self
.influence
)
3710 terminate(global_undo
)
3716 class GStretch(bpy
.types
.Operator
):
3717 bl_idname
= "mesh.looptools_gstretch"
3718 bl_label
= "Gstretch"
3719 bl_description
= "Stretch selected vertices to Grease Pencil stroke"
3720 bl_options
= {'REGISTER', 'UNDO'}
3722 conversion
= bpy
.props
.EnumProperty(name
= "Conversion",
3723 items
= (("distance", "Distance", "Set the distance between vertices "\
3724 "of the converted grease pencil stroke"),
3725 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "\
3726 "number of vertices that converted GP strokes will have"),
3727 ("vertices", "Exact vertices", "Set the exact number of vertices "\
3728 "that converted grease pencil strokes will have. Short strokes "\
3729 "with few points may contain less vertices than this number."),
3730 ("none", "No simplification", "Convert each grease pencil point "\
3732 description
= "If grease pencil strokes are converted to geometry, "\
3733 "use this simplification method",
3734 default
= 'limit_vertices')
3735 conversion_distance
= bpy
.props
.FloatProperty(name
= "Distance",
3736 description
= "Absolute distance between vertices along the converted "\
3737 "grease pencil stroke",
3742 conversion_max
= bpy
.props
.IntProperty(name
= "Max Vertices",
3743 description
= "Maximum number of vertices grease pencil strokes will "\
3744 "have, when they are converted to geomtery",
3748 update
= gstretch_update_min
)
3749 conversion_min
= bpy
.props
.IntProperty(name
= "Min Vertices",
3750 description
= "Minimum number of vertices grease pencil strokes will "\
3751 "have, when they are converted to geomtery",
3755 update
= gstretch_update_max
)
3756 conversion_vertices
= bpy
.props
.IntProperty(name
= "Vertices",
3757 description
= "Number of vertices grease pencil strokes will "\
3758 "have, when they are converted to geometry. If strokes have less "\
3759 "points than required, the 'Spread evenly' method is used",
3763 delete_strokes
= bpy
.props
.BoolProperty(name
="Delete strokes",
3764 description
= "Remove Grease Pencil strokes if they have been used "\
3765 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
3767 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3768 description
= "Force of the tool",
3773 subtype
= 'PERCENTAGE')
3774 lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
3775 description
= "Lock editing of the x-coordinate",
3777 lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
3778 description
= "Lock editing of the y-coordinate",
3780 lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
3781 description
= "Lock editing of the z-coordinate",
3783 method
= bpy
.props
.EnumProperty(name
= "Method",
3784 items
= (("project", "Project", "Project vertices onto the stroke, "\
3785 "using vertex normals and connected edges"),
3786 ("irregular", "Spread", "Distribute vertices along the full "\
3787 "stroke, retaining relative distances between the vertices"),
3788 ("regular", "Spread evenly", "Distribute vertices at regular "\
3789 "distances along the full stroke")),
3790 description
= "Method of distributing the vertices over the Grease "\
3792 default
= 'regular')
3795 def poll(cls
, context
):
3796 ob
= context
.active_object
3797 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3799 def draw(self
, context
):
3800 layout
= self
.layout
3801 col
= layout
.column()
3803 col
.prop(self
, "method")
3804 col
.prop(self
, "delete_strokes")
3807 col_conv
= col
.column(align
=True)
3808 col_conv
.prop(self
, "conversion", text
="")
3809 if self
.conversion
== 'distance':
3810 col_conv
.prop(self
, "conversion_distance")
3811 elif self
.conversion
== 'limit_vertices':
3812 row
= col_conv
.row(align
=True)
3813 row
.prop(self
, "conversion_min", text
="Min")
3814 row
.prop(self
, "conversion_max", text
="Max")
3815 elif self
.conversion
== 'vertices':
3816 col_conv
.prop(self
, "conversion_vertices")
3819 col_move
= col
.column(align
=True)
3820 row
= col_move
.row(align
=True)
3822 row
.prop(self
, "lock_x", text
= "X", icon
='LOCKED')
3824 row
.prop(self
, "lock_x", text
= "X", icon
='UNLOCKED')
3826 row
.prop(self
, "lock_y", text
= "Y", icon
='LOCKED')
3828 row
.prop(self
, "lock_y", text
= "Y", icon
='UNLOCKED')
3830 row
.prop(self
, "lock_z", text
= "Z", icon
='LOCKED')
3832 row
.prop(self
, "lock_z", text
= "Z", icon
='UNLOCKED')
3833 col_move
.prop(self
, "influence")
3835 def invoke(self
, context
, event
):
3836 # flush cached strokes
3837 if 'Gstretch' in looptools_cache
:
3838 looptools_cache
['Gstretch']['single_loops'] = []
3839 # load custom settings
3841 return self
.execute(context
)
3843 def execute(self
, context
):
3845 global_undo
, object, bm
= initialise()
3846 settings_write(self
)
3848 # check cache to see if we can save time
3849 cached
, safe_strokes
, loops
, derived
, mapping
= cache_read("Gstretch",
3850 object, bm
, False, False)
3852 straightening
= False
3854 strokes
= gstretch_safe_to_true_strokes(safe_strokes
)
3855 # cached strokes were flushed (see operator's invoke function)
3856 elif get_grease_pencil(object, context
):
3857 strokes
= gstretch_get_strokes(object, context
)
3859 # straightening function (no GP) -> loops ignore modifiers
3860 straightening
= True
3863 bm_mod
.verts
.ensure_lookup_table()
3864 bm_mod
.edges
.ensure_lookup_table()
3865 bm_mod
.faces
.ensure_lookup_table()
3866 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
3867 if not straightening
:
3868 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3870 # get loops and strokes
3871 if get_grease_pencil(object, context
):
3873 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
3874 context
.scene
, input='selected')
3875 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
3876 loops
= check_loops(loops
, mapping
, bm_mod
)
3878 strokes
= gstretch_get_strokes(object, context
)
3880 # straightening function (no GP) -> loops ignore modifiers
3884 bm_mod
.verts
.ensure_lookup_table()
3885 bm_mod
.edges
.ensure_lookup_table()
3886 bm_mod
.faces
.ensure_lookup_table()
3887 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if \
3888 edge
.select
and not edge
.hide
]
3889 loops
= get_connected_selections(edge_keys
)
3890 loops
= check_loops(loops
, mapping
, bm_mod
)
3891 # create fake strokes
3892 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
3894 # saving cache for faster execution next time
3897 safe_strokes
= gstretch_true_to_safe_strokes(strokes
)
3900 cache_write("Gstretch", object, bm
, False, False,
3901 safe_strokes
, loops
, derived
, mapping
)
3903 # pair loops and strokes
3904 ls_pairs
= gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
)
3905 ls_pairs
= gstretch_align_pairs(ls_pairs
, object, bm_mod
, self
.method
)
3909 # no selected geometry, convert GP to verts
3911 move
.append(gstretch_create_verts(object, bm
, strokes
,
3912 self
.method
, self
.conversion
, self
.conversion_distance
,
3913 self
.conversion_max
, self
.conversion_min
,
3914 self
.conversion_vertices
))
3915 for stroke
in strokes
:
3916 gstretch_erase_stroke(stroke
, context
)
3918 for (loop
, stroke
) in ls_pairs
:
3919 move
.append(gstretch_calculate_verts(loop
, stroke
, object,
3920 bm_mod
, self
.method
))
3921 if self
.delete_strokes
:
3922 if type(stroke
) != bpy
.types
.GPencilStroke
:
3923 # in case of cached fake stroke, get the real one
3924 if get_grease_pencil(object, context
):
3925 strokes
= gstretch_get_strokes(object, context
)
3926 if loops
and strokes
:
3927 ls_pairs
= gstretch_match_loops_strokes(loops
,
3928 strokes
, object, bm_mod
)
3929 ls_pairs
= gstretch_align_pairs(ls_pairs
,
3930 object, bm_mod
, self
.method
)
3931 for (l
, s
) in ls_pairs
:
3935 gstretch_erase_stroke(stroke
, context
)
3937 # move vertices to new locations
3938 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3939 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3942 bmesh
.update_edit_mesh(object.data
, tessface
=True, destructive
=True)
3943 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
3948 terminate(global_undo
)
3954 class Relax(bpy
.types
.Operator
):
3955 bl_idname
= "mesh.looptools_relax"
3957 bl_description
= "Relax the loop, so it is smoother"
3958 bl_options
= {'REGISTER', 'UNDO'}
3960 input = bpy
.props
.EnumProperty(name
= "Input",
3961 items
= (("all", "Parallel (all)", "Also use non-selected "\
3962 "parallel loops as input"),
3963 ("selected", "Selection","Only use selected vertices as input")),
3964 description
= "Loops that are relaxed",
3965 default
= 'selected')
3966 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
3967 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3968 ("linear", "Linear", "Simple and fast linear algorithm")),
3969 description
= "Algorithm used for interpolation",
3971 iterations
= bpy
.props
.EnumProperty(name
= "Iterations",
3972 items
= (("1", "1", "One"),
3973 ("3", "3", "Three"),
3975 ("10", "10", "Ten"),
3976 ("25", "25", "Twenty-five")),
3977 description
= "Number of times the loop is relaxed",
3979 regular
= bpy
.props
.BoolProperty(name
= "Regular",
3980 description
= "Distribute vertices at constant distances along the" \
3985 def poll(cls
, context
):
3986 ob
= context
.active_object
3987 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3989 def draw(self
, context
):
3990 layout
= self
.layout
3991 col
= layout
.column()
3993 col
.prop(self
, "interpolation")
3994 col
.prop(self
, "input")
3995 col
.prop(self
, "iterations")
3996 col
.prop(self
, "regular")
3998 def invoke(self
, context
, event
):
3999 # load custom settings
4001 return self
.execute(context
)
4003 def execute(self
, context
):
4005 global_undo
, object, bm
= initialise()
4006 settings_write(self
)
4007 # check cache to see if we can save time
4008 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Relax",
4009 object, bm
, self
.input, False)
4011 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
4014 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
4015 context
.scene
, self
.input)
4016 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4017 loops
= check_loops(loops
, mapping
, bm_mod
)
4018 knots
, points
= relax_calculate_knots(loops
)
4020 # saving cache for faster execution next time
4022 cache_write("Relax", object, bm
, self
.input, False, False, loops
,
4025 for iteration
in range(int(self
.iterations
)):
4026 # calculate splines and new positions
4027 tknots
, tpoints
= relax_calculate_t(bm_mod
, knots
, points
,
4030 for i
in range(len(knots
)):
4031 splines
.append(calculate_splines(self
.interpolation
, bm_mod
,
4032 tknots
[i
], knots
[i
]))
4033 move
= [relax_calculate_verts(bm_mod
, self
.interpolation
,
4034 tknots
, knots
, tpoints
, points
, splines
)]
4035 move_verts(object, bm
, mapping
, move
, False, -1)
4040 terminate(global_undo
)
4046 class Space(bpy
.types
.Operator
):
4047 bl_idname
= "mesh.looptools_space"
4049 bl_description
= "Space the vertices in a regular distrubtion on the loop"
4050 bl_options
= {'REGISTER', 'UNDO'}
4052 influence
= bpy
.props
.FloatProperty(name
= "Influence",
4053 description
= "Force of the tool",
4058 subtype
= 'PERCENTAGE')
4059 input = bpy
.props
.EnumProperty(name
= "Input",
4060 items
= (("all", "Parallel (all)", "Also use non-selected "\
4061 "parallel loops as input"),
4062 ("selected", "Selection","Only use selected vertices as input")),
4063 description
= "Loops that are spaced",
4064 default
= 'selected')
4065 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4066 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4067 ("linear", "Linear", "Vertices are projected on existing edges")),
4068 description
= "Algorithm used for interpolation",
4070 lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4071 description
= "Lock editing of the x-coordinate",
4073 lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4074 description
= "Lock editing of the y-coordinate",
4076 lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4077 description
= "Lock editing of the z-coordinate",
4081 def poll(cls
, context
):
4082 ob
= context
.active_object
4083 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4085 def draw(self
, context
):
4086 layout
= self
.layout
4087 col
= layout
.column()
4089 col
.prop(self
, "interpolation")
4090 col
.prop(self
, "input")
4093 col_move
= col
.column(align
=True)
4094 row
= col_move
.row(align
=True)
4096 row
.prop(self
, "lock_x", text
= "X", icon
='LOCKED')
4098 row
.prop(self
, "lock_x", text
= "X", icon
='UNLOCKED')
4100 row
.prop(self
, "lock_y", text
= "Y", icon
='LOCKED')
4102 row
.prop(self
, "lock_y", text
= "Y", icon
='UNLOCKED')
4104 row
.prop(self
, "lock_z", text
= "Z", icon
='LOCKED')
4106 row
.prop(self
, "lock_z", text
= "Z", icon
='UNLOCKED')
4107 col_move
.prop(self
, "influence")
4109 def invoke(self
, context
, event
):
4110 # load custom settings
4112 return self
.execute(context
)
4114 def execute(self
, context
):
4116 global_undo
, object, bm
= initialise()
4117 settings_write(self
)
4118 # check cache to see if we can save time
4119 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Space",
4120 object, bm
, self
.input, False)
4122 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
4125 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
4126 context
.scene
, self
.input)
4127 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4128 loops
= check_loops(loops
, mapping
, bm_mod
)
4130 # saving cache for faster execution next time
4132 cache_write("Space", object, bm
, self
.input, False, False, loops
,
4137 # calculate splines and new positions
4138 if loop
[1]: # circular
4139 loop
[0].append(loop
[0][0])
4140 tknots
, tpoints
= space_calculate_t(bm_mod
, loop
[0][:])
4141 splines
= calculate_splines(self
.interpolation
, bm_mod
,
4143 move
.append(space_calculate_verts(bm_mod
, self
.interpolation
,
4144 tknots
, tpoints
, loop
[0][:-1], splines
))
4145 # move vertices to new locations
4146 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4147 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4150 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4155 terminate(global_undo
)
4160 ##########################################
4161 ####### GUI and registration #############
4162 ##########################################
4164 # menu containing all tools
4165 class VIEW3D_MT_edit_mesh_looptools(bpy
.types
.Menu
):
4166 bl_label
= "LoopTools"
4168 def draw(self
, context
):
4169 layout
= self
.layout
4171 layout
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4172 layout
.operator("mesh.looptools_circle")
4173 layout
.operator("mesh.looptools_curve")
4174 layout
.operator("mesh.looptools_flatten")
4175 layout
.operator("mesh.looptools_gstretch")
4176 layout
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4177 layout
.operator("mesh.looptools_relax")
4178 layout
.operator("mesh.looptools_space")
4181 # panel containing all tools
4182 class VIEW3D_PT_tools_looptools(bpy
.types
.Panel
):
4183 bl_space_type
= 'VIEW_3D'
4184 bl_region_type
= 'TOOLS'
4185 bl_category
= 'Tools'
4186 bl_context
= "mesh_edit"
4187 bl_label
= "LoopTools"
4188 bl_options
= {'DEFAULT_CLOSED'}
4190 def draw(self
, context
):
4191 layout
= self
.layout
4192 col
= layout
.column(align
=True)
4193 lt
= context
.window_manager
.looptools
4195 # bridge - first line
4196 split
= col
.split(percentage
=0.15, align
=True)
4197 if lt
.display_bridge
:
4198 split
.prop(lt
, "display_bridge", text
="", icon
='DOWNARROW_HLT')
4200 split
.prop(lt
, "display_bridge", text
="", icon
='RIGHTARROW')
4201 split
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4203 if lt
.display_bridge
:
4204 box
= col
.column(align
=True).box().column()
4205 #box.prop(self, "mode")
4208 col_top
= box
.column(align
=True)
4209 row
= col_top
.row(align
=True)
4210 col_left
= row
.column(align
=True)
4211 col_right
= row
.column(align
=True)
4212 col_right
.active
= lt
.bridge_segments
!= 1
4213 col_left
.prop(lt
, "bridge_segments")
4214 col_right
.prop(lt
, "bridge_min_width", text
="")
4216 bottom_left
= col_left
.row()
4217 bottom_left
.active
= lt
.bridge_segments
!= 1
4218 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4219 bottom_right
= col_right
.row()
4220 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4221 bottom_right
.prop(lt
, "bridge_cubic_strength")
4222 # boolean properties
4223 col_top
.prop(lt
, "bridge_remove_faces")
4225 # override properties
4227 row
= box
.row(align
= True)
4228 row
.prop(lt
, "bridge_twist")
4229 row
.prop(lt
, "bridge_reverse")
4231 # circle - first line
4232 split
= col
.split(percentage
=0.15, align
=True)
4233 if lt
.display_circle
:
4234 split
.prop(lt
, "display_circle", text
="", icon
='DOWNARROW_HLT')
4236 split
.prop(lt
, "display_circle", text
="", icon
='RIGHTARROW')
4237 split
.operator("mesh.looptools_circle")
4239 if lt
.display_circle
:
4240 box
= col
.column(align
=True).box().column()
4241 box
.prop(lt
, "circle_fit")
4244 box
.prop(lt
, "circle_flatten")
4245 row
= box
.row(align
=True)
4246 row
.prop(lt
, "circle_custom_radius")
4247 row_right
= row
.row(align
=True)
4248 row_right
.active
= lt
.circle_custom_radius
4249 row_right
.prop(lt
, "circle_radius", text
="")
4250 box
.prop(lt
, "circle_regular")
4253 col_move
= box
.column(align
=True)
4254 row
= col_move
.row(align
=True)
4255 if lt
.circle_lock_x
:
4256 row
.prop(lt
, "circle_lock_x", text
= "X", icon
='LOCKED')
4258 row
.prop(lt
, "circle_lock_x", text
= "X", icon
='UNLOCKED')
4259 if lt
.circle_lock_y
:
4260 row
.prop(lt
, "circle_lock_y", text
= "Y", icon
='LOCKED')
4262 row
.prop(lt
, "circle_lock_y", text
= "Y", icon
='UNLOCKED')
4263 if lt
.circle_lock_z
:
4264 row
.prop(lt
, "circle_lock_z", text
= "Z", icon
='LOCKED')
4266 row
.prop(lt
, "circle_lock_z", text
= "Z", icon
='UNLOCKED')
4267 col_move
.prop(lt
, "circle_influence")
4269 # curve - first line
4270 split
= col
.split(percentage
=0.15, align
=True)
4271 if lt
.display_curve
:
4272 split
.prop(lt
, "display_curve", text
="", icon
='DOWNARROW_HLT')
4274 split
.prop(lt
, "display_curve", text
="", icon
='RIGHTARROW')
4275 split
.operator("mesh.looptools_curve")
4277 if lt
.display_curve
:
4278 box
= col
.column(align
=True).box().column()
4279 box
.prop(lt
, "curve_interpolation")
4280 box
.prop(lt
, "curve_restriction")
4281 box
.prop(lt
, "curve_boundaries")
4282 box
.prop(lt
, "curve_regular")
4285 col_move
= box
.column(align
=True)
4286 row
= col_move
.row(align
=True)
4288 row
.prop(lt
, "curve_lock_x", text
= "X", icon
='LOCKED')
4290 row
.prop(lt
, "curve_lock_x", text
= "X", icon
='UNLOCKED')
4292 row
.prop(lt
, "curve_lock_y", text
= "Y", icon
='LOCKED')
4294 row
.prop(lt
, "curve_lock_y", text
= "Y", icon
='UNLOCKED')
4296 row
.prop(lt
, "curve_lock_z", text
= "Z", icon
='LOCKED')
4298 row
.prop(lt
, "curve_lock_z", text
= "Z", icon
='UNLOCKED')
4299 col_move
.prop(lt
, "curve_influence")
4301 # flatten - first line
4302 split
= col
.split(percentage
=0.15, align
=True)
4303 if lt
.display_flatten
:
4304 split
.prop(lt
, "display_flatten", text
="", icon
='DOWNARROW_HLT')
4306 split
.prop(lt
, "display_flatten", text
="", icon
='RIGHTARROW')
4307 split
.operator("mesh.looptools_flatten")
4308 # flatten - settings
4309 if lt
.display_flatten
:
4310 box
= col
.column(align
=True).box().column()
4311 box
.prop(lt
, "flatten_plane")
4312 #box.prop(lt, "flatten_restriction")
4315 col_move
= box
.column(align
=True)
4316 row
= col_move
.row(align
=True)
4317 if lt
.flatten_lock_x
:
4318 row
.prop(lt
, "flatten_lock_x", text
= "X", icon
='LOCKED')
4320 row
.prop(lt
, "flatten_lock_x", text
= "X", icon
='UNLOCKED')
4321 if lt
.flatten_lock_y
:
4322 row
.prop(lt
, "flatten_lock_y", text
= "Y", icon
='LOCKED')
4324 row
.prop(lt
, "flatten_lock_y", text
= "Y", icon
='UNLOCKED')
4325 if lt
.flatten_lock_z
:
4326 row
.prop(lt
, "flatten_lock_z", text
= "Z", icon
='LOCKED')
4328 row
.prop(lt
, "flatten_lock_z", text
= "Z", icon
='UNLOCKED')
4329 col_move
.prop(lt
, "flatten_influence")
4331 # gstretch - first line
4332 split
= col
.split(percentage
=0.15, align
=True)
4333 if lt
.display_gstretch
:
4334 split
.prop(lt
, "display_gstretch", text
="", icon
='DOWNARROW_HLT')
4336 split
.prop(lt
, "display_gstretch", text
="", icon
='RIGHTARROW')
4337 split
.operator("mesh.looptools_gstretch")
4339 if lt
.display_gstretch
:
4340 box
= col
.column(align
=True).box().column()
4341 box
.prop(lt
, "gstretch_method")
4342 box
.prop(lt
, "gstretch_delete_strokes")
4345 col_conv
= box
.column(align
=True)
4346 col_conv
.prop(lt
, "gstretch_conversion", text
="")
4347 if lt
.gstretch_conversion
== 'distance':
4348 col_conv
.prop(lt
, "gstretch_conversion_distance")
4349 elif lt
.gstretch_conversion
== 'limit_vertices':
4350 row
= col_conv
.row(align
=True)
4351 row
.prop(lt
, "gstretch_conversion_min", text
="Min")
4352 row
.prop(lt
, "gstretch_conversion_max", text
="Max")
4353 elif lt
.gstretch_conversion
== 'vertices':
4354 col_conv
.prop(lt
, "gstretch_conversion_vertices")
4357 col_move
= box
.column(align
=True)
4358 row
= col_move
.row(align
=True)
4359 if lt
.gstretch_lock_x
:
4360 row
.prop(lt
, "gstretch_lock_x", text
= "X", icon
='LOCKED')
4362 row
.prop(lt
, "gstretch_lock_x", text
= "X", icon
='UNLOCKED')
4363 if lt
.gstretch_lock_y
:
4364 row
.prop(lt
, "gstretch_lock_y", text
= "Y", icon
='LOCKED')
4366 row
.prop(lt
, "gstretch_lock_y", text
= "Y", icon
='UNLOCKED')
4367 if lt
.gstretch_lock_z
:
4368 row
.prop(lt
, "gstretch_lock_z", text
= "Z", icon
='LOCKED')
4370 row
.prop(lt
, "gstretch_lock_z", text
= "Z", icon
='UNLOCKED')
4371 col_move
.prop(lt
, "gstretch_influence")
4374 split
= col
.split(percentage
=0.15, align
=True)
4376 split
.prop(lt
, "display_loft", text
="", icon
='DOWNARROW_HLT')
4378 split
.prop(lt
, "display_loft", text
="", icon
='RIGHTARROW')
4379 split
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4382 box
= col
.column(align
=True).box().column()
4383 #box.prop(self, "mode")
4386 col_top
= box
.column(align
=True)
4387 row
= col_top
.row(align
=True)
4388 col_left
= row
.column(align
=True)
4389 col_right
= row
.column(align
=True)
4390 col_right
.active
= lt
.bridge_segments
!= 1
4391 col_left
.prop(lt
, "bridge_segments")
4392 col_right
.prop(lt
, "bridge_min_width", text
="")
4394 bottom_left
= col_left
.row()
4395 bottom_left
.active
= lt
.bridge_segments
!= 1
4396 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4397 bottom_right
= col_right
.row()
4398 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4399 bottom_right
.prop(lt
, "bridge_cubic_strength")
4400 # boolean properties
4401 col_top
.prop(lt
, "bridge_remove_faces")
4402 col_top
.prop(lt
, "bridge_loft_loop")
4404 # override properties
4406 row
= box
.row(align
= True)
4407 row
.prop(lt
, "bridge_twist")
4408 row
.prop(lt
, "bridge_reverse")
4410 # relax - first line
4411 split
= col
.split(percentage
=0.15, align
=True)
4412 if lt
.display_relax
:
4413 split
.prop(lt
, "display_relax", text
="", icon
='DOWNARROW_HLT')
4415 split
.prop(lt
, "display_relax", text
="", icon
='RIGHTARROW')
4416 split
.operator("mesh.looptools_relax")
4418 if lt
.display_relax
:
4419 box
= col
.column(align
=True).box().column()
4420 box
.prop(lt
, "relax_interpolation")
4421 box
.prop(lt
, "relax_input")
4422 box
.prop(lt
, "relax_iterations")
4423 box
.prop(lt
, "relax_regular")
4425 # space - first line
4426 split
= col
.split(percentage
=0.15, align
=True)
4427 if lt
.display_space
:
4428 split
.prop(lt
, "display_space", text
="", icon
='DOWNARROW_HLT')
4430 split
.prop(lt
, "display_space", text
="", icon
='RIGHTARROW')
4431 split
.operator("mesh.looptools_space")
4433 if lt
.display_space
:
4434 box
= col
.column(align
=True).box().column()
4435 box
.prop(lt
, "space_interpolation")
4436 box
.prop(lt
, "space_input")
4439 col_move
= box
.column(align
=True)
4440 row
= col_move
.row(align
=True)
4442 row
.prop(lt
, "space_lock_x", text
= "X", icon
='LOCKED')
4444 row
.prop(lt
, "space_lock_x", text
= "X", icon
='UNLOCKED')
4446 row
.prop(lt
, "space_lock_y", text
= "Y", icon
='LOCKED')
4448 row
.prop(lt
, "space_lock_y", text
= "Y", icon
='UNLOCKED')
4450 row
.prop(lt
, "space_lock_z", text
= "Z", icon
='LOCKED')
4452 row
.prop(lt
, "space_lock_z", text
= "Z", icon
='UNLOCKED')
4453 col_move
.prop(lt
, "space_influence")
4456 # property group containing all properties for the gui in the panel
4457 class LoopToolsProps(bpy
.types
.PropertyGroup
):
4459 Fake module like class
4460 bpy.context.window_manager.looptools
4463 # general display properties
4464 display_bridge
= bpy
.props
.BoolProperty(name
= "Bridge settings",
4465 description
= "Display settings of the Bridge tool",
4467 display_circle
= bpy
.props
.BoolProperty(name
= "Circle settings",
4468 description
= "Display settings of the Circle tool",
4470 display_curve
= bpy
.props
.BoolProperty(name
= "Curve settings",
4471 description
= "Display settings of the Curve tool",
4473 display_flatten
= bpy
.props
.BoolProperty(name
= "Flatten settings",
4474 description
= "Display settings of the Flatten tool",
4476 display_gstretch
= bpy
.props
.BoolProperty(name
= "Gstretch settings",
4477 description
= "Display settings of the Gstretch tool",
4479 display_loft
= bpy
.props
.BoolProperty(name
= "Loft settings",
4480 description
= "Display settings of the Loft tool",
4482 display_relax
= bpy
.props
.BoolProperty(name
= "Relax settings",
4483 description
= "Display settings of the Relax tool",
4485 display_space
= bpy
.props
.BoolProperty(name
= "Space settings",
4486 description
= "Display settings of the Space tool",
4490 bridge_cubic_strength
= bpy
.props
.FloatProperty(name
= "Strength",
4491 description
= "Higher strength results in more fluid curves",
4495 bridge_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation mode",
4496 items
= (('cubic', "Cubic", "Gives curved results"),
4497 ('linear', "Linear", "Basic, fast, straight interpolation")),
4498 description
= "Interpolation mode: algorithm used when creating "\
4501 bridge_loft
= bpy
.props
.BoolProperty(name
= "Loft",
4502 description
= "Loft multiple loops, instead of considering them as "\
4503 "a multi-input for bridging",
4505 bridge_loft_loop
= bpy
.props
.BoolProperty(name
= "Loop",
4506 description
= "Connect the first and the last loop with each other",
4508 bridge_min_width
= bpy
.props
.IntProperty(name
= "Minimum width",
4509 description
= "Segments with an edge smaller than this are merged "\
4510 "(compared to base edge)",
4514 subtype
= 'PERCENTAGE')
4515 bridge_mode
= bpy
.props
.EnumProperty(name
= "Mode",
4516 items
= (('basic', "Basic", "Fast algorithm"),
4517 ('shortest', "Shortest edge", "Slower algorithm with " \
4518 "better vertex matching")),
4519 description
= "Algorithm used for bridging",
4520 default
= 'shortest')
4521 bridge_remove_faces
= bpy
.props
.BoolProperty(name
= "Remove faces",
4522 description
= "Remove faces that are internal after bridging",
4524 bridge_reverse
= bpy
.props
.BoolProperty(name
= "Reverse",
4525 description
= "Manually override the direction in which the loops "\
4526 "are bridged. Only use if the tool gives the wrong " \
4529 bridge_segments
= bpy
.props
.IntProperty(name
= "Segments",
4530 description
= "Number of segments used to bridge the gap "\
4535 bridge_twist
= bpy
.props
.IntProperty(name
= "Twist",
4536 description
= "Twist what vertices are connected to each other",
4540 circle_custom_radius
= bpy
.props
.BoolProperty(name
= "Radius",
4541 description
= "Force a custom radius",
4543 circle_fit
= bpy
.props
.EnumProperty(name
= "Method",
4544 items
= (("best", "Best fit", "Non-linear least squares"),
4545 ("inside", "Fit inside","Only move vertices towards the center")),
4546 description
= "Method used for fitting a circle to the vertices",
4548 circle_flatten
= bpy
.props
.BoolProperty(name
= "Flatten",
4549 description
= "Flatten the circle, instead of projecting it on the " \
4552 circle_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4553 description
= "Force of the tool",
4558 subtype
= 'PERCENTAGE')
4559 circle_lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4560 description
= "Lock editing of the x-coordinate",
4562 circle_lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4563 description
= "Lock editing of the y-coordinate",
4565 circle_lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4566 description
= "Lock editing of the z-coordinate",
4568 circle_radius
= bpy
.props
.FloatProperty(name
= "Radius",
4569 description
= "Custom radius for circle",
4573 circle_regular
= bpy
.props
.BoolProperty(name
= "Regular",
4574 description
= "Distribute vertices at constant distances along the " \
4579 curve_boundaries
= bpy
.props
.BoolProperty(name
= "Boundaries",
4580 description
= "Limit the tool to work within the boundaries of the "\
4581 "selected vertices",
4583 curve_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4584 description
= "Force of the tool",
4589 subtype
= 'PERCENTAGE')
4590 curve_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4591 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4592 ("linear", "Linear", "Simple and fast linear algorithm")),
4593 description
= "Algorithm used for interpolation",
4595 curve_lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4596 description
= "Lock editing of the x-coordinate",
4598 curve_lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4599 description
= "Lock editing of the y-coordinate",
4601 curve_lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4602 description
= "Lock editing of the z-coordinate",
4604 curve_regular
= bpy
.props
.BoolProperty(name
= "Regular",
4605 description
= "Distribute vertices at constant distances along the " \
4608 curve_restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
4609 items
= (("none", "None", "No restrictions on vertex movement"),
4610 ("extrude", "Extrude only","Only allow extrusions (no "\
4612 ("indent", "Indent only", "Only allow indentation (no "\
4614 description
= "Restrictions on how the vertices can be moved",
4617 # flatten properties
4618 flatten_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4619 description
= "Force of the tool",
4624 subtype
= 'PERCENTAGE')
4625 flatten_lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4626 description
= "Lock editing of the x-coordinate",
4628 flatten_lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4629 description
= "Lock editing of the y-coordinate",
4631 flatten_lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4632 description
= "Lock editing of the z-coordinate",
4634 flatten_plane
= bpy
.props
.EnumProperty(name
= "Plane",
4635 items
= (("best_fit", "Best fit", "Calculate a best fitting plane"),
4636 ("normal", "Normal", "Derive plane from averaging vertex "\
4638 ("view", "View", "Flatten on a plane perpendicular to the "\
4640 description
= "Plane on which vertices are flattened",
4641 default
= 'best_fit')
4642 flatten_restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
4643 items
= (("none", "None", "No restrictions on vertex movement"),
4644 ("bounding_box", "Bounding box", "Vertices are restricted to "\
4645 "movement inside the bounding box of the selection")),
4646 description
= "Restrictions on how the vertices can be moved",
4649 # gstretch properties
4650 gstretch_conversion
= bpy
.props
.EnumProperty(name
= "Conversion",
4651 items
= (("distance", "Distance", "Set the distance between vertices "\
4652 "of the converted grease pencil stroke"),
4653 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "\
4654 "number of vertices that converted GP strokes will have"),
4655 ("vertices", "Exact vertices", "Set the exact number of vertices "\
4656 "that converted grease pencil strokes will have. Short strokes "\
4657 "with few points may contain less vertices than this number."),
4658 ("none", "No simplification", "Convert each grease pencil point "\
4660 description
= "If grease pencil strokes are converted to geometry, "\
4661 "use this simplification method",
4662 default
= 'limit_vertices')
4663 gstretch_conversion_distance
= bpy
.props
.FloatProperty(name
= "Distance",
4664 description
= "Absolute distance between vertices along the converted "\
4665 "grease pencil stroke",
4670 gstretch_conversion_max
= bpy
.props
.IntProperty(name
= "Max Vertices",
4671 description
= "Maximum number of vertices grease pencil strokes will "\
4672 "have, when they are converted to geomtery",
4676 update
= gstretch_update_min
)
4677 gstretch_conversion_min
= bpy
.props
.IntProperty(name
= "Min Vertices",
4678 description
= "Minimum number of vertices grease pencil strokes will "\
4679 "have, when they are converted to geomtery",
4683 update
= gstretch_update_max
)
4684 gstretch_conversion_vertices
= bpy
.props
.IntProperty(name
= "Vertices",
4685 description
= "Number of vertices grease pencil strokes will "\
4686 "have, when they are converted to geometry. If strokes have less "\
4687 "points than required, the 'Spread evenly' method is used",
4691 gstretch_delete_strokes
= bpy
.props
.BoolProperty(name
="Delete strokes",
4692 description
= "Remove Grease Pencil strokes if they have been used "\
4693 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
4695 gstretch_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4696 description
= "Force of the tool",
4701 subtype
= 'PERCENTAGE')
4702 gstretch_lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4703 description
= "Lock editing of the x-coordinate",
4705 gstretch_lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4706 description
= "Lock editing of the y-coordinate",
4708 gstretch_lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4709 description
= "Lock editing of the z-coordinate",
4711 gstretch_method
= bpy
.props
.EnumProperty(name
= "Method",
4712 items
= (("project", "Project", "Project vertices onto the stroke, "\
4713 "using vertex normals and connected edges"),
4714 ("irregular", "Spread", "Distribute vertices along the full "\
4715 "stroke, retaining relative distances between the vertices"),
4716 ("regular", "Spread evenly", "Distribute vertices at regular "\
4717 "distances along the full stroke")),
4718 description
= "Method of distributing the vertices over the Grease "\
4720 default
= 'regular')
4723 relax_input
= bpy
.props
.EnumProperty(name
= "Input",
4724 items
= (("all", "Parallel (all)", "Also use non-selected "\
4725 "parallel loops as input"),
4726 ("selected", "Selection","Only use selected vertices as input")),
4727 description
= "Loops that are relaxed",
4728 default
= 'selected')
4729 relax_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4730 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4731 ("linear", "Linear", "Simple and fast linear algorithm")),
4732 description
= "Algorithm used for interpolation",
4734 relax_iterations
= bpy
.props
.EnumProperty(name
= "Iterations",
4735 items
= (("1", "1", "One"),
4736 ("3", "3", "Three"),
4738 ("10", "10", "Ten"),
4739 ("25", "25", "Twenty-five")),
4740 description
= "Number of times the loop is relaxed",
4742 relax_regular
= bpy
.props
.BoolProperty(name
= "Regular",
4743 description
= "Distribute vertices at constant distances along the" \
4748 space_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4749 description
= "Force of the tool",
4754 subtype
= 'PERCENTAGE')
4755 space_input
= bpy
.props
.EnumProperty(name
= "Input",
4756 items
= (("all", "Parallel (all)", "Also use non-selected "\
4757 "parallel loops as input"),
4758 ("selected", "Selection","Only use selected vertices as input")),
4759 description
= "Loops that are spaced",
4760 default
= 'selected')
4761 space_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4762 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4763 ("linear", "Linear", "Vertices are projected on existing edges")),
4764 description
= "Algorithm used for interpolation",
4766 space_lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4767 description
= "Lock editing of the x-coordinate",
4769 space_lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4770 description
= "Lock editing of the y-coordinate",
4772 space_lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4773 description
= "Lock editing of the z-coordinate",
4777 # draw function for integration in menus
4778 def menu_func(self
, context
):
4779 self
.layout
.menu("VIEW3D_MT_edit_mesh_looptools")
4780 self
.layout
.separator()
4783 # define classes for registration
4784 classes
= [VIEW3D_MT_edit_mesh_looptools
,
4785 VIEW3D_PT_tools_looptools
,
4796 # registering and menu integration
4799 bpy
.utils
.register_class(c
)
4800 bpy
.types
.VIEW3D_MT_edit_mesh_specials
.prepend(menu_func
)
4801 bpy
.types
.WindowManager
.looptools
= bpy
.props
.PointerProperty(\
4802 type = LoopToolsProps
)
4805 # unregistering and removing menus
4808 bpy
.utils
.unregister_class(c
)
4809 bpy
.types
.VIEW3D_MT_edit_mesh_specials
.remove(menu_func
)
4811 del bpy
.types
.WindowManager
.looptools
4816 if __name__
== "__main__":