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()
263 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
264 for i
in range(itermax
):
272 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
275 elif method
== 'normal':
276 # averaging the vertex normals
277 v_normals
= [bm_mod
.verts
[v
].normal
for v
in loop
[0]]
278 normal
= mathutils
.Vector()
279 for v_normal
in v_normals
:
281 normal
/= len(v_normals
)
284 elif method
== 'view':
285 # calculate view normal
286 rotation
= bpy
.context
.space_data
.region_3d
.view_matrix
.to_3x3().\
288 normal
= rotation
* mathutils
.Vector((0.0, 0.0, 1.0))
290 normal
= object.matrix_world
.inverted().to_euler().to_matrix() * \
296 # calculate splines based on given interpolation method (controller function)
297 def calculate_splines(interpolation
, bm_mod
, tknots
, knots
):
298 if interpolation
== 'cubic':
299 splines
= calculate_cubic_splines(bm_mod
, tknots
, knots
[:])
300 else: # interpolations == 'linear'
301 splines
= calculate_linear_splines(bm_mod
, tknots
, knots
[:])
306 # check loops and only return valid ones
307 def check_loops(loops
, mapping
, bm_mod
):
309 for loop
, circular
in loops
:
310 # loop needs to have at least 3 vertices
313 # loop needs at least 1 vertex in the original, non-mirrored mesh
317 if mapping
[vert
] > -1:
322 # vertices can not all be at the same location
324 for i
in range(len(loop
) - 1):
325 if (bm_mod
.verts
[loop
[i
]].co
- \
326 bm_mod
.verts
[loop
[i
+1]].co
).length
> 1e-6:
331 # passed all tests, loop is valid
332 valid_loops
.append([loop
, circular
])
337 # input: bmesh, output: dict with the edge-key as key and face-index as value
338 def dict_edge_faces(bm
):
339 edge_faces
= dict([[edgekey(edge
), []] for edge
in bm
.edges
if \
341 for face
in bm
.faces
:
344 for key
in face_edgekeys(face
):
345 edge_faces
[key
].append(face
.index
)
350 # input: bmesh (edge-faces optional), output: dict with face-face connections
351 def dict_face_faces(bm
, edge_faces
=False):
353 edge_faces
= dict_edge_faces(bm
)
355 connected_faces
= dict([[face
.index
, []] for face
in bm
.faces
if \
357 for face
in bm
.faces
:
360 for edge_key
in face_edgekeys(face
):
361 for connected_face
in edge_faces
[edge_key
]:
362 if connected_face
== face
.index
:
364 connected_faces
[face
.index
].append(connected_face
)
366 return(connected_faces
)
369 # input: bmesh, output: dict with the vert index as key and edge-keys as value
370 def dict_vert_edges(bm
):
371 vert_edges
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
372 for edge
in bm
.edges
:
377 vert_edges
[vert
].append(ek
)
382 # input: bmesh, output: dict with the vert index as key and face index as value
383 def dict_vert_faces(bm
):
384 vert_faces
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
385 for face
in bm
.faces
:
387 for vert
in face
.verts
:
388 vert_faces
[vert
.index
].append(face
.index
)
393 # input: list of edge-keys, output: dictionary with vertex-vertex connections
394 def dict_vert_verts(edge_keys
):
395 # create connection data
399 if ek
[i
] in vert_verts
:
400 vert_verts
[ek
[i
]].append(ek
[1-i
])
402 vert_verts
[ek
[i
]] = [ek
[1-i
]]
407 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
409 return(tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])))
412 # returns the edgekeys of a bmesh face
413 def face_edgekeys(face
):
414 return([tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])) for \
418 # calculate input loops
419 def get_connected_input(object, bm
, scene
, input):
420 # get mesh with modifiers applied
421 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
423 # calculate selected loops
424 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if \
425 edge
.select
and not edge
.hide
]
426 loops
= get_connected_selections(edge_keys
)
428 # if only selected loops are needed, we're done
429 if input == 'selected':
430 return(derived
, bm_mod
, loops
)
431 # elif input == 'all':
432 loops
= get_parallel_loops(bm_mod
, loops
)
434 return(derived
, bm_mod
, loops
)
437 # sorts all edge-keys into a list of loops
438 def get_connected_selections(edge_keys
):
439 # create connection data
440 vert_verts
= dict_vert_verts(edge_keys
)
442 # find loops consisting of connected selected edges
444 while len(vert_verts
) > 0:
445 loop
= [iter(vert_verts
.keys()).__next
__()]
451 # no more connection data for current vertex
452 if loop
[-1] not in vert_verts
:
460 for i
, next_vert
in enumerate(vert_verts
[loop
[-1]]):
461 if next_vert
not in loop
:
462 vert_verts
[loop
[-1]].pop(i
)
463 if len(vert_verts
[loop
[-1]]) == 0:
464 del vert_verts
[loop
[-1]]
465 # remove connection both ways
466 if next_vert
in vert_verts
:
467 if len(vert_verts
[next_vert
]) == 1:
468 del vert_verts
[next_vert
]
470 vert_verts
[next_vert
].remove(loop
[-1])
471 loop
.append(next_vert
)
475 # found one end of the loop, continue with next
479 # found both ends of the loop, stop growing
483 # check if loop is circular
484 if loop
[0] in vert_verts
:
485 if loop
[-1] in vert_verts
[loop
[0]]:
487 if len(vert_verts
[loop
[0]]) == 1:
488 del vert_verts
[loop
[0]]
490 vert_verts
[loop
[0]].remove(loop
[-1])
491 if len(vert_verts
[loop
[-1]]) == 1:
492 del vert_verts
[loop
[-1]]
494 vert_verts
[loop
[-1]].remove(loop
[0])
508 # get the derived mesh data, if there is a mirror modifier
509 def get_derived_bmesh(object, bm
, scene
):
510 # check for mirror modifiers
511 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
513 # disable other modifiers
514 show_viewport
= [mod
.name
for mod
in object.modifiers
if \
516 for mod
in object.modifiers
:
517 if mod
.type != 'MIRROR':
518 mod
.show_viewport
= False
521 mesh_mod
= object.to_mesh(scene
, True, 'PREVIEW')
522 bm_mod
.from_mesh(mesh_mod
)
523 bpy
.context
.blend_data
.meshes
.remove(mesh_mod
)
524 # re-enable other modifiers
525 for mod_name
in show_viewport
:
526 object.modifiers
[mod_name
].show_viewport
= True
527 # no mirror modifiers, so no derived mesh necessary
532 bm_mod
.verts
.ensure_lookup_table()
533 bm_mod
.edges
.ensure_lookup_table()
534 bm_mod
.faces
.ensure_lookup_table()
536 return(derived
, bm_mod
)
539 # return a mapping of derived indices to indices
540 def get_mapping(derived
, bm
, bm_mod
, single_vertices
, full_search
, loops
):
545 verts
= [v
for v
in bm
.verts
if not v
.hide
]
547 verts
= [v
for v
in bm
.verts
if v
.select
and not v
.hide
]
549 # non-selected vertices around single vertices also need to be mapped
551 mapping
= dict([[vert
, -1] for vert
in single_vertices
])
552 verts_mod
= [bm_mod
.verts
[vert
] for vert
in single_vertices
]
554 for v_mod
in verts_mod
:
555 if (v
.co
- v_mod
.co
).length
< 1e-6:
556 mapping
[v_mod
.index
] = v
.index
558 real_singles
= [v_real
for v_real
in mapping
.values() if v_real
>-1]
560 verts_indices
= [vert
.index
for vert
in verts
]
561 for face
in [face
for face
in bm
.faces
if not face
.select \
563 for vert
in face
.verts
:
564 if vert
.index
in real_singles
:
566 if not v
.index
in verts_indices
:
571 # create mapping of derived indices to indices
572 mapping
= dict([[vert
, -1] for loop
in loops
for vert
in loop
[0]])
574 for single
in single_vertices
:
576 verts_mod
= [bm_mod
.verts
[i
] for i
in mapping
.keys()]
578 for v_mod
in verts_mod
:
579 if (v
.co
- v_mod
.co
).length
< 1e-6:
580 mapping
[v_mod
.index
] = v
.index
581 verts_mod
.remove(v_mod
)
587 # calculate the determinant of a matrix
588 def matrix_determinant(m
):
589 determinant
= m
[0][0] * m
[1][1] * m
[2][2] + m
[0][1] * m
[1][2] * m
[2][0] \
590 + m
[0][2] * m
[1][0] * m
[2][1] - m
[0][2] * m
[1][1] * m
[2][0] \
591 - m
[0][1] * m
[1][0] * m
[2][2] - m
[0][0] * m
[1][2] * m
[2][1]
596 # custom matrix inversion, to provide higher precision than the built-in one
597 def matrix_invert(m
):
598 r
= mathutils
.Matrix((
599 (m
[1][1]*m
[2][2] - m
[1][2]*m
[2][1], m
[0][2]*m
[2][1] - m
[0][1]*m
[2][2],
600 m
[0][1]*m
[1][2] - m
[0][2]*m
[1][1]),
601 (m
[1][2]*m
[2][0] - m
[1][0]*m
[2][2], m
[0][0]*m
[2][2] - m
[0][2]*m
[2][0],
602 m
[0][2]*m
[1][0] - m
[0][0]*m
[1][2]),
603 (m
[1][0]*m
[2][1] - m
[1][1]*m
[2][0], m
[0][1]*m
[2][0] - m
[0][0]*m
[2][1],
604 m
[0][0]*m
[1][1] - m
[0][1]*m
[1][0])))
606 return (r
* (1 / matrix_determinant(m
)))
609 # returns a list of all loops parallel to the input, input included
610 def get_parallel_loops(bm_mod
, loops
):
611 # get required dictionaries
612 edge_faces
= dict_edge_faces(bm_mod
)
613 connected_faces
= dict_face_faces(bm_mod
, edge_faces
)
614 # turn vertex loops into edge loops
617 edgeloop
= [[sorted([loop
[0][i
], loop
[0][i
+1]]) for i
in \
618 range(len(loop
[0])-1)], loop
[1]]
619 if loop
[1]: # circular
620 edgeloop
[0].append(sorted([loop
[0][-1], loop
[0][0]]))
621 edgeloops
.append(edgeloop
[:])
622 # variables to keep track while iterating
626 for loop
in edgeloops
:
627 # initialise with original loop
628 all_edgeloops
.append(loop
[0])
632 if edge
[0] not in verts_used
:
633 verts_used
.append(edge
[0])
634 if edge
[1] not in verts_used
:
635 verts_used
.append(edge
[1])
637 # find parallel loops
638 while len(newloops
) > 0:
641 for i
in newloops
[-1]:
643 forbidden_side
= False
644 if not i
in edge_faces
:
645 # weird input with branches
648 for face
in edge_faces
[i
]:
649 if len(side_a
) == 0 and forbidden_side
!= "a":
655 elif side_a
[-1] in connected_faces
[face
] and \
656 forbidden_side
!= "a":
662 if len(side_b
) == 0 and forbidden_side
!= "b":
668 elif side_b
[-1] in connected_faces
[face
] and \
669 forbidden_side
!= "b":
677 # weird input with branches
690 for key
in face_edgekeys(bm_mod
.faces
[fi
]):
691 if key
[0] not in verts_used
and key
[1] not in \
693 extraloop
.append(key
)
696 for key
in extraloop
:
698 if new_vert
not in verts_used
:
699 verts_used
.append(new_vert
)
700 newloops
.append(extraloop
)
701 all_edgeloops
.append(extraloop
)
703 # input contains branches, only return selected loop
707 # change edgeloops into normal loops
709 for edgeloop
in all_edgeloops
:
711 # grow loop by comparing vertices between consecutive edge-keys
712 for i
in range(len(edgeloop
)-1):
713 for vert
in range(2):
714 if edgeloop
[i
][vert
] in edgeloop
[i
+1]:
715 loop
.append(edgeloop
[i
][vert
])
718 # add starting vertex
719 for vert
in range(2):
720 if edgeloop
[0][vert
] != loop
[0]:
721 loop
= [edgeloop
[0][vert
]] + loop
724 for vert
in range(2):
725 if edgeloop
[-1][vert
] != loop
[-1]:
726 loop
.append(edgeloop
[-1][vert
])
728 # check if loop is circular
729 if loop
[0] == loop
[-1]:
734 loops
.append([loop
, circular
])
739 # gather initial data
741 global_undo
= bpy
.context
.user_preferences
.edit
.use_global_undo
742 bpy
.context
.user_preferences
.edit
.use_global_undo
= False
743 object = bpy
.context
.active_object
744 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
745 # ensure that selection is synced for the derived mesh
746 bpy
.ops
.object.mode_set(mode
='OBJECT')
747 bpy
.ops
.object.mode_set(mode
='EDIT')
748 bm
= bmesh
.from_edit_mesh(object.data
)
750 bm
.verts
.ensure_lookup_table()
751 bm
.edges
.ensure_lookup_table()
752 bm
.faces
.ensure_lookup_table()
754 return(global_undo
, object, bm
)
757 # move the vertices to their new locations
758 def move_verts(object, bm
, mapping
, move
, lock
, influence
):
760 lock_x
, lock_y
, lock_z
= lock
761 orientation
= bpy
.context
.space_data
.transform_orientation
762 custom
= bpy
.context
.space_data
.current_orientation
764 mat
= custom
.matrix
.to_4x4().inverted() * object.matrix_world
.copy()
765 elif orientation
== 'LOCAL':
766 mat
= mathutils
.Matrix
.Identity(4)
767 elif orientation
== 'VIEW':
768 mat
= bpy
.context
.region_data
.view_matrix
.copy() * \
769 object.matrix_world
.copy()
770 else: # orientation == 'GLOBAL'
771 mat
= object.matrix_world
.copy()
772 mat_inv
= mat
.inverted()
775 for index
, loc
in loop
:
777 if mapping
[index
] == -1:
780 index
= mapping
[index
]
782 delta
= (loc
- bm
.verts
[index
].co
) * mat_inv
790 loc
= bm
.verts
[index
].co
+ delta
794 new_loc
= loc
*(influence
/100) + \
795 bm
.verts
[index
].co
*((100-influence
)/100)
796 bm
.verts
[index
].co
= new_loc
800 bm
.verts
.ensure_lookup_table()
801 bm
.edges
.ensure_lookup_table()
802 bm
.faces
.ensure_lookup_table()
805 # load custom tool settings
806 def settings_load(self
):
807 lt
= bpy
.context
.window_manager
.looptools
808 tool
= self
.name
.split()[0].lower()
809 keys
= self
.as_keywords().keys()
811 setattr(self
, key
, getattr(lt
, tool
+ "_" + key
))
814 # store custom tool settings
815 def settings_write(self
):
816 lt
= bpy
.context
.window_manager
.looptools
817 tool
= self
.name
.split()[0].lower()
818 keys
= self
.as_keywords().keys()
820 setattr(lt
, tool
+ "_" + key
, getattr(self
, key
))
823 # clean up and set settings back to original state
824 def terminate(global_undo
):
825 # update editmesh cached data
826 obj
= bpy
.context
.active_object
827 if obj
.mode
== 'EDIT':
828 bmesh
.update_edit_mesh(obj
.data
, tessface
=True, destructive
=True)
830 bpy
.context
.user_preferences
.edit
.use_global_undo
= global_undo
833 ##########################################
834 ####### Bridge functions #################
835 ##########################################
837 # calculate a cubic spline through the middle section of 4 given coordinates
838 def bridge_calculate_cubic_spline(bm
, coordinates
):
844 for i
in coordinates
:
845 a
.append(float(i
[j
]))
848 h
.append(x
[i
+1]-x
[i
])
851 q
.append(3.0/h
[i
]*(a
[i
+1]-a
[i
])-3.0/h
[i
-1]*(a
[i
]-a
[i
-1]))
856 l
.append(2.0*(x
[i
+1]-x
[i
-1])-h
[i
-1]*u
[i
-1])
858 z
.append((q
[i
]-h
[i
-1]*z
[i
-1])/l
[i
])
861 b
= [False for i
in range(3)]
862 c
= [False for i
in range(4)]
863 d
= [False for i
in range(3)]
865 for i
in range(2,-1,-1):
866 c
[i
] = z
[i
]-u
[i
]*c
[i
+1]
867 b
[i
] = (a
[i
+1]-a
[i
])/h
[i
]-h
[i
]*(c
[i
+1]+2.0*c
[i
])/3.0
868 d
[i
] = (c
[i
+1]-c
[i
])/(3.0*h
[i
])
870 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
871 spline
= [result
[1], result
[4], result
[7]]
876 # return a list with new vertex location vectors, a list with face vertex
877 # integers, and the highest vertex integer in the virtual mesh
878 def bridge_calculate_geometry(bm
, lines
, vertex_normals
, segments
,
879 interpolation
, cubic_strength
, min_width
, max_vert_index
):
883 # calculate location based on interpolation method
884 def get_location(line
, segment
, splines
):
885 v1
= bm
.verts
[lines
[line
][0]].co
886 v2
= bm
.verts
[lines
[line
][1]].co
887 if interpolation
== 'linear':
888 return v1
+ (segment
/segments
) * (v2
-v1
)
889 else: # interpolation == 'cubic'
890 m
= (segment
/segments
)
891 ax
,bx
,cx
,dx
,tx
= splines
[line
][0]
892 x
= ax
+bx
*m
+cx
*m
**2+dx
*m
**3
893 ay
,by
,cy
,dy
,ty
= splines
[line
][1]
894 y
= ay
+by
*m
+cy
*m
**2+dy
*m
**3
895 az
,bz
,cz
,dz
,tz
= splines
[line
][2]
896 z
= az
+bz
*m
+cz
*m
**2+dz
*m
**3
897 return mathutils
.Vector((x
, y
, z
))
899 # no interpolation needed
901 for i
, line
in enumerate(lines
):
903 faces
.append([line
[0], lines
[i
+1][0], lines
[i
+1][1], line
[1]])
904 # more than 1 segment, interpolate
906 # calculate splines (if necessary) once, so no recalculations needed
907 if interpolation
== 'cubic':
910 v1
= bm
.verts
[line
[0]].co
911 v2
= bm
.verts
[line
[1]].co
912 size
= (v2
-v1
).length
* cubic_strength
913 splines
.append(bridge_calculate_cubic_spline(bm
,
914 [v1
+size
*vertex_normals
[line
[0]], v1
, v2
,
915 v2
+size
*vertex_normals
[line
[1]]]))
919 # create starting situation
920 virtual_width
= [(bm
.verts
[lines
[i
][0]].co
-
921 bm
.verts
[lines
[i
+1][0]].co
).length
for i
922 in range(len(lines
)-1)]
923 new_verts
= [get_location(0, seg
, splines
) for seg
in range(1,
925 first_line_indices
= [i
for i
in range(max_vert_index
+1,
926 max_vert_index
+segments
)]
928 prev_verts
= new_verts
[:] # vertex locations of verts on previous line
929 prev_vert_indices
= first_line_indices
[:]
930 max_vert_index
+= segments
- 1 # highest vertex index in virtual mesh
931 next_verts
= [] # vertex locations of verts on current line
932 next_vert_indices
= []
934 for i
, line
in enumerate(lines
):
939 for seg
in range(1, segments
):
940 loc1
= prev_verts
[seg
-1]
941 loc2
= get_location(i
+1, seg
, splines
)
942 if (loc1
-loc2
).length
< (min_width
/100)*virtual_width
[i
] \
943 and line
[1]==lines
[i
+1][1]:
944 # triangle, no new vertex
945 faces
.append([v1
, v2
, prev_vert_indices
[seg
-1],
946 prev_vert_indices
[seg
-1]])
947 next_verts
+= prev_verts
[seg
-1:]
948 next_vert_indices
+= prev_vert_indices
[seg
-1:]
952 if i
== len(lines
)-2 and lines
[0] == lines
[-1]:
953 # quad with first line, no new vertex
954 faces
.append([v1
, v2
, first_line_indices
[seg
-1],
955 prev_vert_indices
[seg
-1]])
956 v2
= first_line_indices
[seg
-1]
957 v1
= prev_vert_indices
[seg
-1]
959 # quad, add new vertex
961 faces
.append([v1
, v2
, max_vert_index
,
962 prev_vert_indices
[seg
-1]])
964 v1
= prev_vert_indices
[seg
-1]
965 new_verts
.append(loc2
)
966 next_verts
.append(loc2
)
967 next_vert_indices
.append(max_vert_index
)
969 faces
.append([v1
, v2
, lines
[i
+1][1], line
[1]])
971 prev_verts
= next_verts
[:]
972 prev_vert_indices
= next_vert_indices
[:]
974 next_vert_indices
= []
976 return(new_verts
, faces
, max_vert_index
)
979 # calculate lines (list of lists, vertex indices) that are used for bridging
980 def bridge_calculate_lines(bm
, loops
, mode
, twist
, reverse
):
982 loop1
, loop2
= [i
[0] for i
in loops
]
983 loop1_circular
, loop2_circular
= [i
[1] for i
in loops
]
984 circular
= loop1_circular
or loop2_circular
987 # calculate loop centers
989 for loop
in [loop1
, loop2
]:
990 center
= mathutils
.Vector()
992 center
+= bm
.verts
[vertex
].co
994 centers
.append(center
)
995 for i
, loop
in enumerate([loop1
, loop2
]):
997 if bm
.verts
[vertex
].co
== centers
[i
]:
998 # prevent zero-length vectors in angle comparisons
999 centers
[i
] += mathutils
.Vector((0.01, 0, 0))
1001 center1
, center2
= centers
1003 # calculate the normals of the virtual planes that the loops are on
1005 normal_plurity
= False
1006 for i
, loop
in enumerate([loop1
, loop2
]):
1008 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1011 x
, y
, z
= centers
[i
]
1012 for loc
in [bm
.verts
[vertex
].co
for vertex
in loop
]:
1013 mat
[0][0] += (loc
[0]-x
)**2
1014 mat
[1][0] += (loc
[0]-x
)*(loc
[1]-y
)
1015 mat
[2][0] += (loc
[0]-x
)*(loc
[2]-z
)
1016 mat
[0][1] += (loc
[1]-y
)*(loc
[0]-x
)
1017 mat
[1][1] += (loc
[1]-y
)**2
1018 mat
[2][1] += (loc
[1]-y
)*(loc
[2]-z
)
1019 mat
[0][2] += (loc
[2]-z
)*(loc
[0]-x
)
1020 mat
[1][2] += (loc
[2]-z
)*(loc
[1]-y
)
1021 mat
[2][2] += (loc
[2]-z
)**2
1024 if sum(mat
[0]) < 1e-6 or sum(mat
[1]) < 1e-6 or sum(mat
[2]) < 1e-6:
1025 normal_plurity
= True
1029 if sum(mat
[0]) == 0:
1030 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
1031 elif sum(mat
[1]) == 0:
1032 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
1033 elif sum(mat
[2]) == 0:
1034 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
1036 # warning! this is different from .normalize()
1039 vec
= mathutils
.Vector((1.0, 1.0, 1.0))
1040 vec2
= (mat
* vec
)/(mat
* vec
).length
1041 while vec
!= vec2
and iter<itermax
:
1045 if vec2
.length
!= 0:
1047 if vec2
.length
== 0:
1048 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
1050 normals
.append(normal
)
1051 # have plane normals face in the same direction (maximum angle: 90 degrees)
1052 if ((center1
+ normals
[0]) - center2
).length
< \
1053 ((center1
- normals
[0]) - center2
).length
:
1055 if ((center2
+ normals
[1]) - center1
).length
> \
1056 ((center2
- normals
[1]) - center1
).length
:
1059 # rotation matrix, representing the difference between the plane normals
1060 axis
= normals
[0].cross(normals
[1])
1061 axis
= mathutils
.Vector([loc
if abs(loc
) > 1e-8 else 0 for loc
in axis
])
1062 if axis
.angle(mathutils
.Vector((0, 0, 1)), 0) > 1.5707964:
1064 angle
= normals
[0].dot(normals
[1])
1065 rotation_matrix
= mathutils
.Matrix
.Rotation(angle
, 4, axis
)
1067 # if circular, rotate loops so they are aligned
1069 # make sure loop1 is the circular one (or both are circular)
1070 if loop2_circular
and not loop1_circular
:
1071 loop1_circular
, loop2_circular
= True, False
1072 loop1
, loop2
= loop2
, loop1
1074 # match start vertex of loop1 with loop2
1075 target_vector
= bm
.verts
[loop2
[0]].co
- center2
1076 dif_angles
= [[(rotation_matrix
* (bm
.verts
[vertex
].co
- center1
)
1077 ).angle(target_vector
, 0), False, i
] for
1078 i
, vertex
in enumerate(loop1
)]
1080 if len(loop1
) != len(loop2
):
1081 angle_limit
= dif_angles
[0][0] * 1.2 # 20% margin
1082 dif_angles
= [[(bm
.verts
[loop2
[0]].co
- \
1083 bm
.verts
[loop1
[index
]].co
).length
, angle
, index
] for \
1084 angle
, distance
, index
in dif_angles
if angle
<= angle_limit
]
1086 loop1
= loop1
[dif_angles
[0][2]:] + loop1
[:dif_angles
[0][2]]
1088 # have both loops face the same way
1089 if normal_plurity
and not circular
:
1090 second_to_first
, second_to_second
, second_to_last
= \
1091 [(bm
.verts
[loop1
[1]].co
- center1
).\
1092 angle(bm
.verts
[loop2
[i
]].co
- center2
) for i
in [0, 1, -1]]
1093 last_to_first
, last_to_second
= [(bm
.verts
[loop1
[-1]].co
- \
1094 center1
).angle(bm
.verts
[loop2
[i
]].co
- center2
) for \
1096 if (min(last_to_first
, last_to_second
)*1.1 < min(second_to_first
, \
1097 second_to_second
)) or (loop2_circular
and second_to_last
*1.1 < \
1098 min(second_to_first
, second_to_second
)):
1101 loop1
= [loop1
[-1]] + loop1
[:-1]
1103 angle
= (bm
.verts
[loop1
[0]].co
- center1
).\
1104 cross(bm
.verts
[loop1
[1]].co
- center1
).angle(normals
[0], 0)
1105 target_angle
= (bm
.verts
[loop2
[0]].co
- center2
).\
1106 cross(bm
.verts
[loop2
[1]].co
- center2
).angle(normals
[1], 0)
1107 limit
= 1.5707964 # 0.5*pi, 90 degrees
1108 if not ((angle
> limit
and target_angle
> limit
) or \
1109 (angle
< limit
and target_angle
< limit
)):
1112 loop1
= [loop1
[-1]] + loop1
[:-1]
1113 elif normals
[0].angle(normals
[1]) > limit
:
1116 loop1
= [loop1
[-1]] + loop1
[:-1]
1118 # both loops have the same length
1119 if len(loop1
) == len(loop2
):
1122 if abs(twist
) < len(loop1
):
1123 loop1
= loop1
[twist
:]+loop1
[:twist
]
1127 lines
.append([loop1
[0], loop2
[0]])
1128 for i
in range(1, len(loop1
)):
1129 lines
.append([loop1
[i
], loop2
[i
]])
1131 # loops of different lengths
1133 # make loop1 longest loop
1134 if len(loop2
) > len(loop1
):
1135 loop1
, loop2
= loop2
, loop1
1136 loop1_circular
, loop2_circular
= loop2_circular
, loop1_circular
1140 if abs(twist
) < len(loop1
):
1141 loop1
= loop1
[twist
:]+loop1
[:twist
]
1145 # shortest angle difference doesn't always give correct start vertex
1146 if loop1_circular
and not loop2_circular
:
1149 if len(loop1
) - shifting
< len(loop2
):
1152 to_last
, to_first
= [(rotation_matrix
*
1153 (bm
.verts
[loop1
[-1]].co
- center1
)).angle((bm
.\
1154 verts
[loop2
[i
]].co
- center2
), 0) for i
in [-1, 0]]
1155 if to_first
< to_last
:
1156 loop1
= [loop1
[-1]] + loop1
[:-1]
1162 # basic shortest side first
1164 lines
.append([loop1
[0], loop2
[0]])
1165 for i
in range(1, len(loop1
)):
1166 if i
>= len(loop2
) - 1:
1168 lines
.append([loop1
[i
], loop2
[-1]])
1171 lines
.append([loop1
[i
], loop2
[i
]])
1173 # shortest edge algorithm
1174 else: # mode == 'shortest'
1175 lines
.append([loop1
[0], loop2
[0]])
1177 for i
in range(len(loop1
) -1):
1178 if prev_vert2
== len(loop2
) - 1 and not loop2_circular
:
1179 # force triangles, reached end of loop2
1181 elif prev_vert2
== len(loop2
) - 1 and loop2_circular
:
1182 # at end of loop2, but circular, so check with first vert
1183 tri
, quad
= [(bm
.verts
[loop1
[i
+1]].co
-
1184 bm
.verts
[loop2
[j
]].co
).length
1185 for j
in [prev_vert2
, 0]]
1187 elif len(loop1
) - 1 - i
== len(loop2
) - 1 - prev_vert2
and \
1189 # force quads, otherwise won't make it to end of loop2
1192 # calculate if tri or quad gives shortest edge
1193 tri
, quad
= [(bm
.verts
[loop1
[i
+1]].co
-
1194 bm
.verts
[loop2
[j
]].co
).length
1195 for j
in range(prev_vert2
, prev_vert2
+2)]
1199 lines
.append([loop1
[i
+1], loop2
[prev_vert2
]])
1200 if circle_full
== 2:
1203 elif not circle_full
:
1204 lines
.append([loop1
[i
+1], loop2
[prev_vert2
+1]])
1206 # quad to first vertex of loop2
1208 lines
.append([loop1
[i
+1], loop2
[0]])
1212 # final face for circular loops
1213 if loop1_circular
and loop2_circular
:
1214 lines
.append([loop1
[0], loop2
[0]])
1219 # calculate number of segments needed
1220 def bridge_calculate_segments(bm
, lines
, loops
, segments
):
1221 # return if amount of segments is set by user
1226 average_edge_length
= [(bm
.verts
[vertex
].co
- \
1227 bm
.verts
[loop
[0][i
+1]].co
).length
for loop
in loops
for \
1228 i
, vertex
in enumerate(loop
[0][:-1])]
1229 # closing edges of circular loops
1230 average_edge_length
+= [(bm
.verts
[loop
[0][-1]].co
- \
1231 bm
.verts
[loop
[0][0]].co
).length
for loop
in loops
if loop
[1]]
1234 average_edge_length
= sum(average_edge_length
) / len(average_edge_length
)
1235 average_bridge_length
= sum([(bm
.verts
[v1
].co
- \
1236 bm
.verts
[v2
].co
).length
for v1
, v2
in lines
]) / len(lines
)
1238 segments
= max(1, round(average_bridge_length
/ average_edge_length
))
1243 # return dictionary with vertex index as key, and the normal vector as value
1244 def bridge_calculate_virtual_vertex_normals(bm
, lines
, loops
, edge_faces
,
1246 if not edge_faces
: # interpolation isn't set to cubic
1249 # pity reduce() isn't one of the basic functions in python anymore
1250 def average_vector_dictionary(dic
):
1251 for key
, vectors
in dic
.items():
1252 #if type(vectors) == type([]) and len(vectors) > 1:
1253 if len(vectors
) > 1:
1254 average
= mathutils
.Vector()
1255 for vector
in vectors
:
1257 average
/= len(vectors
)
1258 dic
[key
] = [average
]
1261 # get all edges of the loop
1262 edges
= [[edgekey_to_edge
[tuple(sorted([loops
[j
][0][i
],
1263 loops
[j
][0][i
+1]]))] for i
in range(len(loops
[j
][0])-1)] for \
1265 edges
= edges
[0] + edges
[1]
1267 if loops
[j
][1]: # circular
1268 edges
.append(edgekey_to_edge
[tuple(sorted([loops
[j
][0][0],
1269 loops
[j
][0][-1]]))])
1272 calculation based on face topology (assign edge-normals to vertices)
1274 edge_normal = face_normal x edge_vector
1275 vertex_normal = average(edge_normals)
1277 vertex_normals
= dict([(vertex
, []) for vertex
in loops
[0][0]+loops
[1][0]])
1279 faces
= edge_faces
[edgekey(edge
)] # valid faces connected to edge
1282 # get edge coordinates
1283 v1
, v2
= [bm
.verts
[edgekey(edge
)[i
]].co
for i
in [0,1]]
1284 edge_vector
= v1
- v2
1285 if edge_vector
.length
< 1e-4:
1286 # zero-length edge, vertices at same location
1288 edge_center
= (v1
+ v2
) / 2
1290 # average face coordinates, if connected to more than 1 valid face
1292 face_normal
= mathutils
.Vector()
1293 face_center
= mathutils
.Vector()
1295 face_normal
+= face
.normal
1296 face_center
+= face
.calc_center_median()
1297 face_normal
/= len(faces
)
1298 face_center
/= len(faces
)
1300 face_normal
= faces
[0].normal
1301 face_center
= faces
[0].calc_center_median()
1302 if face_normal
.length
< 1e-4:
1303 # faces with a surface of 0 have no face normal
1306 # calculate virtual edge normal
1307 edge_normal
= edge_vector
.cross(face_normal
)
1308 edge_normal
.length
= 0.01
1309 if (face_center
- (edge_center
+ edge_normal
)).length
> \
1310 (face_center
- (edge_center
- edge_normal
)).length
:
1311 # make normal face the correct way
1312 edge_normal
.negate()
1313 edge_normal
.normalize()
1314 # add virtual edge normal as entry for both vertices it connects
1315 for vertex
in edgekey(edge
):
1316 vertex_normals
[vertex
].append(edge_normal
)
1319 calculation based on connection with other loop (vertex focused method)
1320 - used for vertices that aren't connected to any valid faces
1322 plane_normal = edge_vector x connection_vector
1323 vertex_normal = plane_normal x edge_vector
1325 vertices
= [vertex
for vertex
, normal
in vertex_normals
.items() if not \
1329 # edge vectors connected to vertices
1330 edge_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1332 for v
in edgekey(edge
):
1333 if v
in edge_vectors
:
1334 edge_vector
= bm
.verts
[edgekey(edge
)[0]].co
- \
1335 bm
.verts
[edgekey(edge
)[1]].co
1336 if edge_vector
.length
< 1e-4:
1337 # zero-length edge, vertices at same location
1339 edge_vectors
[v
].append(edge_vector
)
1341 # connection vectors between vertices of both loops
1342 connection_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1343 connections
= dict([[vertex
, []] for vertex
in vertices
])
1344 for v1
, v2
in lines
:
1345 if v1
in connection_vectors
or v2
in connection_vectors
:
1346 new_vector
= bm
.verts
[v1
].co
- bm
.verts
[v2
].co
1347 if new_vector
.length
< 1e-4:
1348 # zero-length connection vector,
1349 # vertices in different loops at same location
1351 if v1
in connection_vectors
:
1352 connection_vectors
[v1
].append(new_vector
)
1353 connections
[v1
].append(v2
)
1354 if v2
in connection_vectors
:
1355 connection_vectors
[v2
].append(new_vector
)
1356 connections
[v2
].append(v1
)
1357 connection_vectors
= average_vector_dictionary(connection_vectors
)
1358 connection_vectors
= dict([[vertex
, vector
[0]] if vector
else \
1359 [vertex
, []] for vertex
, vector
in connection_vectors
.items()])
1361 for vertex
, values
in edge_vectors
.items():
1362 # vertex normal doesn't matter, just assign a random vector to it
1363 if not connection_vectors
[vertex
]:
1364 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1367 # calculate to what location the vertex is connected,
1368 # used to determine what way to flip the normal
1369 connected_center
= mathutils
.Vector()
1370 for v
in connections
[vertex
]:
1371 connected_center
+= bm
.verts
[v
].co
1372 if len(connections
[vertex
]) > 1:
1373 connected_center
/= len(connections
[vertex
])
1374 if len(connections
[vertex
]) == 0:
1375 # shouldn't be possible, but better safe than sorry
1376 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1379 # can't do proper calculations, because of zero-length vector
1381 if (connected_center
- (bm
.verts
[vertex
].co
+ \
1382 connection_vectors
[vertex
])).length
< (connected_center
- \
1383 (bm
.verts
[vertex
].co
- connection_vectors
[vertex
])).\
1385 connection_vectors
[vertex
].negate()
1386 vertex_normals
[vertex
] = [connection_vectors
[vertex
].\
1390 # calculate vertex normals using edge-vectors,
1391 # connection-vectors and the derived plane normal
1392 for edge_vector
in values
:
1393 plane_normal
= edge_vector
.cross(connection_vectors
[vertex
])
1394 vertex_normal
= edge_vector
.cross(plane_normal
)
1395 vertex_normal
.length
= 0.1
1396 if (connected_center
- (bm
.verts
[vertex
].co
+ \
1397 vertex_normal
)).length
< (connected_center
- \
1398 (bm
.verts
[vertex
].co
- vertex_normal
)).length
:
1399 # make normal face the correct way
1400 vertex_normal
.negate()
1401 vertex_normal
.normalize()
1402 vertex_normals
[vertex
].append(vertex_normal
)
1404 # average virtual vertex normals, based on all edges it's connected to
1405 vertex_normals
= average_vector_dictionary(vertex_normals
)
1406 vertex_normals
= dict([[vertex
, vector
[0]] for vertex
, vector
in \
1407 vertex_normals
.items()])
1409 return(vertex_normals
)
1412 # add vertices to mesh
1413 def bridge_create_vertices(bm
, vertices
):
1414 for i
in range(len(vertices
)):
1415 bm
.verts
.new(vertices
[i
])
1416 bm
.verts
.ensure_lookup_table()
1420 def bridge_create_faces(object, bm
, faces
, twist
):
1421 # have the normal point the correct way
1423 [face
.reverse() for face
in faces
]
1424 faces
= [face
[2:]+face
[:2] if face
[0]==face
[1] else face
for \
1427 # eekadoodle prevention
1428 for i
in range(len(faces
)):
1429 if not faces
[i
][-1]:
1430 if faces
[i
][0] == faces
[i
][-1]:
1431 faces
[i
] = [faces
[i
][1], faces
[i
][2], faces
[i
][3], faces
[i
][1]]
1433 faces
[i
] = [faces
[i
][-1]] + faces
[i
][:-1]
1434 # result of converting from pre-bmesh period
1435 if faces
[i
][-1] == faces
[i
][-2]:
1436 faces
[i
] = faces
[i
][:-1]
1439 for i
in range(len(faces
)):
1440 new_faces
.append(bm
.faces
.new([bm
.verts
[v
] for v
in faces
[i
]]))
1442 object.data
.update(calc_edges
=True) # calc_edges prevents memory-corruption
1444 bm
.verts
.ensure_lookup_table()
1445 bm
.edges
.ensure_lookup_table()
1446 bm
.faces
.ensure_lookup_table()
1451 # calculate input loops
1452 def bridge_get_input(bm
):
1453 # create list of internal edges, which should be skipped
1454 eks_of_selected_faces
= [item
for sublist
in [face_edgekeys(face
) for \
1455 face
in bm
.faces
if face
.select
and not face
.hide
] for item
in sublist
]
1457 for ek
in eks_of_selected_faces
:
1458 if ek
in edge_count
:
1462 internal_edges
= [ek
for ek
in edge_count
if edge_count
[ek
] > 1]
1464 # sort correct edges into loops
1465 selected_edges
= [edgekey(edge
) for edge
in bm
.edges
if edge
.select \
1466 and not edge
.hide
and edgekey(edge
) not in internal_edges
]
1467 loops
= get_connected_selections(selected_edges
)
1472 # return values needed by the bridge operator
1473 def bridge_initialise(bm
, interpolation
):
1474 if interpolation
== 'cubic':
1475 # dict with edge-key as key and list of connected valid faces as value
1476 face_blacklist
= [face
.index
for face
in bm
.faces
if face
.select
or \
1478 edge_faces
= dict([[edgekey(edge
), []] for edge
in bm
.edges
if not \
1480 for face
in bm
.faces
:
1481 if face
.index
in face_blacklist
:
1483 for key
in face_edgekeys(face
):
1484 edge_faces
[key
].append(face
)
1485 # dictionary with the edge-key as key and edge as value
1486 edgekey_to_edge
= dict([[edgekey(edge
), edge
] for edge
in \
1487 bm
.edges
if edge
.select
and not edge
.hide
])
1490 edgekey_to_edge
= False
1492 # selected faces input
1493 old_selected_faces
= [face
.index
for face
in bm
.faces
if face
.select \
1496 # find out if faces created by bridging should be smoothed
1499 if sum([face
.smooth
for face
in bm
.faces
])/len(bm
.faces
) \
1503 return(edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
)
1506 # return a string with the input method
1507 def bridge_input_method(loft
, loft_loop
):
1511 method
= "Loft loop"
1513 method
= "Loft no-loop"
1520 # match up loops in pairs, used for multi-input bridging
1521 def bridge_match_loops(bm
, loops
):
1522 # calculate average loop normals and centers
1525 for vertices
, circular
in loops
:
1526 normal
= mathutils
.Vector()
1527 center
= mathutils
.Vector()
1528 for vertex
in vertices
:
1529 normal
+= bm
.verts
[vertex
].normal
1530 center
+= bm
.verts
[vertex
].co
1531 normals
.append(normal
/ len(vertices
) / 10)
1532 centers
.append(center
/ len(vertices
))
1534 # possible matches if loop normals are faced towards the center
1536 matches
= dict([[i
, []] for i
in range(len(loops
))])
1538 for i
in range(len(loops
) + 1):
1539 for j
in range(i
+1, len(loops
)):
1540 if (centers
[i
] - centers
[j
]).length
> (centers
[i
] - (centers
[j
] \
1541 + normals
[j
])).length
and (centers
[j
] - centers
[i
]).length
> \
1542 (centers
[j
] - (centers
[i
] + normals
[i
])).length
:
1544 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1545 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1546 # if no loops face each other, just make matches between all the loops
1547 if matches_amount
== 0:
1548 for i
in range(len(loops
) + 1):
1549 for j
in range(i
+1, len(loops
)):
1550 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1551 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1552 for key
, value
in matches
.items():
1555 # matches based on distance between centers and number of vertices in loops
1557 for loop_index
in range(len(loops
)):
1558 if loop_index
in new_order
:
1560 loop_matches
= matches
[loop_index
]
1561 if not loop_matches
:
1563 shortest_distance
= loop_matches
[0][0]
1564 shortest_distance
*= 1.1
1565 loop_matches
= [[abs(len(loops
[loop_index
][0]) - \
1566 len(loops
[loop
[2]][0])), loop
[0], loop
[1], loop
[2]] for loop
in \
1567 loop_matches
if loop
[0] < shortest_distance
]
1569 for match
in loop_matches
:
1570 if match
[3] not in new_order
:
1571 new_order
+= [loop_index
, match
[3]]
1574 # reorder loops based on matches
1575 if len(new_order
) >= 2:
1576 loops
= [loops
[i
] for i
in new_order
]
1581 # remove old_selected_faces
1582 def bridge_remove_internal_faces(bm
, old_selected_faces
):
1583 # collect bmesh faces and internal bmesh edges
1584 remove_faces
= [bm
.faces
[face
] for face
in old_selected_faces
]
1585 edges
= collections
.Counter([edge
.index
for face
in remove_faces
for \
1586 edge
in face
.edges
])
1587 remove_edges
= [bm
.edges
[edge
] for edge
in edges
if edges
[edge
] > 1]
1589 # remove internal faces and edges
1590 for face
in remove_faces
:
1591 bm
.faces
.remove(face
)
1592 for edge
in remove_edges
:
1593 bm
.edges
.remove(edge
)
1595 bm
.faces
.ensure_lookup_table()
1596 bm
.edges
.ensure_lookup_table()
1597 bm
.verts
.ensure_lookup_table()
1600 # update list of internal faces that are flagged for removal
1601 def bridge_save_unused_faces(bm
, old_selected_faces
, loops
):
1602 # key: vertex index, value: lists of selected faces using it
1604 vertex_to_face
= dict([[i
, []] for i
in range(len(bm
.verts
))])
1605 [[vertex_to_face
[vertex
.index
].append(face
) for vertex
in \
1606 bm
.faces
[face
].verts
] for face
in old_selected_faces
]
1608 # group selected faces that are connected
1611 for face
in old_selected_faces
:
1612 if face
in grouped_faces
:
1614 grouped_faces
.append(face
)
1618 grow_face
= new_faces
[0]
1619 for vertex
in bm
.faces
[grow_face
].verts
:
1620 vertex_face_group
= [face
for face
in vertex_to_face
[\
1621 vertex
.index
] if face
not in grouped_faces
]
1622 new_faces
+= vertex_face_group
1623 grouped_faces
+= vertex_face_group
1624 group
+= vertex_face_group
1626 groups
.append(group
)
1628 # key: vertex index, value: True/False (is it in a loop that is used)
1629 used_vertices
= dict([[i
, 0] for i
in range(len(bm
.verts
))])
1631 for vertex
in loop
[0]:
1632 used_vertices
[vertex
] = True
1634 # check if group is bridged, if not remove faces from internal faces list
1635 for group
in groups
:
1640 for vertex
in bm
.faces
[face
].verts
:
1641 if used_vertices
[vertex
.index
]:
1646 old_selected_faces
.remove(face
)
1649 # add the newly created faces to the selection
1650 def bridge_select_new_faces(new_faces
, smooth
):
1651 for face
in new_faces
:
1652 face
.select_set(True)
1653 face
.smooth
= smooth
1656 # sort loops, so they are connected in the correct order when lofting
1657 def bridge_sort_loops(bm
, loops
, loft_loop
):
1658 # simplify loops to single points, and prepare for pathfinding
1659 x
, y
, z
= [[sum([bm
.verts
[i
].co
[j
] for i
in loop
[0]]) / \
1660 len(loop
[0]) for loop
in loops
] for j
in range(3)]
1661 nodes
= [mathutils
.Vector((x
[i
], y
[i
], z
[i
])) for i
in range(len(loops
))]
1664 open = [i
for i
in range(1, len(loops
))]
1666 # connect node to path, that is shortest to active_node
1667 while len(open) > 0:
1668 distances
= [(nodes
[active_node
] - nodes
[i
]).length
for i
in open]
1669 active_node
= open[distances
.index(min(distances
))]
1670 open.remove(active_node
)
1671 path
.append([active_node
, min(distances
)])
1672 # check if we didn't start in the middle of the path
1673 for i
in range(2, len(path
)):
1674 if (nodes
[path
[i
][0]]-nodes
[0]).length
< path
[i
][1]:
1677 path
= path
[:-i
] + temp
1681 loops
= [loops
[i
[0]] for i
in path
]
1682 # if requested, duplicate first loop at last position, so loft can loop
1684 loops
= loops
+ [loops
[0]]
1689 # remapping old indices to new position in list
1690 def bridge_update_old_selection(bm
, old_selected_faces
):
1691 #old_indices = old_selected_faces[:]
1692 #old_selected_faces = []
1693 #for i, face in enumerate(bm.faces):
1694 # if face.index in old_indices:
1695 # old_selected_faces.append(i)
1697 old_selected_faces
= [i
for i
, face
in enumerate(bm
.faces
) if face
.index \
1698 in old_selected_faces
]
1700 return(old_selected_faces
)
1703 ##########################################
1704 ####### Circle functions #################
1705 ##########################################
1707 # convert 3d coordinates to 2d coordinates on plane
1708 def circle_3d_to_2d(bm_mod
, loop
, com
, normal
):
1709 # project vertices onto the plane
1710 verts
= [bm_mod
.verts
[v
] for v
in loop
[0]]
1711 verts_projected
= [[v
.co
- (v
.co
- com
).dot(normal
) * normal
, v
.index
]
1714 # calculate two vectors (p and q) along the plane
1715 m
= mathutils
.Vector((normal
[0] + 1.0, normal
[1], normal
[2]))
1716 p
= m
- (m
.dot(normal
) * normal
)
1718 m
= mathutils
.Vector((normal
[0], normal
[1] + 1.0, normal
[2]))
1719 p
= m
- (m
.dot(normal
) * normal
)
1722 # change to 2d coordinates using perpendicular projection
1724 for loc
, vert
in verts_projected
:
1726 x
= p
.dot(vloc
) / p
.dot(p
)
1727 y
= q
.dot(vloc
) / q
.dot(q
)
1728 locs_2d
.append([x
, y
, vert
])
1730 return(locs_2d
, p
, q
)
1733 # calculate a best-fit circle to the 2d locations on the plane
1734 def circle_calculate_best_fit(locs_2d
):
1740 # calculate center and radius (non-linear least squares solution)
1741 for iter in range(500):
1745 d
= (v
[0]**2-2.0*x0
*v
[0]+v
[1]**2-2.0*y0
*v
[1]+x0
**2+y0
**2)**0.5
1746 jmat
.append([(x0
-v
[0])/d
, (y0
-v
[1])/d
, -1.0])
1747 k
.append(-(((v
[0]-x0
)**2+(v
[1]-y0
)**2)**0.5-r
))
1748 jmat2
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1752 k2
= mathutils
.Vector((0.0, 0.0, 0.0))
1753 for i
in range(len(jmat
)):
1754 k2
+= mathutils
.Vector(jmat
[i
])*k
[i
]
1755 jmat2
[0][0] += jmat
[i
][0]**2
1756 jmat2
[1][0] += jmat
[i
][0]*jmat
[i
][1]
1757 jmat2
[2][0] += jmat
[i
][0]*jmat
[i
][2]
1758 jmat2
[1][1] += jmat
[i
][1]**2
1759 jmat2
[2][1] += jmat
[i
][1]*jmat
[i
][2]
1760 jmat2
[2][2] += jmat
[i
][2]**2
1761 jmat2
[0][1] = jmat2
[1][0]
1762 jmat2
[0][2] = jmat2
[2][0]
1763 jmat2
[1][2] = jmat2
[2][1]
1768 dx0
, dy0
, dr
= jmat2
* k2
1772 # stop iterating if we're close enough to optimal solution
1773 if abs(dx0
)<1e-6 and abs(dy0
)<1e-6 and abs(dr
)<1e-6:
1776 # return center of circle and radius
1780 # calculate circle so no vertices have to be moved away from the center
1781 def circle_calculate_min_fit(locs_2d
):
1783 x0
= (min([i
[0] for i
in locs_2d
])+max([i
[0] for i
in locs_2d
]))/2.0
1784 y0
= (min([i
[1] for i
in locs_2d
])+max([i
[1] for i
in locs_2d
]))/2.0
1785 center
= mathutils
.Vector([x0
, y0
])
1787 r
= min([(mathutils
.Vector([i
[0], i
[1]])-center
).length
for i
in locs_2d
])
1789 # return center of circle and radius
1793 # calculate the new locations of the vertices that need to be moved
1794 def circle_calculate_verts(flatten
, bm_mod
, locs_2d
, com
, p
, q
, normal
):
1795 # changing 2d coordinates back to 3d coordinates
1798 locs_3d
.append([loc
[2], loc
[0]*p
+ loc
[1]*q
+ com
])
1800 if flatten
: # flat circle
1803 else: # project the locations on the existing mesh
1804 vert_edges
= dict_vert_edges(bm_mod
)
1805 vert_faces
= dict_vert_faces(bm_mod
)
1806 faces
= [f
for f
in bm_mod
.faces
if not f
.hide
]
1807 rays
= [normal
, -normal
]
1811 if bm_mod
.verts
[loc
[0]].co
== loc
[1]: # vertex hasn't moved
1814 dif
= normal
.angle(loc
[1]-bm_mod
.verts
[loc
[0]].co
)
1815 if -1e-6 < dif
< 1e-6 or math
.pi
-1e-6 < dif
< math
.pi
+1e-6:
1816 # original location is already along projection normal
1817 projection
= bm_mod
.verts
[loc
[0]].co
1819 # quick search through adjacent faces
1820 for face
in vert_faces
[loc
[0]]:
1821 verts
= [v
.co
for v
in bm_mod
.faces
[face
].verts
]
1822 if len(verts
) == 3: # triangle
1826 v1
, v2
, v3
, v4
= verts
[:4]
1828 intersect
= mathutils
.geometry
.\
1829 intersect_ray_tri(v1
, v2
, v3
, ray
, loc
[1])
1831 projection
= intersect
1834 intersect
= mathutils
.geometry
.\
1835 intersect_ray_tri(v1
, v3
, v4
, ray
, loc
[1])
1837 projection
= intersect
1842 # check if projection is on adjacent edges
1843 for edgekey
in vert_edges
[loc
[0]]:
1844 line1
= bm_mod
.verts
[edgekey
[0]].co
1845 line2
= bm_mod
.verts
[edgekey
[1]].co
1846 intersect
, dist
= mathutils
.geometry
.intersect_point_line(\
1847 loc
[1], line1
, line2
)
1848 if 1e-6 < dist
< 1 - 1e-6:
1849 projection
= intersect
1852 # full search through the entire mesh
1855 verts
= [v
.co
for v
in face
.verts
]
1856 if len(verts
) == 3: # triangle
1860 v1
, v2
, v3
, v4
= verts
[:4]
1862 intersect
= mathutils
.geometry
.intersect_ray_tri(\
1863 v1
, v2
, v3
, ray
, loc
[1])
1865 hits
.append([(loc
[1] - intersect
).length
,
1869 intersect
= mathutils
.geometry
.intersect_ray_tri(\
1870 v1
, v3
, v4
, ray
, loc
[1])
1872 hits
.append([(loc
[1] - intersect
).length
,
1876 # if more than 1 hit with mesh, closest hit is new loc
1878 projection
= hits
[0][1]
1880 # nothing to project on, remain at flat location
1882 new_locs
.append([loc
[0], projection
])
1884 # return new positions of projected circle
1888 # check loops and only return valid ones
1889 def circle_check_loops(single_loops
, loops
, mapping
, bm_mod
):
1890 valid_single_loops
= {}
1892 for i
, [loop
, circular
] in enumerate(loops
):
1893 # loop needs to have at least 3 vertices
1896 # loop needs at least 1 vertex in the original, non-mirrored mesh
1900 if mapping
[vert
] > -1:
1905 # loop has to be non-collinear
1907 loc0
= mathutils
.Vector(bm_mod
.verts
[loop
[0]].co
[:])
1908 loc1
= mathutils
.Vector(bm_mod
.verts
[loop
[1]].co
[:])
1910 locn
= mathutils
.Vector(bm_mod
.verts
[v
].co
[:])
1911 if loc0
== loc1
or loc1
== locn
:
1917 if -1e-6 < d1
.angle(d2
, 0) < 1e-6:
1925 # passed all tests, loop is valid
1926 valid_loops
.append([loop
, circular
])
1927 valid_single_loops
[len(valid_loops
)-1] = single_loops
[i
]
1929 return(valid_single_loops
, valid_loops
)
1932 # calculate the location of single input vertices that need to be flattened
1933 def circle_flatten_singles(bm_mod
, com
, p
, q
, normal
, single_loop
):
1935 for vert
in single_loop
:
1936 loc
= mathutils
.Vector(bm_mod
.verts
[vert
].co
[:])
1937 new_locs
.append([vert
, loc
- (loc
-com
).dot(normal
)*normal
])
1942 # calculate input loops
1943 def circle_get_input(object, bm
, scene
):
1944 # get mesh with modifiers applied
1945 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
1947 # create list of edge-keys based on selection state
1949 for face
in bm
.faces
:
1950 if face
.select
and not face
.hide
:
1954 # get selected, non-hidden , non-internal edge-keys
1955 eks_selected
= [key
for keys
in [face_edgekeys(face
) for face
in \
1956 bm_mod
.faces
if face
.select
and not face
.hide
] for key
in keys
]
1958 for ek
in eks_selected
:
1959 if ek
in edge_count
:
1963 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select \
1964 and not edge
.hide
and edge_count
.get(edgekey(edge
), 1)==1]
1966 # no faces, so no internal edges either
1967 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select \
1970 # add edge-keys around single vertices
1971 verts_connected
= dict([[vert
, 1] for edge
in [edge
for edge
in \
1972 bm_mod
.edges
if edge
.select
and not edge
.hide
] for vert
in \
1974 single_vertices
= [vert
.index
for vert
in bm_mod
.verts
if \
1975 vert
.select
and not vert
.hide
and not \
1976 verts_connected
.get(vert
.index
, False)]
1978 if single_vertices
and len(bm
.faces
)>0:
1979 vert_to_single
= dict([[v
.index
, []] for v
in bm_mod
.verts \
1981 for face
in [face
for face
in bm_mod
.faces
if not face
.select \
1983 for vert
in face
.verts
:
1985 if vert
in single_vertices
:
1986 for ek
in face_edgekeys(face
):
1988 edge_keys
.append(ek
)
1989 if vert
not in vert_to_single
[ek
[0]]:
1990 vert_to_single
[ek
[0]].append(vert
)
1991 if vert
not in vert_to_single
[ek
[1]]:
1992 vert_to_single
[ek
[1]].append(vert
)
1995 # sort edge-keys into loops
1996 loops
= get_connected_selections(edge_keys
)
1998 # find out to which loops the single vertices belong
1999 single_loops
= dict([[i
, []] for i
in range(len(loops
))])
2000 if single_vertices
and len(bm
.faces
)>0:
2001 for i
, [loop
, circular
] in enumerate(loops
):
2003 if vert_to_single
[vert
]:
2004 for single
in vert_to_single
[vert
]:
2005 if single
not in single_loops
[i
]:
2006 single_loops
[i
].append(single
)
2008 return(derived
, bm_mod
, single_vertices
, single_loops
, loops
)
2011 # recalculate positions based on the influence of the circle shape
2012 def circle_influence_locs(locs_2d
, new_locs_2d
, influence
):
2013 for i
in range(len(locs_2d
)):
2014 oldx
, oldy
, j
= locs_2d
[i
]
2015 newx
, newy
, k
= new_locs_2d
[i
]
2016 altx
= newx
*(influence
/100)+ oldx
*((100-influence
)/100)
2017 alty
= newy
*(influence
/100)+ oldy
*((100-influence
)/100)
2018 locs_2d
[i
] = [altx
, alty
, j
]
2023 # project 2d locations on circle, respecting distance relations between verts
2024 def circle_project_non_regular(locs_2d
, x0
, y0
, r
):
2025 for i
in range(len(locs_2d
)):
2026 x
, y
, j
= locs_2d
[i
]
2027 loc
= mathutils
.Vector([x
-x0
, y
-y0
])
2029 locs_2d
[i
] = [loc
[0], loc
[1], j
]
2034 # project 2d locations on circle, with equal distance between all vertices
2035 def circle_project_regular(locs_2d
, x0
, y0
, r
):
2036 # find offset angle and circling direction
2037 x
, y
, i
= locs_2d
[0]
2038 loc
= mathutils
.Vector([x
-x0
, y
-y0
])
2040 offset_angle
= loc
.angle(mathutils
.Vector([1.0, 0.0]), 0.0)
2041 loca
= mathutils
.Vector([x
-x0
, y
-y0
, 0.0])
2044 x
, y
, j
= locs_2d
[1]
2045 locb
= mathutils
.Vector([x
-x0
, y
-y0
, 0.0])
2046 if loca
.cross(locb
)[2] >= 0:
2050 # distribute vertices along the circle
2051 for i
in range(len(locs_2d
)):
2052 t
= offset_angle
+ ccw
* (i
/ len(locs_2d
) * 2 * math
.pi
)
2055 locs_2d
[i
] = [x
, y
, locs_2d
[i
][2]]
2060 # shift loop, so the first vertex is closest to the center
2061 def circle_shift_loop(bm_mod
, loop
, com
):
2062 verts
, circular
= loop
2063 distances
= [[(bm_mod
.verts
[vert
].co
- com
).length
, i
] \
2064 for i
, vert
in enumerate(verts
)]
2066 shift
= distances
[0][1]
2067 loop
= [verts
[shift
:] + verts
[:shift
], circular
]
2072 ##########################################
2073 ####### Curve functions ##################
2074 ##########################################
2076 # create lists with knots and points, all correctly sorted
2077 def curve_calculate_knots(loop
, verts_selected
):
2078 knots
= [v
for v
in loop
[0] if v
in verts_selected
]
2080 # circular loop, potential for weird splines
2082 offset
= int(len(loop
[0]) / 4)
2085 kpos
.append(loop
[0].index(k
))
2087 for i
in range(len(kpos
) - 1):
2088 kdif
.append(kpos
[i
+1] - kpos
[i
])
2089 kdif
.append(len(loop
[0]) - kpos
[-1] + kpos
[0])
2093 kadd
.append([kdif
.index(k
), True])
2094 # next 2 lines are optional, they insert
2095 # an extra control point in small gaps
2097 # kadd.append([kdif.index(k), False])
2100 for k
in kadd
: # extra knots to be added
2101 if k
[1]: # big gap (break circular spline)
2102 kpos
= loop
[0].index(knots
[k
[0]]) + offset
2103 if kpos
> len(loop
[0]) - 1:
2104 kpos
-= len(loop
[0])
2105 kins
.append([knots
[k
[0]], loop
[0][kpos
]])
2107 if kpos2
> len(knots
)-1:
2109 kpos2
= loop
[0].index(knots
[kpos2
]) - offset
2111 kpos2
+= len(loop
[0])
2112 kins
.append([loop
[0][kpos
], loop
[0][kpos2
]])
2113 krot
= loop
[0][kpos2
]
2114 else: # small gap (keep circular spline)
2115 k1
= loop
[0].index(knots
[k
[0]])
2117 if k2
> len(knots
)-1:
2119 k2
= loop
[0].index(knots
[k2
])
2121 dif
= len(loop
[0]) - 1 - k1
+ k2
2124 kn
= k1
+ int(dif
/2)
2125 if kn
> len(loop
[0]) - 1:
2127 kins
.append([loop
[0][k1
], loop
[0][kn
]])
2128 for j
in kins
: # insert new knots
2129 knots
.insert(knots
.index(j
[0]) + 1, j
[1])
2130 if not krot
: # circular loop
2131 knots
.append(knots
[0])
2132 points
= loop
[0][loop
[0].index(knots
[0]):]
2133 points
+= loop
[0][0:loop
[0].index(knots
[0]) + 1]
2134 else: # non-circular loop (broken by script)
2135 krot
= knots
.index(krot
)
2136 knots
= knots
[krot
:] + knots
[0:krot
]
2137 if loop
[0].index(knots
[0]) > loop
[0].index(knots
[-1]):
2138 points
= loop
[0][loop
[0].index(knots
[0]):]
2139 points
+= loop
[0][0:loop
[0].index(knots
[-1])+1]
2141 points
= loop
[0][loop
[0].index(knots
[0]):\
2142 loop
[0].index(knots
[-1]) + 1]
2143 # non-circular loop, add first and last point as knots
2145 if loop
[0][0] not in knots
:
2146 knots
.insert(0, loop
[0][0])
2147 if loop
[0][-1] not in knots
:
2148 knots
.append(loop
[0][-1])
2150 return(knots
, points
)
2153 # calculate relative positions compared to first knot
2154 def curve_calculate_t(bm_mod
, knots
, points
, pknots
, regular
, circular
):
2161 loc
= pknots
[knots
.index(p
)] # use projected knot location
2163 loc
= mathutils
.Vector(bm_mod
.verts
[p
].co
[:])
2166 len_total
+= (loc
-loc_prev
).length
2167 tpoints
.append(len_total
)
2172 tknots
.append(tpoints
[points
.index(p
)])
2174 tknots
[-1] = tpoints
[-1]
2178 tpoints_average
= tpoints
[-1] / (len(tpoints
) - 1)
2179 for i
in range(1, len(tpoints
) - 1):
2180 tpoints
[i
] = i
* tpoints_average
2181 for i
in range(len(knots
)):
2182 tknots
[i
] = tpoints
[points
.index(knots
[i
])]
2184 tknots
[-1] = tpoints
[-1]
2186 return(tknots
, tpoints
)
2189 # change the location of non-selected points to their place on the spline
2190 def curve_calculate_vertices(bm_mod
, knots
, tknots
, points
, tpoints
, splines
,
2191 interpolation
, restriction
):
2198 m
= tpoints
[points
.index(p
)]
2206 if n
> len(splines
) - 1:
2207 n
= len(splines
) - 1
2211 if interpolation
== 'cubic':
2212 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
2213 x
= ax
+ bx
*(m
-tx
) + cx
*(m
-tx
)**2 + dx
*(m
-tx
)**3
2214 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
2215 y
= ay
+ by
*(m
-ty
) + cy
*(m
-ty
)**2 + dy
*(m
-ty
)**3
2216 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
2217 z
= az
+ bz
*(m
-tz
) + cz
*(m
-tz
)**2 + dz
*(m
-tz
)**3
2218 newloc
= mathutils
.Vector([x
,y
,z
])
2219 else: # interpolation == 'linear'
2220 a
, d
, t
, u
= splines
[n
]
2221 newloc
= ((m
-t
)/u
)*d
+ a
2223 if restriction
!= 'none': # vertex movement is restricted
2225 else: # set the vertex to its new location
2226 move
.append([p
, newloc
])
2228 if restriction
!= 'none': # vertex movement is restricted
2233 move
.append([p
, bm_mod
.verts
[p
].co
])
2235 oldloc
= bm_mod
.verts
[p
].co
2236 normal
= bm_mod
.verts
[p
].normal
2237 dloc
= newloc
- oldloc
2238 if dloc
.length
< 1e-6:
2239 move
.append([p
, newloc
])
2240 elif restriction
== 'extrude': # only extrusions
2241 if dloc
.angle(normal
, 0) < 0.5 * math
.pi
+ 1e-6:
2242 move
.append([p
, newloc
])
2243 else: # restriction == 'indent' only indentations
2244 if dloc
.angle(normal
) > 0.5 * math
.pi
- 1e-6:
2245 move
.append([p
, newloc
])
2250 # trim loops to part between first and last selected vertices (including)
2251 def curve_cut_boundaries(bm_mod
, loops
):
2253 for loop
, circular
in loops
:
2256 cut_loops
.append([loop
, circular
])
2258 selected
= [bm_mod
.verts
[v
].select
for v
in loop
]
2259 first
= selected
.index(True)
2261 last
= -selected
.index(True)
2263 cut_loops
.append([loop
[first
:], circular
])
2265 cut_loops
.append([loop
[first
:last
], circular
])
2270 # calculate input loops
2271 def curve_get_input(object, bm
, boundaries
, scene
):
2272 # get mesh with modifiers applied
2273 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
2275 # vertices that still need a loop to run through it
2276 verts_unsorted
= [v
.index
for v
in bm_mod
.verts
if \
2277 v
.select
and not v
.hide
]
2278 # necessary dictionaries
2279 vert_edges
= dict_vert_edges(bm_mod
)
2280 edge_faces
= dict_edge_faces(bm_mod
)
2282 # find loops through each selected vertex
2283 while len(verts_unsorted
) > 0:
2284 loops
= curve_vertex_loops(bm_mod
, verts_unsorted
[0], vert_edges
,
2286 verts_unsorted
.pop(0)
2288 # check if loop is fully selected
2289 search_perpendicular
= False
2291 for loop
, circular
in loops
:
2293 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2294 if len(selected
) < 2:
2295 # only one selected vertex on loop, don't use
2298 elif len(selected
) == len(loop
):
2299 search_perpendicular
= loop
2301 # entire loop is selected, find perpendicular loops
2302 if search_perpendicular
:
2304 if vert
in verts_unsorted
:
2305 verts_unsorted
.remove(vert
)
2306 perp_loops
= curve_perpendicular_loops(bm_mod
, loop
,
2307 vert_edges
, edge_faces
)
2308 for perp_loop
in perp_loops
:
2309 correct_loops
.append(perp_loop
)
2312 for loop
, circular
in loops
:
2313 correct_loops
.append([loop
, circular
])
2317 correct_loops
= curve_cut_boundaries(bm_mod
, correct_loops
)
2319 return(derived
, bm_mod
, correct_loops
)
2322 # return all loops that are perpendicular to the given one
2323 def curve_perpendicular_loops(bm_mod
, start_loop
, vert_edges
, edge_faces
):
2324 # find perpendicular loops
2326 for start_vert
in start_loop
:
2327 loops
= curve_vertex_loops(bm_mod
, start_vert
, vert_edges
,
2329 for loop
, circular
in loops
:
2330 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2331 if len(selected
) == len(loop
):
2334 perp_loops
.append([loop
, circular
, loop
.index(start_vert
)])
2336 # trim loops to same lengths
2337 shortest
= [[len(loop
[0]), i
] for i
, loop
in enumerate(perp_loops
)\
2340 # all loops are circular, not trimming
2341 return([[loop
[0], loop
[1]] for loop
in perp_loops
])
2343 shortest
= min(shortest
)
2344 shortest_start
= perp_loops
[shortest
[1]][2]
2345 before_start
= shortest_start
2346 after_start
= shortest
[0] - shortest_start
- 1
2347 bigger_before
= before_start
> after_start
2349 for loop
in perp_loops
:
2350 # have the loop face the same direction as the shortest one
2352 if loop
[2] < len(loop
[0]) / 2:
2354 loop
[2] = len(loop
[0]) - loop
[2] - 1
2356 if loop
[2] > len(loop
[0]) / 2:
2358 loop
[2] = len(loop
[0]) - loop
[2] - 1
2359 # circular loops can shift, to prevent wrong trimming
2361 shift
= shortest_start
- loop
[2]
2362 if loop
[2] + shift
> 0 and loop
[2] + shift
< len(loop
[0]):
2363 loop
[0] = loop
[0][-shift
:] + loop
[0][:-shift
]
2366 loop
[2] += len(loop
[0])
2367 elif loop
[2] > len(loop
[0]) -1:
2368 loop
[2] -= len(loop
[0])
2370 start
= max(0, loop
[2] - before_start
)
2371 end
= min(len(loop
[0]), loop
[2] + after_start
+ 1)
2372 trimmed_loops
.append([loop
[0][start
:end
], False])
2374 return(trimmed_loops
)
2377 # project knots on non-selected geometry
2378 def curve_project_knots(bm_mod
, verts_selected
, knots
, points
, circular
):
2379 # function to project vertex on edge
2380 def project(v1
, v2
, v3
):
2381 # v1 and v2 are part of a line
2382 # v3 is projected onto it
2388 if circular
: # project all knots
2392 else: # first and last knot shouldn't be projected
2395 pknots
= [mathutils
.Vector(bm_mod
.verts
[knots
[0]].co
[:])]
2396 for knot
in knots
[start
:end
]:
2397 if knot
in verts_selected
:
2398 knot_left
= knot_right
= False
2399 for i
in range(points
.index(knot
)-1, -1*len(points
), -1):
2400 if points
[i
] not in knots
:
2401 knot_left
= points
[i
]
2403 for i
in range(points
.index(knot
)+1, 2*len(points
)):
2404 if i
> len(points
) - 1:
2406 if points
[i
] not in knots
:
2407 knot_right
= points
[i
]
2409 if knot_left
and knot_right
and knot_left
!= knot_right
:
2410 knot_left
= mathutils
.Vector(\
2411 bm_mod
.verts
[knot_left
].co
[:])
2412 knot_right
= mathutils
.Vector(\
2413 bm_mod
.verts
[knot_right
].co
[:])
2414 knot
= mathutils
.Vector(bm_mod
.verts
[knot
].co
[:])
2415 pknots
.append(project(knot_left
, knot_right
, knot
))
2417 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2418 else: # knot isn't selected, so shouldn't be changed
2419 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2421 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knots
[-1]].co
[:]))
2426 # find all loops through a given vertex
2427 def curve_vertex_loops(bm_mod
, start_vert
, vert_edges
, edge_faces
):
2431 for edge
in vert_edges
[start_vert
]:
2432 if edge
in edges_used
:
2437 active_faces
= edge_faces
[edge
]
2442 new_edges
= vert_edges
[new_vert
]
2443 loop
.append(new_vert
)
2445 edges_used
.append(tuple(sorted([loop
[-1], loop
[-2]])))
2446 if len(new_edges
) < 3 or len(new_edges
) > 4:
2451 for new_edge
in new_edges
:
2452 if new_edge
in edges_used
:
2455 for new_face
in edge_faces
[new_edge
]:
2456 if new_face
in active_faces
:
2461 # found correct new edge
2462 active_faces
= edge_faces
[new_edge
]
2468 if new_vert
== loop
[0]:
2476 loops
.append([loop
, circular
])
2481 ##########################################
2482 ####### Flatten functions ################
2483 ##########################################
2485 # sort input into loops
2486 def flatten_get_input(bm
):
2487 vert_verts
= dict_vert_verts([edgekey(edge
) for edge
in bm
.edges \
2488 if edge
.select
and not edge
.hide
])
2489 verts
= [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
2491 # no connected verts, consider all selected verts as a single input
2493 return([[verts
, False]])
2496 while len(verts
) > 0:
2500 if loop
[-1] in vert_verts
:
2501 to_grow
= vert_verts
[loop
[-1]]
2505 while len(to_grow
) > 0:
2506 new_vert
= to_grow
[0]
2508 if new_vert
in loop
:
2510 loop
.append(new_vert
)
2511 verts
.remove(new_vert
)
2512 to_grow
+= vert_verts
[new_vert
]
2514 loops
.append([loop
, False])
2519 # calculate position of vertex projections on plane
2520 def flatten_project(bm
, loop
, com
, normal
):
2521 verts
= [bm
.verts
[v
] for v
in loop
[0]]
2522 verts_projected
= [[v
.index
, mathutils
.Vector(v
.co
[:]) - \
2523 (mathutils
.Vector(v
.co
[:])-com
).dot(normal
)*normal
] for v
in verts
]
2525 return(verts_projected
)
2528 ##########################################
2529 ####### Gstretch functions ###############
2530 ##########################################
2532 # fake stroke class, used to create custom strokes if no GP data is found
2533 class gstretch_fake_stroke():
2534 def __init__(self
, points
):
2535 self
.points
= [gstretch_fake_stroke_point(p
) for p
in points
]
2538 # fake stroke point class, used in fake strokes
2539 class gstretch_fake_stroke_point():
2540 def __init__(self
, loc
):
2544 # flips loops, if necessary, to obtain maximum alignment to stroke
2545 def gstretch_align_pairs(ls_pairs
, object, bm_mod
, method
):
2546 # returns total distance between all verts in loop and corresponding stroke
2547 def distance_loop_stroke(loop
, stroke
, object, bm_mod
, method
):
2548 stroke_lengths_cache
= False
2549 loop_length
= len(loop
[0])
2552 if method
!= 'regular':
2553 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2555 for i
, v_index
in enumerate(loop
[0]):
2556 if method
== 'regular':
2557 relative_distance
= i
/ (loop_length
- 1)
2559 relative_distance
= relative_lengths
[i
]
2561 loc1
= object.matrix_world
* bm_mod
.verts
[v_index
].co
2562 loc2
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2563 relative_distance
, stroke_lengths_cache
)
2564 total_distance
+= (loc2
- loc1
).length
2566 return(total_distance
)
2569 for (loop
, stroke
) in ls_pairs
:
2570 total_dist
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2573 total_dist_rev
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2575 if total_dist_rev
> total_dist
:
2581 # calculate vertex positions on stroke
2582 def gstretch_calculate_verts(loop
, stroke
, object, bm_mod
, method
):
2584 stroke_lengths_cache
= False
2585 loop_length
= len(loop
[0])
2586 matrix_inverse
= object.matrix_world
.inverted()
2588 # return intersection of line with stroke, or None
2589 def intersect_line_stroke(vec1
, vec2
, stroke
):
2590 for i
, p
in enumerate(stroke
.points
[1:]):
2591 intersections
= mathutils
.geometry
.intersect_line_line(vec1
, vec2
,
2592 p
.co
, stroke
.points
[i
].co
)
2593 if intersections
and \
2594 (intersections
[0] - intersections
[1]).length
< 1e-2:
2595 x
, dist
= mathutils
.geometry
.intersect_point_line(
2596 intersections
[0], p
.co
, stroke
.points
[i
].co
)
2598 return(intersections
[0])
2601 if method
== 'project':
2602 projection_vectors
= []
2603 vert_edges
= dict_vert_edges(bm_mod
)
2605 for v_index
in loop
[0]:
2607 for ek
in vert_edges
[v_index
]:
2609 v1
= bm_mod
.verts
[v1
]
2610 v2
= bm_mod
.verts
[v2
]
2611 if v1
.select
+ v2
.select
== 1 and not v1
.hide
and not v2
.hide
:
2612 vec1
= object.matrix_world
* v1
.co
2613 vec2
= object.matrix_world
* v2
.co
2614 intersection
= intersect_line_stroke(vec1
, vec2
, stroke
)
2617 if not intersection
:
2618 v
= bm_mod
.verts
[v_index
]
2619 intersection
= intersect_line_stroke(v
.co
, v
.co
+ v
.normal
,
2622 move
.append([v_index
, matrix_inverse
* intersection
])
2625 if method
== 'irregular':
2626 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2628 for i
, v_index
in enumerate(loop
[0]):
2629 if method
== 'regular':
2630 relative_distance
= i
/ (loop_length
- 1)
2631 else: # method == 'irregular'
2632 relative_distance
= relative_lengths
[i
]
2633 loc
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2634 relative_distance
, stroke_lengths_cache
)
2635 loc
= matrix_inverse
* loc
2636 move
.append([v_index
, loc
])
2641 # create new vertices, based on GP strokes
2642 def gstretch_create_verts(object, bm_mod
, strokes
, method
, conversion
,
2643 conversion_distance
, conversion_max
, conversion_min
, conversion_vertices
):
2646 mat_world
= object.matrix_world
.inverted()
2647 singles
= gstretch_match_single_verts(bm_mod
, strokes
, mat_world
)
2649 for stroke
in strokes
:
2650 stroke_verts
.append([stroke
, []])
2652 if conversion
== 'vertices':
2653 min_end_point
= conversion_vertices
2654 end_point
= conversion_vertices
2655 elif conversion
== 'limit_vertices':
2656 min_end_point
= conversion_min
2657 end_point
= conversion_max
2659 end_point
= len(stroke
.points
)
2660 # creation of new vertices at fixed user-defined distances
2661 if conversion
== 'distance':
2663 prev_point
= stroke
.points
[0]
2664 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* \
2667 limit
= conversion_distance
2668 for point
in stroke
.points
:
2669 new_distance
= distance
+ (point
.co
- prev_point
.co
).length
2671 while new_distance
> limit
:
2672 to_cover
= limit
- distance
+ (limit
* iteration
)
2673 new_loc
= prev_point
.co
+ to_cover
* \
2674 (point
.co
- prev_point
.co
).normalized()
2675 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* \
2677 new_distance
-= limit
2679 distance
= new_distance
2681 # creation of new vertices for other methods
2683 # add vertices at stroke points
2684 for point
in stroke
.points
[:end_point
]:
2685 stroke_verts
[-1][1].append(bm_mod
.verts
.new(\
2686 mat_world
* point
.co
))
2687 # add more vertices, beyond the points that are available
2688 if min_end_point
> min(len(stroke
.points
), end_point
):
2689 for i
in range(min_end_point
-
2690 (min(len(stroke
.points
), end_point
))):
2691 stroke_verts
[-1][1].append(bm_mod
.verts
.new(\
2692 mat_world
* point
.co
))
2693 # force even spreading of points, so they are placed on stroke
2695 bm_mod
.verts
.ensure_lookup_table()
2696 bm_mod
.verts
.index_update()
2697 for stroke
, verts_seq
in stroke_verts
:
2698 if len(verts_seq
) < 2:
2700 # spread vertices evenly over the stroke
2701 if method
== 'regular':
2702 loop
= [[vert
.index
for vert
in verts_seq
], False]
2703 move
+= gstretch_calculate_verts(loop
, stroke
, object, bm_mod
,
2706 for i
, vert
in enumerate(verts_seq
):
2708 bm_mod
.edges
.new((verts_seq
[i
-1], verts_seq
[i
]))
2710 # connect single vertices to the closest stroke
2712 for vert
, m_stroke
, point
in singles
:
2713 if m_stroke
!= stroke
:
2715 bm_mod
.edges
.new((vert
, verts_seq
[point
]))
2716 bm_mod
.edges
.ensure_lookup_table()
2717 bmesh
.update_edit_mesh(object.data
)
2722 # erases the grease pencil stroke
2723 def gstretch_erase_stroke(stroke
, context
):
2724 # change 3d coordinate into a stroke-point
2725 def sp(loc
, context
):
2729 'location': (0, 0, 0),
2730 'mouse': (view3d_utils
.location_3d_to_region_2d(\
2731 context
.region
, context
.space_data
.region_3d
, loc
)),
2737 if type(stroke
) != bpy
.types
.GPencilStroke
:
2738 # fake stroke, there is nothing to delete
2741 erase_stroke
= [sp(p
.co
, context
) for p
in stroke
.points
]
2743 erase_stroke
[0]['is_start'] = True
2744 bpy
.ops
.gpencil
.draw(mode
='ERASER', stroke
=erase_stroke
)
2747 # get point on stroke, given by relative distance (0.0 - 1.0)
2748 def gstretch_eval_stroke(stroke
, distance
, stroke_lengths_cache
=False):
2749 # use cache if available
2750 if not stroke_lengths_cache
:
2752 for i
, p
in enumerate(stroke
.points
[1:]):
2753 lengths
.append((p
.co
- stroke
.points
[i
].co
).length
+ \
2755 total_length
= max(lengths
[-1], 1e-7)
2756 stroke_lengths_cache
= [length
/ total_length
for length
in
2758 stroke_lengths
= stroke_lengths_cache
[:]
2760 if distance
in stroke_lengths
:
2761 loc
= stroke
.points
[stroke_lengths
.index(distance
)].co
2762 elif distance
> stroke_lengths
[-1]:
2763 # should be impossible, but better safe than sorry
2764 loc
= stroke
.points
[-1].co
2766 stroke_lengths
.append(distance
)
2767 stroke_lengths
.sort()
2768 stroke_index
= stroke_lengths
.index(distance
)
2769 interval_length
= stroke_lengths
[stroke_index
+1] - \
2770 stroke_lengths
[stroke_index
-1]
2771 distance_relative
= (distance
- stroke_lengths
[stroke_index
-1]) / \
2773 interval_vector
= stroke
.points
[stroke_index
].co
- \
2774 stroke
.points
[stroke_index
-1].co
2775 loc
= stroke
.points
[stroke_index
-1].co
+ \
2776 distance_relative
* interval_vector
2778 return(loc
, stroke_lengths_cache
)
2781 # create fake grease pencil strokes for the active object
2782 def gstretch_get_fake_strokes(object, bm_mod
, loops
):
2785 p1
= object.matrix_world
* bm_mod
.verts
[loop
[0][0]].co
2786 p2
= object.matrix_world
* bm_mod
.verts
[loop
[0][-1]].co
2787 strokes
.append(gstretch_fake_stroke([p1
, p2
]))
2792 # get grease pencil strokes for the active object
2793 def gstretch_get_strokes(object, context
):
2794 gp
= get_grease_pencil(object, context
)
2797 layer
= gp
.layers
.active
2800 frame
= layer
.active_frame
2803 strokes
= frame
.strokes
2804 if len(strokes
) < 1:
2810 # returns a list with loop-stroke pairs
2811 def gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
):
2812 if not loops
or not strokes
:
2815 # calculate loop centers
2817 bm_mod
.verts
.ensure_lookup_table()
2819 center
= mathutils
.Vector()
2820 for v_index
in loop
[0]:
2821 center
+= bm_mod
.verts
[v_index
].co
2822 center
/= len(loop
[0])
2823 center
= object.matrix_world
* center
2824 loop_centers
.append([center
, loop
])
2826 # calculate stroke centers
2828 for stroke
in strokes
:
2829 center
= mathutils
.Vector()
2830 for p
in stroke
.points
:
2832 center
/= len(stroke
.points
)
2833 stroke_centers
.append([center
, stroke
, 0])
2835 # match, first by stroke use count, then by distance
2837 for lc
in loop_centers
:
2839 for i
, sc
in enumerate(stroke_centers
):
2840 distances
.append([sc
[2], (lc
[0] - sc
[0]).length
, i
])
2842 best_stroke
= distances
[0][2]
2843 ls_pairs
.append([lc
[1], stroke_centers
[best_stroke
][1]])
2844 stroke_centers
[best_stroke
][2] += 1 # increase stroke use count
2849 # match single selected vertices to the closest stroke endpoint
2850 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2851 def gstretch_match_single_verts(bm_mod
, strokes
, mat_world
):
2852 # calculate stroke endpoints in object space
2854 for stroke
in strokes
:
2855 endpoints
.append((mat_world
* stroke
.points
[0].co
, stroke
, 0))
2856 endpoints
.append((mat_world
* stroke
.points
[-1].co
, stroke
, -1))
2859 # find single vertices (not connected to other selected verts)
2860 for vert
in bm_mod
.verts
:
2864 for edge
in vert
.link_edges
:
2865 if edge
.other_vert(vert
).select
:
2870 # calculate distances from vertex to endpoints
2871 distance
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2872 endpoint_index
) for endpoint_index
, (loc
, stroke
, stroke_point
) in
2873 enumerate(endpoints
)]
2875 distances
.append(distance
[0])
2877 # create matches, based on shortest distance first
2881 singles
.append((distances
[0][1], distances
[0][2], distances
[0][3]))
2882 endpoints
.pop(distances
[0][4])
2885 for (i
, vert
, j
, k
, l
) in distances
:
2886 distance_new
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2887 endpoint_index
) for endpoint_index
, (loc
, stroke
,
2888 stroke_point
) in enumerate(endpoints
)]
2890 distances_new
.append(distance_new
[0])
2891 distances
= distances_new
2896 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2897 def gstretch_relative_lengths(loop
, bm_mod
):
2899 for i
, v_index
in enumerate(loop
[0][1:]):
2900 lengths
.append((bm_mod
.verts
[v_index
].co
- \
2901 bm_mod
.verts
[loop
[0][i
]].co
).length
+ lengths
[-1])
2902 total_length
= max(lengths
[-1], 1e-7)
2903 relative_lengths
= [length
/ total_length
for length
in
2906 return(relative_lengths
)
2909 # convert cache-stored strokes into usable (fake) GP strokes
2910 def gstretch_safe_to_true_strokes(safe_strokes
):
2912 for safe_stroke
in safe_strokes
:
2913 strokes
.append(gstretch_fake_stroke(safe_stroke
))
2918 # convert a GP stroke into a list of points which can be stored in cache
2919 def gstretch_true_to_safe_strokes(strokes
):
2921 for stroke
in strokes
:
2922 safe_strokes
.append([p
.co
.copy() for p
in stroke
.points
])
2924 return(safe_strokes
)
2927 # force consistency in GUI, max value can never be lower than min value
2928 def gstretch_update_max(self
, context
):
2929 # called from operator settings (after execution)
2930 if 'conversion_min' in self
.keys():
2931 if self
.conversion_min
> self
.conversion_max
:
2932 self
.conversion_max
= self
.conversion_min
2933 # called from toolbar
2935 lt
= context
.window_manager
.looptools
2936 if lt
.gstretch_conversion_min
> lt
.gstretch_conversion_max
:
2937 lt
.gstretch_conversion_max
= lt
.gstretch_conversion_min
2940 # force consistency in GUI, min value can never be higher than max value
2941 def gstretch_update_min(self
, context
):
2942 # called from operator settings (after execution)
2943 if 'conversion_max' in self
.keys():
2944 if self
.conversion_max
< self
.conversion_min
:
2945 self
.conversion_min
= self
.conversion_max
2946 # called from toolbar
2948 lt
= context
.window_manager
.looptools
2949 if lt
.gstretch_conversion_max
< lt
.gstretch_conversion_min
:
2950 lt
.gstretch_conversion_min
= lt
.gstretch_conversion_max
2953 ##########################################
2954 ####### Relax functions ##################
2955 ##########################################
2957 # create lists with knots and points, all correctly sorted
2958 def relax_calculate_knots(loops
):
2961 for loop
, circular
in loops
:
2965 if len(loop
)%2 == 1: # odd
2966 extend
= [False, True, 0, 1, 0, 1]
2968 extend
= [True, False, 0, 1, 1, 2]
2970 if len(loop
)%2 == 1: # odd
2971 extend
= [False, False, 0, 1, 1, 2]
2973 extend
= [False, False, 0, 1, 1, 2]
2976 loop
= [loop
[-1]] + loop
+ [loop
[0]]
2977 for i
in range(extend
[2+2*j
], len(loop
), 2):
2978 knots
[j
].append(loop
[i
])
2979 for i
in range(extend
[3+2*j
], len(loop
), 2):
2980 if loop
[i
] == loop
[-1] and not circular
:
2982 if len(points
[j
]) == 0:
2983 points
[j
].append(loop
[i
])
2984 elif loop
[i
] != points
[j
][0]:
2985 points
[j
].append(loop
[i
])
2987 if knots
[j
][0] != knots
[j
][-1]:
2988 knots
[j
].append(knots
[j
][0])
2989 if len(points
[1]) == 0:
2995 all_points
.append(p
)
2997 return(all_knots
, all_points
)
3000 # calculate relative positions compared to first knot
3001 def relax_calculate_t(bm_mod
, knots
, points
, regular
):
3004 for i
in range(len(knots
)):
3005 amount
= len(knots
[i
]) + len(points
[i
])
3007 for j
in range(amount
):
3009 mix
.append([True, knots
[i
][round(j
/2)]])
3011 mix
.append([True, knots
[i
][-1]])
3013 mix
.append([False, points
[i
][int(j
/2)]])
3019 loc
= mathutils
.Vector(bm_mod
.verts
[m
[1]].co
[:])
3022 len_total
+= (loc
- loc_prev
).length
3024 tknots
.append(len_total
)
3026 tpoints
.append(len_total
)
3030 for p
in range(len(points
[i
])):
3031 tpoints
.append((tknots
[p
] + tknots
[p
+1]) / 2)
3032 all_tknots
.append(tknots
)
3033 all_tpoints
.append(tpoints
)
3035 return(all_tknots
, all_tpoints
)
3038 # change the location of the points to their place on the spline
3039 def relax_calculate_verts(bm_mod
, interpolation
, tknots
, knots
, tpoints
,
3043 for i
in range(len(knots
)):
3045 m
= tpoints
[i
][points
[i
].index(p
)]
3047 n
= tknots
[i
].index(m
)
3053 if n
> len(splines
[i
]) - 1:
3054 n
= len(splines
[i
]) - 1
3058 if interpolation
== 'cubic':
3059 ax
, bx
, cx
, dx
, tx
= splines
[i
][n
][0]
3060 x
= ax
+ bx
*(m
-tx
) + cx
*(m
-tx
)**2 + dx
*(m
-tx
)**3
3061 ay
, by
, cy
, dy
, ty
= splines
[i
][n
][1]
3062 y
= ay
+ by
*(m
-ty
) + cy
*(m
-ty
)**2 + dy
*(m
-ty
)**3
3063 az
, bz
, cz
, dz
, tz
= splines
[i
][n
][2]
3064 z
= az
+ bz
*(m
-tz
) + cz
*(m
-tz
)**2 + dz
*(m
-tz
)**3
3065 change
.append([p
, mathutils
.Vector([x
,y
,z
])])
3066 else: # interpolation == 'linear'
3067 a
, d
, t
, u
= splines
[i
][n
]
3070 change
.append([p
, ((m
-t
)/u
)*d
+ a
])
3072 move
.append([c
[0], (bm_mod
.verts
[c
[0]].co
+ c
[1]) / 2])
3077 ##########################################
3078 ####### Space functions ##################
3079 ##########################################
3081 # calculate relative positions compared to first knot
3082 def space_calculate_t(bm_mod
, knots
):
3087 loc
= mathutils
.Vector(bm_mod
.verts
[k
].co
[:])
3090 len_total
+= (loc
- loc_prev
).length
3091 tknots
.append(len_total
)
3094 t_per_segment
= len_total
/ (amount
- 1)
3095 tpoints
= [i
* t_per_segment
for i
in range(amount
)]
3097 return(tknots
, tpoints
)
3100 # change the location of the points to their place on the spline
3101 def space_calculate_verts(bm_mod
, interpolation
, tknots
, tpoints
, points
,
3105 m
= tpoints
[points
.index(p
)]
3113 if n
> len(splines
) - 1:
3114 n
= len(splines
) - 1
3118 if interpolation
== 'cubic':
3119 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
3120 x
= ax
+ bx
*(m
-tx
) + cx
*(m
-tx
)**2 + dx
*(m
-tx
)**3
3121 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
3122 y
= ay
+ by
*(m
-ty
) + cy
*(m
-ty
)**2 + dy
*(m
-ty
)**3
3123 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
3124 z
= az
+ bz
*(m
-tz
) + cz
*(m
-tz
)**2 + dz
*(m
-tz
)**3
3125 move
.append([p
, mathutils
.Vector([x
,y
,z
])])
3126 else: # interpolation == 'linear'
3127 a
, d
, t
, u
= splines
[n
]
3128 move
.append([p
, ((m
-t
)/u
)*d
+ a
])
3133 ##########################################
3134 ####### Operators ########################
3135 ##########################################
3138 class Bridge(bpy
.types
.Operator
):
3139 bl_idname
= 'mesh.looptools_bridge'
3140 bl_label
= "Bridge / Loft"
3141 bl_description
= "Bridge two, or loft several, loops of vertices"
3142 bl_options
= {'REGISTER', 'UNDO'}
3144 cubic_strength
= bpy
.props
.FloatProperty(name
= "Strength",
3145 description
= "Higher strength results in more fluid curves",
3149 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation mode",
3150 items
= (('cubic', "Cubic", "Gives curved results"),
3151 ('linear', "Linear", "Basic, fast, straight interpolation")),
3152 description
= "Interpolation mode: algorithm used when creating "\
3155 loft
= bpy
.props
.BoolProperty(name
= "Loft",
3156 description
= "Loft multiple loops, instead of considering them as "\
3157 "a multi-input for bridging",
3159 loft_loop
= bpy
.props
.BoolProperty(name
= "Loop",
3160 description
= "Connect the first and the last loop with each other",
3162 min_width
= bpy
.props
.IntProperty(name
= "Minimum width",
3163 description
= "Segments with an edge smaller than this are merged "\
3164 "(compared to base edge)",
3168 subtype
= 'PERCENTAGE')
3169 mode
= bpy
.props
.EnumProperty(name
= "Mode",
3170 items
= (('basic', "Basic", "Fast algorithm"), ('shortest',
3171 "Shortest edge", "Slower algorithm with better vertex matching")),
3172 description
= "Algorithm used for bridging",
3173 default
= 'shortest')
3174 remove_faces
= bpy
.props
.BoolProperty(name
= "Remove faces",
3175 description
= "Remove faces that are internal after bridging",
3177 reverse
= bpy
.props
.BoolProperty(name
= "Reverse",
3178 description
= "Manually override the direction in which the loops "\
3179 "are bridged. Only use if the tool gives the wrong " \
3182 segments
= bpy
.props
.IntProperty(name
= "Segments",
3183 description
= "Number of segments used to bridge the gap "\
3188 twist
= bpy
.props
.IntProperty(name
= "Twist",
3189 description
= "Twist what vertices are connected to each other",
3193 def poll(cls
, context
):
3194 ob
= context
.active_object
3195 return (ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3197 def draw(self
, context
):
3198 layout
= self
.layout
3199 #layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3202 col_top
= layout
.column(align
=True)
3203 row
= col_top
.row(align
=True)
3204 col_left
= row
.column(align
=True)
3205 col_right
= row
.column(align
=True)
3206 col_right
.active
= self
.segments
!= 1
3207 col_left
.prop(self
, "segments")
3208 col_right
.prop(self
, "min_width", text
="")
3210 bottom_left
= col_left
.row()
3211 bottom_left
.active
= self
.segments
!= 1
3212 bottom_left
.prop(self
, "interpolation", text
="")
3213 bottom_right
= col_right
.row()
3214 bottom_right
.active
= self
.interpolation
== 'cubic'
3215 bottom_right
.prop(self
, "cubic_strength")
3216 # boolean properties
3217 col_top
.prop(self
, "remove_faces")
3219 col_top
.prop(self
, "loft_loop")
3221 # override properties
3223 row
= layout
.row(align
= True)
3224 row
.prop(self
, "twist")
3225 row
.prop(self
, "reverse")
3227 def invoke(self
, context
, event
):
3228 # load custom settings
3229 context
.window_manager
.looptools
.bridge_loft
= self
.loft
3231 return self
.execute(context
)
3233 def execute(self
, context
):
3235 global_undo
, object, bm
= initialise()
3236 edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
= \
3237 bridge_initialise(bm
, self
.interpolation
)
3238 settings_write(self
)
3240 # check cache to see if we can save time
3241 input_method
= bridge_input_method(self
.loft
, self
.loft_loop
)
3242 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Bridge",
3243 object, bm
, input_method
, False)
3246 loops
= bridge_get_input(bm
)
3248 # reorder loops if there are more than 2
3251 loops
= bridge_sort_loops(bm
, loops
, self
.loft_loop
)
3253 loops
= bridge_match_loops(bm
, loops
)
3255 # saving cache for faster execution next time
3257 cache_write("Bridge", object, bm
, input_method
, False, False,
3258 loops
, False, False)
3261 # calculate new geometry
3264 max_vert_index
= len(bm
.verts
)-1
3265 for i
in range(1, len(loops
)):
3266 if not self
.loft
and i
%2 == 0:
3268 lines
= bridge_calculate_lines(bm
, loops
[i
-1:i
+1],
3269 self
.mode
, self
.twist
, self
.reverse
)
3270 vertex_normals
= bridge_calculate_virtual_vertex_normals(bm
,
3271 lines
, loops
[i
-1:i
+1], edge_faces
, edgekey_to_edge
)
3272 segments
= bridge_calculate_segments(bm
, lines
,
3273 loops
[i
-1:i
+1], self
.segments
)
3274 new_verts
, new_faces
, max_vert_index
= \
3275 bridge_calculate_geometry(bm
, lines
, vertex_normals
,
3276 segments
, self
.interpolation
, self
.cubic_strength
,
3277 self
.min_width
, max_vert_index
)
3279 vertices
+= new_verts
3282 # make sure faces in loops that aren't used, aren't removed
3283 if self
.remove_faces
and old_selected_faces
:
3284 bridge_save_unused_faces(bm
, old_selected_faces
, loops
)
3287 bridge_create_vertices(bm
, vertices
)
3290 new_faces
= bridge_create_faces(object, bm
, faces
, self
.twist
)
3291 old_selected_faces
= [i
for i
, face
in enumerate(bm
.faces
) \
3292 if face
.index
in old_selected_faces
] # updating list
3293 bridge_select_new_faces(new_faces
, smooth
)
3294 # edge-data could have changed, can't use cache next run
3295 if faces
and not vertices
:
3296 cache_delete("Bridge")
3297 # delete internal faces
3298 if self
.remove_faces
and old_selected_faces
:
3299 bridge_remove_internal_faces(bm
, old_selected_faces
)
3300 # make sure normals are facing outside
3301 bmesh
.update_edit_mesh(object.data
, tessface
=False,
3303 bpy
.ops
.mesh
.normals_make_consistent()
3306 terminate(global_undo
)
3312 class Circle(bpy
.types
.Operator
):
3313 bl_idname
= "mesh.looptools_circle"
3315 bl_description
= "Move selected vertices into a circle shape"
3316 bl_options
= {'REGISTER', 'UNDO'}
3318 custom_radius
= bpy
.props
.BoolProperty(name
= "Radius",
3319 description
= "Force a custom radius",
3321 fit
= bpy
.props
.EnumProperty(name
= "Method",
3322 items
= (("best", "Best fit", "Non-linear least squares"),
3323 ("inside", "Fit inside","Only move vertices towards the center")),
3324 description
= "Method used for fitting a circle to the vertices",
3326 flatten
= bpy
.props
.BoolProperty(name
= "Flatten",
3327 description
= "Flatten the circle, instead of projecting it on the " \
3330 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3331 description
= "Force of the tool",
3336 subtype
= 'PERCENTAGE')
3337 lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
3338 description
= "Lock editing of the x-coordinate",
3340 lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
3341 description
= "Lock editing of the y-coordinate",
3343 lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
3344 description
= "Lock editing of the z-coordinate",
3346 radius
= bpy
.props
.FloatProperty(name
= "Radius",
3347 description
= "Custom radius for circle",
3351 regular
= bpy
.props
.BoolProperty(name
= "Regular",
3352 description
= "Distribute vertices at constant distances along the " \
3357 def poll(cls
, context
):
3358 ob
= context
.active_object
3359 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3361 def draw(self
, context
):
3362 layout
= self
.layout
3363 col
= layout
.column()
3365 col
.prop(self
, "fit")
3368 col
.prop(self
, "flatten")
3369 row
= col
.row(align
=True)
3370 row
.prop(self
, "custom_radius")
3371 row_right
= row
.row(align
=True)
3372 row_right
.active
= self
.custom_radius
3373 row_right
.prop(self
, "radius", text
="")
3374 col
.prop(self
, "regular")
3377 col_move
= col
.column(align
=True)
3378 row
= col_move
.row(align
=True)
3380 row
.prop(self
, "lock_x", text
= "X", icon
='LOCKED')
3382 row
.prop(self
, "lock_x", text
= "X", icon
='UNLOCKED')
3384 row
.prop(self
, "lock_y", text
= "Y", icon
='LOCKED')
3386 row
.prop(self
, "lock_y", text
= "Y", icon
='UNLOCKED')
3388 row
.prop(self
, "lock_z", text
= "Z", icon
='LOCKED')
3390 row
.prop(self
, "lock_z", text
= "Z", icon
='UNLOCKED')
3391 col_move
.prop(self
, "influence")
3393 def invoke(self
, context
, event
):
3394 # load custom settings
3396 return self
.execute(context
)
3398 def execute(self
, context
):
3400 global_undo
, object, bm
= initialise()
3401 settings_write(self
)
3402 # check cache to see if we can save time
3403 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Circle",
3404 object, bm
, False, False)
3406 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3409 derived
, bm_mod
, single_vertices
, single_loops
, loops
= \
3410 circle_get_input(object, bm
, context
.scene
)
3411 mapping
= get_mapping(derived
, bm
, bm_mod
, single_vertices
,
3413 single_loops
, loops
= circle_check_loops(single_loops
, loops
,
3416 # saving cache for faster execution next time
3418 cache_write("Circle", object, bm
, False, False, single_loops
,
3419 loops
, derived
, mapping
)
3422 for i
, loop
in enumerate(loops
):
3423 # best fitting flat plane
3424 com
, normal
= calculate_plane(bm_mod
, loop
)
3425 # if circular, shift loop so we get a good starting vertex
3427 loop
= circle_shift_loop(bm_mod
, loop
, com
)
3428 # flatten vertices on plane
3429 locs_2d
, p
, q
= circle_3d_to_2d(bm_mod
, loop
, com
, normal
)
3431 if self
.fit
== 'best':
3432 x0
, y0
, r
= circle_calculate_best_fit(locs_2d
)
3433 else: # self.fit == 'inside'
3434 x0
, y0
, r
= circle_calculate_min_fit(locs_2d
)
3436 if self
.custom_radius
:
3437 r
= self
.radius
/ p
.length
3438 # calculate positions on circle
3440 new_locs_2d
= circle_project_regular(locs_2d
[:], x0
, y0
, r
)
3442 new_locs_2d
= circle_project_non_regular(locs_2d
[:], x0
, y0
, r
)
3443 # take influence into account
3444 locs_2d
= circle_influence_locs(locs_2d
, new_locs_2d
,
3446 # calculate 3d positions of the created 2d input
3447 move
.append(circle_calculate_verts(self
.flatten
, bm_mod
,
3448 locs_2d
, com
, p
, q
, normal
))
3449 # flatten single input vertices on plane defined by loop
3450 if self
.flatten
and single_loops
:
3451 move
.append(circle_flatten_singles(bm_mod
, com
, p
, q
,
3452 normal
, single_loops
[i
]))
3454 # move vertices to new locations
3455 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3456 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3459 move_verts(object, bm
, mapping
, move
, lock
, -1)
3464 terminate(global_undo
)
3470 class Curve(bpy
.types
.Operator
):
3471 bl_idname
= "mesh.looptools_curve"
3473 bl_description
= "Turn a loop into a smooth curve"
3474 bl_options
= {'REGISTER', 'UNDO'}
3476 boundaries
= bpy
.props
.BoolProperty(name
= "Boundaries",
3477 description
= "Limit the tool to work within the boundaries of the "\
3478 "selected vertices",
3480 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3481 description
= "Force of the tool",
3486 subtype
= 'PERCENTAGE')
3487 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
3488 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3489 ("linear", "Linear", "Simple and fast linear algorithm")),
3490 description
= "Algorithm used for interpolation",
3492 lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
3493 description
= "Lock editing of the x-coordinate",
3495 lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
3496 description
= "Lock editing of the y-coordinate",
3498 lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
3499 description
= "Lock editing of the z-coordinate",
3501 regular
= bpy
.props
.BoolProperty(name
= "Regular",
3502 description
= "Distribute vertices at constant distances along the" \
3505 restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
3506 items
= (("none", "None", "No restrictions on vertex movement"),
3507 ("extrude", "Extrude only","Only allow extrusions (no "\
3509 ("indent", "Indent only", "Only allow indentation (no "\
3511 description
= "Restrictions on how the vertices can be moved",
3515 def poll(cls
, context
):
3516 ob
= context
.active_object
3517 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3519 def draw(self
, context
):
3520 layout
= self
.layout
3521 col
= layout
.column()
3523 col
.prop(self
, "interpolation")
3524 col
.prop(self
, "restriction")
3525 col
.prop(self
, "boundaries")
3526 col
.prop(self
, "regular")
3529 col_move
= col
.column(align
=True)
3530 row
= col_move
.row(align
=True)
3532 row
.prop(self
, "lock_x", text
= "X", icon
='LOCKED')
3534 row
.prop(self
, "lock_x", text
= "X", icon
='UNLOCKED')
3536 row
.prop(self
, "lock_y", text
= "Y", icon
='LOCKED')
3538 row
.prop(self
, "lock_y", text
= "Y", icon
='UNLOCKED')
3540 row
.prop(self
, "lock_z", text
= "Z", icon
='LOCKED')
3542 row
.prop(self
, "lock_z", text
= "Z", icon
='UNLOCKED')
3543 col_move
.prop(self
, "influence")
3545 def invoke(self
, context
, event
):
3546 # load custom settings
3548 return self
.execute(context
)
3550 def execute(self
, context
):
3552 global_undo
, object, bm
= initialise()
3553 settings_write(self
)
3554 # check cache to see if we can save time
3555 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Curve",
3556 object, bm
, False, self
.boundaries
)
3558 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3561 derived
, bm_mod
, loops
= curve_get_input(object, bm
,
3562 self
.boundaries
, context
.scene
)
3563 mapping
= get_mapping(derived
, bm
, bm_mod
, False, True, loops
)
3564 loops
= check_loops(loops
, mapping
, bm_mod
)
3565 verts_selected
= [v
.index
for v
in bm_mod
.verts
if v
.select \
3568 # saving cache for faster execution next time
3570 cache_write("Curve", object, bm
, False, self
.boundaries
, False,
3571 loops
, derived
, mapping
)
3575 knots
, points
= curve_calculate_knots(loop
, verts_selected
)
3576 pknots
= curve_project_knots(bm_mod
, verts_selected
, knots
,
3578 tknots
, tpoints
= curve_calculate_t(bm_mod
, knots
, points
,
3579 pknots
, self
.regular
, loop
[1])
3580 splines
= calculate_splines(self
.interpolation
, bm_mod
,
3582 move
.append(curve_calculate_vertices(bm_mod
, knots
, tknots
,
3583 points
, tpoints
, splines
, self
.interpolation
,
3586 # move vertices to new locations
3587 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3588 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3591 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
3596 terminate(global_undo
)
3602 class Flatten(bpy
.types
.Operator
):
3603 bl_idname
= "mesh.looptools_flatten"
3604 bl_label
= "Flatten"
3605 bl_description
= "Flatten vertices on a best-fitting plane"
3606 bl_options
= {'REGISTER', 'UNDO'}
3608 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3609 description
= "Force of the tool",
3614 subtype
= 'PERCENTAGE')
3615 lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
3616 description
= "Lock editing of the x-coordinate",
3618 lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
3619 description
= "Lock editing of the y-coordinate",
3621 lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
3622 description
= "Lock editing of the z-coordinate",
3624 plane
= bpy
.props
.EnumProperty(name
= "Plane",
3625 items
= (("best_fit", "Best fit", "Calculate a best fitting plane"),
3626 ("normal", "Normal", "Derive plane from averaging vertex "\
3628 ("view", "View", "Flatten on a plane perpendicular to the "\
3630 description
= "Plane on which vertices are flattened",
3631 default
= 'best_fit')
3632 restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
3633 items
= (("none", "None", "No restrictions on vertex movement"),
3634 ("bounding_box", "Bounding box", "Vertices are restricted to "\
3635 "movement inside the bounding box of the selection")),
3636 description
= "Restrictions on how the vertices can be moved",
3640 def poll(cls
, context
):
3641 ob
= context
.active_object
3642 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3644 def draw(self
, context
):
3645 layout
= self
.layout
3646 col
= layout
.column()
3648 col
.prop(self
, "plane")
3649 #col.prop(self, "restriction")
3652 col_move
= col
.column(align
=True)
3653 row
= col_move
.row(align
=True)
3655 row
.prop(self
, "lock_x", text
= "X", icon
='LOCKED')
3657 row
.prop(self
, "lock_x", text
= "X", icon
='UNLOCKED')
3659 row
.prop(self
, "lock_y", text
= "Y", icon
='LOCKED')
3661 row
.prop(self
, "lock_y", text
= "Y", icon
='UNLOCKED')
3663 row
.prop(self
, "lock_z", text
= "Z", icon
='LOCKED')
3665 row
.prop(self
, "lock_z", text
= "Z", icon
='UNLOCKED')
3666 col_move
.prop(self
, "influence")
3668 def invoke(self
, context
, event
):
3669 # load custom settings
3671 return self
.execute(context
)
3673 def execute(self
, context
):
3675 global_undo
, object, bm
= initialise()
3676 settings_write(self
)
3677 # check cache to see if we can save time
3678 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Flatten",
3679 object, bm
, False, False)
3681 # order input into virtual loops
3682 loops
= flatten_get_input(bm
)
3683 loops
= check_loops(loops
, mapping
, bm
)
3685 # saving cache for faster execution next time
3687 cache_write("Flatten", object, bm
, False, False, False, loops
,
3692 # calculate plane and position of vertices on them
3693 com
, normal
= calculate_plane(bm
, loop
, method
=self
.plane
,
3695 to_move
= flatten_project(bm
, loop
, com
, normal
)
3696 if self
.restriction
== 'none':
3697 move
.append(to_move
)
3699 move
.append(to_move
)
3701 # move vertices to new locations
3702 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3703 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3706 move_verts(object, bm
, False, move
, lock
, self
.influence
)
3709 terminate(global_undo
)
3715 class GStretch(bpy
.types
.Operator
):
3716 bl_idname
= "mesh.looptools_gstretch"
3717 bl_label
= "Gstretch"
3718 bl_description
= "Stretch selected vertices to Grease Pencil stroke"
3719 bl_options
= {'REGISTER', 'UNDO'}
3721 conversion
= bpy
.props
.EnumProperty(name
= "Conversion",
3722 items
= (("distance", "Distance", "Set the distance between vertices "\
3723 "of the converted grease pencil stroke"),
3724 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "\
3725 "number of vertices that converted GP strokes will have"),
3726 ("vertices", "Exact vertices", "Set the exact number of vertices "\
3727 "that converted grease pencil strokes will have. Short strokes "\
3728 "with few points may contain less vertices than this number."),
3729 ("none", "No simplification", "Convert each grease pencil point "\
3731 description
= "If grease pencil strokes are converted to geometry, "\
3732 "use this simplification method",
3733 default
= 'limit_vertices')
3734 conversion_distance
= bpy
.props
.FloatProperty(name
= "Distance",
3735 description
= "Absolute distance between vertices along the converted "\
3736 "grease pencil stroke",
3741 conversion_max
= bpy
.props
.IntProperty(name
= "Max Vertices",
3742 description
= "Maximum number of vertices grease pencil strokes will "\
3743 "have, when they are converted to geomtery",
3747 update
= gstretch_update_min
)
3748 conversion_min
= bpy
.props
.IntProperty(name
= "Min Vertices",
3749 description
= "Minimum number of vertices grease pencil strokes will "\
3750 "have, when they are converted to geomtery",
3754 update
= gstretch_update_max
)
3755 conversion_vertices
= bpy
.props
.IntProperty(name
= "Vertices",
3756 description
= "Number of vertices grease pencil strokes will "\
3757 "have, when they are converted to geometry. If strokes have less "\
3758 "points than required, the 'Spread evenly' method is used",
3762 delete_strokes
= bpy
.props
.BoolProperty(name
="Delete strokes",
3763 description
= "Remove Grease Pencil strokes if they have been used "\
3764 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
3766 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3767 description
= "Force of the tool",
3772 subtype
= 'PERCENTAGE')
3773 lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
3774 description
= "Lock editing of the x-coordinate",
3776 lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
3777 description
= "Lock editing of the y-coordinate",
3779 lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
3780 description
= "Lock editing of the z-coordinate",
3782 method
= bpy
.props
.EnumProperty(name
= "Method",
3783 items
= (("project", "Project", "Project vertices onto the stroke, "\
3784 "using vertex normals and connected edges"),
3785 ("irregular", "Spread", "Distribute vertices along the full "\
3786 "stroke, retaining relative distances between the vertices"),
3787 ("regular", "Spread evenly", "Distribute vertices at regular "\
3788 "distances along the full stroke")),
3789 description
= "Method of distributing the vertices over the Grease "\
3791 default
= 'regular')
3794 def poll(cls
, context
):
3795 ob
= context
.active_object
3796 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3798 def draw(self
, context
):
3799 layout
= self
.layout
3800 col
= layout
.column()
3802 col
.prop(self
, "method")
3803 col
.prop(self
, "delete_strokes")
3806 col_conv
= col
.column(align
=True)
3807 col_conv
.prop(self
, "conversion", text
="")
3808 if self
.conversion
== 'distance':
3809 col_conv
.prop(self
, "conversion_distance")
3810 elif self
.conversion
== 'limit_vertices':
3811 row
= col_conv
.row(align
=True)
3812 row
.prop(self
, "conversion_min", text
="Min")
3813 row
.prop(self
, "conversion_max", text
="Max")
3814 elif self
.conversion
== 'vertices':
3815 col_conv
.prop(self
, "conversion_vertices")
3818 col_move
= col
.column(align
=True)
3819 row
= col_move
.row(align
=True)
3821 row
.prop(self
, "lock_x", text
= "X", icon
='LOCKED')
3823 row
.prop(self
, "lock_x", text
= "X", icon
='UNLOCKED')
3825 row
.prop(self
, "lock_y", text
= "Y", icon
='LOCKED')
3827 row
.prop(self
, "lock_y", text
= "Y", icon
='UNLOCKED')
3829 row
.prop(self
, "lock_z", text
= "Z", icon
='LOCKED')
3831 row
.prop(self
, "lock_z", text
= "Z", icon
='UNLOCKED')
3832 col_move
.prop(self
, "influence")
3834 def invoke(self
, context
, event
):
3835 # flush cached strokes
3836 if 'Gstretch' in looptools_cache
:
3837 looptools_cache
['Gstretch']['single_loops'] = []
3838 # load custom settings
3840 return self
.execute(context
)
3842 def execute(self
, context
):
3844 global_undo
, object, bm
= initialise()
3845 settings_write(self
)
3847 # check cache to see if we can save time
3848 cached
, safe_strokes
, loops
, derived
, mapping
= cache_read("Gstretch",
3849 object, bm
, False, False)
3851 straightening
= False
3853 strokes
= gstretch_safe_to_true_strokes(safe_strokes
)
3854 # cached strokes were flushed (see operator's invoke function)
3855 elif get_grease_pencil(object, context
):
3856 strokes
= gstretch_get_strokes(object, context
)
3858 # straightening function (no GP) -> loops ignore modifiers
3859 straightening
= True
3862 bm_mod
.verts
.ensure_lookup_table()
3863 bm_mod
.edges
.ensure_lookup_table()
3864 bm_mod
.faces
.ensure_lookup_table()
3865 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
3866 if not straightening
:
3867 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3869 # get loops and strokes
3870 if get_grease_pencil(object, context
):
3872 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
3873 context
.scene
, input='selected')
3874 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
3875 loops
= check_loops(loops
, mapping
, bm_mod
)
3877 strokes
= gstretch_get_strokes(object, context
)
3879 # straightening function (no GP) -> loops ignore modifiers
3883 bm_mod
.verts
.ensure_lookup_table()
3884 bm_mod
.edges
.ensure_lookup_table()
3885 bm_mod
.faces
.ensure_lookup_table()
3886 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if \
3887 edge
.select
and not edge
.hide
]
3888 loops
= get_connected_selections(edge_keys
)
3889 loops
= check_loops(loops
, mapping
, bm_mod
)
3890 # create fake strokes
3891 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
3893 # saving cache for faster execution next time
3896 safe_strokes
= gstretch_true_to_safe_strokes(strokes
)
3899 cache_write("Gstretch", object, bm
, False, False,
3900 safe_strokes
, loops
, derived
, mapping
)
3902 # pair loops and strokes
3903 ls_pairs
= gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
)
3904 ls_pairs
= gstretch_align_pairs(ls_pairs
, object, bm_mod
, self
.method
)
3908 # no selected geometry, convert GP to verts
3910 move
.append(gstretch_create_verts(object, bm
, strokes
,
3911 self
.method
, self
.conversion
, self
.conversion_distance
,
3912 self
.conversion_max
, self
.conversion_min
,
3913 self
.conversion_vertices
))
3914 for stroke
in strokes
:
3915 gstretch_erase_stroke(stroke
, context
)
3917 for (loop
, stroke
) in ls_pairs
:
3918 move
.append(gstretch_calculate_verts(loop
, stroke
, object,
3919 bm_mod
, self
.method
))
3920 if self
.delete_strokes
:
3921 if type(stroke
) != bpy
.types
.GPencilStroke
:
3922 # in case of cached fake stroke, get the real one
3923 if get_grease_pencil(object, context
):
3924 strokes
= gstretch_get_strokes(object, context
)
3925 if loops
and strokes
:
3926 ls_pairs
= gstretch_match_loops_strokes(loops
,
3927 strokes
, object, bm_mod
)
3928 ls_pairs
= gstretch_align_pairs(ls_pairs
,
3929 object, bm_mod
, self
.method
)
3930 for (l
, s
) in ls_pairs
:
3934 gstretch_erase_stroke(stroke
, context
)
3936 # move vertices to new locations
3937 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3938 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3941 bmesh
.update_edit_mesh(object.data
, tessface
=True, destructive
=True)
3942 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
3947 terminate(global_undo
)
3953 class Relax(bpy
.types
.Operator
):
3954 bl_idname
= "mesh.looptools_relax"
3956 bl_description
= "Relax the loop, so it is smoother"
3957 bl_options
= {'REGISTER', 'UNDO'}
3959 input = bpy
.props
.EnumProperty(name
= "Input",
3960 items
= (("all", "Parallel (all)", "Also use non-selected "\
3961 "parallel loops as input"),
3962 ("selected", "Selection","Only use selected vertices as input")),
3963 description
= "Loops that are relaxed",
3964 default
= 'selected')
3965 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
3966 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3967 ("linear", "Linear", "Simple and fast linear algorithm")),
3968 description
= "Algorithm used for interpolation",
3970 iterations
= bpy
.props
.EnumProperty(name
= "Iterations",
3971 items
= (("1", "1", "One"),
3972 ("3", "3", "Three"),
3974 ("10", "10", "Ten"),
3975 ("25", "25", "Twenty-five")),
3976 description
= "Number of times the loop is relaxed",
3978 regular
= bpy
.props
.BoolProperty(name
= "Regular",
3979 description
= "Distribute vertices at constant distances along the" \
3984 def poll(cls
, context
):
3985 ob
= context
.active_object
3986 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3988 def draw(self
, context
):
3989 layout
= self
.layout
3990 col
= layout
.column()
3992 col
.prop(self
, "interpolation")
3993 col
.prop(self
, "input")
3994 col
.prop(self
, "iterations")
3995 col
.prop(self
, "regular")
3997 def invoke(self
, context
, event
):
3998 # load custom settings
4000 return self
.execute(context
)
4002 def execute(self
, context
):
4004 global_undo
, object, bm
= initialise()
4005 settings_write(self
)
4006 # check cache to see if we can save time
4007 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Relax",
4008 object, bm
, self
.input, False)
4010 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
4013 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
4014 context
.scene
, self
.input)
4015 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4016 loops
= check_loops(loops
, mapping
, bm_mod
)
4017 knots
, points
= relax_calculate_knots(loops
)
4019 # saving cache for faster execution next time
4021 cache_write("Relax", object, bm
, self
.input, False, False, loops
,
4024 for iteration
in range(int(self
.iterations
)):
4025 # calculate splines and new positions
4026 tknots
, tpoints
= relax_calculate_t(bm_mod
, knots
, points
,
4029 for i
in range(len(knots
)):
4030 splines
.append(calculate_splines(self
.interpolation
, bm_mod
,
4031 tknots
[i
], knots
[i
]))
4032 move
= [relax_calculate_verts(bm_mod
, self
.interpolation
,
4033 tknots
, knots
, tpoints
, points
, splines
)]
4034 move_verts(object, bm
, mapping
, move
, False, -1)
4039 terminate(global_undo
)
4045 class Space(bpy
.types
.Operator
):
4046 bl_idname
= "mesh.looptools_space"
4048 bl_description
= "Space the vertices in a regular distrubtion on the loop"
4049 bl_options
= {'REGISTER', 'UNDO'}
4051 influence
= bpy
.props
.FloatProperty(name
= "Influence",
4052 description
= "Force of the tool",
4057 subtype
= 'PERCENTAGE')
4058 input = bpy
.props
.EnumProperty(name
= "Input",
4059 items
= (("all", "Parallel (all)", "Also use non-selected "\
4060 "parallel loops as input"),
4061 ("selected", "Selection","Only use selected vertices as input")),
4062 description
= "Loops that are spaced",
4063 default
= 'selected')
4064 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4065 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4066 ("linear", "Linear", "Vertices are projected on existing edges")),
4067 description
= "Algorithm used for interpolation",
4069 lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4070 description
= "Lock editing of the x-coordinate",
4072 lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4073 description
= "Lock editing of the y-coordinate",
4075 lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4076 description
= "Lock editing of the z-coordinate",
4080 def poll(cls
, context
):
4081 ob
= context
.active_object
4082 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4084 def draw(self
, context
):
4085 layout
= self
.layout
4086 col
= layout
.column()
4088 col
.prop(self
, "interpolation")
4089 col
.prop(self
, "input")
4092 col_move
= col
.column(align
=True)
4093 row
= col_move
.row(align
=True)
4095 row
.prop(self
, "lock_x", text
= "X", icon
='LOCKED')
4097 row
.prop(self
, "lock_x", text
= "X", icon
='UNLOCKED')
4099 row
.prop(self
, "lock_y", text
= "Y", icon
='LOCKED')
4101 row
.prop(self
, "lock_y", text
= "Y", icon
='UNLOCKED')
4103 row
.prop(self
, "lock_z", text
= "Z", icon
='LOCKED')
4105 row
.prop(self
, "lock_z", text
= "Z", icon
='UNLOCKED')
4106 col_move
.prop(self
, "influence")
4108 def invoke(self
, context
, event
):
4109 # load custom settings
4111 return self
.execute(context
)
4113 def execute(self
, context
):
4115 global_undo
, object, bm
= initialise()
4116 settings_write(self
)
4117 # check cache to see if we can save time
4118 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Space",
4119 object, bm
, self
.input, False)
4121 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
4124 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
4125 context
.scene
, self
.input)
4126 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4127 loops
= check_loops(loops
, mapping
, bm_mod
)
4129 # saving cache for faster execution next time
4131 cache_write("Space", object, bm
, self
.input, False, False, loops
,
4136 # calculate splines and new positions
4137 if loop
[1]: # circular
4138 loop
[0].append(loop
[0][0])
4139 tknots
, tpoints
= space_calculate_t(bm_mod
, loop
[0][:])
4140 splines
= calculate_splines(self
.interpolation
, bm_mod
,
4142 move
.append(space_calculate_verts(bm_mod
, self
.interpolation
,
4143 tknots
, tpoints
, loop
[0][:-1], splines
))
4144 # move vertices to new locations
4145 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4146 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4149 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4154 terminate(global_undo
)
4159 ##########################################
4160 ####### GUI and registration #############
4161 ##########################################
4163 # menu containing all tools
4164 class VIEW3D_MT_edit_mesh_looptools(bpy
.types
.Menu
):
4165 bl_label
= "LoopTools"
4167 def draw(self
, context
):
4168 layout
= self
.layout
4170 layout
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4171 layout
.operator("mesh.looptools_circle")
4172 layout
.operator("mesh.looptools_curve")
4173 layout
.operator("mesh.looptools_flatten")
4174 layout
.operator("mesh.looptools_gstretch")
4175 layout
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4176 layout
.operator("mesh.looptools_relax")
4177 layout
.operator("mesh.looptools_space")
4180 # panel containing all tools
4181 class VIEW3D_PT_tools_looptools(bpy
.types
.Panel
):
4182 bl_space_type
= 'VIEW_3D'
4183 bl_region_type
= 'TOOLS'
4184 bl_category
= 'Tools'
4185 bl_context
= "mesh_edit"
4186 bl_label
= "LoopTools"
4187 bl_options
= {'DEFAULT_CLOSED'}
4189 def draw(self
, context
):
4190 layout
= self
.layout
4191 col
= layout
.column(align
=True)
4192 lt
= context
.window_manager
.looptools
4194 # bridge - first line
4195 split
= col
.split(percentage
=0.15, align
=True)
4196 if lt
.display_bridge
:
4197 split
.prop(lt
, "display_bridge", text
="", icon
='DOWNARROW_HLT')
4199 split
.prop(lt
, "display_bridge", text
="", icon
='RIGHTARROW')
4200 split
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4202 if lt
.display_bridge
:
4203 box
= col
.column(align
=True).box().column()
4204 #box.prop(self, "mode")
4207 col_top
= box
.column(align
=True)
4208 row
= col_top
.row(align
=True)
4209 col_left
= row
.column(align
=True)
4210 col_right
= row
.column(align
=True)
4211 col_right
.active
= lt
.bridge_segments
!= 1
4212 col_left
.prop(lt
, "bridge_segments")
4213 col_right
.prop(lt
, "bridge_min_width", text
="")
4215 bottom_left
= col_left
.row()
4216 bottom_left
.active
= lt
.bridge_segments
!= 1
4217 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4218 bottom_right
= col_right
.row()
4219 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4220 bottom_right
.prop(lt
, "bridge_cubic_strength")
4221 # boolean properties
4222 col_top
.prop(lt
, "bridge_remove_faces")
4224 # override properties
4226 row
= box
.row(align
= True)
4227 row
.prop(lt
, "bridge_twist")
4228 row
.prop(lt
, "bridge_reverse")
4230 # circle - first line
4231 split
= col
.split(percentage
=0.15, align
=True)
4232 if lt
.display_circle
:
4233 split
.prop(lt
, "display_circle", text
="", icon
='DOWNARROW_HLT')
4235 split
.prop(lt
, "display_circle", text
="", icon
='RIGHTARROW')
4236 split
.operator("mesh.looptools_circle")
4238 if lt
.display_circle
:
4239 box
= col
.column(align
=True).box().column()
4240 box
.prop(lt
, "circle_fit")
4243 box
.prop(lt
, "circle_flatten")
4244 row
= box
.row(align
=True)
4245 row
.prop(lt
, "circle_custom_radius")
4246 row_right
= row
.row(align
=True)
4247 row_right
.active
= lt
.circle_custom_radius
4248 row_right
.prop(lt
, "circle_radius", text
="")
4249 box
.prop(lt
, "circle_regular")
4252 col_move
= box
.column(align
=True)
4253 row
= col_move
.row(align
=True)
4254 if lt
.circle_lock_x
:
4255 row
.prop(lt
, "circle_lock_x", text
= "X", icon
='LOCKED')
4257 row
.prop(lt
, "circle_lock_x", text
= "X", icon
='UNLOCKED')
4258 if lt
.circle_lock_y
:
4259 row
.prop(lt
, "circle_lock_y", text
= "Y", icon
='LOCKED')
4261 row
.prop(lt
, "circle_lock_y", text
= "Y", icon
='UNLOCKED')
4262 if lt
.circle_lock_z
:
4263 row
.prop(lt
, "circle_lock_z", text
= "Z", icon
='LOCKED')
4265 row
.prop(lt
, "circle_lock_z", text
= "Z", icon
='UNLOCKED')
4266 col_move
.prop(lt
, "circle_influence")
4268 # curve - first line
4269 split
= col
.split(percentage
=0.15, align
=True)
4270 if lt
.display_curve
:
4271 split
.prop(lt
, "display_curve", text
="", icon
='DOWNARROW_HLT')
4273 split
.prop(lt
, "display_curve", text
="", icon
='RIGHTARROW')
4274 split
.operator("mesh.looptools_curve")
4276 if lt
.display_curve
:
4277 box
= col
.column(align
=True).box().column()
4278 box
.prop(lt
, "curve_interpolation")
4279 box
.prop(lt
, "curve_restriction")
4280 box
.prop(lt
, "curve_boundaries")
4281 box
.prop(lt
, "curve_regular")
4284 col_move
= box
.column(align
=True)
4285 row
= col_move
.row(align
=True)
4287 row
.prop(lt
, "curve_lock_x", text
= "X", icon
='LOCKED')
4289 row
.prop(lt
, "curve_lock_x", text
= "X", icon
='UNLOCKED')
4291 row
.prop(lt
, "curve_lock_y", text
= "Y", icon
='LOCKED')
4293 row
.prop(lt
, "curve_lock_y", text
= "Y", icon
='UNLOCKED')
4295 row
.prop(lt
, "curve_lock_z", text
= "Z", icon
='LOCKED')
4297 row
.prop(lt
, "curve_lock_z", text
= "Z", icon
='UNLOCKED')
4298 col_move
.prop(lt
, "curve_influence")
4300 # flatten - first line
4301 split
= col
.split(percentage
=0.15, align
=True)
4302 if lt
.display_flatten
:
4303 split
.prop(lt
, "display_flatten", text
="", icon
='DOWNARROW_HLT')
4305 split
.prop(lt
, "display_flatten", text
="", icon
='RIGHTARROW')
4306 split
.operator("mesh.looptools_flatten")
4307 # flatten - settings
4308 if lt
.display_flatten
:
4309 box
= col
.column(align
=True).box().column()
4310 box
.prop(lt
, "flatten_plane")
4311 #box.prop(lt, "flatten_restriction")
4314 col_move
= box
.column(align
=True)
4315 row
= col_move
.row(align
=True)
4316 if lt
.flatten_lock_x
:
4317 row
.prop(lt
, "flatten_lock_x", text
= "X", icon
='LOCKED')
4319 row
.prop(lt
, "flatten_lock_x", text
= "X", icon
='UNLOCKED')
4320 if lt
.flatten_lock_y
:
4321 row
.prop(lt
, "flatten_lock_y", text
= "Y", icon
='LOCKED')
4323 row
.prop(lt
, "flatten_lock_y", text
= "Y", icon
='UNLOCKED')
4324 if lt
.flatten_lock_z
:
4325 row
.prop(lt
, "flatten_lock_z", text
= "Z", icon
='LOCKED')
4327 row
.prop(lt
, "flatten_lock_z", text
= "Z", icon
='UNLOCKED')
4328 col_move
.prop(lt
, "flatten_influence")
4330 # gstretch - first line
4331 split
= col
.split(percentage
=0.15, align
=True)
4332 if lt
.display_gstretch
:
4333 split
.prop(lt
, "display_gstretch", text
="", icon
='DOWNARROW_HLT')
4335 split
.prop(lt
, "display_gstretch", text
="", icon
='RIGHTARROW')
4336 split
.operator("mesh.looptools_gstretch")
4338 if lt
.display_gstretch
:
4339 box
= col
.column(align
=True).box().column()
4340 box
.prop(lt
, "gstretch_method")
4341 box
.prop(lt
, "gstretch_delete_strokes")
4344 col_conv
= box
.column(align
=True)
4345 col_conv
.prop(lt
, "gstretch_conversion", text
="")
4346 if lt
.gstretch_conversion
== 'distance':
4347 col_conv
.prop(lt
, "gstretch_conversion_distance")
4348 elif lt
.gstretch_conversion
== 'limit_vertices':
4349 row
= col_conv
.row(align
=True)
4350 row
.prop(lt
, "gstretch_conversion_min", text
="Min")
4351 row
.prop(lt
, "gstretch_conversion_max", text
="Max")
4352 elif lt
.gstretch_conversion
== 'vertices':
4353 col_conv
.prop(lt
, "gstretch_conversion_vertices")
4356 col_move
= box
.column(align
=True)
4357 row
= col_move
.row(align
=True)
4358 if lt
.gstretch_lock_x
:
4359 row
.prop(lt
, "gstretch_lock_x", text
= "X", icon
='LOCKED')
4361 row
.prop(lt
, "gstretch_lock_x", text
= "X", icon
='UNLOCKED')
4362 if lt
.gstretch_lock_y
:
4363 row
.prop(lt
, "gstretch_lock_y", text
= "Y", icon
='LOCKED')
4365 row
.prop(lt
, "gstretch_lock_y", text
= "Y", icon
='UNLOCKED')
4366 if lt
.gstretch_lock_z
:
4367 row
.prop(lt
, "gstretch_lock_z", text
= "Z", icon
='LOCKED')
4369 row
.prop(lt
, "gstretch_lock_z", text
= "Z", icon
='UNLOCKED')
4370 col_move
.prop(lt
, "gstretch_influence")
4373 split
= col
.split(percentage
=0.15, align
=True)
4375 split
.prop(lt
, "display_loft", text
="", icon
='DOWNARROW_HLT')
4377 split
.prop(lt
, "display_loft", text
="", icon
='RIGHTARROW')
4378 split
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4381 box
= col
.column(align
=True).box().column()
4382 #box.prop(self, "mode")
4385 col_top
= box
.column(align
=True)
4386 row
= col_top
.row(align
=True)
4387 col_left
= row
.column(align
=True)
4388 col_right
= row
.column(align
=True)
4389 col_right
.active
= lt
.bridge_segments
!= 1
4390 col_left
.prop(lt
, "bridge_segments")
4391 col_right
.prop(lt
, "bridge_min_width", text
="")
4393 bottom_left
= col_left
.row()
4394 bottom_left
.active
= lt
.bridge_segments
!= 1
4395 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4396 bottom_right
= col_right
.row()
4397 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4398 bottom_right
.prop(lt
, "bridge_cubic_strength")
4399 # boolean properties
4400 col_top
.prop(lt
, "bridge_remove_faces")
4401 col_top
.prop(lt
, "bridge_loft_loop")
4403 # override properties
4405 row
= box
.row(align
= True)
4406 row
.prop(lt
, "bridge_twist")
4407 row
.prop(lt
, "bridge_reverse")
4409 # relax - first line
4410 split
= col
.split(percentage
=0.15, align
=True)
4411 if lt
.display_relax
:
4412 split
.prop(lt
, "display_relax", text
="", icon
='DOWNARROW_HLT')
4414 split
.prop(lt
, "display_relax", text
="", icon
='RIGHTARROW')
4415 split
.operator("mesh.looptools_relax")
4417 if lt
.display_relax
:
4418 box
= col
.column(align
=True).box().column()
4419 box
.prop(lt
, "relax_interpolation")
4420 box
.prop(lt
, "relax_input")
4421 box
.prop(lt
, "relax_iterations")
4422 box
.prop(lt
, "relax_regular")
4424 # space - first line
4425 split
= col
.split(percentage
=0.15, align
=True)
4426 if lt
.display_space
:
4427 split
.prop(lt
, "display_space", text
="", icon
='DOWNARROW_HLT')
4429 split
.prop(lt
, "display_space", text
="", icon
='RIGHTARROW')
4430 split
.operator("mesh.looptools_space")
4432 if lt
.display_space
:
4433 box
= col
.column(align
=True).box().column()
4434 box
.prop(lt
, "space_interpolation")
4435 box
.prop(lt
, "space_input")
4438 col_move
= box
.column(align
=True)
4439 row
= col_move
.row(align
=True)
4441 row
.prop(lt
, "space_lock_x", text
= "X", icon
='LOCKED')
4443 row
.prop(lt
, "space_lock_x", text
= "X", icon
='UNLOCKED')
4445 row
.prop(lt
, "space_lock_y", text
= "Y", icon
='LOCKED')
4447 row
.prop(lt
, "space_lock_y", text
= "Y", icon
='UNLOCKED')
4449 row
.prop(lt
, "space_lock_z", text
= "Z", icon
='LOCKED')
4451 row
.prop(lt
, "space_lock_z", text
= "Z", icon
='UNLOCKED')
4452 col_move
.prop(lt
, "space_influence")
4455 # property group containing all properties for the gui in the panel
4456 class LoopToolsProps(bpy
.types
.PropertyGroup
):
4458 Fake module like class
4459 bpy.context.window_manager.looptools
4462 # general display properties
4463 display_bridge
= bpy
.props
.BoolProperty(name
= "Bridge settings",
4464 description
= "Display settings of the Bridge tool",
4466 display_circle
= bpy
.props
.BoolProperty(name
= "Circle settings",
4467 description
= "Display settings of the Circle tool",
4469 display_curve
= bpy
.props
.BoolProperty(name
= "Curve settings",
4470 description
= "Display settings of the Curve tool",
4472 display_flatten
= bpy
.props
.BoolProperty(name
= "Flatten settings",
4473 description
= "Display settings of the Flatten tool",
4475 display_gstretch
= bpy
.props
.BoolProperty(name
= "Gstretch settings",
4476 description
= "Display settings of the Gstretch tool",
4478 display_loft
= bpy
.props
.BoolProperty(name
= "Loft settings",
4479 description
= "Display settings of the Loft tool",
4481 display_relax
= bpy
.props
.BoolProperty(name
= "Relax settings",
4482 description
= "Display settings of the Relax tool",
4484 display_space
= bpy
.props
.BoolProperty(name
= "Space settings",
4485 description
= "Display settings of the Space tool",
4489 bridge_cubic_strength
= bpy
.props
.FloatProperty(name
= "Strength",
4490 description
= "Higher strength results in more fluid curves",
4494 bridge_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation mode",
4495 items
= (('cubic', "Cubic", "Gives curved results"),
4496 ('linear', "Linear", "Basic, fast, straight interpolation")),
4497 description
= "Interpolation mode: algorithm used when creating "\
4500 bridge_loft
= bpy
.props
.BoolProperty(name
= "Loft",
4501 description
= "Loft multiple loops, instead of considering them as "\
4502 "a multi-input for bridging",
4504 bridge_loft_loop
= bpy
.props
.BoolProperty(name
= "Loop",
4505 description
= "Connect the first and the last loop with each other",
4507 bridge_min_width
= bpy
.props
.IntProperty(name
= "Minimum width",
4508 description
= "Segments with an edge smaller than this are merged "\
4509 "(compared to base edge)",
4513 subtype
= 'PERCENTAGE')
4514 bridge_mode
= bpy
.props
.EnumProperty(name
= "Mode",
4515 items
= (('basic', "Basic", "Fast algorithm"),
4516 ('shortest', "Shortest edge", "Slower algorithm with " \
4517 "better vertex matching")),
4518 description
= "Algorithm used for bridging",
4519 default
= 'shortest')
4520 bridge_remove_faces
= bpy
.props
.BoolProperty(name
= "Remove faces",
4521 description
= "Remove faces that are internal after bridging",
4523 bridge_reverse
= bpy
.props
.BoolProperty(name
= "Reverse",
4524 description
= "Manually override the direction in which the loops "\
4525 "are bridged. Only use if the tool gives the wrong " \
4528 bridge_segments
= bpy
.props
.IntProperty(name
= "Segments",
4529 description
= "Number of segments used to bridge the gap "\
4534 bridge_twist
= bpy
.props
.IntProperty(name
= "Twist",
4535 description
= "Twist what vertices are connected to each other",
4539 circle_custom_radius
= bpy
.props
.BoolProperty(name
= "Radius",
4540 description
= "Force a custom radius",
4542 circle_fit
= bpy
.props
.EnumProperty(name
= "Method",
4543 items
= (("best", "Best fit", "Non-linear least squares"),
4544 ("inside", "Fit inside","Only move vertices towards the center")),
4545 description
= "Method used for fitting a circle to the vertices",
4547 circle_flatten
= bpy
.props
.BoolProperty(name
= "Flatten",
4548 description
= "Flatten the circle, instead of projecting it on the " \
4551 circle_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4552 description
= "Force of the tool",
4557 subtype
= 'PERCENTAGE')
4558 circle_lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4559 description
= "Lock editing of the x-coordinate",
4561 circle_lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4562 description
= "Lock editing of the y-coordinate",
4564 circle_lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4565 description
= "Lock editing of the z-coordinate",
4567 circle_radius
= bpy
.props
.FloatProperty(name
= "Radius",
4568 description
= "Custom radius for circle",
4572 circle_regular
= bpy
.props
.BoolProperty(name
= "Regular",
4573 description
= "Distribute vertices at constant distances along the " \
4578 curve_boundaries
= bpy
.props
.BoolProperty(name
= "Boundaries",
4579 description
= "Limit the tool to work within the boundaries of the "\
4580 "selected vertices",
4582 curve_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4583 description
= "Force of the tool",
4588 subtype
= 'PERCENTAGE')
4589 curve_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4590 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4591 ("linear", "Linear", "Simple and fast linear algorithm")),
4592 description
= "Algorithm used for interpolation",
4594 curve_lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4595 description
= "Lock editing of the x-coordinate",
4597 curve_lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4598 description
= "Lock editing of the y-coordinate",
4600 curve_lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4601 description
= "Lock editing of the z-coordinate",
4603 curve_regular
= bpy
.props
.BoolProperty(name
= "Regular",
4604 description
= "Distribute vertices at constant distances along the " \
4607 curve_restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
4608 items
= (("none", "None", "No restrictions on vertex movement"),
4609 ("extrude", "Extrude only","Only allow extrusions (no "\
4611 ("indent", "Indent only", "Only allow indentation (no "\
4613 description
= "Restrictions on how the vertices can be moved",
4616 # flatten properties
4617 flatten_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4618 description
= "Force of the tool",
4623 subtype
= 'PERCENTAGE')
4624 flatten_lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4625 description
= "Lock editing of the x-coordinate",
4627 flatten_lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4628 description
= "Lock editing of the y-coordinate",
4630 flatten_lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4631 description
= "Lock editing of the z-coordinate",
4633 flatten_plane
= bpy
.props
.EnumProperty(name
= "Plane",
4634 items
= (("best_fit", "Best fit", "Calculate a best fitting plane"),
4635 ("normal", "Normal", "Derive plane from averaging vertex "\
4637 ("view", "View", "Flatten on a plane perpendicular to the "\
4639 description
= "Plane on which vertices are flattened",
4640 default
= 'best_fit')
4641 flatten_restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
4642 items
= (("none", "None", "No restrictions on vertex movement"),
4643 ("bounding_box", "Bounding box", "Vertices are restricted to "\
4644 "movement inside the bounding box of the selection")),
4645 description
= "Restrictions on how the vertices can be moved",
4648 # gstretch properties
4649 gstretch_conversion
= bpy
.props
.EnumProperty(name
= "Conversion",
4650 items
= (("distance", "Distance", "Set the distance between vertices "\
4651 "of the converted grease pencil stroke"),
4652 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "\
4653 "number of vertices that converted GP strokes will have"),
4654 ("vertices", "Exact vertices", "Set the exact number of vertices "\
4655 "that converted grease pencil strokes will have. Short strokes "\
4656 "with few points may contain less vertices than this number."),
4657 ("none", "No simplification", "Convert each grease pencil point "\
4659 description
= "If grease pencil strokes are converted to geometry, "\
4660 "use this simplification method",
4661 default
= 'limit_vertices')
4662 gstretch_conversion_distance
= bpy
.props
.FloatProperty(name
= "Distance",
4663 description
= "Absolute distance between vertices along the converted "\
4664 "grease pencil stroke",
4669 gstretch_conversion_max
= bpy
.props
.IntProperty(name
= "Max Vertices",
4670 description
= "Maximum number of vertices grease pencil strokes will "\
4671 "have, when they are converted to geomtery",
4675 update
= gstretch_update_min
)
4676 gstretch_conversion_min
= bpy
.props
.IntProperty(name
= "Min Vertices",
4677 description
= "Minimum number of vertices grease pencil strokes will "\
4678 "have, when they are converted to geomtery",
4682 update
= gstretch_update_max
)
4683 gstretch_conversion_vertices
= bpy
.props
.IntProperty(name
= "Vertices",
4684 description
= "Number of vertices grease pencil strokes will "\
4685 "have, when they are converted to geometry. If strokes have less "\
4686 "points than required, the 'Spread evenly' method is used",
4690 gstretch_delete_strokes
= bpy
.props
.BoolProperty(name
="Delete strokes",
4691 description
= "Remove Grease Pencil strokes if they have been used "\
4692 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
4694 gstretch_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4695 description
= "Force of the tool",
4700 subtype
= 'PERCENTAGE')
4701 gstretch_lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4702 description
= "Lock editing of the x-coordinate",
4704 gstretch_lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4705 description
= "Lock editing of the y-coordinate",
4707 gstretch_lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4708 description
= "Lock editing of the z-coordinate",
4710 gstretch_method
= bpy
.props
.EnumProperty(name
= "Method",
4711 items
= (("project", "Project", "Project vertices onto the stroke, "\
4712 "using vertex normals and connected edges"),
4713 ("irregular", "Spread", "Distribute vertices along the full "\
4714 "stroke, retaining relative distances between the vertices"),
4715 ("regular", "Spread evenly", "Distribute vertices at regular "\
4716 "distances along the full stroke")),
4717 description
= "Method of distributing the vertices over the Grease "\
4719 default
= 'regular')
4722 relax_input
= bpy
.props
.EnumProperty(name
= "Input",
4723 items
= (("all", "Parallel (all)", "Also use non-selected "\
4724 "parallel loops as input"),
4725 ("selected", "Selection","Only use selected vertices as input")),
4726 description
= "Loops that are relaxed",
4727 default
= 'selected')
4728 relax_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4729 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4730 ("linear", "Linear", "Simple and fast linear algorithm")),
4731 description
= "Algorithm used for interpolation",
4733 relax_iterations
= bpy
.props
.EnumProperty(name
= "Iterations",
4734 items
= (("1", "1", "One"),
4735 ("3", "3", "Three"),
4737 ("10", "10", "Ten"),
4738 ("25", "25", "Twenty-five")),
4739 description
= "Number of times the loop is relaxed",
4741 relax_regular
= bpy
.props
.BoolProperty(name
= "Regular",
4742 description
= "Distribute vertices at constant distances along the" \
4747 space_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4748 description
= "Force of the tool",
4753 subtype
= 'PERCENTAGE')
4754 space_input
= bpy
.props
.EnumProperty(name
= "Input",
4755 items
= (("all", "Parallel (all)", "Also use non-selected "\
4756 "parallel loops as input"),
4757 ("selected", "Selection","Only use selected vertices as input")),
4758 description
= "Loops that are spaced",
4759 default
= 'selected')
4760 space_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4761 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4762 ("linear", "Linear", "Vertices are projected on existing edges")),
4763 description
= "Algorithm used for interpolation",
4765 space_lock_x
= bpy
.props
.BoolProperty(name
= "Lock X",
4766 description
= "Lock editing of the x-coordinate",
4768 space_lock_y
= bpy
.props
.BoolProperty(name
= "Lock Y",
4769 description
= "Lock editing of the y-coordinate",
4771 space_lock_z
= bpy
.props
.BoolProperty(name
= "Lock Z",
4772 description
= "Lock editing of the z-coordinate",
4776 # draw function for integration in menus
4777 def menu_func(self
, context
):
4778 self
.layout
.menu("VIEW3D_MT_edit_mesh_looptools")
4779 self
.layout
.separator()
4782 # define classes for registration
4783 classes
= [VIEW3D_MT_edit_mesh_looptools
,
4784 VIEW3D_PT_tools_looptools
,
4795 # registering and menu integration
4798 bpy
.utils
.register_class(c
)
4799 bpy
.types
.VIEW3D_MT_edit_mesh_specials
.prepend(menu_func
)
4800 bpy
.types
.WindowManager
.looptools
= bpy
.props
.PointerProperty(\
4801 type = LoopToolsProps
)
4804 # unregistering and removing menus
4807 bpy
.utils
.unregister_class(c
)
4808 bpy
.types
.VIEW3D_MT_edit_mesh_specials
.remove(menu_func
)
4810 del bpy
.types
.WindowManager
.looptools
4815 if __name__
== "__main__":