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, 80, 0),
24 "location": "View3D > Toolbar and View3D > Specials (W-key)",
26 "description": "Mesh modelling toolkit. Several tools to aid modelling",
27 "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
28 "Scripts/Modeling/LoopTools",
38 from bpy_extras
import view3d_utils
39 from bpy
.types
import (
46 from bpy
.props
import (
55 # ########################################
56 # ##### General functions ################
57 # ########################################
59 # used by all tools to improve speed on reruns Unlink
63 def get_grease_pencil(object, context
):
64 gp
= object.grease_pencil
66 gp
= context
.scene
.grease_pencil
70 # force a full recalculation next time
71 def cache_delete(tool
):
72 if tool
in looptools_cache
:
73 del looptools_cache
[tool
]
76 # check cache for stored information
77 def cache_read(tool
, object, bm
, input_method
, boundaries
):
78 # current tool not cached yet
79 if tool
not in looptools_cache
:
80 return(False, False, False, False, False)
81 # check if selected object didn't change
82 if object.name
!= looptools_cache
[tool
]["object"]:
83 return(False, False, False, False, False)
84 # check if input didn't change
85 if input_method
!= looptools_cache
[tool
]["input_method"]:
86 return(False, False, False, False, False)
87 if boundaries
!= looptools_cache
[tool
]["boundaries"]:
88 return(False, False, False, False, False)
89 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
and
91 if modifiers
!= looptools_cache
[tool
]["modifiers"]:
92 return(False, False, False, False, False)
93 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
94 if input != looptools_cache
[tool
]["input"]:
95 return(False, False, False, False, False)
97 single_loops
= looptools_cache
[tool
]["single_loops"]
98 loops
= looptools_cache
[tool
]["loops"]
99 derived
= looptools_cache
[tool
]["derived"]
100 mapping
= looptools_cache
[tool
]["mapping"]
102 return(True, single_loops
, loops
, derived
, mapping
)
105 # store information in the cache
106 def cache_write(tool
, object, bm
, input_method
, boundaries
, single_loops
,
107 loops
, derived
, mapping
):
108 # clear cache of current tool
109 if tool
in looptools_cache
:
110 del looptools_cache
[tool
]
111 # prepare values to be saved to cache
112 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
113 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
114 and mod
.type == 'MIRROR']
116 looptools_cache
[tool
] = {
117 "input": input, "object": object.name
,
118 "input_method": input_method
, "boundaries": boundaries
,
119 "single_loops": single_loops
, "loops": loops
,
120 "derived": derived
, "mapping": mapping
, "modifiers": modifiers
}
123 # calculates natural cubic splines through all given knots
124 def calculate_cubic_splines(bm_mod
, tknots
, knots
):
125 # hack for circular loops
126 if knots
[0] == knots
[-1] and len(knots
) > 1:
129 for k
in range(-1, -5, -1):
130 if k
- 1 < -len(knots
):
132 k_new1
.append(knots
[k
- 1])
135 if k
+ 1 > len(knots
) - 1:
137 k_new2
.append(knots
[k
+ 1])
144 for t
in range(-1, -5, -1):
145 if t
- 1 < -len(tknots
):
147 total1
+= tknots
[t
] - tknots
[t
- 1]
148 t_new1
.append(tknots
[0] - total1
)
152 if t
+ 1 > len(tknots
) - 1:
154 total2
+= tknots
[t
+ 1] - tknots
[t
]
155 t_new2
.append(tknots
[-1] + total2
)
168 locs
= [bm_mod
.verts
[k
].co
[:] for k
in knots
]
175 for i
in range(n
- 1):
176 if x
[i
+ 1] - x
[i
] == 0:
179 h
.append(x
[i
+ 1] - x
[i
])
181 for i
in range(1, n
- 1):
182 q
.append(3 / h
[i
] * (a
[i
+ 1] - a
[i
]) - 3 / h
[i
- 1] * (a
[i
] - a
[i
- 1]))
186 for i
in range(1, n
- 1):
187 l
.append(2 * (x
[i
+ 1] - x
[i
- 1]) - h
[i
- 1] * u
[i
- 1])
190 u
.append(h
[i
] / l
[i
])
191 z
.append((q
[i
] - h
[i
- 1] * z
[i
- 1]) / l
[i
])
194 b
= [False for i
in range(n
- 1)]
195 c
= [False for i
in range(n
)]
196 d
= [False for i
in range(n
- 1)]
198 for i
in range(n
- 2, -1, -1):
199 c
[i
] = z
[i
] - u
[i
] * c
[i
+ 1]
200 b
[i
] = (a
[i
+ 1] - a
[i
]) / h
[i
] - h
[i
] * (c
[i
+ 1] + 2 * c
[i
]) / 3
201 d
[i
] = (c
[i
+ 1] - c
[i
]) / (3 * h
[i
])
202 for i
in range(n
- 1):
203 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
205 for i
in range(len(knots
) - 1):
206 splines
.append([result
[i
], result
[i
+ n
- 1], result
[i
+ (n
- 1) * 2]])
207 if circular
: # cleaning up after hack
209 tknots
= tknots
[4:-4]
214 # calculates linear splines through all given knots
215 def calculate_linear_splines(bm_mod
, tknots
, knots
):
217 for i
in range(len(knots
) - 1):
218 a
= bm_mod
.verts
[knots
[i
]].co
219 b
= bm_mod
.verts
[knots
[i
+ 1]].co
222 u
= tknots
[i
+ 1] - t
223 splines
.append([a
, d
, t
, u
]) # [locStart, locDif, tStart, tDif]
228 # calculate a best-fit plane to the given vertices
229 def calculate_plane(bm_mod
, loop
, method
="best_fit", object=False):
230 # getting the vertex locations
231 locs
= [bm_mod
.verts
[v
].co
.copy() for v
in loop
[0]]
233 # calculating the center of masss
234 com
= mathutils
.Vector()
240 if method
== 'best_fit':
241 # creating the covariance matrix
242 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
247 mat
[0][0] += (loc
[0] - x
) ** 2
248 mat
[1][0] += (loc
[0] - x
) * (loc
[1] - y
)
249 mat
[2][0] += (loc
[0] - x
) * (loc
[2] - z
)
250 mat
[0][1] += (loc
[1] - y
) * (loc
[0] - x
)
251 mat
[1][1] += (loc
[1] - y
) ** 2
252 mat
[2][1] += (loc
[1] - y
) * (loc
[2] - z
)
253 mat
[0][2] += (loc
[2] - z
) * (loc
[0] - x
)
254 mat
[1][2] += (loc
[2] - z
) * (loc
[1] - y
)
255 mat
[2][2] += (loc
[2] - z
) ** 2
257 # calculating the normal to the plane
260 mat
= matrix_invert(mat
)
263 if math
.fabs(sum(mat
[0])) < math
.fabs(sum(mat
[1])):
264 if math
.fabs(sum(mat
[0])) < math
.fabs(sum(mat
[2])):
266 elif math
.fabs(sum(mat
[1])) < math
.fabs(sum(mat
[2])):
269 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
271 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
273 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
275 # warning! this is different from .normalize()
277 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
278 for i
in range(itermax
):
286 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
289 elif method
== 'normal':
290 # averaging the vertex normals
291 v_normals
= [bm_mod
.verts
[v
].normal
for v
in loop
[0]]
292 normal
= mathutils
.Vector()
293 for v_normal
in v_normals
:
295 normal
/= len(v_normals
)
298 elif method
== 'view':
299 # calculate view normal
300 rotation
= bpy
.context
.space_data
.region_3d
.view_matrix
.to_3x3().\
302 normal
= rotation
* mathutils
.Vector((0.0, 0.0, 1.0))
304 normal
= object.matrix_world
.inverted().to_euler().to_matrix() * \
310 # calculate splines based on given interpolation method (controller function)
311 def calculate_splines(interpolation
, bm_mod
, tknots
, knots
):
312 if interpolation
== 'cubic':
313 splines
= calculate_cubic_splines(bm_mod
, tknots
, knots
[:])
314 else: # interpolations == 'linear'
315 splines
= calculate_linear_splines(bm_mod
, tknots
, knots
[:])
320 # check loops and only return valid ones
321 def check_loops(loops
, mapping
, bm_mod
):
323 for loop
, circular
in loops
:
324 # loop needs to have at least 3 vertices
327 # loop needs at least 1 vertex in the original, non-mirrored mesh
331 if mapping
[vert
] > -1:
336 # vertices can not all be at the same location
338 for i
in range(len(loop
) - 1):
339 if (bm_mod
.verts
[loop
[i
]].co
- bm_mod
.verts
[loop
[i
+ 1]].co
).length
> 1e-6:
344 # passed all tests, loop is valid
345 valid_loops
.append([loop
, circular
])
350 # input: bmesh, output: dict with the edge-key as key and face-index as value
351 def dict_edge_faces(bm
):
352 edge_faces
= dict([[edgekey(edge
), []] for edge
in bm
.edges
if not edge
.hide
])
353 for face
in bm
.faces
:
356 for key
in face_edgekeys(face
):
357 edge_faces
[key
].append(face
.index
)
362 # input: bmesh (edge-faces optional), output: dict with face-face connections
363 def dict_face_faces(bm
, edge_faces
=False):
365 edge_faces
= dict_edge_faces(bm
)
367 connected_faces
= dict([[face
.index
, []] for face
in bm
.faces
if not face
.hide
])
368 for face
in bm
.faces
:
371 for edge_key
in face_edgekeys(face
):
372 for connected_face
in edge_faces
[edge_key
]:
373 if connected_face
== face
.index
:
375 connected_faces
[face
.index
].append(connected_face
)
377 return(connected_faces
)
380 # input: bmesh, output: dict with the vert index as key and edge-keys as value
381 def dict_vert_edges(bm
):
382 vert_edges
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
383 for edge
in bm
.edges
:
388 vert_edges
[vert
].append(ek
)
393 # input: bmesh, output: dict with the vert index as key and face index as value
394 def dict_vert_faces(bm
):
395 vert_faces
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
396 for face
in bm
.faces
:
398 for vert
in face
.verts
:
399 vert_faces
[vert
.index
].append(face
.index
)
404 # input: list of edge-keys, output: dictionary with vertex-vertex connections
405 def dict_vert_verts(edge_keys
):
406 # create connection data
410 if ek
[i
] in vert_verts
:
411 vert_verts
[ek
[i
]].append(ek
[1 - i
])
413 vert_verts
[ek
[i
]] = [ek
[1 - i
]]
418 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
420 return(tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])))
423 # returns the edgekeys of a bmesh face
424 def face_edgekeys(face
):
425 return([tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])) for edge
in face
.edges
])
428 # calculate input loops
429 def get_connected_input(object, bm
, input):
430 # get mesh with modifiers applied
431 derived
, bm_mod
= get_derived_bmesh(object, bm
)
433 # calculate selected loops
434 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and not edge
.hide
]
435 loops
= get_connected_selections(edge_keys
)
437 # if only selected loops are needed, we're done
438 if input == 'selected':
439 return(derived
, bm_mod
, loops
)
440 # elif input == 'all':
441 loops
= get_parallel_loops(bm_mod
, loops
)
443 return(derived
, bm_mod
, loops
)
446 # sorts all edge-keys into a list of loops
447 def get_connected_selections(edge_keys
):
448 # create connection data
449 vert_verts
= dict_vert_verts(edge_keys
)
451 # find loops consisting of connected selected edges
453 while len(vert_verts
) > 0:
454 loop
= [iter(vert_verts
.keys()).__next
__()]
460 # no more connection data for current vertex
461 if loop
[-1] not in vert_verts
:
469 for i
, next_vert
in enumerate(vert_verts
[loop
[-1]]):
470 if next_vert
not in loop
:
471 vert_verts
[loop
[-1]].pop(i
)
472 if len(vert_verts
[loop
[-1]]) == 0:
473 del vert_verts
[loop
[-1]]
474 # remove connection both ways
475 if next_vert
in vert_verts
:
476 if len(vert_verts
[next_vert
]) == 1:
477 del vert_verts
[next_vert
]
479 vert_verts
[next_vert
].remove(loop
[-1])
480 loop
.append(next_vert
)
484 # found one end of the loop, continue with next
488 # found both ends of the loop, stop growing
492 # check if loop is circular
493 if loop
[0] in vert_verts
:
494 if loop
[-1] in vert_verts
[loop
[0]]:
496 if len(vert_verts
[loop
[0]]) == 1:
497 del vert_verts
[loop
[0]]
499 vert_verts
[loop
[0]].remove(loop
[-1])
500 if len(vert_verts
[loop
[-1]]) == 1:
501 del vert_verts
[loop
[-1]]
503 vert_verts
[loop
[-1]].remove(loop
[0])
517 # get the derived mesh data, if there is a mirror modifier
518 def get_derived_bmesh(object, bm
):
519 # check for mirror modifiers
520 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
522 # disable other modifiers
523 show_viewport
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
]
524 for mod
in object.modifiers
:
525 if mod
.type != 'MIRROR':
526 mod
.show_viewport
= False
529 mesh_mod
= object.to_mesh(bpy
.context
.depsgraph
, True)
530 bm_mod
.from_mesh(mesh_mod
)
531 bpy
.context
.blend_data
.meshes
.remove(mesh_mod
)
532 # re-enable other modifiers
533 for mod_name
in show_viewport
:
534 object.modifiers
[mod_name
].show_viewport
= True
535 # no mirror modifiers, so no derived mesh necessary
540 bm_mod
.verts
.ensure_lookup_table()
541 bm_mod
.edges
.ensure_lookup_table()
542 bm_mod
.faces
.ensure_lookup_table()
544 return(derived
, bm_mod
)
547 # return a mapping of derived indices to indices
548 def get_mapping(derived
, bm
, bm_mod
, single_vertices
, full_search
, loops
):
553 verts
= [v
for v
in bm
.verts
if not v
.hide
]
555 verts
= [v
for v
in bm
.verts
if v
.select
and not v
.hide
]
557 # non-selected vertices around single vertices also need to be mapped
559 mapping
= dict([[vert
, -1] for vert
in single_vertices
])
560 verts_mod
= [bm_mod
.verts
[vert
] for vert
in single_vertices
]
562 for v_mod
in verts_mod
:
563 if (v
.co
- v_mod
.co
).length
< 1e-6:
564 mapping
[v_mod
.index
] = v
.index
566 real_singles
= [v_real
for v_real
in mapping
.values() if v_real
> -1]
568 verts_indices
= [vert
.index
for vert
in verts
]
569 for face
in [face
for face
in bm
.faces
if not face
.select
and not face
.hide
]:
570 for vert
in face
.verts
:
571 if vert
.index
in real_singles
:
573 if v
.index
not in verts_indices
:
578 # create mapping of derived indices to indices
579 mapping
= dict([[vert
, -1] for loop
in loops
for vert
in loop
[0]])
581 for single
in single_vertices
:
583 verts_mod
= [bm_mod
.verts
[i
] for i
in mapping
.keys()]
585 for v_mod
in verts_mod
:
586 if (v
.co
- v_mod
.co
).length
< 1e-6:
587 mapping
[v_mod
.index
] = v
.index
588 verts_mod
.remove(v_mod
)
594 # calculate the determinant of a matrix
595 def matrix_determinant(m
):
596 determinant
= m
[0][0] * m
[1][1] * m
[2][2] + m
[0][1] * m
[1][2] * m
[2][0] \
597 + m
[0][2] * m
[1][0] * m
[2][1] - m
[0][2] * m
[1][1] * m
[2][0] \
598 - m
[0][1] * m
[1][0] * m
[2][2] - m
[0][0] * m
[1][2] * m
[2][1]
603 # custom matrix inversion, to provide higher precision than the built-in one
604 def matrix_invert(m
):
605 r
= mathutils
.Matrix((
606 (m
[1][1] * m
[2][2] - m
[1][2] * m
[2][1], m
[0][2] * m
[2][1] - m
[0][1] * m
[2][2],
607 m
[0][1] * m
[1][2] - m
[0][2] * m
[1][1]),
608 (m
[1][2] * m
[2][0] - m
[1][0] * m
[2][2], m
[0][0] * m
[2][2] - m
[0][2] * m
[2][0],
609 m
[0][2] * m
[1][0] - m
[0][0] * m
[1][2]),
610 (m
[1][0] * m
[2][1] - m
[1][1] * m
[2][0], m
[0][1] * m
[2][0] - m
[0][0] * m
[2][1],
611 m
[0][0] * m
[1][1] - m
[0][1] * m
[1][0])))
613 return (r
* (1 / matrix_determinant(m
)))
616 # returns a list of all loops parallel to the input, input included
617 def get_parallel_loops(bm_mod
, loops
):
618 # get required dictionaries
619 edge_faces
= dict_edge_faces(bm_mod
)
620 connected_faces
= dict_face_faces(bm_mod
, edge_faces
)
621 # turn vertex loops into edge loops
624 edgeloop
= [[sorted([loop
[0][i
], loop
[0][i
+ 1]]) for i
in
625 range(len(loop
[0]) - 1)], loop
[1]]
626 if loop
[1]: # circular
627 edgeloop
[0].append(sorted([loop
[0][-1], loop
[0][0]]))
628 edgeloops
.append(edgeloop
[:])
629 # variables to keep track while iterating
633 for loop
in edgeloops
:
634 # initialise with original loop
635 all_edgeloops
.append(loop
[0])
639 if edge
[0] not in verts_used
:
640 verts_used
.append(edge
[0])
641 if edge
[1] not in verts_used
:
642 verts_used
.append(edge
[1])
644 # find parallel loops
645 while len(newloops
) > 0:
648 for i
in newloops
[-1]:
650 forbidden_side
= False
651 if i
not in edge_faces
:
652 # weird input with branches
655 for face
in edge_faces
[i
]:
656 if len(side_a
) == 0 and forbidden_side
!= "a":
662 elif side_a
[-1] in connected_faces
[face
] and \
663 forbidden_side
!= "a":
669 if len(side_b
) == 0 and forbidden_side
!= "b":
675 elif side_b
[-1] in connected_faces
[face
] and \
676 forbidden_side
!= "b":
684 # weird input with branches
697 for key
in face_edgekeys(bm_mod
.faces
[fi
]):
698 if key
[0] not in verts_used
and key
[1] not in \
700 extraloop
.append(key
)
703 for key
in extraloop
:
705 if new_vert
not in verts_used
:
706 verts_used
.append(new_vert
)
707 newloops
.append(extraloop
)
708 all_edgeloops
.append(extraloop
)
710 # input contains branches, only return selected loop
714 # change edgeloops into normal loops
716 for edgeloop
in all_edgeloops
:
718 # grow loop by comparing vertices between consecutive edge-keys
719 for i
in range(len(edgeloop
) - 1):
720 for vert
in range(2):
721 if edgeloop
[i
][vert
] in edgeloop
[i
+ 1]:
722 loop
.append(edgeloop
[i
][vert
])
725 # add starting vertex
726 for vert
in range(2):
727 if edgeloop
[0][vert
] != loop
[0]:
728 loop
= [edgeloop
[0][vert
]] + loop
731 for vert
in range(2):
732 if edgeloop
[-1][vert
] != loop
[-1]:
733 loop
.append(edgeloop
[-1][vert
])
735 # check if loop is circular
736 if loop
[0] == loop
[-1]:
741 loops
.append([loop
, circular
])
746 # gather initial data
748 global_undo
= bpy
.context
.user_preferences
.edit
.use_global_undo
749 bpy
.context
.user_preferences
.edit
.use_global_undo
= False
750 object = bpy
.context
.active_object
751 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
752 # ensure that selection is synced for the derived mesh
753 bpy
.ops
.object.mode_set(mode
='OBJECT')
754 bpy
.ops
.object.mode_set(mode
='EDIT')
755 bm
= bmesh
.from_edit_mesh(object.data
)
757 bm
.verts
.ensure_lookup_table()
758 bm
.edges
.ensure_lookup_table()
759 bm
.faces
.ensure_lookup_table()
761 return(global_undo
, object, bm
)
764 # move the vertices to their new locations
765 def move_verts(object, bm
, mapping
, move
, lock
, influence
):
767 lock_x
, lock_y
, lock_z
= lock
768 orientation
= bpy
.context
.space_data
.transform_orientation
769 custom
= bpy
.context
.space_data
.current_orientation
771 mat
= custom
.matrix
.to_4x4().inverted() * object.matrix_world
.copy()
772 elif orientation
== 'LOCAL':
773 mat
= mathutils
.Matrix
.Identity(4)
774 elif orientation
== 'VIEW':
775 mat
= bpy
.context
.region_data
.view_matrix
.copy() * \
776 object.matrix_world
.copy()
777 else: # orientation == 'GLOBAL'
778 mat
= object.matrix_world
.copy()
779 mat_inv
= mat
.inverted()
782 for index
, loc
in loop
:
784 if mapping
[index
] == -1:
787 index
= mapping
[index
]
789 delta
= (loc
- bm
.verts
[index
].co
) * mat_inv
797 loc
= bm
.verts
[index
].co
+ delta
801 new_loc
= loc
* (influence
/ 100) + \
802 bm
.verts
[index
].co
* ((100 - influence
) / 100)
803 bm
.verts
[index
].co
= new_loc
807 bm
.verts
.ensure_lookup_table()
808 bm
.edges
.ensure_lookup_table()
809 bm
.faces
.ensure_lookup_table()
812 # load custom tool settings
813 def settings_load(self
):
814 lt
= bpy
.context
.window_manager
.looptools
815 tool
= self
.name
.split()[0].lower()
816 keys
= self
.as_keywords().keys()
818 setattr(self
, key
, getattr(lt
, tool
+ "_" + key
))
821 # store custom tool settings
822 def settings_write(self
):
823 lt
= bpy
.context
.window_manager
.looptools
824 tool
= self
.name
.split()[0].lower()
825 keys
= self
.as_keywords().keys()
827 setattr(lt
, tool
+ "_" + key
, getattr(self
, key
))
830 # clean up and set settings back to original state
831 def terminate(global_undo
):
832 # update editmesh cached data
833 obj
= bpy
.context
.active_object
834 if obj
.mode
== 'EDIT':
835 bmesh
.update_edit_mesh(obj
.data
, loop_triangles
=True, destructive
=True)
837 bpy
.context
.user_preferences
.edit
.use_global_undo
= global_undo
840 # ########################################
841 # ##### Bridge functions #################
842 # ########################################
844 # calculate a cubic spline through the middle section of 4 given coordinates
845 def bridge_calculate_cubic_spline(bm
, coordinates
):
851 for i
in coordinates
:
852 a
.append(float(i
[j
]))
855 h
.append(x
[i
+ 1] - x
[i
])
857 for i
in range(1, 3):
858 q
.append(3.0 / h
[i
] * (a
[i
+ 1] - a
[i
]) - 3.0 / h
[i
- 1] * (a
[i
] - a
[i
- 1]))
862 for i
in range(1, 3):
863 l
.append(2.0 * (x
[i
+ 1] - x
[i
- 1]) - h
[i
- 1] * u
[i
- 1])
864 u
.append(h
[i
] / l
[i
])
865 z
.append((q
[i
] - h
[i
- 1] * z
[i
- 1]) / l
[i
])
868 b
= [False for i
in range(3)]
869 c
= [False for i
in range(4)]
870 d
= [False for i
in range(3)]
872 for i
in range(2, -1, -1):
873 c
[i
] = z
[i
] - u
[i
] * c
[i
+ 1]
874 b
[i
] = (a
[i
+ 1] - a
[i
]) / h
[i
] - h
[i
] * (c
[i
+ 1] + 2.0 * c
[i
]) / 3.0
875 d
[i
] = (c
[i
+ 1] - c
[i
]) / (3.0 * h
[i
])
877 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
878 spline
= [result
[1], result
[4], result
[7]]
883 # return a list with new vertex location vectors, a list with face vertex
884 # integers, and the highest vertex integer in the virtual mesh
885 def bridge_calculate_geometry(bm
, lines
, vertex_normals
, segments
,
886 interpolation
, cubic_strength
, min_width
, max_vert_index
):
890 # calculate location based on interpolation method
891 def get_location(line
, segment
, splines
):
892 v1
= bm
.verts
[lines
[line
][0]].co
893 v2
= bm
.verts
[lines
[line
][1]].co
894 if interpolation
== 'linear':
895 return v1
+ (segment
/ segments
) * (v2
- v1
)
896 else: # interpolation == 'cubic'
897 m
= (segment
/ segments
)
898 ax
, bx
, cx
, dx
, tx
= splines
[line
][0]
899 x
= ax
+ bx
* m
+ cx
* m
** 2 + dx
* m
** 3
900 ay
, by
, cy
, dy
, ty
= splines
[line
][1]
901 y
= ay
+ by
* m
+ cy
* m
** 2 + dy
* m
** 3
902 az
, bz
, cz
, dz
, tz
= splines
[line
][2]
903 z
= az
+ bz
* m
+ cz
* m
** 2 + dz
* m
** 3
904 return mathutils
.Vector((x
, y
, z
))
906 # no interpolation needed
908 for i
, line
in enumerate(lines
):
909 if i
< len(lines
) - 1:
910 faces
.append([line
[0], lines
[i
+ 1][0], lines
[i
+ 1][1], line
[1]])
911 # more than 1 segment, interpolate
913 # calculate splines (if necessary) once, so no recalculations needed
914 if interpolation
== 'cubic':
917 v1
= bm
.verts
[line
[0]].co
918 v2
= bm
.verts
[line
[1]].co
919 size
= (v2
- v1
).length
* cubic_strength
920 splines
.append(bridge_calculate_cubic_spline(bm
,
921 [v1
+ size
* vertex_normals
[line
[0]], v1
, v2
,
922 v2
+ size
* vertex_normals
[line
[1]]]))
926 # create starting situation
927 virtual_width
= [(bm
.verts
[lines
[i
][0]].co
-
928 bm
.verts
[lines
[i
+ 1][0]].co
).length
for i
929 in range(len(lines
) - 1)]
930 new_verts
= [get_location(0, seg
, splines
) for seg
in range(1,
932 first_line_indices
= [i
for i
in range(max_vert_index
+ 1,
933 max_vert_index
+ segments
)]
935 prev_verts
= new_verts
[:] # vertex locations of verts on previous line
936 prev_vert_indices
= first_line_indices
[:]
937 max_vert_index
+= segments
- 1 # highest vertex index in virtual mesh
938 next_verts
= [] # vertex locations of verts on current line
939 next_vert_indices
= []
941 for i
, line
in enumerate(lines
):
942 if i
< len(lines
) - 1:
946 for seg
in range(1, segments
):
947 loc1
= prev_verts
[seg
- 1]
948 loc2
= get_location(i
+ 1, seg
, splines
)
949 if (loc1
- loc2
).length
< (min_width
/ 100) * virtual_width
[i
] \
950 and line
[1] == lines
[i
+ 1][1]:
951 # triangle, no new vertex
952 faces
.append([v1
, v2
, prev_vert_indices
[seg
- 1],
953 prev_vert_indices
[seg
- 1]])
954 next_verts
+= prev_verts
[seg
- 1:]
955 next_vert_indices
+= prev_vert_indices
[seg
- 1:]
959 if i
== len(lines
) - 2 and lines
[0] == lines
[-1]:
960 # quad with first line, no new vertex
961 faces
.append([v1
, v2
, first_line_indices
[seg
- 1],
962 prev_vert_indices
[seg
- 1]])
963 v2
= first_line_indices
[seg
- 1]
964 v1
= prev_vert_indices
[seg
- 1]
966 # quad, add new vertex
968 faces
.append([v1
, v2
, max_vert_index
,
969 prev_vert_indices
[seg
- 1]])
971 v1
= prev_vert_indices
[seg
- 1]
972 new_verts
.append(loc2
)
973 next_verts
.append(loc2
)
974 next_vert_indices
.append(max_vert_index
)
976 faces
.append([v1
, v2
, lines
[i
+ 1][1], line
[1]])
978 prev_verts
= next_verts
[:]
979 prev_vert_indices
= next_vert_indices
[:]
981 next_vert_indices
= []
983 return(new_verts
, faces
, max_vert_index
)
986 # calculate lines (list of lists, vertex indices) that are used for bridging
987 def bridge_calculate_lines(bm
, loops
, mode
, twist
, reverse
):
989 loop1
, loop2
= [i
[0] for i
in loops
]
990 loop1_circular
, loop2_circular
= [i
[1] for i
in loops
]
991 circular
= loop1_circular
or loop2_circular
994 # calculate loop centers
996 for loop
in [loop1
, loop2
]:
997 center
= mathutils
.Vector()
999 center
+= bm
.verts
[vertex
].co
1001 centers
.append(center
)
1002 for i
, loop
in enumerate([loop1
, loop2
]):
1004 if bm
.verts
[vertex
].co
== centers
[i
]:
1005 # prevent zero-length vectors in angle comparisons
1006 centers
[i
] += mathutils
.Vector((0.01, 0, 0))
1008 center1
, center2
= centers
1010 # calculate the normals of the virtual planes that the loops are on
1012 normal_plurity
= False
1013 for i
, loop
in enumerate([loop1
, loop2
]):
1015 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1018 x
, y
, z
= centers
[i
]
1019 for loc
in [bm
.verts
[vertex
].co
for vertex
in loop
]:
1020 mat
[0][0] += (loc
[0] - x
) ** 2
1021 mat
[1][0] += (loc
[0] - x
) * (loc
[1] - y
)
1022 mat
[2][0] += (loc
[0] - x
) * (loc
[2] - z
)
1023 mat
[0][1] += (loc
[1] - y
) * (loc
[0] - x
)
1024 mat
[1][1] += (loc
[1] - y
) ** 2
1025 mat
[2][1] += (loc
[1] - y
) * (loc
[2] - z
)
1026 mat
[0][2] += (loc
[2] - z
) * (loc
[0] - x
)
1027 mat
[1][2] += (loc
[2] - z
) * (loc
[1] - y
)
1028 mat
[2][2] += (loc
[2] - z
) ** 2
1031 if sum(mat
[0]) < 1e-6 or sum(mat
[1]) < 1e-6 or sum(mat
[2]) < 1e-6:
1032 normal_plurity
= True
1036 if sum(mat
[0]) == 0:
1037 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
1038 elif sum(mat
[1]) == 0:
1039 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
1040 elif sum(mat
[2]) == 0:
1041 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
1043 # warning! this is different from .normalize()
1046 vec
= mathutils
.Vector((1.0, 1.0, 1.0))
1047 vec2
= (mat
@ vec
) / (mat
@ vec
).length
1048 while vec
!= vec2
and iter < itermax
:
1052 if vec2
.length
!= 0:
1054 if vec2
.length
== 0:
1055 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
1057 normals
.append(normal
)
1058 # have plane normals face in the same direction (maximum angle: 90 degrees)
1059 if ((center1
+ normals
[0]) - center2
).length
< \
1060 ((center1
- normals
[0]) - center2
).length
:
1062 if ((center2
+ normals
[1]) - center1
).length
> \
1063 ((center2
- normals
[1]) - center1
).length
:
1066 # rotation matrix, representing the difference between the plane normals
1067 axis
= normals
[0].cross(normals
[1])
1068 axis
= mathutils
.Vector([loc
if abs(loc
) > 1e-8 else 0 for loc
in axis
])
1069 if axis
.angle(mathutils
.Vector((0, 0, 1)), 0) > 1.5707964:
1071 angle
= normals
[0].dot(normals
[1])
1072 rotation_matrix
= mathutils
.Matrix
.Rotation(angle
, 4, axis
)
1074 # if circular, rotate loops so they are aligned
1076 # make sure loop1 is the circular one (or both are circular)
1077 if loop2_circular
and not loop1_circular
:
1078 loop1_circular
, loop2_circular
= True, False
1079 loop1
, loop2
= loop2
, loop1
1081 # match start vertex of loop1 with loop2
1082 target_vector
= bm
.verts
[loop2
[0]].co
- center2
1083 dif_angles
= [[(rotation_matrix
@ (bm
.verts
[vertex
].co
- center1
)
1084 ).angle(target_vector
, 0), False, i
] for
1085 i
, vertex
in enumerate(loop1
)]
1087 if len(loop1
) != len(loop2
):
1088 angle_limit
= dif_angles
[0][0] * 1.2 # 20% margin
1090 [(bm
.verts
[loop2
[0]].co
-
1091 bm
.verts
[loop1
[index
]].co
).length
, angle
, index
] for
1092 angle
, distance
, index
in dif_angles
if angle
<= angle_limit
1095 loop1
= loop1
[dif_angles
[0][2]:] + loop1
[:dif_angles
[0][2]]
1097 # have both loops face the same way
1098 if normal_plurity
and not circular
:
1099 second_to_first
, second_to_second
, second_to_last
= [
1100 (bm
.verts
[loop1
[1]].co
- center1
).angle(
1101 bm
.verts
[loop2
[i
]].co
- center2
) for i
in [0, 1, -1]
1103 last_to_first
, last_to_second
= [
1104 (bm
.verts
[loop1
[-1]].co
-
1105 center1
).angle(bm
.verts
[loop2
[i
]].co
- center2
) for
1108 if (min(last_to_first
, last_to_second
) * 1.1 < min(second_to_first
,
1109 second_to_second
)) or (loop2_circular
and second_to_last
* 1.1 <
1110 min(second_to_first
, second_to_second
)):
1113 loop1
= [loop1
[-1]] + loop1
[:-1]
1115 angle
= (bm
.verts
[loop1
[0]].co
- center1
).\
1116 cross(bm
.verts
[loop1
[1]].co
- center1
).angle(normals
[0], 0)
1117 target_angle
= (bm
.verts
[loop2
[0]].co
- center2
).\
1118 cross(bm
.verts
[loop2
[1]].co
- center2
).angle(normals
[1], 0)
1119 limit
= 1.5707964 # 0.5*pi, 90 degrees
1120 if not ((angle
> limit
and target_angle
> limit
) or
1121 (angle
< limit
and target_angle
< limit
)):
1124 loop1
= [loop1
[-1]] + loop1
[:-1]
1125 elif normals
[0].angle(normals
[1]) > limit
:
1128 loop1
= [loop1
[-1]] + loop1
[:-1]
1130 # both loops have the same length
1131 if len(loop1
) == len(loop2
):
1134 if abs(twist
) < len(loop1
):
1135 loop1
= loop1
[twist
:] + loop1
[:twist
]
1139 lines
.append([loop1
[0], loop2
[0]])
1140 for i
in range(1, len(loop1
)):
1141 lines
.append([loop1
[i
], loop2
[i
]])
1143 # loops of different lengths
1145 # make loop1 longest loop
1146 if len(loop2
) > len(loop1
):
1147 loop1
, loop2
= loop2
, loop1
1148 loop1_circular
, loop2_circular
= loop2_circular
, loop1_circular
1152 if abs(twist
) < len(loop1
):
1153 loop1
= loop1
[twist
:] + loop1
[:twist
]
1157 # shortest angle difference doesn't always give correct start vertex
1158 if loop1_circular
and not loop2_circular
:
1161 if len(loop1
) - shifting
< len(loop2
):
1164 to_last
, to_first
= [
1165 (rotation_matrix
@ (bm
.verts
[loop1
[-1]].co
- center1
)).angle(
1166 (bm
.verts
[loop2
[i
]].co
- center2
), 0) for i
in [-1, 0]
1168 if to_first
< to_last
:
1169 loop1
= [loop1
[-1]] + loop1
[:-1]
1175 # basic shortest side first
1177 lines
.append([loop1
[0], loop2
[0]])
1178 for i
in range(1, len(loop1
)):
1179 if i
>= len(loop2
) - 1:
1181 lines
.append([loop1
[i
], loop2
[-1]])
1184 lines
.append([loop1
[i
], loop2
[i
]])
1186 # shortest edge algorithm
1187 else: # mode == 'shortest'
1188 lines
.append([loop1
[0], loop2
[0]])
1190 for i
in range(len(loop1
) - 1):
1191 if prev_vert2
== len(loop2
) - 1 and not loop2_circular
:
1192 # force triangles, reached end of loop2
1194 elif prev_vert2
== len(loop2
) - 1 and loop2_circular
:
1195 # at end of loop2, but circular, so check with first vert
1196 tri
, quad
= [(bm
.verts
[loop1
[i
+ 1]].co
-
1197 bm
.verts
[loop2
[j
]].co
).length
1198 for j
in [prev_vert2
, 0]]
1200 elif len(loop1
) - 1 - i
== len(loop2
) - 1 - prev_vert2
and \
1202 # force quads, otherwise won't make it to end of loop2
1205 # calculate if tri or quad gives shortest edge
1206 tri
, quad
= [(bm
.verts
[loop1
[i
+ 1]].co
-
1207 bm
.verts
[loop2
[j
]].co
).length
1208 for j
in range(prev_vert2
, prev_vert2
+ 2)]
1212 lines
.append([loop1
[i
+ 1], loop2
[prev_vert2
]])
1213 if circle_full
== 2:
1216 elif not circle_full
:
1217 lines
.append([loop1
[i
+ 1], loop2
[prev_vert2
+ 1]])
1219 # quad to first vertex of loop2
1221 lines
.append([loop1
[i
+ 1], loop2
[0]])
1225 # final face for circular loops
1226 if loop1_circular
and loop2_circular
:
1227 lines
.append([loop1
[0], loop2
[0]])
1232 # calculate number of segments needed
1233 def bridge_calculate_segments(bm
, lines
, loops
, segments
):
1234 # return if amount of segments is set by user
1239 average_edge_length
= [
1240 (bm
.verts
[vertex
].co
-
1241 bm
.verts
[loop
[0][i
+ 1]].co
).length
for loop
in loops
for
1242 i
, vertex
in enumerate(loop
[0][:-1])
1244 # closing edges of circular loops
1245 average_edge_length
+= [
1246 (bm
.verts
[loop
[0][-1]].co
-
1247 bm
.verts
[loop
[0][0]].co
).length
for loop
in loops
if loop
[1]
1251 average_edge_length
= sum(average_edge_length
) / len(average_edge_length
)
1252 average_bridge_length
= sum(
1254 bm
.verts
[v2
].co
).length
for v1
, v2
in lines
]
1257 segments
= max(1, round(average_bridge_length
/ average_edge_length
))
1262 # return dictionary with vertex index as key, and the normal vector as value
1263 def bridge_calculate_virtual_vertex_normals(bm
, lines
, loops
, edge_faces
,
1265 if not edge_faces
: # interpolation isn't set to cubic
1268 # pity reduce() isn't one of the basic functions in python anymore
1269 def average_vector_dictionary(dic
):
1270 for key
, vectors
in dic
.items():
1271 # if type(vectors) == type([]) and len(vectors) > 1:
1272 if len(vectors
) > 1:
1273 average
= mathutils
.Vector()
1274 for vector
in vectors
:
1276 average
/= len(vectors
)
1277 dic
[key
] = [average
]
1280 # get all edges of the loop
1282 [edgekey_to_edge
[tuple(sorted([loops
[j
][0][i
],
1283 loops
[j
][0][i
+ 1]]))] for i
in range(len(loops
[j
][0]) - 1)] for
1286 edges
= edges
[0] + edges
[1]
1288 if loops
[j
][1]: # circular
1289 edges
.append(edgekey_to_edge
[tuple(sorted([loops
[j
][0][0],
1290 loops
[j
][0][-1]]))])
1293 calculation based on face topology (assign edge-normals to vertices)
1295 edge_normal = face_normal x edge_vector
1296 vertex_normal = average(edge_normals)
1298 vertex_normals
= dict([(vertex
, []) for vertex
in loops
[0][0] + loops
[1][0]])
1300 faces
= edge_faces
[edgekey(edge
)] # valid faces connected to edge
1303 # get edge coordinates
1304 v1
, v2
= [bm
.verts
[edgekey(edge
)[i
]].co
for i
in [0, 1]]
1305 edge_vector
= v1
- v2
1306 if edge_vector
.length
< 1e-4:
1307 # zero-length edge, vertices at same location
1309 edge_center
= (v1
+ v2
) / 2
1311 # average face coordinates, if connected to more than 1 valid face
1313 face_normal
= mathutils
.Vector()
1314 face_center
= mathutils
.Vector()
1316 face_normal
+= face
.normal
1317 face_center
+= face
.calc_center_median()
1318 face_normal
/= len(faces
)
1319 face_center
/= len(faces
)
1321 face_normal
= faces
[0].normal
1322 face_center
= faces
[0].calc_center_median()
1323 if face_normal
.length
< 1e-4:
1324 # faces with a surface of 0 have no face normal
1327 # calculate virtual edge normal
1328 edge_normal
= edge_vector
.cross(face_normal
)
1329 edge_normal
.length
= 0.01
1330 if (face_center
- (edge_center
+ edge_normal
)).length
> \
1331 (face_center
- (edge_center
- edge_normal
)).length
:
1332 # make normal face the correct way
1333 edge_normal
.negate()
1334 edge_normal
.normalize()
1335 # add virtual edge normal as entry for both vertices it connects
1336 for vertex
in edgekey(edge
):
1337 vertex_normals
[vertex
].append(edge_normal
)
1340 calculation based on connection with other loop (vertex focused method)
1341 - used for vertices that aren't connected to any valid faces
1343 plane_normal = edge_vector x connection_vector
1344 vertex_normal = plane_normal x edge_vector
1347 vertex
for vertex
, normal
in vertex_normals
.items() if not normal
1351 # edge vectors connected to vertices
1352 edge_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1354 for v
in edgekey(edge
):
1355 if v
in edge_vectors
:
1356 edge_vector
= bm
.verts
[edgekey(edge
)[0]].co
- \
1357 bm
.verts
[edgekey(edge
)[1]].co
1358 if edge_vector
.length
< 1e-4:
1359 # zero-length edge, vertices at same location
1361 edge_vectors
[v
].append(edge_vector
)
1363 # connection vectors between vertices of both loops
1364 connection_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1365 connections
= dict([[vertex
, []] for vertex
in vertices
])
1366 for v1
, v2
in lines
:
1367 if v1
in connection_vectors
or v2
in connection_vectors
:
1368 new_vector
= bm
.verts
[v1
].co
- bm
.verts
[v2
].co
1369 if new_vector
.length
< 1e-4:
1370 # zero-length connection vector,
1371 # vertices in different loops at same location
1373 if v1
in connection_vectors
:
1374 connection_vectors
[v1
].append(new_vector
)
1375 connections
[v1
].append(v2
)
1376 if v2
in connection_vectors
:
1377 connection_vectors
[v2
].append(new_vector
)
1378 connections
[v2
].append(v1
)
1379 connection_vectors
= average_vector_dictionary(connection_vectors
)
1380 connection_vectors
= dict(
1381 [[vertex
, vector
[0]] if vector
else
1382 [vertex
, []] for vertex
, vector
in connection_vectors
.items()]
1385 for vertex
, values
in edge_vectors
.items():
1386 # vertex normal doesn't matter, just assign a random vector to it
1387 if not connection_vectors
[vertex
]:
1388 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1391 # calculate to what location the vertex is connected,
1392 # used to determine what way to flip the normal
1393 connected_center
= mathutils
.Vector()
1394 for v
in connections
[vertex
]:
1395 connected_center
+= bm
.verts
[v
].co
1396 if len(connections
[vertex
]) > 1:
1397 connected_center
/= len(connections
[vertex
])
1398 if len(connections
[vertex
]) == 0:
1399 # shouldn't be possible, but better safe than sorry
1400 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1403 # can't do proper calculations, because of zero-length vector
1405 if (connected_center
- (bm
.verts
[vertex
].co
+
1406 connection_vectors
[vertex
])).length
< (connected_center
-
1407 (bm
.verts
[vertex
].co
- connection_vectors
[vertex
])).length
:
1408 connection_vectors
[vertex
].negate()
1409 vertex_normals
[vertex
] = [connection_vectors
[vertex
].normalized()]
1412 # calculate vertex normals using edge-vectors,
1413 # connection-vectors and the derived plane normal
1414 for edge_vector
in values
:
1415 plane_normal
= edge_vector
.cross(connection_vectors
[vertex
])
1416 vertex_normal
= edge_vector
.cross(plane_normal
)
1417 vertex_normal
.length
= 0.1
1418 if (connected_center
- (bm
.verts
[vertex
].co
+
1419 vertex_normal
)).length
< (connected_center
-
1420 (bm
.verts
[vertex
].co
- vertex_normal
)).length
:
1421 # make normal face the correct way
1422 vertex_normal
.negate()
1423 vertex_normal
.normalize()
1424 vertex_normals
[vertex
].append(vertex_normal
)
1426 # average virtual vertex normals, based on all edges it's connected to
1427 vertex_normals
= average_vector_dictionary(vertex_normals
)
1428 vertex_normals
= dict([[vertex
, vector
[0]] for vertex
, vector
in vertex_normals
.items()])
1430 return(vertex_normals
)
1433 # add vertices to mesh
1434 def bridge_create_vertices(bm
, vertices
):
1435 for i
in range(len(vertices
)):
1436 bm
.verts
.new(vertices
[i
])
1437 bm
.verts
.ensure_lookup_table()
1441 def bridge_create_faces(object, bm
, faces
, twist
):
1442 # have the normal point the correct way
1444 [face
.reverse() for face
in faces
]
1445 faces
= [face
[2:] + face
[:2] if face
[0] == face
[1] else face
for face
in faces
]
1447 # eekadoodle prevention
1448 for i
in range(len(faces
)):
1449 if not faces
[i
][-1]:
1450 if faces
[i
][0] == faces
[i
][-1]:
1451 faces
[i
] = [faces
[i
][1], faces
[i
][2], faces
[i
][3], faces
[i
][1]]
1453 faces
[i
] = [faces
[i
][-1]] + faces
[i
][:-1]
1454 # result of converting from pre-bmesh period
1455 if faces
[i
][-1] == faces
[i
][-2]:
1456 faces
[i
] = faces
[i
][:-1]
1459 for i
in range(len(faces
)):
1460 new_faces
.append(bm
.faces
.new([bm
.verts
[v
] for v
in faces
[i
]]))
1462 object.data
.update(calc_edges
=True) # calc_edges prevents memory-corruption
1464 bm
.verts
.ensure_lookup_table()
1465 bm
.edges
.ensure_lookup_table()
1466 bm
.faces
.ensure_lookup_table()
1471 # calculate input loops
1472 def bridge_get_input(bm
):
1473 # create list of internal edges, which should be skipped
1474 eks_of_selected_faces
= [
1475 item
for sublist
in [face_edgekeys(face
) for
1476 face
in bm
.faces
if face
.select
and not face
.hide
] for item
in sublist
1479 for ek
in eks_of_selected_faces
:
1480 if ek
in edge_count
:
1484 internal_edges
= [ek
for ek
in edge_count
if edge_count
[ek
] > 1]
1486 # sort correct edges into loops
1488 edgekey(edge
) for edge
in bm
.edges
if edge
.select
and
1489 not edge
.hide
and edgekey(edge
) not in internal_edges
1491 loops
= get_connected_selections(selected_edges
)
1496 # return values needed by the bridge operator
1497 def bridge_initialise(bm
, interpolation
):
1498 if interpolation
== 'cubic':
1499 # dict with edge-key as key and list of connected valid faces as value
1501 face
.index
for face
in bm
.faces
if face
.select
or
1505 [[edgekey(edge
), []] for edge
in bm
.edges
if not edge
.hide
]
1507 for face
in bm
.faces
:
1508 if face
.index
in face_blacklist
:
1510 for key
in face_edgekeys(face
):
1511 edge_faces
[key
].append(face
)
1512 # dictionary with the edge-key as key and edge as value
1513 edgekey_to_edge
= dict(
1514 [[edgekey(edge
), edge
] for edge
in bm
.edges
if edge
.select
and not edge
.hide
]
1518 edgekey_to_edge
= False
1520 # selected faces input
1521 old_selected_faces
= [
1522 face
.index
for face
in bm
.faces
if face
.select
and not face
.hide
1525 # find out if faces created by bridging should be smoothed
1528 if sum([face
.smooth
for face
in bm
.faces
]) / len(bm
.faces
) >= 0.5:
1531 return(edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
)
1534 # return a string with the input method
1535 def bridge_input_method(loft
, loft_loop
):
1539 method
= "Loft loop"
1541 method
= "Loft no-loop"
1548 # match up loops in pairs, used for multi-input bridging
1549 def bridge_match_loops(bm
, loops
):
1550 # calculate average loop normals and centers
1553 for vertices
, circular
in loops
:
1554 normal
= mathutils
.Vector()
1555 center
= mathutils
.Vector()
1556 for vertex
in vertices
:
1557 normal
+= bm
.verts
[vertex
].normal
1558 center
+= bm
.verts
[vertex
].co
1559 normals
.append(normal
/ len(vertices
) / 10)
1560 centers
.append(center
/ len(vertices
))
1562 # possible matches if loop normals are faced towards the center
1564 matches
= dict([[i
, []] for i
in range(len(loops
))])
1566 for i
in range(len(loops
) + 1):
1567 for j
in range(i
+ 1, len(loops
)):
1568 if (centers
[i
] - centers
[j
]).length
> \
1569 (centers
[i
] - (centers
[j
] + normals
[j
])).length
and \
1570 (centers
[j
] - centers
[i
]).length
> \
1571 (centers
[j
] - (centers
[i
] + normals
[i
])).length
:
1573 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1574 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1575 # if no loops face each other, just make matches between all the loops
1576 if matches_amount
== 0:
1577 for i
in range(len(loops
) + 1):
1578 for j
in range(i
+ 1, len(loops
)):
1579 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1580 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1581 for key
, value
in matches
.items():
1584 # matches based on distance between centers and number of vertices in loops
1586 for loop_index
in range(len(loops
)):
1587 if loop_index
in new_order
:
1589 loop_matches
= matches
[loop_index
]
1590 if not loop_matches
:
1592 shortest_distance
= loop_matches
[0][0]
1593 shortest_distance
*= 1.1
1595 [abs(len(loops
[loop_index
][0]) -
1596 len(loops
[loop
[2]][0])), loop
[0], loop
[1], loop
[2]] for loop
in
1597 loop_matches
if loop
[0] < shortest_distance
1600 for match
in loop_matches
:
1601 if match
[3] not in new_order
:
1602 new_order
+= [loop_index
, match
[3]]
1605 # reorder loops based on matches
1606 if len(new_order
) >= 2:
1607 loops
= [loops
[i
] for i
in new_order
]
1612 # remove old_selected_faces
1613 def bridge_remove_internal_faces(bm
, old_selected_faces
):
1614 # collect bmesh faces and internal bmesh edges
1615 remove_faces
= [bm
.faces
[face
] for face
in old_selected_faces
]
1616 edges
= collections
.Counter(
1617 [edge
.index
for face
in remove_faces
for edge
in face
.edges
]
1619 remove_edges
= [bm
.edges
[edge
] for edge
in edges
if edges
[edge
] > 1]
1621 # remove internal faces and edges
1622 for face
in remove_faces
:
1623 bm
.faces
.remove(face
)
1624 for edge
in remove_edges
:
1625 bm
.edges
.remove(edge
)
1627 bm
.faces
.ensure_lookup_table()
1628 bm
.edges
.ensure_lookup_table()
1629 bm
.verts
.ensure_lookup_table()
1632 # update list of internal faces that are flagged for removal
1633 def bridge_save_unused_faces(bm
, old_selected_faces
, loops
):
1634 # key: vertex index, value: lists of selected faces using it
1636 vertex_to_face
= dict([[i
, []] for i
in range(len(bm
.verts
))])
1637 [[vertex_to_face
[vertex
.index
].append(face
) for vertex
in
1638 bm
.faces
[face
].verts
] for face
in old_selected_faces
]
1640 # group selected faces that are connected
1643 for face
in old_selected_faces
:
1644 if face
in grouped_faces
:
1646 grouped_faces
.append(face
)
1650 grow_face
= new_faces
[0]
1651 for vertex
in bm
.faces
[grow_face
].verts
:
1652 vertex_face_group
= [
1653 face
for face
in vertex_to_face
[vertex
.index
] if
1654 face
not in grouped_faces
1656 new_faces
+= vertex_face_group
1657 grouped_faces
+= vertex_face_group
1658 group
+= vertex_face_group
1660 groups
.append(group
)
1662 # key: vertex index, value: True/False (is it in a loop that is used)
1663 used_vertices
= dict([[i
, 0] for i
in range(len(bm
.verts
))])
1665 for vertex
in loop
[0]:
1666 used_vertices
[vertex
] = True
1668 # check if group is bridged, if not remove faces from internal faces list
1669 for group
in groups
:
1674 for vertex
in bm
.faces
[face
].verts
:
1675 if used_vertices
[vertex
.index
]:
1680 old_selected_faces
.remove(face
)
1683 # add the newly created faces to the selection
1684 def bridge_select_new_faces(new_faces
, smooth
):
1685 for face
in new_faces
:
1686 face
.select_set(True)
1687 face
.smooth
= smooth
1690 # sort loops, so they are connected in the correct order when lofting
1691 def bridge_sort_loops(bm
, loops
, loft_loop
):
1692 # simplify loops to single points, and prepare for pathfinding
1694 [sum([bm
.verts
[i
].co
[j
] for i
in loop
[0]]) /
1695 len(loop
[0]) for loop
in loops
] for j
in range(3)
1697 nodes
= [mathutils
.Vector((x
[i
], y
[i
], z
[i
])) for i
in range(len(loops
))]
1700 open = [i
for i
in range(1, len(loops
))]
1702 # connect node to path, that is shortest to active_node
1703 while len(open) > 0:
1704 distances
= [(nodes
[active_node
] - nodes
[i
]).length
for i
in open]
1705 active_node
= open[distances
.index(min(distances
))]
1706 open.remove(active_node
)
1707 path
.append([active_node
, min(distances
)])
1708 # check if we didn't start in the middle of the path
1709 for i
in range(2, len(path
)):
1710 if (nodes
[path
[i
][0]] - nodes
[0]).length
< path
[i
][1]:
1713 path
= path
[:-i
] + temp
1717 loops
= [loops
[i
[0]] for i
in path
]
1718 # if requested, duplicate first loop at last position, so loft can loop
1720 loops
= loops
+ [loops
[0]]
1725 # remapping old indices to new position in list
1726 def bridge_update_old_selection(bm
, old_selected_faces
):
1728 old_indices = old_selected_faces[:]
1729 old_selected_faces = []
1730 for i, face in enumerate(bm.faces):
1731 if face.index in old_indices:
1732 old_selected_faces.append(i)
1734 old_selected_faces
= [
1735 i
for i
, face
in enumerate(bm
.faces
) if face
.index
in old_selected_faces
1738 return(old_selected_faces
)
1741 # ########################################
1742 # ##### Circle functions #################
1743 # ########################################
1745 # convert 3d coordinates to 2d coordinates on plane
1746 def circle_3d_to_2d(bm_mod
, loop
, com
, normal
):
1747 # project vertices onto the plane
1748 verts
= [bm_mod
.verts
[v
] for v
in loop
[0]]
1749 verts_projected
= [[v
.co
- (v
.co
- com
).dot(normal
) * normal
, v
.index
]
1752 # calculate two vectors (p and q) along the plane
1753 m
= mathutils
.Vector((normal
[0] + 1.0, normal
[1], normal
[2]))
1754 p
= m
- (m
.dot(normal
) * normal
)
1756 m
= mathutils
.Vector((normal
[0], normal
[1] + 1.0, normal
[2]))
1757 p
= m
- (m
.dot(normal
) * normal
)
1760 # change to 2d coordinates using perpendicular projection
1762 for loc
, vert
in verts_projected
:
1764 x
= p
.dot(vloc
) / p
.dot(p
)
1765 y
= q
.dot(vloc
) / q
.dot(q
)
1766 locs_2d
.append([x
, y
, vert
])
1768 return(locs_2d
, p
, q
)
1771 # calculate a best-fit circle to the 2d locations on the plane
1772 def circle_calculate_best_fit(locs_2d
):
1778 # calculate center and radius (non-linear least squares solution)
1779 for iter in range(500):
1783 d
= (v
[0] ** 2 - 2.0 * x0
* v
[0] + v
[1] ** 2 - 2.0 * y0
* v
[1] + x0
** 2 + y0
** 2) ** 0.5
1784 jmat
.append([(x0
- v
[0]) / d
, (y0
- v
[1]) / d
, -1.0])
1785 k
.append(-(((v
[0] - x0
) ** 2 + (v
[1] - y0
) ** 2) ** 0.5 - r
))
1786 jmat2
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1790 k2
= mathutils
.Vector((0.0, 0.0, 0.0))
1791 for i
in range(len(jmat
)):
1792 k2
+= mathutils
.Vector(jmat
[i
]) * k
[i
]
1793 jmat2
[0][0] += jmat
[i
][0] ** 2
1794 jmat2
[1][0] += jmat
[i
][0] * jmat
[i
][1]
1795 jmat2
[2][0] += jmat
[i
][0] * jmat
[i
][2]
1796 jmat2
[1][1] += jmat
[i
][1] ** 2
1797 jmat2
[2][1] += jmat
[i
][1] * jmat
[i
][2]
1798 jmat2
[2][2] += jmat
[i
][2] ** 2
1799 jmat2
[0][1] = jmat2
[1][0]
1800 jmat2
[0][2] = jmat2
[2][0]
1801 jmat2
[1][2] = jmat2
[2][1]
1806 dx0
, dy0
, dr
= jmat2
@ k2
1810 # stop iterating if we're close enough to optimal solution
1811 if abs(dx0
) < 1e-6 and abs(dy0
) < 1e-6 and abs(dr
) < 1e-6:
1814 # return center of circle and radius
1818 # calculate circle so no vertices have to be moved away from the center
1819 def circle_calculate_min_fit(locs_2d
):
1821 x0
= (min([i
[0] for i
in locs_2d
]) + max([i
[0] for i
in locs_2d
])) / 2.0
1822 y0
= (min([i
[1] for i
in locs_2d
]) + max([i
[1] for i
in locs_2d
])) / 2.0
1823 center
= mathutils
.Vector([x0
, y0
])
1825 r
= min([(mathutils
.Vector([i
[0], i
[1]]) - center
).length
for i
in locs_2d
])
1827 # return center of circle and radius
1831 # calculate the new locations of the vertices that need to be moved
1832 def circle_calculate_verts(flatten
, bm_mod
, locs_2d
, com
, p
, q
, normal
):
1833 # changing 2d coordinates back to 3d coordinates
1836 locs_3d
.append([loc
[2], loc
[0] * p
+ loc
[1] * q
+ com
])
1838 if flatten
: # flat circle
1841 else: # project the locations on the existing mesh
1842 vert_edges
= dict_vert_edges(bm_mod
)
1843 vert_faces
= dict_vert_faces(bm_mod
)
1844 faces
= [f
for f
in bm_mod
.faces
if not f
.hide
]
1845 rays
= [normal
, -normal
]
1849 if bm_mod
.verts
[loc
[0]].co
== loc
[1]: # vertex hasn't moved
1852 dif
= normal
.angle(loc
[1] - bm_mod
.verts
[loc
[0]].co
)
1853 if -1e-6 < dif
< 1e-6 or math
.pi
- 1e-6 < dif
< math
.pi
+ 1e-6:
1854 # original location is already along projection normal
1855 projection
= bm_mod
.verts
[loc
[0]].co
1857 # quick search through adjacent faces
1858 for face
in vert_faces
[loc
[0]]:
1859 verts
= [v
.co
for v
in bm_mod
.faces
[face
].verts
]
1860 if len(verts
) == 3: # triangle
1864 v1
, v2
, v3
, v4
= verts
[:4]
1866 intersect
= mathutils
.geometry
.\
1867 intersect_ray_tri(v1
, v2
, v3
, ray
, loc
[1])
1869 projection
= intersect
1872 intersect
= mathutils
.geometry
.\
1873 intersect_ray_tri(v1
, v3
, v4
, ray
, loc
[1])
1875 projection
= intersect
1880 # check if projection is on adjacent edges
1881 for edgekey
in vert_edges
[loc
[0]]:
1882 line1
= bm_mod
.verts
[edgekey
[0]].co
1883 line2
= bm_mod
.verts
[edgekey
[1]].co
1884 intersect
, dist
= mathutils
.geometry
.intersect_point_line(
1885 loc
[1], line1
, line2
1887 if 1e-6 < dist
< 1 - 1e-6:
1888 projection
= intersect
1891 # full search through the entire mesh
1894 verts
= [v
.co
for v
in face
.verts
]
1895 if len(verts
) == 3: # triangle
1899 v1
, v2
, v3
, v4
= verts
[:4]
1901 intersect
= mathutils
.geometry
.intersect_ray_tri(
1902 v1
, v2
, v3
, ray
, loc
[1]
1905 hits
.append([(loc
[1] - intersect
).length
,
1909 intersect
= mathutils
.geometry
.intersect_ray_tri(
1910 v1
, v3
, v4
, ray
, loc
[1]
1913 hits
.append([(loc
[1] - intersect
).length
,
1917 # if more than 1 hit with mesh, closest hit is new loc
1919 projection
= hits
[0][1]
1921 # nothing to project on, remain at flat location
1923 new_locs
.append([loc
[0], projection
])
1925 # return new positions of projected circle
1929 # check loops and only return valid ones
1930 def circle_check_loops(single_loops
, loops
, mapping
, bm_mod
):
1931 valid_single_loops
= {}
1933 for i
, [loop
, circular
] in enumerate(loops
):
1934 # loop needs to have at least 3 vertices
1937 # loop needs at least 1 vertex in the original, non-mirrored mesh
1941 if mapping
[vert
] > -1:
1946 # loop has to be non-collinear
1948 loc0
= mathutils
.Vector(bm_mod
.verts
[loop
[0]].co
[:])
1949 loc1
= mathutils
.Vector(bm_mod
.verts
[loop
[1]].co
[:])
1951 locn
= mathutils
.Vector(bm_mod
.verts
[v
].co
[:])
1952 if loc0
== loc1
or loc1
== locn
:
1958 if -1e-6 < d1
.angle(d2
, 0) < 1e-6:
1966 # passed all tests, loop is valid
1967 valid_loops
.append([loop
, circular
])
1968 valid_single_loops
[len(valid_loops
) - 1] = single_loops
[i
]
1970 return(valid_single_loops
, valid_loops
)
1973 # calculate the location of single input vertices that need to be flattened
1974 def circle_flatten_singles(bm_mod
, com
, p
, q
, normal
, single_loop
):
1976 for vert
in single_loop
:
1977 loc
= mathutils
.Vector(bm_mod
.verts
[vert
].co
[:])
1978 new_locs
.append([vert
, loc
- (loc
- com
).dot(normal
) * normal
])
1983 # calculate input loops
1984 def circle_get_input(object, bm
):
1985 # get mesh with modifiers applied
1986 derived
, bm_mod
= get_derived_bmesh(object, bm
)
1988 # create list of edge-keys based on selection state
1990 for face
in bm
.faces
:
1991 if face
.select
and not face
.hide
:
1995 # get selected, non-hidden , non-internal edge-keys
1997 key
for keys
in [face_edgekeys(face
) for face
in
1998 bm_mod
.faces
if face
.select
and not face
.hide
] for key
in keys
2001 for ek
in eks_selected
:
2002 if ek
in edge_count
:
2007 edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and
2008 not edge
.hide
and edge_count
.get(edgekey(edge
), 1) == 1
2011 # no faces, so no internal edges either
2013 edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and not edge
.hide
2016 # add edge-keys around single vertices
2017 verts_connected
= dict(
2018 [[vert
, 1] for edge
in [edge
for edge
in
2019 bm_mod
.edges
if edge
.select
and not edge
.hide
] for vert
in
2023 vert
.index
for vert
in bm_mod
.verts
if
2024 vert
.select
and not vert
.hide
and
2025 not verts_connected
.get(vert
.index
, False)
2028 if single_vertices
and len(bm
.faces
) > 0:
2029 vert_to_single
= dict(
2030 [[v
.index
, []] for v
in bm_mod
.verts
if not v
.hide
]
2032 for face
in [face
for face
in bm_mod
.faces
if not face
.select
and not face
.hide
]:
2033 for vert
in face
.verts
:
2035 if vert
in single_vertices
:
2036 for ek
in face_edgekeys(face
):
2038 edge_keys
.append(ek
)
2039 if vert
not in vert_to_single
[ek
[0]]:
2040 vert_to_single
[ek
[0]].append(vert
)
2041 if vert
not in vert_to_single
[ek
[1]]:
2042 vert_to_single
[ek
[1]].append(vert
)
2045 # sort edge-keys into loops
2046 loops
= get_connected_selections(edge_keys
)
2048 # find out to which loops the single vertices belong
2049 single_loops
= dict([[i
, []] for i
in range(len(loops
))])
2050 if single_vertices
and len(bm
.faces
) > 0:
2051 for i
, [loop
, circular
] in enumerate(loops
):
2053 if vert_to_single
[vert
]:
2054 for single
in vert_to_single
[vert
]:
2055 if single
not in single_loops
[i
]:
2056 single_loops
[i
].append(single
)
2058 return(derived
, bm_mod
, single_vertices
, single_loops
, loops
)
2061 # recalculate positions based on the influence of the circle shape
2062 def circle_influence_locs(locs_2d
, new_locs_2d
, influence
):
2063 for i
in range(len(locs_2d
)):
2064 oldx
, oldy
, j
= locs_2d
[i
]
2065 newx
, newy
, k
= new_locs_2d
[i
]
2066 altx
= newx
* (influence
/ 100) + oldx
* ((100 - influence
) / 100)
2067 alty
= newy
* (influence
/ 100) + oldy
* ((100 - influence
) / 100)
2068 locs_2d
[i
] = [altx
, alty
, j
]
2073 # project 2d locations on circle, respecting distance relations between verts
2074 def circle_project_non_regular(locs_2d
, x0
, y0
, r
):
2075 for i
in range(len(locs_2d
)):
2076 x
, y
, j
= locs_2d
[i
]
2077 loc
= mathutils
.Vector([x
- x0
, y
- y0
])
2079 locs_2d
[i
] = [loc
[0], loc
[1], j
]
2084 # project 2d locations on circle, with equal distance between all vertices
2085 def circle_project_regular(locs_2d
, x0
, y0
, r
):
2086 # find offset angle and circling direction
2087 x
, y
, i
= locs_2d
[0]
2088 loc
= mathutils
.Vector([x
- x0
, y
- y0
])
2090 offset_angle
= loc
.angle(mathutils
.Vector([1.0, 0.0]), 0.0)
2091 loca
= mathutils
.Vector([x
- x0
, y
- y0
, 0.0])
2094 x
, y
, j
= locs_2d
[1]
2095 locb
= mathutils
.Vector([x
- x0
, y
- y0
, 0.0])
2096 if loca
.cross(locb
)[2] >= 0:
2100 # distribute vertices along the circle
2101 for i
in range(len(locs_2d
)):
2102 t
= offset_angle
+ ccw
* (i
/ len(locs_2d
) * 2 * math
.pi
)
2105 locs_2d
[i
] = [x
, y
, locs_2d
[i
][2]]
2110 # shift loop, so the first vertex is closest to the center
2111 def circle_shift_loop(bm_mod
, loop
, com
):
2112 verts
, circular
= loop
2114 [(bm_mod
.verts
[vert
].co
- com
).length
, i
] for i
, vert
in enumerate(verts
)
2117 shift
= distances
[0][1]
2118 loop
= [verts
[shift
:] + verts
[:shift
], circular
]
2123 # ########################################
2124 # ##### Curve functions ##################
2125 # ########################################
2127 # create lists with knots and points, all correctly sorted
2128 def curve_calculate_knots(loop
, verts_selected
):
2129 knots
= [v
for v
in loop
[0] if v
in verts_selected
]
2131 # circular loop, potential for weird splines
2133 offset
= int(len(loop
[0]) / 4)
2136 kpos
.append(loop
[0].index(k
))
2138 for i
in range(len(kpos
) - 1):
2139 kdif
.append(kpos
[i
+ 1] - kpos
[i
])
2140 kdif
.append(len(loop
[0]) - kpos
[-1] + kpos
[0])
2144 kadd
.append([kdif
.index(k
), True])
2145 # next 2 lines are optional, they insert
2146 # an extra control point in small gaps
2148 # kadd.append([kdif.index(k), False])
2151 for k
in kadd
: # extra knots to be added
2152 if k
[1]: # big gap (break circular spline)
2153 kpos
= loop
[0].index(knots
[k
[0]]) + offset
2154 if kpos
> len(loop
[0]) - 1:
2155 kpos
-= len(loop
[0])
2156 kins
.append([knots
[k
[0]], loop
[0][kpos
]])
2158 if kpos2
> len(knots
) - 1:
2160 kpos2
= loop
[0].index(knots
[kpos2
]) - offset
2162 kpos2
+= len(loop
[0])
2163 kins
.append([loop
[0][kpos
], loop
[0][kpos2
]])
2164 krot
= loop
[0][kpos2
]
2165 else: # small gap (keep circular spline)
2166 k1
= loop
[0].index(knots
[k
[0]])
2168 if k2
> len(knots
) - 1:
2170 k2
= loop
[0].index(knots
[k2
])
2172 dif
= len(loop
[0]) - 1 - k1
+ k2
2175 kn
= k1
+ int(dif
/ 2)
2176 if kn
> len(loop
[0]) - 1:
2178 kins
.append([loop
[0][k1
], loop
[0][kn
]])
2179 for j
in kins
: # insert new knots
2180 knots
.insert(knots
.index(j
[0]) + 1, j
[1])
2181 if not krot
: # circular loop
2182 knots
.append(knots
[0])
2183 points
= loop
[0][loop
[0].index(knots
[0]):]
2184 points
+= loop
[0][0:loop
[0].index(knots
[0]) + 1]
2185 else: # non-circular loop (broken by script)
2186 krot
= knots
.index(krot
)
2187 knots
= knots
[krot
:] + knots
[0:krot
]
2188 if loop
[0].index(knots
[0]) > loop
[0].index(knots
[-1]):
2189 points
= loop
[0][loop
[0].index(knots
[0]):]
2190 points
+= loop
[0][0:loop
[0].index(knots
[-1]) + 1]
2192 points
= loop
[0][loop
[0].index(knots
[0]):loop
[0].index(knots
[-1]) + 1]
2193 # non-circular loop, add first and last point as knots
2195 if loop
[0][0] not in knots
:
2196 knots
.insert(0, loop
[0][0])
2197 if loop
[0][-1] not in knots
:
2198 knots
.append(loop
[0][-1])
2200 return(knots
, points
)
2203 # calculate relative positions compared to first knot
2204 def curve_calculate_t(bm_mod
, knots
, points
, pknots
, regular
, circular
):
2211 loc
= pknots
[knots
.index(p
)] # use projected knot location
2213 loc
= mathutils
.Vector(bm_mod
.verts
[p
].co
[:])
2216 len_total
+= (loc
- loc_prev
).length
2217 tpoints
.append(len_total
)
2222 tknots
.append(tpoints
[points
.index(p
)])
2224 tknots
[-1] = tpoints
[-1]
2228 tpoints_average
= tpoints
[-1] / (len(tpoints
) - 1)
2229 for i
in range(1, len(tpoints
) - 1):
2230 tpoints
[i
] = i
* tpoints_average
2231 for i
in range(len(knots
)):
2232 tknots
[i
] = tpoints
[points
.index(knots
[i
])]
2234 tknots
[-1] = tpoints
[-1]
2236 return(tknots
, tpoints
)
2239 # change the location of non-selected points to their place on the spline
2240 def curve_calculate_vertices(bm_mod
, knots
, tknots
, points
, tpoints
, splines
,
2241 interpolation
, restriction
):
2248 m
= tpoints
[points
.index(p
)]
2256 if n
> len(splines
) - 1:
2257 n
= len(splines
) - 1
2261 if interpolation
== 'cubic':
2262 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
2263 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
2264 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
2265 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
2266 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
2267 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
2268 newloc
= mathutils
.Vector([x
, y
, z
])
2269 else: # interpolation == 'linear'
2270 a
, d
, t
, u
= splines
[n
]
2271 newloc
= ((m
- t
) / u
) * d
+ a
2273 if restriction
!= 'none': # vertex movement is restricted
2275 else: # set the vertex to its new location
2276 move
.append([p
, newloc
])
2278 if restriction
!= 'none': # vertex movement is restricted
2283 move
.append([p
, bm_mod
.verts
[p
].co
])
2285 oldloc
= bm_mod
.verts
[p
].co
2286 normal
= bm_mod
.verts
[p
].normal
2287 dloc
= newloc
- oldloc
2288 if dloc
.length
< 1e-6:
2289 move
.append([p
, newloc
])
2290 elif restriction
== 'extrude': # only extrusions
2291 if dloc
.angle(normal
, 0) < 0.5 * math
.pi
+ 1e-6:
2292 move
.append([p
, newloc
])
2293 else: # restriction == 'indent' only indentations
2294 if dloc
.angle(normal
) > 0.5 * math
.pi
- 1e-6:
2295 move
.append([p
, newloc
])
2300 # trim loops to part between first and last selected vertices (including)
2301 def curve_cut_boundaries(bm_mod
, loops
):
2303 for loop
, circular
in loops
:
2306 cut_loops
.append([loop
, circular
])
2308 selected
= [bm_mod
.verts
[v
].select
for v
in loop
]
2309 first
= selected
.index(True)
2311 last
= -selected
.index(True)
2313 cut_loops
.append([loop
[first
:], circular
])
2315 cut_loops
.append([loop
[first
:last
], circular
])
2320 # calculate input loops
2321 def curve_get_input(object, bm
, boundaries
):
2322 # get mesh with modifiers applied
2323 derived
, bm_mod
= get_derived_bmesh(object, bm
)
2325 # vertices that still need a loop to run through it
2327 v
.index
for v
in bm_mod
.verts
if v
.select
and not v
.hide
2329 # necessary dictionaries
2330 vert_edges
= dict_vert_edges(bm_mod
)
2331 edge_faces
= dict_edge_faces(bm_mod
)
2333 # find loops through each selected vertex
2334 while len(verts_unsorted
) > 0:
2335 loops
= curve_vertex_loops(bm_mod
, verts_unsorted
[0], vert_edges
,
2337 verts_unsorted
.pop(0)
2339 # check if loop is fully selected
2340 search_perpendicular
= False
2342 for loop
, circular
in loops
:
2344 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2345 if len(selected
) < 2:
2346 # only one selected vertex on loop, don't use
2349 elif len(selected
) == len(loop
):
2350 search_perpendicular
= loop
2352 # entire loop is selected, find perpendicular loops
2353 if search_perpendicular
:
2355 if vert
in verts_unsorted
:
2356 verts_unsorted
.remove(vert
)
2357 perp_loops
= curve_perpendicular_loops(bm_mod
, loop
,
2358 vert_edges
, edge_faces
)
2359 for perp_loop
in perp_loops
:
2360 correct_loops
.append(perp_loop
)
2363 for loop
, circular
in loops
:
2364 correct_loops
.append([loop
, circular
])
2368 correct_loops
= curve_cut_boundaries(bm_mod
, correct_loops
)
2370 return(derived
, bm_mod
, correct_loops
)
2373 # return all loops that are perpendicular to the given one
2374 def curve_perpendicular_loops(bm_mod
, start_loop
, vert_edges
, edge_faces
):
2375 # find perpendicular loops
2377 for start_vert
in start_loop
:
2378 loops
= curve_vertex_loops(bm_mod
, start_vert
, vert_edges
,
2380 for loop
, circular
in loops
:
2381 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2382 if len(selected
) == len(loop
):
2385 perp_loops
.append([loop
, circular
, loop
.index(start_vert
)])
2387 # trim loops to same lengths
2389 [len(loop
[0]), i
] for i
, loop
in enumerate(perp_loops
) if not loop
[1]
2392 # all loops are circular, not trimming
2393 return([[loop
[0], loop
[1]] for loop
in perp_loops
])
2395 shortest
= min(shortest
)
2396 shortest_start
= perp_loops
[shortest
[1]][2]
2397 before_start
= shortest_start
2398 after_start
= shortest
[0] - shortest_start
- 1
2399 bigger_before
= before_start
> after_start
2401 for loop
in perp_loops
:
2402 # have the loop face the same direction as the shortest one
2404 if loop
[2] < len(loop
[0]) / 2:
2406 loop
[2] = len(loop
[0]) - loop
[2] - 1
2408 if loop
[2] > len(loop
[0]) / 2:
2410 loop
[2] = len(loop
[0]) - loop
[2] - 1
2411 # circular loops can shift, to prevent wrong trimming
2413 shift
= shortest_start
- loop
[2]
2414 if loop
[2] + shift
> 0 and loop
[2] + shift
< len(loop
[0]):
2415 loop
[0] = loop
[0][-shift
:] + loop
[0][:-shift
]
2418 loop
[2] += len(loop
[0])
2419 elif loop
[2] > len(loop
[0]) - 1:
2420 loop
[2] -= len(loop
[0])
2422 start
= max(0, loop
[2] - before_start
)
2423 end
= min(len(loop
[0]), loop
[2] + after_start
+ 1)
2424 trimmed_loops
.append([loop
[0][start
:end
], False])
2426 return(trimmed_loops
)
2429 # project knots on non-selected geometry
2430 def curve_project_knots(bm_mod
, verts_selected
, knots
, points
, circular
):
2431 # function to project vertex on edge
2432 def project(v1
, v2
, v3
):
2433 # v1 and v2 are part of a line
2434 # v3 is projected onto it
2440 if circular
: # project all knots
2444 else: # first and last knot shouldn't be projected
2447 pknots
= [mathutils
.Vector(bm_mod
.verts
[knots
[0]].co
[:])]
2448 for knot
in knots
[start
:end
]:
2449 if knot
in verts_selected
:
2450 knot_left
= knot_right
= False
2451 for i
in range(points
.index(knot
) - 1, -1 * len(points
), -1):
2452 if points
[i
] not in knots
:
2453 knot_left
= points
[i
]
2455 for i
in range(points
.index(knot
) + 1, 2 * len(points
)):
2456 if i
> len(points
) - 1:
2458 if points
[i
] not in knots
:
2459 knot_right
= points
[i
]
2461 if knot_left
and knot_right
and knot_left
!= knot_right
:
2462 knot_left
= mathutils
.Vector(bm_mod
.verts
[knot_left
].co
[:])
2463 knot_right
= mathutils
.Vector(bm_mod
.verts
[knot_right
].co
[:])
2464 knot
= mathutils
.Vector(bm_mod
.verts
[knot
].co
[:])
2465 pknots
.append(project(knot_left
, knot_right
, knot
))
2467 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2468 else: # knot isn't selected, so shouldn't be changed
2469 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2471 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knots
[-1]].co
[:]))
2476 # find all loops through a given vertex
2477 def curve_vertex_loops(bm_mod
, start_vert
, vert_edges
, edge_faces
):
2481 for edge
in vert_edges
[start_vert
]:
2482 if edge
in edges_used
:
2487 active_faces
= edge_faces
[edge
]
2492 new_edges
= vert_edges
[new_vert
]
2493 loop
.append(new_vert
)
2495 edges_used
.append(tuple(sorted([loop
[-1], loop
[-2]])))
2496 if len(new_edges
) < 3 or len(new_edges
) > 4:
2501 for new_edge
in new_edges
:
2502 if new_edge
in edges_used
:
2505 for new_face
in edge_faces
[new_edge
]:
2506 if new_face
in active_faces
:
2511 # found correct new edge
2512 active_faces
= edge_faces
[new_edge
]
2518 if new_vert
== loop
[0]:
2526 loops
.append([loop
, circular
])
2531 # ########################################
2532 # ##### Flatten functions ################
2533 # ########################################
2535 # sort input into loops
2536 def flatten_get_input(bm
):
2537 vert_verts
= dict_vert_verts(
2538 [edgekey(edge
) for edge
in bm
.edges
if edge
.select
and not edge
.hide
]
2540 verts
= [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
2542 # no connected verts, consider all selected verts as a single input
2544 return([[verts
, False]])
2547 while len(verts
) > 0:
2551 if loop
[-1] in vert_verts
:
2552 to_grow
= vert_verts
[loop
[-1]]
2556 while len(to_grow
) > 0:
2557 new_vert
= to_grow
[0]
2559 if new_vert
in loop
:
2561 loop
.append(new_vert
)
2562 verts
.remove(new_vert
)
2563 to_grow
+= vert_verts
[new_vert
]
2565 loops
.append([loop
, False])
2570 # calculate position of vertex projections on plane
2571 def flatten_project(bm
, loop
, com
, normal
):
2572 verts
= [bm
.verts
[v
] for v
in loop
[0]]
2574 [v
.index
, mathutils
.Vector(v
.co
[:]) -
2575 (mathutils
.Vector(v
.co
[:]) - com
).dot(normal
) * normal
] for v
in verts
2578 return(verts_projected
)
2581 # ########################################
2582 # ##### Gstretch functions ###############
2583 # ########################################
2585 # fake stroke class, used to create custom strokes if no GP data is found
2586 class gstretch_fake_stroke():
2587 def __init__(self
, points
):
2588 self
.points
= [gstretch_fake_stroke_point(p
) for p
in points
]
2591 # fake stroke point class, used in fake strokes
2592 class gstretch_fake_stroke_point():
2593 def __init__(self
, loc
):
2597 # flips loops, if necessary, to obtain maximum alignment to stroke
2598 def gstretch_align_pairs(ls_pairs
, object, bm_mod
, method
):
2599 # returns total distance between all verts in loop and corresponding stroke
2600 def distance_loop_stroke(loop
, stroke
, object, bm_mod
, method
):
2601 stroke_lengths_cache
= False
2602 loop_length
= len(loop
[0])
2605 if method
!= 'regular':
2606 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2608 for i
, v_index
in enumerate(loop
[0]):
2609 if method
== 'regular':
2610 relative_distance
= i
/ (loop_length
- 1)
2612 relative_distance
= relative_lengths
[i
]
2614 loc1
= object.matrix_world
@ bm_mod
.verts
[v_index
].co
2615 loc2
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2616 relative_distance
, stroke_lengths_cache
)
2617 total_distance
+= (loc2
- loc1
).length
2619 return(total_distance
)
2622 for (loop
, stroke
) in ls_pairs
:
2623 total_dist
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2626 total_dist_rev
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2628 if total_dist_rev
> total_dist
:
2634 # calculate vertex positions on stroke
2635 def gstretch_calculate_verts(loop
, stroke
, object, bm_mod
, method
):
2637 stroke_lengths_cache
= False
2638 loop_length
= len(loop
[0])
2639 matrix_inverse
= object.matrix_world
.inverted()
2641 # return intersection of line with stroke, or None
2642 def intersect_line_stroke(vec1
, vec2
, stroke
):
2643 for i
, p
in enumerate(stroke
.points
[1:]):
2644 intersections
= mathutils
.geometry
.intersect_line_line(vec1
, vec2
,
2645 p
.co
, stroke
.points
[i
].co
)
2646 if intersections
and \
2647 (intersections
[0] - intersections
[1]).length
< 1e-2:
2648 x
, dist
= mathutils
.geometry
.intersect_point_line(
2649 intersections
[0], p
.co
, stroke
.points
[i
].co
)
2651 return(intersections
[0])
2654 if method
== 'project':
2655 vert_edges
= dict_vert_edges(bm_mod
)
2657 for v_index
in loop
[0]:
2659 for ek
in vert_edges
[v_index
]:
2661 v1
= bm_mod
.verts
[v1
]
2662 v2
= bm_mod
.verts
[v2
]
2663 if v1
.select
+ v2
.select
== 1 and not v1
.hide
and not v2
.hide
:
2664 vec1
= object.matrix_world
* v1
.co
2665 vec2
= object.matrix_world
* v2
.co
2666 intersection
= intersect_line_stroke(vec1
, vec2
, stroke
)
2669 if not intersection
:
2670 v
= bm_mod
.verts
[v_index
]
2671 intersection
= intersect_line_stroke(v
.co
, v
.co
+ v
.normal
,
2674 move
.append([v_index
, matrix_inverse
* intersection
])
2677 if method
== 'irregular':
2678 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2680 for i
, v_index
in enumerate(loop
[0]):
2681 if method
== 'regular':
2682 relative_distance
= i
/ (loop_length
- 1)
2683 else: # method == 'irregular'
2684 relative_distance
= relative_lengths
[i
]
2685 loc
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2686 relative_distance
, stroke_lengths_cache
)
2687 loc
= matrix_inverse
@ loc
2688 move
.append([v_index
, loc
])
2693 # create new vertices, based on GP strokes
2694 def gstretch_create_verts(object, bm_mod
, strokes
, method
, conversion
,
2695 conversion_distance
, conversion_max
, conversion_min
, conversion_vertices
):
2698 mat_world
= object.matrix_world
.inverted()
2699 singles
= gstretch_match_single_verts(bm_mod
, strokes
, mat_world
)
2701 for stroke
in strokes
:
2702 stroke_verts
.append([stroke
, []])
2704 if conversion
== 'vertices':
2705 min_end_point
= conversion_vertices
2706 end_point
= conversion_vertices
2707 elif conversion
== 'limit_vertices':
2708 min_end_point
= conversion_min
2709 end_point
= conversion_max
2711 end_point
= len(stroke
.points
)
2712 # creation of new vertices at fixed user-defined distances
2713 if conversion
== 'distance':
2715 prev_point
= stroke
.points
[0]
2716 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* prev_point
.co
))
2718 limit
= conversion_distance
2719 for point
in stroke
.points
:
2720 new_distance
= distance
+ (point
.co
- prev_point
.co
).length
2722 while new_distance
> limit
:
2723 to_cover
= limit
- distance
+ (limit
* iteration
)
2724 new_loc
= prev_point
.co
+ to_cover
* \
2725 (point
.co
- prev_point
.co
).normalized()
2726 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* new_loc
))
2727 new_distance
-= limit
2729 distance
= new_distance
2731 # creation of new vertices for other methods
2733 # add vertices at stroke points
2734 for point
in stroke
.points
[:end_point
]:
2735 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* point
.co
))
2736 # add more vertices, beyond the points that are available
2737 if min_end_point
> min(len(stroke
.points
), end_point
):
2738 for i
in range(min_end_point
-
2739 (min(len(stroke
.points
), end_point
))):
2740 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* point
.co
))
2741 # force even spreading of points, so they are placed on stroke
2743 bm_mod
.verts
.ensure_lookup_table()
2744 bm_mod
.verts
.index_update()
2745 for stroke
, verts_seq
in stroke_verts
:
2746 if len(verts_seq
) < 2:
2748 # spread vertices evenly over the stroke
2749 if method
== 'regular':
2750 loop
= [[vert
.index
for vert
in verts_seq
], False]
2751 move
+= gstretch_calculate_verts(loop
, stroke
, object, bm_mod
,
2754 for i
, vert
in enumerate(verts_seq
):
2756 bm_mod
.edges
.new((verts_seq
[i
- 1], verts_seq
[i
]))
2758 # connect single vertices to the closest stroke
2760 for vert
, m_stroke
, point
in singles
:
2761 if m_stroke
!= stroke
:
2763 bm_mod
.edges
.new((vert
, verts_seq
[point
]))
2764 bm_mod
.edges
.ensure_lookup_table()
2765 bmesh
.update_edit_mesh(object.data
)
2770 # erases the grease pencil stroke
2771 def gstretch_erase_stroke(stroke
, context
):
2772 # change 3d coordinate into a stroke-point
2773 def sp(loc
, context
):
2777 'location': (0, 0, 0),
2779 view3d_utils
.location_3d_to_region_2d(
2780 context
.region
, context
.space_data
.region_3d
, loc
)
2787 if type(stroke
) != bpy
.types
.GPencilStroke
:
2788 # fake stroke, there is nothing to delete
2791 erase_stroke
= [sp(p
.co
, context
) for p
in stroke
.points
]
2793 erase_stroke
[0]['is_start'] = True
2794 bpy
.ops
.gpencil
.draw(mode
='ERASER', stroke
=erase_stroke
)
2797 # get point on stroke, given by relative distance (0.0 - 1.0)
2798 def gstretch_eval_stroke(stroke
, distance
, stroke_lengths_cache
=False):
2799 # use cache if available
2800 if not stroke_lengths_cache
:
2802 for i
, p
in enumerate(stroke
.points
[1:]):
2803 lengths
.append((p
.co
- stroke
.points
[i
].co
).length
+ lengths
[-1])
2804 total_length
= max(lengths
[-1], 1e-7)
2805 stroke_lengths_cache
= [length
/ total_length
for length
in
2807 stroke_lengths
= stroke_lengths_cache
[:]
2809 if distance
in stroke_lengths
:
2810 loc
= stroke
.points
[stroke_lengths
.index(distance
)].co
2811 elif distance
> stroke_lengths
[-1]:
2812 # should be impossible, but better safe than sorry
2813 loc
= stroke
.points
[-1].co
2815 stroke_lengths
.append(distance
)
2816 stroke_lengths
.sort()
2817 stroke_index
= stroke_lengths
.index(distance
)
2818 interval_length
= stroke_lengths
[
2819 stroke_index
+ 1] - stroke_lengths
[stroke_index
- 1
2821 distance_relative
= (distance
- stroke_lengths
[stroke_index
- 1]) / interval_length
2822 interval_vector
= stroke
.points
[stroke_index
].co
- stroke
.points
[stroke_index
- 1].co
2823 loc
= stroke
.points
[stroke_index
- 1].co
+ distance_relative
* interval_vector
2825 return(loc
, stroke_lengths_cache
)
2828 # create fake grease pencil strokes for the active object
2829 def gstretch_get_fake_strokes(object, bm_mod
, loops
):
2832 p1
= object.matrix_world
@ bm_mod
.verts
[loop
[0][0]].co
2833 p2
= object.matrix_world
@ bm_mod
.verts
[loop
[0][-1]].co
2834 strokes
.append(gstretch_fake_stroke([p1
, p2
]))
2839 # get grease pencil strokes for the active object
2840 def gstretch_get_strokes(object, context
):
2841 gp
= get_grease_pencil(object, context
)
2844 layer
= gp
.layers
.active
2847 frame
= layer
.active_frame
2850 strokes
= frame
.strokes
2851 if len(strokes
) < 1:
2857 # returns a list with loop-stroke pairs
2858 def gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
):
2859 if not loops
or not strokes
:
2862 # calculate loop centers
2864 bm_mod
.verts
.ensure_lookup_table()
2866 center
= mathutils
.Vector()
2867 for v_index
in loop
[0]:
2868 center
+= bm_mod
.verts
[v_index
].co
2869 center
/= len(loop
[0])
2870 center
= object.matrix_world
@ center
2871 loop_centers
.append([center
, loop
])
2873 # calculate stroke centers
2875 for stroke
in strokes
:
2876 center
= mathutils
.Vector()
2877 for p
in stroke
.points
:
2879 center
/= len(stroke
.points
)
2880 stroke_centers
.append([center
, stroke
, 0])
2882 # match, first by stroke use count, then by distance
2884 for lc
in loop_centers
:
2886 for i
, sc
in enumerate(stroke_centers
):
2887 distances
.append([sc
[2], (lc
[0] - sc
[0]).length
, i
])
2889 best_stroke
= distances
[0][2]
2890 ls_pairs
.append([lc
[1], stroke_centers
[best_stroke
][1]])
2891 stroke_centers
[best_stroke
][2] += 1 # increase stroke use count
2896 # match single selected vertices to the closest stroke endpoint
2897 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2898 def gstretch_match_single_verts(bm_mod
, strokes
, mat_world
):
2899 # calculate stroke endpoints in object space
2901 for stroke
in strokes
:
2902 endpoints
.append((mat_world
* stroke
.points
[0].co
, stroke
, 0))
2903 endpoints
.append((mat_world
* stroke
.points
[-1].co
, stroke
, -1))
2906 # find single vertices (not connected to other selected verts)
2907 for vert
in bm_mod
.verts
:
2911 for edge
in vert
.link_edges
:
2912 if edge
.other_vert(vert
).select
:
2917 # calculate distances from vertex to endpoints
2918 distance
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2919 endpoint_index
) for endpoint_index
, (loc
, stroke
, stroke_point
) in
2920 enumerate(endpoints
)]
2922 distances
.append(distance
[0])
2924 # create matches, based on shortest distance first
2928 singles
.append((distances
[0][1], distances
[0][2], distances
[0][3]))
2929 endpoints
.pop(distances
[0][4])
2932 for (i
, vert
, j
, k
, l
) in distances
:
2933 distance_new
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2934 endpoint_index
) for endpoint_index
, (loc
, stroke
,
2935 stroke_point
) in enumerate(endpoints
)]
2937 distances_new
.append(distance_new
[0])
2938 distances
= distances_new
2943 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2944 def gstretch_relative_lengths(loop
, bm_mod
):
2946 for i
, v_index
in enumerate(loop
[0][1:]):
2948 (bm_mod
.verts
[v_index
].co
-
2949 bm_mod
.verts
[loop
[0][i
]].co
).length
+ lengths
[-1]
2951 total_length
= max(lengths
[-1], 1e-7)
2952 relative_lengths
= [length
/ total_length
for length
in
2955 return(relative_lengths
)
2958 # convert cache-stored strokes into usable (fake) GP strokes
2959 def gstretch_safe_to_true_strokes(safe_strokes
):
2961 for safe_stroke
in safe_strokes
:
2962 strokes
.append(gstretch_fake_stroke(safe_stroke
))
2967 # convert a GP stroke into a list of points which can be stored in cache
2968 def gstretch_true_to_safe_strokes(strokes
):
2970 for stroke
in strokes
:
2971 safe_strokes
.append([p
.co
.copy() for p
in stroke
.points
])
2973 return(safe_strokes
)
2976 # force consistency in GUI, max value can never be lower than min value
2977 def gstretch_update_max(self
, context
):
2978 # called from operator settings (after execution)
2979 if 'conversion_min' in self
.keys():
2980 if self
.conversion_min
> self
.conversion_max
:
2981 self
.conversion_max
= self
.conversion_min
2982 # called from toolbar
2984 lt
= context
.window_manager
.looptools
2985 if lt
.gstretch_conversion_min
> lt
.gstretch_conversion_max
:
2986 lt
.gstretch_conversion_max
= lt
.gstretch_conversion_min
2989 # force consistency in GUI, min value can never be higher than max value
2990 def gstretch_update_min(self
, context
):
2991 # called from operator settings (after execution)
2992 if 'conversion_max' in self
.keys():
2993 if self
.conversion_max
< self
.conversion_min
:
2994 self
.conversion_min
= self
.conversion_max
2995 # called from toolbar
2997 lt
= context
.window_manager
.looptools
2998 if lt
.gstretch_conversion_max
< lt
.gstretch_conversion_min
:
2999 lt
.gstretch_conversion_min
= lt
.gstretch_conversion_max
3002 # ########################################
3003 # ##### Relax functions ##################
3004 # ########################################
3006 # create lists with knots and points, all correctly sorted
3007 def relax_calculate_knots(loops
):
3010 for loop
, circular
in loops
:
3014 if len(loop
) % 2 == 1: # odd
3015 extend
= [False, True, 0, 1, 0, 1]
3017 extend
= [True, False, 0, 1, 1, 2]
3019 if len(loop
) % 2 == 1: # odd
3020 extend
= [False, False, 0, 1, 1, 2]
3022 extend
= [False, False, 0, 1, 1, 2]
3025 loop
= [loop
[-1]] + loop
+ [loop
[0]]
3026 for i
in range(extend
[2 + 2 * j
], len(loop
), 2):
3027 knots
[j
].append(loop
[i
])
3028 for i
in range(extend
[3 + 2 * j
], len(loop
), 2):
3029 if loop
[i
] == loop
[-1] and not circular
:
3031 if len(points
[j
]) == 0:
3032 points
[j
].append(loop
[i
])
3033 elif loop
[i
] != points
[j
][0]:
3034 points
[j
].append(loop
[i
])
3036 if knots
[j
][0] != knots
[j
][-1]:
3037 knots
[j
].append(knots
[j
][0])
3038 if len(points
[1]) == 0:
3044 all_points
.append(p
)
3046 return(all_knots
, all_points
)
3049 # calculate relative positions compared to first knot
3050 def relax_calculate_t(bm_mod
, knots
, points
, regular
):
3053 for i
in range(len(knots
)):
3054 amount
= len(knots
[i
]) + len(points
[i
])
3056 for j
in range(amount
):
3058 mix
.append([True, knots
[i
][round(j
/ 2)]])
3059 elif j
== amount
- 1:
3060 mix
.append([True, knots
[i
][-1]])
3062 mix
.append([False, points
[i
][int(j
/ 2)]])
3068 loc
= mathutils
.Vector(bm_mod
.verts
[m
[1]].co
[:])
3071 len_total
+= (loc
- loc_prev
).length
3073 tknots
.append(len_total
)
3075 tpoints
.append(len_total
)
3079 for p
in range(len(points
[i
])):
3080 tpoints
.append((tknots
[p
] + tknots
[p
+ 1]) / 2)
3081 all_tknots
.append(tknots
)
3082 all_tpoints
.append(tpoints
)
3084 return(all_tknots
, all_tpoints
)
3087 # change the location of the points to their place on the spline
3088 def relax_calculate_verts(bm_mod
, interpolation
, tknots
, knots
, tpoints
,
3092 for i
in range(len(knots
)):
3094 m
= tpoints
[i
][points
[i
].index(p
)]
3096 n
= tknots
[i
].index(m
)
3102 if n
> len(splines
[i
]) - 1:
3103 n
= len(splines
[i
]) - 1
3107 if interpolation
== 'cubic':
3108 ax
, bx
, cx
, dx
, tx
= splines
[i
][n
][0]
3109 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
3110 ay
, by
, cy
, dy
, ty
= splines
[i
][n
][1]
3111 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
3112 az
, bz
, cz
, dz
, tz
= splines
[i
][n
][2]
3113 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
3114 change
.append([p
, mathutils
.Vector([x
, y
, z
])])
3115 else: # interpolation == 'linear'
3116 a
, d
, t
, u
= splines
[i
][n
]
3119 change
.append([p
, ((m
- t
) / u
) * d
+ a
])
3121 move
.append([c
[0], (bm_mod
.verts
[c
[0]].co
+ c
[1]) / 2])
3126 # ########################################
3127 # ##### Space functions ##################
3128 # ########################################
3130 # calculate relative positions compared to first knot
3131 def space_calculate_t(bm_mod
, knots
):
3136 loc
= mathutils
.Vector(bm_mod
.verts
[k
].co
[:])
3139 len_total
+= (loc
- loc_prev
).length
3140 tknots
.append(len_total
)
3143 t_per_segment
= len_total
/ (amount
- 1)
3144 tpoints
= [i
* t_per_segment
for i
in range(amount
)]
3146 return(tknots
, tpoints
)
3149 # change the location of the points to their place on the spline
3150 def space_calculate_verts(bm_mod
, interpolation
, tknots
, tpoints
, points
,
3154 m
= tpoints
[points
.index(p
)]
3162 if n
> len(splines
) - 1:
3163 n
= len(splines
) - 1
3167 if interpolation
== 'cubic':
3168 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
3169 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
3170 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
3171 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
3172 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
3173 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
3174 move
.append([p
, mathutils
.Vector([x
, y
, z
])])
3175 else: # interpolation == 'linear'
3176 a
, d
, t
, u
= splines
[n
]
3177 move
.append([p
, ((m
- t
) / u
) * d
+ a
])
3182 # ########################################
3183 # ##### Operators ########################
3184 # ########################################
3187 class Bridge(Operator
):
3188 bl_idname
= 'mesh.looptools_bridge'
3189 bl_label
= "Bridge / Loft"
3190 bl_description
= "Bridge two, or loft several, loops of vertices"
3191 bl_options
= {'REGISTER', 'UNDO'}
3193 cubic_strength
: FloatProperty(
3195 description
="Higher strength results in more fluid curves",
3200 interpolation
: EnumProperty(
3201 name
="Interpolation mode",
3202 items
=(('cubic', "Cubic", "Gives curved results"),
3203 ('linear', "Linear", "Basic, fast, straight interpolation")),
3204 description
="Interpolation mode: algorithm used when creating "
3210 description
="Loft multiple loops, instead of considering them as "
3211 "a multi-input for bridging",
3214 loft_loop
: BoolProperty(
3216 description
="Connect the first and the last loop with each other",
3219 min_width
: IntProperty(
3220 name
="Minimum width",
3221 description
="Segments with an edge smaller than this are merged "
3222 "(compared to base edge)",
3226 subtype
='PERCENTAGE'
3230 items
=(('basic', "Basic", "Fast algorithm"),
3231 ('shortest', "Shortest edge", "Slower algorithm with better vertex matching")),
3232 description
="Algorithm used for bridging",
3235 remove_faces
: BoolProperty(
3236 name
="Remove faces",
3237 description
="Remove faces that are internal after bridging",
3240 reverse
: BoolProperty(
3242 description
="Manually override the direction in which the loops "
3243 "are bridged. Only use if the tool gives the wrong result",
3246 segments
: IntProperty(
3248 description
="Number of segments used to bridge the gap (0=automatic)",
3255 description
="Twist what vertices are connected to each other",
3260 def poll(cls
, context
):
3261 ob
= context
.active_object
3262 return (ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3264 def draw(self
, context
):
3265 layout
= self
.layout
3266 # layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3269 col_top
= layout
.column(align
=True)
3270 row
= col_top
.row(align
=True)
3271 col_left
= row
.column(align
=True)
3272 col_right
= row
.column(align
=True)
3273 col_right
.active
= self
.segments
!= 1
3274 col_left
.prop(self
, "segments")
3275 col_right
.prop(self
, "min_width", text
="")
3277 bottom_left
= col_left
.row()
3278 bottom_left
.active
= self
.segments
!= 1
3279 bottom_left
.prop(self
, "interpolation", text
="")
3280 bottom_right
= col_right
.row()
3281 bottom_right
.active
= self
.interpolation
== 'cubic'
3282 bottom_right
.prop(self
, "cubic_strength")
3283 # boolean properties
3284 col_top
.prop(self
, "remove_faces")
3286 col_top
.prop(self
, "loft_loop")
3288 # override properties
3290 row
= layout
.row(align
=True)
3291 row
.prop(self
, "twist")
3292 row
.prop(self
, "reverse")
3294 def invoke(self
, context
, event
):
3295 # load custom settings
3296 context
.window_manager
.looptools
.bridge_loft
= self
.loft
3298 return self
.execute(context
)
3300 def execute(self
, context
):
3302 global_undo
, object, bm
= initialise()
3303 edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
= \
3304 bridge_initialise(bm
, self
.interpolation
)
3305 settings_write(self
)
3307 # check cache to see if we can save time
3308 input_method
= bridge_input_method(self
.loft
, self
.loft_loop
)
3309 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Bridge",
3310 object, bm
, input_method
, False)
3313 loops
= bridge_get_input(bm
)
3315 # reorder loops if there are more than 2
3318 loops
= bridge_sort_loops(bm
, loops
, self
.loft_loop
)
3320 loops
= bridge_match_loops(bm
, loops
)
3322 # saving cache for faster execution next time
3324 cache_write("Bridge", object, bm
, input_method
, False, False,
3325 loops
, False, False)
3328 # calculate new geometry
3331 max_vert_index
= len(bm
.verts
) - 1
3332 for i
in range(1, len(loops
)):
3333 if not self
.loft
and i
% 2 == 0:
3335 lines
= bridge_calculate_lines(bm
, loops
[i
- 1:i
+ 1],
3336 self
.mode
, self
.twist
, self
.reverse
)
3337 vertex_normals
= bridge_calculate_virtual_vertex_normals(bm
,
3338 lines
, loops
[i
- 1:i
+ 1], edge_faces
, edgekey_to_edge
)
3339 segments
= bridge_calculate_segments(bm
, lines
,
3340 loops
[i
- 1:i
+ 1], self
.segments
)
3341 new_verts
, new_faces
, max_vert_index
= \
3342 bridge_calculate_geometry(
3343 bm
, lines
, vertex_normals
,
3344 segments
, self
.interpolation
, self
.cubic_strength
,
3345 self
.min_width
, max_vert_index
3348 vertices
+= new_verts
3351 # make sure faces in loops that aren't used, aren't removed
3352 if self
.remove_faces
and old_selected_faces
:
3353 bridge_save_unused_faces(bm
, old_selected_faces
, loops
)
3356 bridge_create_vertices(bm
, vertices
)
3359 new_faces
= bridge_create_faces(object, bm
, faces
, self
.twist
)
3360 old_selected_faces
= [
3361 i
for i
, face
in enumerate(bm
.faces
) if face
.index
in old_selected_faces
3363 bridge_select_new_faces(new_faces
, smooth
)
3364 # edge-data could have changed, can't use cache next run
3365 if faces
and not vertices
:
3366 cache_delete("Bridge")
3367 # delete internal faces
3368 if self
.remove_faces
and old_selected_faces
:
3369 bridge_remove_internal_faces(bm
, old_selected_faces
)
3370 # make sure normals are facing outside
3371 bmesh
.update_edit_mesh(object.data
, loop_triangles
=False,
3373 bpy
.ops
.mesh
.normals_make_consistent()
3376 terminate(global_undo
)
3382 class Circle(Operator
):
3383 bl_idname
= "mesh.looptools_circle"
3385 bl_description
= "Move selected vertices into a circle shape"
3386 bl_options
= {'REGISTER', 'UNDO'}
3388 custom_radius
: BoolProperty(
3390 description
="Force a custom radius",
3395 items
=(("best", "Best fit", "Non-linear least squares"),
3396 ("inside", "Fit inside", "Only move vertices towards the center")),
3397 description
="Method used for fitting a circle to the vertices",
3400 flatten
: BoolProperty(
3402 description
="Flatten the circle, instead of projecting it on the mesh",
3405 influence
: FloatProperty(
3407 description
="Force of the tool",
3412 subtype
='PERCENTAGE'
3414 lock_x
: BoolProperty(
3416 description
="Lock editing of the x-coordinate",
3419 lock_y
: BoolProperty(
3421 description
="Lock editing of the y-coordinate",
3424 lock_z
: BoolProperty(name
="Lock Z",
3425 description
="Lock editing of the z-coordinate",
3428 radius
: FloatProperty(
3430 description
="Custom radius for circle",
3435 regular
: BoolProperty(
3437 description
="Distribute vertices at constant distances along the circle",
3442 def poll(cls
, context
):
3443 ob
= context
.active_object
3444 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3446 def draw(self
, context
):
3447 layout
= self
.layout
3448 col
= layout
.column()
3450 col
.prop(self
, "fit")
3453 col
.prop(self
, "flatten")
3454 row
= col
.row(align
=True)
3455 row
.prop(self
, "custom_radius")
3456 row_right
= row
.row(align
=True)
3457 row_right
.active
= self
.custom_radius
3458 row_right
.prop(self
, "radius", text
="")
3459 col
.prop(self
, "regular")
3462 col_move
= col
.column(align
=True)
3463 row
= col_move
.row(align
=True)
3465 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3467 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3469 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3471 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3473 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3475 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3476 col_move
.prop(self
, "influence")
3478 def invoke(self
, context
, event
):
3479 # load custom settings
3481 return self
.execute(context
)
3483 def execute(self
, context
):
3485 global_undo
, object, bm
= initialise()
3486 settings_write(self
)
3487 # check cache to see if we can save time
3488 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Circle",
3489 object, bm
, False, False)
3491 derived
, bm_mod
= get_derived_bmesh(object, bm
)
3494 derived
, bm_mod
, single_vertices
, single_loops
, loops
= \
3495 circle_get_input(object, bm
)
3496 mapping
= get_mapping(derived
, bm
, bm_mod
, single_vertices
,
3498 single_loops
, loops
= circle_check_loops(single_loops
, loops
,
3501 # saving cache for faster execution next time
3503 cache_write("Circle", object, bm
, False, False, single_loops
,
3504 loops
, derived
, mapping
)
3507 for i
, loop
in enumerate(loops
):
3508 # best fitting flat plane
3509 com
, normal
= calculate_plane(bm_mod
, loop
)
3510 # if circular, shift loop so we get a good starting vertex
3512 loop
= circle_shift_loop(bm_mod
, loop
, com
)
3513 # flatten vertices on plane
3514 locs_2d
, p
, q
= circle_3d_to_2d(bm_mod
, loop
, com
, normal
)
3516 if self
.fit
== 'best':
3517 x0
, y0
, r
= circle_calculate_best_fit(locs_2d
)
3518 else: # self.fit == 'inside'
3519 x0
, y0
, r
= circle_calculate_min_fit(locs_2d
)
3521 if self
.custom_radius
:
3522 r
= self
.radius
/ p
.length
3523 # calculate positions on circle
3525 new_locs_2d
= circle_project_regular(locs_2d
[:], x0
, y0
, r
)
3527 new_locs_2d
= circle_project_non_regular(locs_2d
[:], x0
, y0
, r
)
3528 # take influence into account
3529 locs_2d
= circle_influence_locs(locs_2d
, new_locs_2d
,
3531 # calculate 3d positions of the created 2d input
3532 move
.append(circle_calculate_verts(self
.flatten
, bm_mod
,
3533 locs_2d
, com
, p
, q
, normal
))
3534 # flatten single input vertices on plane defined by loop
3535 if self
.flatten
and single_loops
:
3536 move
.append(circle_flatten_singles(bm_mod
, com
, p
, q
,
3537 normal
, single_loops
[i
]))
3539 # move vertices to new locations
3540 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3541 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3544 move_verts(object, bm
, mapping
, move
, lock
, -1)
3549 terminate(global_undo
)
3555 class Curve(Operator
):
3556 bl_idname
= "mesh.looptools_curve"
3558 bl_description
= "Turn a loop into a smooth curve"
3559 bl_options
= {'REGISTER', 'UNDO'}
3561 boundaries
: BoolProperty(
3563 description
="Limit the tool to work within the boundaries of the selected vertices",
3566 influence
: FloatProperty(
3568 description
="Force of the tool",
3573 subtype
='PERCENTAGE'
3575 interpolation
: EnumProperty(
3576 name
="Interpolation",
3577 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
3578 ("linear", "Linear", "Simple and fast linear algorithm")),
3579 description
="Algorithm used for interpolation",
3582 lock_x
: BoolProperty(
3584 description
="Lock editing of the x-coordinate",
3587 lock_y
: BoolProperty(
3589 description
="Lock editing of the y-coordinate",
3592 lock_z
: BoolProperty(
3594 description
="Lock editing of the z-coordinate",
3597 regular
: BoolProperty(
3599 description
="Distribute vertices at constant distances along the curve",
3602 restriction
: EnumProperty(
3604 items
=(("none", "None", "No restrictions on vertex movement"),
3605 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
3606 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
3607 description
="Restrictions on how the vertices can be moved",
3612 def poll(cls
, context
):
3613 ob
= context
.active_object
3614 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3616 def draw(self
, context
):
3617 layout
= self
.layout
3618 col
= layout
.column()
3620 col
.prop(self
, "interpolation")
3621 col
.prop(self
, "restriction")
3622 col
.prop(self
, "boundaries")
3623 col
.prop(self
, "regular")
3626 col_move
= col
.column(align
=True)
3627 row
= col_move
.row(align
=True)
3629 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3631 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3633 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3635 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3637 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3639 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3640 col_move
.prop(self
, "influence")
3642 def invoke(self
, context
, event
):
3643 # load custom settings
3645 return self
.execute(context
)
3647 def execute(self
, context
):
3649 global_undo
, object, bm
= initialise()
3650 settings_write(self
)
3651 # check cache to see if we can save time
3652 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Curve",
3653 object, bm
, False, self
.boundaries
)
3655 derived
, bm_mod
= get_derived_bmesh(object, bm
)
3658 derived
, bm_mod
, loops
= curve_get_input(object, bm
, self
.boundaries
)
3659 mapping
= get_mapping(derived
, bm
, bm_mod
, False, True, loops
)
3660 loops
= check_loops(loops
, mapping
, bm_mod
)
3662 v
.index
for v
in bm_mod
.verts
if v
.select
and not v
.hide
3665 # saving cache for faster execution next time
3667 cache_write("Curve", object, bm
, False, self
.boundaries
, False,
3668 loops
, derived
, mapping
)
3672 knots
, points
= curve_calculate_knots(loop
, verts_selected
)
3673 pknots
= curve_project_knots(bm_mod
, verts_selected
, knots
,
3675 tknots
, tpoints
= curve_calculate_t(bm_mod
, knots
, points
,
3676 pknots
, self
.regular
, loop
[1])
3677 splines
= calculate_splines(self
.interpolation
, bm_mod
,
3679 move
.append(curve_calculate_vertices(bm_mod
, knots
, tknots
,
3680 points
, tpoints
, splines
, self
.interpolation
,
3683 # move vertices to new locations
3684 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3685 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3688 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
3693 terminate(global_undo
)
3699 class Flatten(Operator
):
3700 bl_idname
= "mesh.looptools_flatten"
3701 bl_label
= "Flatten"
3702 bl_description
= "Flatten vertices on a best-fitting plane"
3703 bl_options
= {'REGISTER', 'UNDO'}
3705 influence
: FloatProperty(
3707 description
="Force of the tool",
3712 subtype
='PERCENTAGE'
3714 lock_x
: BoolProperty(
3716 description
="Lock editing of the x-coordinate",
3719 lock_y
: BoolProperty(
3721 description
="Lock editing of the y-coordinate",
3724 lock_z
: BoolProperty(name
="Lock Z",
3725 description
="Lock editing of the z-coordinate",
3728 plane
: EnumProperty(
3730 items
=(("best_fit", "Best fit", "Calculate a best fitting plane"),
3731 ("normal", "Normal", "Derive plane from averaging vertex normals"),
3732 ("view", "View", "Flatten on a plane perpendicular to the viewing angle")),
3733 description
="Plane on which vertices are flattened",
3736 restriction
: EnumProperty(
3738 items
=(("none", "None", "No restrictions on vertex movement"),
3739 ("bounding_box", "Bounding box", "Vertices are restricted to "
3740 "movement inside the bounding box of the selection")),
3741 description
="Restrictions on how the vertices can be moved",
3746 def poll(cls
, context
):
3747 ob
= context
.active_object
3748 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3750 def draw(self
, context
):
3751 layout
= self
.layout
3752 col
= layout
.column()
3754 col
.prop(self
, "plane")
3755 # col.prop(self, "restriction")
3758 col_move
= col
.column(align
=True)
3759 row
= col_move
.row(align
=True)
3761 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3763 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3765 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3767 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3769 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3771 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3772 col_move
.prop(self
, "influence")
3774 def invoke(self
, context
, event
):
3775 # load custom settings
3777 return self
.execute(context
)
3779 def execute(self
, context
):
3781 global_undo
, object, bm
= initialise()
3782 settings_write(self
)
3783 # check cache to see if we can save time
3784 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Flatten",
3785 object, bm
, False, False)
3787 # order input into virtual loops
3788 loops
= flatten_get_input(bm
)
3789 loops
= check_loops(loops
, mapping
, bm
)
3791 # saving cache for faster execution next time
3793 cache_write("Flatten", object, bm
, False, False, False, loops
,
3798 # calculate plane and position of vertices on them
3799 com
, normal
= calculate_plane(bm
, loop
, method
=self
.plane
,
3801 to_move
= flatten_project(bm
, loop
, com
, normal
)
3802 if self
.restriction
== 'none':
3803 move
.append(to_move
)
3805 move
.append(to_move
)
3807 # move vertices to new locations
3808 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3809 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3812 move_verts(object, bm
, False, move
, lock
, self
.influence
)
3815 terminate(global_undo
)
3821 class RemoveGP(Operator
):
3822 bl_idname
= "remove.gp"
3823 bl_label
= "Remove GP"
3824 bl_description
= "Remove all Grease Pencil Strokes"
3825 bl_options
= {'REGISTER', 'UNDO'}
3827 def execute(self
, context
):
3829 if context
.gpencil_data
is not None:
3830 bpy
.ops
.gpencil
.data_unlink()
3832 self
.report({'INFO'}, "No Grease Pencil data to Unlink")
3833 return {'CANCELLED'}
3838 class GStretch(Operator
):
3839 bl_idname
= "mesh.looptools_gstretch"
3840 bl_label
= "Gstretch"
3841 bl_description
= "Stretch selected vertices to Grease Pencil stroke"
3842 bl_options
= {'REGISTER', 'UNDO'}
3844 conversion
: EnumProperty(
3846 items
=(("distance", "Distance", "Set the distance between vertices "
3847 "of the converted grease pencil stroke"),
3848 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
3849 "number of vertices that converted GP strokes will have"),
3850 ("vertices", "Exact vertices", "Set the exact number of vertices "
3851 "that converted grease pencil strokes will have. Short strokes "
3852 "with few points may contain less vertices than this number."),
3853 ("none", "No simplification", "Convert each grease pencil point "
3855 description
="If grease pencil strokes are converted to geometry, "
3856 "use this simplification method",
3857 default
='limit_vertices'
3859 conversion_distance
: FloatProperty(
3861 description
="Absolute distance between vertices along the converted "
3862 "grease pencil stroke",
3868 conversion_max
: IntProperty(
3869 name
="Max Vertices",
3870 description
="Maximum number of vertices grease pencil strokes will "
3871 "have, when they are converted to geomtery",
3875 update
=gstretch_update_min
3877 conversion_min
: IntProperty(
3878 name
="Min Vertices",
3879 description
="Minimum number of vertices grease pencil strokes will "
3880 "have, when they are converted to geomtery",
3884 update
=gstretch_update_max
3886 conversion_vertices
: IntProperty(
3888 description
="Number of vertices grease pencil strokes will "
3889 "have, when they are converted to geometry. If strokes have less "
3890 "points than required, the 'Spread evenly' method is used",
3895 delete_strokes
: BoolProperty(
3896 name
="Delete strokes",
3897 description
="Remove Grease Pencil strokes if they have been used "
3898 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
3901 influence
: FloatProperty(
3903 description
="Force of the tool",
3908 subtype
='PERCENTAGE'
3910 lock_x
: BoolProperty(
3912 description
="Lock editing of the x-coordinate",
3915 lock_y
: BoolProperty(
3917 description
="Lock editing of the y-coordinate",
3920 lock_z
: BoolProperty(
3922 description
="Lock editing of the z-coordinate",
3925 method
: EnumProperty(
3927 items
=(("project", "Project", "Project vertices onto the stroke, "
3928 "using vertex normals and connected edges"),
3929 ("irregular", "Spread", "Distribute vertices along the full "
3930 "stroke, retaining relative distances between the vertices"),
3931 ("regular", "Spread evenly", "Distribute vertices at regular "
3932 "distances along the full stroke")),
3933 description
="Method of distributing the vertices over the Grease "
3939 def poll(cls
, context
):
3940 ob
= context
.active_object
3941 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3943 def draw(self
, context
):
3944 layout
= self
.layout
3945 col
= layout
.column()
3947 col
.prop(self
, "method")
3950 col_conv
= col
.column(align
=True)
3951 col_conv
.prop(self
, "conversion", text
="")
3952 if self
.conversion
== 'distance':
3953 col_conv
.prop(self
, "conversion_distance")
3954 elif self
.conversion
== 'limit_vertices':
3955 row
= col_conv
.row(align
=True)
3956 row
.prop(self
, "conversion_min", text
="Min")
3957 row
.prop(self
, "conversion_max", text
="Max")
3958 elif self
.conversion
== 'vertices':
3959 col_conv
.prop(self
, "conversion_vertices")
3962 col_move
= col
.column(align
=True)
3963 row
= col_move
.row(align
=True)
3965 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3967 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3969 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3971 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3973 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3975 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3976 col_move
.prop(self
, "influence")
3978 col
.operator("remove.gp", text
="Delete GP Strokes")
3980 def invoke(self
, context
, event
):
3981 # flush cached strokes
3982 if 'Gstretch' in looptools_cache
:
3983 looptools_cache
['Gstretch']['single_loops'] = []
3984 # load custom settings
3986 return self
.execute(context
)
3988 def execute(self
, context
):
3990 global_undo
, object, bm
= initialise()
3991 settings_write(self
)
3993 # check cache to see if we can save time
3994 cached
, safe_strokes
, loops
, derived
, mapping
= cache_read("Gstretch",
3995 object, bm
, False, False)
3997 straightening
= False
3999 strokes
= gstretch_safe_to_true_strokes(safe_strokes
)
4000 # cached strokes were flushed (see operator's invoke function)
4001 elif get_grease_pencil(object, context
):
4002 strokes
= gstretch_get_strokes(object, context
)
4004 # straightening function (no GP) -> loops ignore modifiers
4005 straightening
= True
4008 bm_mod
.verts
.ensure_lookup_table()
4009 bm_mod
.edges
.ensure_lookup_table()
4010 bm_mod
.faces
.ensure_lookup_table()
4011 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
4012 if not straightening
:
4013 derived
, bm_mod
= get_derived_bmesh(object, bm
)
4015 # get loops and strokes
4016 if get_grease_pencil(object, context
):
4018 derived
, bm_mod
, loops
= get_connected_input(object, bm
, input='selected')
4019 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4020 loops
= check_loops(loops
, mapping
, bm_mod
)
4022 strokes
= gstretch_get_strokes(object, context
)
4024 # straightening function (no GP) -> loops ignore modifiers
4028 bm_mod
.verts
.ensure_lookup_table()
4029 bm_mod
.edges
.ensure_lookup_table()
4030 bm_mod
.faces
.ensure_lookup_table()
4032 edgekey(edge
) for edge
in bm_mod
.edges
if
4033 edge
.select
and not edge
.hide
4035 loops
= get_connected_selections(edge_keys
)
4036 loops
= check_loops(loops
, mapping
, bm_mod
)
4037 # create fake strokes
4038 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
4040 # saving cache for faster execution next time
4043 safe_strokes
= gstretch_true_to_safe_strokes(strokes
)
4046 cache_write("Gstretch", object, bm
, False, False,
4047 safe_strokes
, loops
, derived
, mapping
)
4049 # pair loops and strokes
4050 ls_pairs
= gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
)
4051 ls_pairs
= gstretch_align_pairs(ls_pairs
, object, bm_mod
, self
.method
)
4055 # no selected geometry, convert GP to verts
4057 move
.append(gstretch_create_verts(object, bm
, strokes
,
4058 self
.method
, self
.conversion
, self
.conversion_distance
,
4059 self
.conversion_max
, self
.conversion_min
,
4060 self
.conversion_vertices
))
4061 for stroke
in strokes
:
4062 gstretch_erase_stroke(stroke
, context
)
4064 for (loop
, stroke
) in ls_pairs
:
4065 move
.append(gstretch_calculate_verts(loop
, stroke
, object,
4066 bm_mod
, self
.method
))
4067 if self
.delete_strokes
:
4068 if type(stroke
) != bpy
.types
.GPencilStroke
:
4069 # in case of cached fake stroke, get the real one
4070 if get_grease_pencil(object, context
):
4071 strokes
= gstretch_get_strokes(object, context
)
4072 if loops
and strokes
:
4073 ls_pairs
= gstretch_match_loops_strokes(loops
,
4074 strokes
, object, bm_mod
)
4075 ls_pairs
= gstretch_align_pairs(ls_pairs
,
4076 object, bm_mod
, self
.method
)
4077 for (l
, s
) in ls_pairs
:
4081 gstretch_erase_stroke(stroke
, context
)
4083 # move vertices to new locations
4084 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4085 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4088 bmesh
.update_edit_mesh(object.data
, loop_triangles
=True, destructive
=True)
4089 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4094 terminate(global_undo
)
4100 class Relax(Operator
):
4101 bl_idname
= "mesh.looptools_relax"
4103 bl_description
= "Relax the loop, so it is smoother"
4104 bl_options
= {'REGISTER', 'UNDO'}
4106 input: EnumProperty(
4108 items
=(("all", "Parallel (all)", "Also use non-selected "
4109 "parallel loops as input"),
4110 ("selected", "Selection", "Only use selected vertices as input")),
4111 description
="Loops that are relaxed",
4114 interpolation
: EnumProperty(
4115 name
="Interpolation",
4116 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4117 ("linear", "Linear", "Simple and fast linear algorithm")),
4118 description
="Algorithm used for interpolation",
4121 iterations
: EnumProperty(
4123 items
=(("1", "1", "One"),
4124 ("3", "3", "Three"),
4126 ("10", "10", "Ten"),
4127 ("25", "25", "Twenty-five")),
4128 description
="Number of times the loop is relaxed",
4131 regular
: BoolProperty(
4133 description
="Distribute vertices at constant distances along the loop",
4138 def poll(cls
, context
):
4139 ob
= context
.active_object
4140 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4142 def draw(self
, context
):
4143 layout
= self
.layout
4144 col
= layout
.column()
4146 col
.prop(self
, "interpolation")
4147 col
.prop(self
, "input")
4148 col
.prop(self
, "iterations")
4149 col
.prop(self
, "regular")
4151 def invoke(self
, context
, event
):
4152 # load custom settings
4154 return self
.execute(context
)
4156 def execute(self
, context
):
4158 global_undo
, object, bm
= initialise()
4159 settings_write(self
)
4160 # check cache to see if we can save time
4161 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Relax",
4162 object, bm
, self
.input, False)
4164 derived
, bm_mod
= get_derived_bmesh(object, bm
)
4167 derived
, bm_mod
, loops
= get_connected_input(object, bm
, self
.input)
4168 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4169 loops
= check_loops(loops
, mapping
, bm_mod
)
4170 knots
, points
= relax_calculate_knots(loops
)
4172 # saving cache for faster execution next time
4174 cache_write("Relax", object, bm
, self
.input, False, False, loops
,
4177 for iteration
in range(int(self
.iterations
)):
4178 # calculate splines and new positions
4179 tknots
, tpoints
= relax_calculate_t(bm_mod
, knots
, points
,
4182 for i
in range(len(knots
)):
4183 splines
.append(calculate_splines(self
.interpolation
, bm_mod
,
4184 tknots
[i
], knots
[i
]))
4185 move
= [relax_calculate_verts(bm_mod
, self
.interpolation
,
4186 tknots
, knots
, tpoints
, points
, splines
)]
4187 move_verts(object, bm
, mapping
, move
, False, -1)
4192 terminate(global_undo
)
4198 class Space(Operator
):
4199 bl_idname
= "mesh.looptools_space"
4201 bl_description
= "Space the vertices in a regular distrubtion on the loop"
4202 bl_options
= {'REGISTER', 'UNDO'}
4204 influence
: FloatProperty(
4206 description
="Force of the tool",
4211 subtype
='PERCENTAGE'
4213 input: EnumProperty(
4215 items
=(("all", "Parallel (all)", "Also use non-selected "
4216 "parallel loops as input"),
4217 ("selected", "Selection", "Only use selected vertices as input")),
4218 description
="Loops that are spaced",
4221 interpolation
: EnumProperty(
4222 name
="Interpolation",
4223 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4224 ("linear", "Linear", "Vertices are projected on existing edges")),
4225 description
="Algorithm used for interpolation",
4228 lock_x
: BoolProperty(
4230 description
="Lock editing of the x-coordinate",
4233 lock_y
: BoolProperty(
4235 description
="Lock editing of the y-coordinate",
4238 lock_z
: BoolProperty(
4240 description
="Lock editing of the z-coordinate",
4245 def poll(cls
, context
):
4246 ob
= context
.active_object
4247 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4249 def draw(self
, context
):
4250 layout
= self
.layout
4251 col
= layout
.column()
4253 col
.prop(self
, "interpolation")
4254 col
.prop(self
, "input")
4257 col_move
= col
.column(align
=True)
4258 row
= col_move
.row(align
=True)
4260 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
4262 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
4264 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
4266 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
4268 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
4270 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
4271 col_move
.prop(self
, "influence")
4273 def invoke(self
, context
, event
):
4274 # load custom settings
4276 return self
.execute(context
)
4278 def execute(self
, context
):
4280 global_undo
, object, bm
= initialise()
4281 settings_write(self
)
4282 # check cache to see if we can save time
4283 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Space",
4284 object, bm
, self
.input, False)
4286 derived
, bm_mod
= get_derived_bmesh(object, bm
)
4289 derived
, bm_mod
, loops
= get_connected_input(object, bm
, self
.input)
4290 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4291 loops
= check_loops(loops
, mapping
, bm_mod
)
4293 # saving cache for faster execution next time
4295 cache_write("Space", object, bm
, self
.input, False, False, loops
,
4300 # calculate splines and new positions
4301 if loop
[1]: # circular
4302 loop
[0].append(loop
[0][0])
4303 tknots
, tpoints
= space_calculate_t(bm_mod
, loop
[0][:])
4304 splines
= calculate_splines(self
.interpolation
, bm_mod
,
4306 move
.append(space_calculate_verts(bm_mod
, self
.interpolation
,
4307 tknots
, tpoints
, loop
[0][:-1], splines
))
4308 # move vertices to new locations
4309 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4310 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4313 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4318 terminate(global_undo
)
4323 # ########################################
4324 # ##### GUI and registration #############
4325 # ########################################
4327 # menu containing all tools
4328 class VIEW3D_MT_edit_mesh_looptools(Menu
):
4329 bl_label
= "LoopTools"
4331 def draw(self
, context
):
4332 layout
= self
.layout
4334 layout
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4335 layout
.operator("mesh.looptools_circle")
4336 layout
.operator("mesh.looptools_curve")
4337 layout
.operator("mesh.looptools_flatten")
4338 layout
.operator("mesh.looptools_gstretch")
4339 layout
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4340 layout
.operator("mesh.looptools_relax")
4341 layout
.operator("mesh.looptools_space")
4344 # panel containing all tools
4345 class VIEW3D_PT_tools_looptools(Panel
):
4346 bl_space_type
= 'VIEW_3D'
4347 bl_region_type
= 'UI'
4348 bl_category
= 'View'
4349 bl_context
= "mesh_edit"
4350 bl_label
= "LoopTools"
4351 bl_options
= {'DEFAULT_CLOSED'}
4353 def draw(self
, context
):
4354 layout
= self
.layout
4355 col
= layout
.column(align
=True)
4356 lt
= context
.window_manager
.looptools
4358 # bridge - first line
4359 split
= col
.split(factor
=0.15, align
=True)
4360 if lt
.display_bridge
:
4361 split
.prop(lt
, "display_bridge", text
="", icon
='DOWNARROW_HLT')
4363 split
.prop(lt
, "display_bridge", text
="", icon
='RIGHTARROW')
4364 split
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4366 if lt
.display_bridge
:
4367 box
= col
.column(align
=True).box().column()
4368 # box.prop(self, "mode")
4371 col_top
= box
.column(align
=True)
4372 row
= col_top
.row(align
=True)
4373 col_left
= row
.column(align
=True)
4374 col_right
= row
.column(align
=True)
4375 col_right
.active
= lt
.bridge_segments
!= 1
4376 col_left
.prop(lt
, "bridge_segments")
4377 col_right
.prop(lt
, "bridge_min_width", text
="")
4379 bottom_left
= col_left
.row()
4380 bottom_left
.active
= lt
.bridge_segments
!= 1
4381 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4382 bottom_right
= col_right
.row()
4383 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4384 bottom_right
.prop(lt
, "bridge_cubic_strength")
4385 # boolean properties
4386 col_top
.prop(lt
, "bridge_remove_faces")
4388 # override properties
4390 row
= box
.row(align
=True)
4391 row
.prop(lt
, "bridge_twist")
4392 row
.prop(lt
, "bridge_reverse")
4394 # circle - first line
4395 split
= col
.split(factor
=0.15, align
=True)
4396 if lt
.display_circle
:
4397 split
.prop(lt
, "display_circle", text
="", icon
='DOWNARROW_HLT')
4399 split
.prop(lt
, "display_circle", text
="", icon
='RIGHTARROW')
4400 split
.operator("mesh.looptools_circle")
4402 if lt
.display_circle
:
4403 box
= col
.column(align
=True).box().column()
4404 box
.prop(lt
, "circle_fit")
4407 box
.prop(lt
, "circle_flatten")
4408 row
= box
.row(align
=True)
4409 row
.prop(lt
, "circle_custom_radius")
4410 row_right
= row
.row(align
=True)
4411 row_right
.active
= lt
.circle_custom_radius
4412 row_right
.prop(lt
, "circle_radius", text
="")
4413 box
.prop(lt
, "circle_regular")
4416 col_move
= box
.column(align
=True)
4417 row
= col_move
.row(align
=True)
4418 if lt
.circle_lock_x
:
4419 row
.prop(lt
, "circle_lock_x", text
="X", icon
='LOCKED')
4421 row
.prop(lt
, "circle_lock_x", text
="X", icon
='UNLOCKED')
4422 if lt
.circle_lock_y
:
4423 row
.prop(lt
, "circle_lock_y", text
="Y", icon
='LOCKED')
4425 row
.prop(lt
, "circle_lock_y", text
="Y", icon
='UNLOCKED')
4426 if lt
.circle_lock_z
:
4427 row
.prop(lt
, "circle_lock_z", text
="Z", icon
='LOCKED')
4429 row
.prop(lt
, "circle_lock_z", text
="Z", icon
='UNLOCKED')
4430 col_move
.prop(lt
, "circle_influence")
4432 # curve - first line
4433 split
= col
.split(factor
=0.15, align
=True)
4434 if lt
.display_curve
:
4435 split
.prop(lt
, "display_curve", text
="", icon
='DOWNARROW_HLT')
4437 split
.prop(lt
, "display_curve", text
="", icon
='RIGHTARROW')
4438 split
.operator("mesh.looptools_curve")
4440 if lt
.display_curve
:
4441 box
= col
.column(align
=True).box().column()
4442 box
.prop(lt
, "curve_interpolation")
4443 box
.prop(lt
, "curve_restriction")
4444 box
.prop(lt
, "curve_boundaries")
4445 box
.prop(lt
, "curve_regular")
4448 col_move
= box
.column(align
=True)
4449 row
= col_move
.row(align
=True)
4451 row
.prop(lt
, "curve_lock_x", text
="X", icon
='LOCKED')
4453 row
.prop(lt
, "curve_lock_x", text
="X", icon
='UNLOCKED')
4455 row
.prop(lt
, "curve_lock_y", text
="Y", icon
='LOCKED')
4457 row
.prop(lt
, "curve_lock_y", text
="Y", icon
='UNLOCKED')
4459 row
.prop(lt
, "curve_lock_z", text
="Z", icon
='LOCKED')
4461 row
.prop(lt
, "curve_lock_z", text
="Z", icon
='UNLOCKED')
4462 col_move
.prop(lt
, "curve_influence")
4464 # flatten - first line
4465 split
= col
.split(factor
=0.15, align
=True)
4466 if lt
.display_flatten
:
4467 split
.prop(lt
, "display_flatten", text
="", icon
='DOWNARROW_HLT')
4469 split
.prop(lt
, "display_flatten", text
="", icon
='RIGHTARROW')
4470 split
.operator("mesh.looptools_flatten")
4471 # flatten - settings
4472 if lt
.display_flatten
:
4473 box
= col
.column(align
=True).box().column()
4474 box
.prop(lt
, "flatten_plane")
4475 # box.prop(lt, "flatten_restriction")
4478 col_move
= box
.column(align
=True)
4479 row
= col_move
.row(align
=True)
4480 if lt
.flatten_lock_x
:
4481 row
.prop(lt
, "flatten_lock_x", text
="X", icon
='LOCKED')
4483 row
.prop(lt
, "flatten_lock_x", text
="X", icon
='UNLOCKED')
4484 if lt
.flatten_lock_y
:
4485 row
.prop(lt
, "flatten_lock_y", text
="Y", icon
='LOCKED')
4487 row
.prop(lt
, "flatten_lock_y", text
="Y", icon
='UNLOCKED')
4488 if lt
.flatten_lock_z
:
4489 row
.prop(lt
, "flatten_lock_z", text
="Z", icon
='LOCKED')
4491 row
.prop(lt
, "flatten_lock_z", text
="Z", icon
='UNLOCKED')
4492 col_move
.prop(lt
, "flatten_influence")
4494 # gstretch - first line
4495 split
= col
.split(factor
=0.15, align
=True)
4496 if lt
.display_gstretch
:
4497 split
.prop(lt
, "display_gstretch", text
="", icon
='DOWNARROW_HLT')
4499 split
.prop(lt
, "display_gstretch", text
="", icon
='RIGHTARROW')
4500 split
.operator("mesh.looptools_gstretch")
4502 if lt
.display_gstretch
:
4503 box
= col
.column(align
=True).box().column()
4504 box
.prop(lt
, "gstretch_method")
4506 col_conv
= box
.column(align
=True)
4507 col_conv
.prop(lt
, "gstretch_conversion", text
="")
4508 if lt
.gstretch_conversion
== 'distance':
4509 col_conv
.prop(lt
, "gstretch_conversion_distance")
4510 elif lt
.gstretch_conversion
== 'limit_vertices':
4511 row
= col_conv
.row(align
=True)
4512 row
.prop(lt
, "gstretch_conversion_min", text
="Min")
4513 row
.prop(lt
, "gstretch_conversion_max", text
="Max")
4514 elif lt
.gstretch_conversion
== 'vertices':
4515 col_conv
.prop(lt
, "gstretch_conversion_vertices")
4518 col_move
= box
.column(align
=True)
4519 row
= col_move
.row(align
=True)
4520 if lt
.gstretch_lock_x
:
4521 row
.prop(lt
, "gstretch_lock_x", text
="X", icon
='LOCKED')
4523 row
.prop(lt
, "gstretch_lock_x", text
="X", icon
='UNLOCKED')
4524 if lt
.gstretch_lock_y
:
4525 row
.prop(lt
, "gstretch_lock_y", text
="Y", icon
='LOCKED')
4527 row
.prop(lt
, "gstretch_lock_y", text
="Y", icon
='UNLOCKED')
4528 if lt
.gstretch_lock_z
:
4529 row
.prop(lt
, "gstretch_lock_z", text
="Z", icon
='LOCKED')
4531 row
.prop(lt
, "gstretch_lock_z", text
="Z", icon
='UNLOCKED')
4532 col_move
.prop(lt
, "gstretch_influence")
4533 box
.operator("remove.gp", text
="Delete GP Strokes")
4536 split
= col
.split(factor
=0.15, align
=True)
4538 split
.prop(lt
, "display_loft", text
="", icon
='DOWNARROW_HLT')
4540 split
.prop(lt
, "display_loft", text
="", icon
='RIGHTARROW')
4541 split
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4544 box
= col
.column(align
=True).box().column()
4545 # box.prop(self, "mode")
4548 col_top
= box
.column(align
=True)
4549 row
= col_top
.row(align
=True)
4550 col_left
= row
.column(align
=True)
4551 col_right
= row
.column(align
=True)
4552 col_right
.active
= lt
.bridge_segments
!= 1
4553 col_left
.prop(lt
, "bridge_segments")
4554 col_right
.prop(lt
, "bridge_min_width", text
="")
4556 bottom_left
= col_left
.row()
4557 bottom_left
.active
= lt
.bridge_segments
!= 1
4558 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4559 bottom_right
= col_right
.row()
4560 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4561 bottom_right
.prop(lt
, "bridge_cubic_strength")
4562 # boolean properties
4563 col_top
.prop(lt
, "bridge_remove_faces")
4564 col_top
.prop(lt
, "bridge_loft_loop")
4566 # override properties
4568 row
= box
.row(align
=True)
4569 row
.prop(lt
, "bridge_twist")
4570 row
.prop(lt
, "bridge_reverse")
4572 # relax - first line
4573 split
= col
.split(factor
=0.15, align
=True)
4574 if lt
.display_relax
:
4575 split
.prop(lt
, "display_relax", text
="", icon
='DOWNARROW_HLT')
4577 split
.prop(lt
, "display_relax", text
="", icon
='RIGHTARROW')
4578 split
.operator("mesh.looptools_relax")
4580 if lt
.display_relax
:
4581 box
= col
.column(align
=True).box().column()
4582 box
.prop(lt
, "relax_interpolation")
4583 box
.prop(lt
, "relax_input")
4584 box
.prop(lt
, "relax_iterations")
4585 box
.prop(lt
, "relax_regular")
4587 # space - first line
4588 split
= col
.split(factor
=0.15, align
=True)
4589 if lt
.display_space
:
4590 split
.prop(lt
, "display_space", text
="", icon
='DOWNARROW_HLT')
4592 split
.prop(lt
, "display_space", text
="", icon
='RIGHTARROW')
4593 split
.operator("mesh.looptools_space")
4595 if lt
.display_space
:
4596 box
= col
.column(align
=True).box().column()
4597 box
.prop(lt
, "space_interpolation")
4598 box
.prop(lt
, "space_input")
4601 col_move
= box
.column(align
=True)
4602 row
= col_move
.row(align
=True)
4604 row
.prop(lt
, "space_lock_x", text
="X", icon
='LOCKED')
4606 row
.prop(lt
, "space_lock_x", text
="X", icon
='UNLOCKED')
4608 row
.prop(lt
, "space_lock_y", text
="Y", icon
='LOCKED')
4610 row
.prop(lt
, "space_lock_y", text
="Y", icon
='UNLOCKED')
4612 row
.prop(lt
, "space_lock_z", text
="Z", icon
='LOCKED')
4614 row
.prop(lt
, "space_lock_z", text
="Z", icon
='UNLOCKED')
4615 col_move
.prop(lt
, "space_influence")
4618 # property group containing all properties for the gui in the panel
4619 class LoopToolsProps(PropertyGroup
):
4621 Fake module like class
4622 bpy.context.window_manager.looptools
4624 # general display properties
4625 display_bridge
: BoolProperty(
4626 name
="Bridge settings",
4627 description
="Display settings of the Bridge tool",
4630 display_circle
: BoolProperty(
4631 name
="Circle settings",
4632 description
="Display settings of the Circle tool",
4635 display_curve
: BoolProperty(
4636 name
="Curve settings",
4637 description
="Display settings of the Curve tool",
4640 display_flatten
: BoolProperty(
4641 name
="Flatten settings",
4642 description
="Display settings of the Flatten tool",
4645 display_gstretch
: BoolProperty(
4646 name
="Gstretch settings",
4647 description
="Display settings of the Gstretch tool",
4650 display_loft
: BoolProperty(
4651 name
="Loft settings",
4652 description
="Display settings of the Loft tool",
4655 display_relax
: BoolProperty(
4656 name
="Relax settings",
4657 description
="Display settings of the Relax tool",
4660 display_space
: BoolProperty(
4661 name
="Space settings",
4662 description
="Display settings of the Space tool",
4667 bridge_cubic_strength
: FloatProperty(
4669 description
="Higher strength results in more fluid curves",
4674 bridge_interpolation
: EnumProperty(
4675 name
="Interpolation mode",
4676 items
=(('cubic', "Cubic", "Gives curved results"),
4677 ('linear', "Linear", "Basic, fast, straight interpolation")),
4678 description
="Interpolation mode: algorithm used when creating segments",
4681 bridge_loft
: BoolProperty(
4683 description
="Loft multiple loops, instead of considering them as "
4684 "a multi-input for bridging",
4687 bridge_loft_loop
: BoolProperty(
4689 description
="Connect the first and the last loop with each other",
4692 bridge_min_width
: IntProperty(
4693 name
="Minimum width",
4694 description
="Segments with an edge smaller than this are merged "
4695 "(compared to base edge)",
4699 subtype
='PERCENTAGE'
4701 bridge_mode
: EnumProperty(
4703 items
=(('basic', "Basic", "Fast algorithm"),
4704 ('shortest', "Shortest edge", "Slower algorithm with "
4705 "better vertex matching")),
4706 description
="Algorithm used for bridging",
4709 bridge_remove_faces
: BoolProperty(
4710 name
="Remove faces",
4711 description
="Remove faces that are internal after bridging",
4714 bridge_reverse
: BoolProperty(
4716 description
="Manually override the direction in which the loops "
4717 "are bridged. Only use if the tool gives the wrong result",
4720 bridge_segments
: IntProperty(
4722 description
="Number of segments used to bridge the gap (0=automatic)",
4727 bridge_twist
: IntProperty(
4729 description
="Twist what vertices are connected to each other",
4734 circle_custom_radius
: BoolProperty(
4736 description
="Force a custom radius",
4739 circle_fit
: EnumProperty(
4741 items
=(("best", "Best fit", "Non-linear least squares"),
4742 ("inside", "Fit inside", "Only move vertices towards the center")),
4743 description
="Method used for fitting a circle to the vertices",
4746 circle_flatten
: BoolProperty(
4748 description
="Flatten the circle, instead of projecting it on the mesh",
4751 circle_influence
: FloatProperty(
4753 description
="Force of the tool",
4758 subtype
='PERCENTAGE'
4760 circle_lock_x
: BoolProperty(
4762 description
="Lock editing of the x-coordinate",
4765 circle_lock_y
: BoolProperty(
4767 description
="Lock editing of the y-coordinate",
4770 circle_lock_z
: BoolProperty(
4772 description
="Lock editing of the z-coordinate",
4775 circle_radius
: FloatProperty(
4777 description
="Custom radius for circle",
4782 circle_regular
: BoolProperty(
4784 description
="Distribute vertices at constant distances along the circle",
4788 curve_boundaries
: BoolProperty(
4790 description
="Limit the tool to work within the boundaries of the "
4791 "selected vertices",
4794 curve_influence
: FloatProperty(
4796 description
="Force of the tool",
4801 subtype
='PERCENTAGE'
4803 curve_interpolation
: EnumProperty(
4804 name
="Interpolation",
4805 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4806 ("linear", "Linear", "Simple and fast linear algorithm")),
4807 description
="Algorithm used for interpolation",
4810 curve_lock_x
: BoolProperty(
4812 description
="Lock editing of the x-coordinate",
4815 curve_lock_y
: BoolProperty(
4817 description
="Lock editing of the y-coordinate",
4820 curve_lock_z
: BoolProperty(
4822 description
="Lock editing of the z-coordinate",
4825 curve_regular
: BoolProperty(
4827 description
="Distribute vertices at constant distances along the curve",
4830 curve_restriction
: EnumProperty(
4832 items
=(("none", "None", "No restrictions on vertex movement"),
4833 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
4834 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
4835 description
="Restrictions on how the vertices can be moved",
4839 # flatten properties
4840 flatten_influence
: FloatProperty(
4842 description
="Force of the tool",
4847 subtype
='PERCENTAGE'
4849 flatten_lock_x
: BoolProperty(
4851 description
="Lock editing of the x-coordinate",
4853 flatten_lock_y
: BoolProperty(name
="Lock Y",
4854 description
="Lock editing of the y-coordinate",
4857 flatten_lock_z
: BoolProperty(
4859 description
="Lock editing of the z-coordinate",
4862 flatten_plane
: EnumProperty(
4864 items
=(("best_fit", "Best fit", "Calculate a best fitting plane"),
4865 ("normal", "Normal", "Derive plane from averaging vertex "
4867 ("view", "View", "Flatten on a plane perpendicular to the "
4869 description
="Plane on which vertices are flattened",
4872 flatten_restriction
: EnumProperty(
4874 items
=(("none", "None", "No restrictions on vertex movement"),
4875 ("bounding_box", "Bounding box", "Vertices are restricted to "
4876 "movement inside the bounding box of the selection")),
4877 description
="Restrictions on how the vertices can be moved",
4881 # gstretch properties
4882 gstretch_conversion
: EnumProperty(
4884 items
=(("distance", "Distance", "Set the distance between vertices "
4885 "of the converted grease pencil stroke"),
4886 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
4887 "number of vertices that converted GP strokes will have"),
4888 ("vertices", "Exact vertices", "Set the exact number of vertices "
4889 "that converted grease pencil strokes will have. Short strokes "
4890 "with few points may contain less vertices than this number."),
4891 ("none", "No simplification", "Convert each grease pencil point "
4893 description
="If grease pencil strokes are converted to geometry, "
4894 "use this simplification method",
4895 default
='limit_vertices'
4897 gstretch_conversion_distance
: FloatProperty(
4899 description
="Absolute distance between vertices along the converted "
4900 "grease pencil stroke",
4906 gstretch_conversion_max
: IntProperty(
4907 name
="Max Vertices",
4908 description
="Maximum number of vertices grease pencil strokes will "
4909 "have, when they are converted to geomtery",
4913 update
=gstretch_update_min
4915 gstretch_conversion_min
: IntProperty(
4916 name
="Min Vertices",
4917 description
="Minimum number of vertices grease pencil strokes will "
4918 "have, when they are converted to geomtery",
4922 update
=gstretch_update_max
4924 gstretch_conversion_vertices
: IntProperty(
4926 description
="Number of vertices grease pencil strokes will "
4927 "have, when they are converted to geometry. If strokes have less "
4928 "points than required, the 'Spread evenly' method is used",
4933 gstretch_delete_strokes
: BoolProperty(
4934 name
="Delete strokes",
4935 description
="Remove Grease Pencil strokes if they have been used "
4936 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
4939 gstretch_influence
: FloatProperty(
4941 description
="Force of the tool",
4946 subtype
='PERCENTAGE'
4948 gstretch_lock_x
: BoolProperty(
4950 description
="Lock editing of the x-coordinate",
4953 gstretch_lock_y
: BoolProperty(
4955 description
="Lock editing of the y-coordinate",
4958 gstretch_lock_z
: BoolProperty(
4960 description
="Lock editing of the z-coordinate",
4963 gstretch_method
: EnumProperty(
4965 items
=(("project", "Project", "Project vertices onto the stroke, "
4966 "using vertex normals and connected edges"),
4967 ("irregular", "Spread", "Distribute vertices along the full "
4968 "stroke, retaining relative distances between the vertices"),
4969 ("regular", "Spread evenly", "Distribute vertices at regular "
4970 "distances along the full stroke")),
4971 description
="Method of distributing the vertices over the Grease "
4977 relax_input
: EnumProperty(name
="Input",
4978 items
=(("all", "Parallel (all)", "Also use non-selected "
4979 "parallel loops as input"),
4980 ("selected", "Selection", "Only use selected vertices as input")),
4981 description
="Loops that are relaxed",
4984 relax_interpolation
: EnumProperty(
4985 name
="Interpolation",
4986 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4987 ("linear", "Linear", "Simple and fast linear algorithm")),
4988 description
="Algorithm used for interpolation",
4991 relax_iterations
: EnumProperty(name
="Iterations",
4992 items
=(("1", "1", "One"),
4993 ("3", "3", "Three"),
4995 ("10", "10", "Ten"),
4996 ("25", "25", "Twenty-five")),
4997 description
="Number of times the loop is relaxed",
5000 relax_regular
: BoolProperty(
5002 description
="Distribute vertices at constant distances along the loop",
5007 space_influence
: FloatProperty(
5009 description
="Force of the tool",
5014 subtype
='PERCENTAGE'
5016 space_input
: EnumProperty(
5018 items
=(("all", "Parallel (all)", "Also use non-selected "
5019 "parallel loops as input"),
5020 ("selected", "Selection", "Only use selected vertices as input")),
5021 description
="Loops that are spaced",
5024 space_interpolation
: EnumProperty(
5025 name
="Interpolation",
5026 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5027 ("linear", "Linear", "Vertices are projected on existing edges")),
5028 description
="Algorithm used for interpolation",
5031 space_lock_x
: BoolProperty(
5033 description
="Lock editing of the x-coordinate",
5036 space_lock_y
: BoolProperty(
5038 description
="Lock editing of the y-coordinate",
5041 space_lock_z
: BoolProperty(
5043 description
="Lock editing of the z-coordinate",
5048 # draw function for integration in menus
5049 def menu_func(self
, context
):
5050 self
.layout
.menu("VIEW3D_MT_edit_mesh_looptools")
5051 self
.layout
.separator()
5054 # Add-ons Preferences Update Panel
5056 # Define Panel classes for updating
5058 VIEW3D_PT_tools_looptools
,
5062 def update_panel(self
, context
):
5063 message
= "LoopTools: Updating Panel locations has failed"
5065 for panel
in panels
:
5066 if "bl_rna" in panel
.__dict
__:
5067 bpy
.utils
.unregister_class(panel
)
5069 for panel
in panels
:
5070 panel
.bl_category
= context
.user_preferences
.addons
[__name__
].preferences
.category
5071 bpy
.utils
.register_class(panel
)
5073 except Exception as e
:
5074 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
5078 class LoopPreferences(AddonPreferences
):
5079 # this must match the addon name, use '__package__'
5080 # when defining this in a submodule of a python package.
5081 bl_idname
= __name__
5083 category
: StringProperty(
5084 name
="Tab Category",
5085 description
="Choose a name for the category of the panel",
5090 def draw(self
, context
):
5091 layout
= self
.layout
5095 col
.label(text
="Tab Category:")
5096 col
.prop(self
, "category", text
="")
5099 # define classes for registration
5101 VIEW3D_MT_edit_mesh_looptools
,
5102 VIEW3D_PT_tools_looptools
,
5116 # registering and menu integration
5119 bpy
.utils
.register_class(cls
)
5120 bpy
.types
.VIEW3D_MT_edit_mesh_specials
.prepend(menu_func
)
5121 bpy
.types
.WindowManager
.looptools
= PointerProperty(type=LoopToolsProps
)
5122 update_panel(None, bpy
.context
)
5125 # unregistering and removing menus
5127 for cls
in reversed(classes
):
5128 bpy
.utils
.unregister_class(cls
)
5129 bpy
.types
.VIEW3D_MT_edit_mesh_specials
.remove(menu_func
)
5131 del bpy
.types
.WindowManager
.looptools
5132 except Exception as e
:
5133 print('unregister fail:\n', e
)
5137 if __name__
== "__main__":