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 > Sidebar > Edit Tab / Edit Mode Context Menu",
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_strokes(self
, context
):
64 looptools
= context
.window_manager
.looptools
65 if looptools
.gstretch_use_guide
== "Annotation":
67 strokes
= bpy
.data
.grease_pencils
[0].layers
.active
.active_frame
.strokes
70 self
.report({'WARNING'}, "active Annotation strokes not found")
72 if looptools
.gstretch_use_guide
== "GPencil" and not looptools
.gstretch_guide
== None:
74 strokes
= looptools
.gstretch_guide
.data
.layers
.active
.active_frame
.strokes
77 self
.report({'WARNING'}, "active GPencil strokes not found")
82 # force a full recalculation next time
83 def cache_delete(tool
):
84 if tool
in looptools_cache
:
85 del looptools_cache
[tool
]
88 # check cache for stored information
89 def cache_read(tool
, object, bm
, input_method
, boundaries
):
90 # current tool not cached yet
91 if tool
not in looptools_cache
:
92 return(False, False, False, False, False)
93 # check if selected object didn't change
94 if object.name
!= looptools_cache
[tool
]["object"]:
95 return(False, False, False, False, False)
96 # check if input didn't change
97 if input_method
!= looptools_cache
[tool
]["input_method"]:
98 return(False, False, False, False, False)
99 if boundaries
!= looptools_cache
[tool
]["boundaries"]:
100 return(False, False, False, False, False)
101 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
and
102 mod
.type == 'MIRROR']
103 if modifiers
!= looptools_cache
[tool
]["modifiers"]:
104 return(False, False, False, False, False)
105 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
106 if input != looptools_cache
[tool
]["input"]:
107 return(False, False, False, False, False)
109 single_loops
= looptools_cache
[tool
]["single_loops"]
110 loops
= looptools_cache
[tool
]["loops"]
111 derived
= looptools_cache
[tool
]["derived"]
112 mapping
= looptools_cache
[tool
]["mapping"]
114 return(True, single_loops
, loops
, derived
, mapping
)
117 # store information in the cache
118 def cache_write(tool
, object, bm
, input_method
, boundaries
, single_loops
,
119 loops
, derived
, mapping
):
120 # clear cache of current tool
121 if tool
in looptools_cache
:
122 del looptools_cache
[tool
]
123 # prepare values to be saved to cache
124 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
125 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
126 and mod
.type == 'MIRROR']
128 looptools_cache
[tool
] = {
129 "input": input, "object": object.name
,
130 "input_method": input_method
, "boundaries": boundaries
,
131 "single_loops": single_loops
, "loops": loops
,
132 "derived": derived
, "mapping": mapping
, "modifiers": modifiers
}
135 # calculates natural cubic splines through all given knots
136 def calculate_cubic_splines(bm_mod
, tknots
, knots
):
137 # hack for circular loops
138 if knots
[0] == knots
[-1] and len(knots
) > 1:
141 for k
in range(-1, -5, -1):
142 if k
- 1 < -len(knots
):
144 k_new1
.append(knots
[k
- 1])
147 if k
+ 1 > len(knots
) - 1:
149 k_new2
.append(knots
[k
+ 1])
156 for t
in range(-1, -5, -1):
157 if t
- 1 < -len(tknots
):
159 total1
+= tknots
[t
] - tknots
[t
- 1]
160 t_new1
.append(tknots
[0] - total1
)
164 if t
+ 1 > len(tknots
) - 1:
166 total2
+= tknots
[t
+ 1] - tknots
[t
]
167 t_new2
.append(tknots
[-1] + total2
)
180 locs
= [bm_mod
.verts
[k
].co
[:] for k
in knots
]
187 for i
in range(n
- 1):
188 if x
[i
+ 1] - x
[i
] == 0:
191 h
.append(x
[i
+ 1] - x
[i
])
193 for i
in range(1, n
- 1):
194 q
.append(3 / h
[i
] * (a
[i
+ 1] - a
[i
]) - 3 / h
[i
- 1] * (a
[i
] - a
[i
- 1]))
198 for i
in range(1, n
- 1):
199 l
.append(2 * (x
[i
+ 1] - x
[i
- 1]) - h
[i
- 1] * u
[i
- 1])
202 u
.append(h
[i
] / l
[i
])
203 z
.append((q
[i
] - h
[i
- 1] * z
[i
- 1]) / l
[i
])
206 b
= [False for i
in range(n
- 1)]
207 c
= [False for i
in range(n
)]
208 d
= [False for i
in range(n
- 1)]
210 for i
in range(n
- 2, -1, -1):
211 c
[i
] = z
[i
] - u
[i
] * c
[i
+ 1]
212 b
[i
] = (a
[i
+ 1] - a
[i
]) / h
[i
] - h
[i
] * (c
[i
+ 1] + 2 * c
[i
]) / 3
213 d
[i
] = (c
[i
+ 1] - c
[i
]) / (3 * h
[i
])
214 for i
in range(n
- 1):
215 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
217 for i
in range(len(knots
) - 1):
218 splines
.append([result
[i
], result
[i
+ n
- 1], result
[i
+ (n
- 1) * 2]])
219 if circular
: # cleaning up after hack
221 tknots
= tknots
[4:-4]
226 # calculates linear splines through all given knots
227 def calculate_linear_splines(bm_mod
, tknots
, knots
):
229 for i
in range(len(knots
) - 1):
230 a
= bm_mod
.verts
[knots
[i
]].co
231 b
= bm_mod
.verts
[knots
[i
+ 1]].co
234 u
= tknots
[i
+ 1] - t
235 splines
.append([a
, d
, t
, u
]) # [locStart, locDif, tStart, tDif]
240 # calculate a best-fit plane to the given vertices
241 def calculate_plane(bm_mod
, loop
, method
="best_fit", object=False):
242 # getting the vertex locations
243 locs
= [bm_mod
.verts
[v
].co
.copy() for v
in loop
[0]]
245 # calculating the center of masss
246 com
= mathutils
.Vector()
252 if method
== 'best_fit':
253 # creating the covariance matrix
254 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
259 mat
[0][0] += (loc
[0] - x
) ** 2
260 mat
[1][0] += (loc
[0] - x
) * (loc
[1] - y
)
261 mat
[2][0] += (loc
[0] - x
) * (loc
[2] - z
)
262 mat
[0][1] += (loc
[1] - y
) * (loc
[0] - x
)
263 mat
[1][1] += (loc
[1] - y
) ** 2
264 mat
[2][1] += (loc
[1] - y
) * (loc
[2] - z
)
265 mat
[0][2] += (loc
[2] - z
) * (loc
[0] - x
)
266 mat
[1][2] += (loc
[2] - z
) * (loc
[1] - y
)
267 mat
[2][2] += (loc
[2] - z
) ** 2
269 # calculating the normal to the plane
272 mat
= matrix_invert(mat
)
275 if math
.fabs(sum(mat
[0])) < math
.fabs(sum(mat
[1])):
276 if math
.fabs(sum(mat
[0])) < math
.fabs(sum(mat
[2])):
278 elif math
.fabs(sum(mat
[1])) < math
.fabs(sum(mat
[2])):
281 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
283 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
285 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
287 # warning! this is different from .normalize()
289 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
290 for i
in range(itermax
):
298 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
301 elif method
== 'normal':
302 # averaging the vertex normals
303 v_normals
= [bm_mod
.verts
[v
].normal
for v
in loop
[0]]
304 normal
= mathutils
.Vector()
305 for v_normal
in v_normals
:
307 normal
/= len(v_normals
)
310 elif method
== 'view':
311 # calculate view normal
312 rotation
= bpy
.context
.space_data
.region_3d
.view_matrix
.to_3x3().\
314 normal
= rotation
@ mathutils
.Vector((0.0, 0.0, 1.0))
316 normal
= object.matrix_world
.inverted().to_euler().to_matrix() @ \
322 # calculate splines based on given interpolation method (controller function)
323 def calculate_splines(interpolation
, bm_mod
, tknots
, knots
):
324 if interpolation
== 'cubic':
325 splines
= calculate_cubic_splines(bm_mod
, tknots
, knots
[:])
326 else: # interpolations == 'linear'
327 splines
= calculate_linear_splines(bm_mod
, tknots
, knots
[:])
332 # check loops and only return valid ones
333 def check_loops(loops
, mapping
, bm_mod
):
335 for loop
, circular
in loops
:
336 # loop needs to have at least 3 vertices
339 # loop needs at least 1 vertex in the original, non-mirrored mesh
343 if mapping
[vert
] > -1:
348 # vertices can not all be at the same location
350 for i
in range(len(loop
) - 1):
351 if (bm_mod
.verts
[loop
[i
]].co
- bm_mod
.verts
[loop
[i
+ 1]].co
).length
> 1e-6:
356 # passed all tests, loop is valid
357 valid_loops
.append([loop
, circular
])
362 # input: bmesh, output: dict with the edge-key as key and face-index as value
363 def dict_edge_faces(bm
):
364 edge_faces
= dict([[edgekey(edge
), []] for edge
in bm
.edges
if not edge
.hide
])
365 for face
in bm
.faces
:
368 for key
in face_edgekeys(face
):
369 edge_faces
[key
].append(face
.index
)
374 # input: bmesh (edge-faces optional), output: dict with face-face connections
375 def dict_face_faces(bm
, edge_faces
=False):
377 edge_faces
= dict_edge_faces(bm
)
379 connected_faces
= dict([[face
.index
, []] for face
in bm
.faces
if not face
.hide
])
380 for face
in bm
.faces
:
383 for edge_key
in face_edgekeys(face
):
384 for connected_face
in edge_faces
[edge_key
]:
385 if connected_face
== face
.index
:
387 connected_faces
[face
.index
].append(connected_face
)
389 return(connected_faces
)
392 # input: bmesh, output: dict with the vert index as key and edge-keys as value
393 def dict_vert_edges(bm
):
394 vert_edges
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
395 for edge
in bm
.edges
:
400 vert_edges
[vert
].append(ek
)
405 # input: bmesh, output: dict with the vert index as key and face index as value
406 def dict_vert_faces(bm
):
407 vert_faces
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
408 for face
in bm
.faces
:
410 for vert
in face
.verts
:
411 vert_faces
[vert
.index
].append(face
.index
)
416 # input: list of edge-keys, output: dictionary with vertex-vertex connections
417 def dict_vert_verts(edge_keys
):
418 # create connection data
422 if ek
[i
] in vert_verts
:
423 vert_verts
[ek
[i
]].append(ek
[1 - i
])
425 vert_verts
[ek
[i
]] = [ek
[1 - i
]]
430 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
432 return(tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])))
435 # returns the edgekeys of a bmesh face
436 def face_edgekeys(face
):
437 return([tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])) for edge
in face
.edges
])
440 # calculate input loops
441 def get_connected_input(object, bm
, input):
442 # get mesh with modifiers applied
443 derived
, bm_mod
= get_derived_bmesh(object, bm
)
445 # calculate selected loops
446 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and not edge
.hide
]
447 loops
= get_connected_selections(edge_keys
)
449 # if only selected loops are needed, we're done
450 if input == 'selected':
451 return(derived
, bm_mod
, loops
)
452 # elif input == 'all':
453 loops
= get_parallel_loops(bm_mod
, loops
)
455 return(derived
, bm_mod
, loops
)
458 # sorts all edge-keys into a list of loops
459 def get_connected_selections(edge_keys
):
460 # create connection data
461 vert_verts
= dict_vert_verts(edge_keys
)
463 # find loops consisting of connected selected edges
465 while len(vert_verts
) > 0:
466 loop
= [iter(vert_verts
.keys()).__next
__()]
472 # no more connection data for current vertex
473 if loop
[-1] not in vert_verts
:
481 for i
, next_vert
in enumerate(vert_verts
[loop
[-1]]):
482 if next_vert
not in loop
:
483 vert_verts
[loop
[-1]].pop(i
)
484 if len(vert_verts
[loop
[-1]]) == 0:
485 del vert_verts
[loop
[-1]]
486 # remove connection both ways
487 if next_vert
in vert_verts
:
488 if len(vert_verts
[next_vert
]) == 1:
489 del vert_verts
[next_vert
]
491 vert_verts
[next_vert
].remove(loop
[-1])
492 loop
.append(next_vert
)
496 # found one end of the loop, continue with next
500 # found both ends of the loop, stop growing
504 # check if loop is circular
505 if loop
[0] in vert_verts
:
506 if loop
[-1] in vert_verts
[loop
[0]]:
508 if len(vert_verts
[loop
[0]]) == 1:
509 del vert_verts
[loop
[0]]
511 vert_verts
[loop
[0]].remove(loop
[-1])
512 if len(vert_verts
[loop
[-1]]) == 1:
513 del vert_verts
[loop
[-1]]
515 vert_verts
[loop
[-1]].remove(loop
[0])
529 # get the derived mesh data, if there is a mirror modifier
530 def get_derived_bmesh(object, bm
):
531 # check for mirror modifiers
532 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
534 # disable other modifiers
535 show_viewport
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
]
536 for mod
in object.modifiers
:
537 if mod
.type != 'MIRROR':
538 mod
.show_viewport
= False
541 depsgraph
= bpy
.context
.evaluated_depsgraph_get()
542 object_eval
= object.evaluated_get(depsgraph
)
543 mesh_mod
= object_eval
.to_mesh()
544 bm_mod
.from_mesh(mesh_mod
)
545 object_eval
.to_mesh_clear()
546 # re-enable other modifiers
547 for mod_name
in show_viewport
:
548 object.modifiers
[mod_name
].show_viewport
= True
549 # no mirror modifiers, so no derived mesh necessary
554 bm_mod
.verts
.ensure_lookup_table()
555 bm_mod
.edges
.ensure_lookup_table()
556 bm_mod
.faces
.ensure_lookup_table()
558 return(derived
, bm_mod
)
561 # return a mapping of derived indices to indices
562 def get_mapping(derived
, bm
, bm_mod
, single_vertices
, full_search
, loops
):
567 verts
= [v
for v
in bm
.verts
if not v
.hide
]
569 verts
= [v
for v
in bm
.verts
if v
.select
and not v
.hide
]
571 # non-selected vertices around single vertices also need to be mapped
573 mapping
= dict([[vert
, -1] for vert
in single_vertices
])
574 verts_mod
= [bm_mod
.verts
[vert
] for vert
in single_vertices
]
576 for v_mod
in verts_mod
:
577 if (v
.co
- v_mod
.co
).length
< 1e-6:
578 mapping
[v_mod
.index
] = v
.index
580 real_singles
= [v_real
for v_real
in mapping
.values() if v_real
> -1]
582 verts_indices
= [vert
.index
for vert
in verts
]
583 for face
in [face
for face
in bm
.faces
if not face
.select
and not face
.hide
]:
584 for vert
in face
.verts
:
585 if vert
.index
in real_singles
:
587 if v
.index
not in verts_indices
:
592 # create mapping of derived indices to indices
593 mapping
= dict([[vert
, -1] for loop
in loops
for vert
in loop
[0]])
595 for single
in single_vertices
:
597 verts_mod
= [bm_mod
.verts
[i
] for i
in mapping
.keys()]
599 for v_mod
in verts_mod
:
600 if (v
.co
- v_mod
.co
).length
< 1e-6:
601 mapping
[v_mod
.index
] = v
.index
602 verts_mod
.remove(v_mod
)
608 # calculate the determinant of a matrix
609 def matrix_determinant(m
):
610 determinant
= m
[0][0] * m
[1][1] * m
[2][2] + m
[0][1] * m
[1][2] * m
[2][0] \
611 + m
[0][2] * m
[1][0] * m
[2][1] - m
[0][2] * m
[1][1] * m
[2][0] \
612 - m
[0][1] * m
[1][0] * m
[2][2] - m
[0][0] * m
[1][2] * m
[2][1]
617 # custom matrix inversion, to provide higher precision than the built-in one
618 def matrix_invert(m
):
619 r
= mathutils
.Matrix((
620 (m
[1][1] * m
[2][2] - m
[1][2] * m
[2][1], m
[0][2] * m
[2][1] - m
[0][1] * m
[2][2],
621 m
[0][1] * m
[1][2] - m
[0][2] * m
[1][1]),
622 (m
[1][2] * m
[2][0] - m
[1][0] * m
[2][2], m
[0][0] * m
[2][2] - m
[0][2] * m
[2][0],
623 m
[0][2] * m
[1][0] - m
[0][0] * m
[1][2]),
624 (m
[1][0] * m
[2][1] - m
[1][1] * m
[2][0], m
[0][1] * m
[2][0] - m
[0][0] * m
[2][1],
625 m
[0][0] * m
[1][1] - m
[0][1] * m
[1][0])))
627 return (r
* (1 / matrix_determinant(m
)))
630 # returns a list of all loops parallel to the input, input included
631 def get_parallel_loops(bm_mod
, loops
):
632 # get required dictionaries
633 edge_faces
= dict_edge_faces(bm_mod
)
634 connected_faces
= dict_face_faces(bm_mod
, edge_faces
)
635 # turn vertex loops into edge loops
638 edgeloop
= [[sorted([loop
[0][i
], loop
[0][i
+ 1]]) for i
in
639 range(len(loop
[0]) - 1)], loop
[1]]
640 if loop
[1]: # circular
641 edgeloop
[0].append(sorted([loop
[0][-1], loop
[0][0]]))
642 edgeloops
.append(edgeloop
[:])
643 # variables to keep track while iterating
647 for loop
in edgeloops
:
648 # initialise with original loop
649 all_edgeloops
.append(loop
[0])
653 if edge
[0] not in verts_used
:
654 verts_used
.append(edge
[0])
655 if edge
[1] not in verts_used
:
656 verts_used
.append(edge
[1])
658 # find parallel loops
659 while len(newloops
) > 0:
662 for i
in newloops
[-1]:
664 forbidden_side
= False
665 if i
not in edge_faces
:
666 # weird input with branches
669 for face
in edge_faces
[i
]:
670 if len(side_a
) == 0 and forbidden_side
!= "a":
676 elif side_a
[-1] in connected_faces
[face
] and \
677 forbidden_side
!= "a":
683 if len(side_b
) == 0 and forbidden_side
!= "b":
689 elif side_b
[-1] in connected_faces
[face
] and \
690 forbidden_side
!= "b":
698 # weird input with branches
711 for key
in face_edgekeys(bm_mod
.faces
[fi
]):
712 if key
[0] not in verts_used
and key
[1] not in \
714 extraloop
.append(key
)
717 for key
in extraloop
:
719 if new_vert
not in verts_used
:
720 verts_used
.append(new_vert
)
721 newloops
.append(extraloop
)
722 all_edgeloops
.append(extraloop
)
724 # input contains branches, only return selected loop
728 # change edgeloops into normal loops
730 for edgeloop
in all_edgeloops
:
732 # grow loop by comparing vertices between consecutive edge-keys
733 for i
in range(len(edgeloop
) - 1):
734 for vert
in range(2):
735 if edgeloop
[i
][vert
] in edgeloop
[i
+ 1]:
736 loop
.append(edgeloop
[i
][vert
])
739 # add starting vertex
740 for vert
in range(2):
741 if edgeloop
[0][vert
] != loop
[0]:
742 loop
= [edgeloop
[0][vert
]] + loop
745 for vert
in range(2):
746 if edgeloop
[-1][vert
] != loop
[-1]:
747 loop
.append(edgeloop
[-1][vert
])
749 # check if loop is circular
750 if loop
[0] == loop
[-1]:
755 loops
.append([loop
, circular
])
760 # gather initial data
762 object = bpy
.context
.active_object
763 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
764 # ensure that selection is synced for the derived mesh
765 bpy
.ops
.object.mode_set(mode
='OBJECT')
766 bpy
.ops
.object.mode_set(mode
='EDIT')
767 bm
= bmesh
.from_edit_mesh(object.data
)
769 bm
.verts
.ensure_lookup_table()
770 bm
.edges
.ensure_lookup_table()
771 bm
.faces
.ensure_lookup_table()
776 # move the vertices to their new locations
777 def move_verts(object, bm
, mapping
, move
, lock
, influence
):
779 lock_x
, lock_y
, lock_z
= lock
780 orient_slot
= bpy
.context
.scene
.transform_orientation_slots
[0]
781 custom
= orient_slot
.custom_orientation
783 mat
= custom
.matrix
.to_4x4().inverted() @ object.matrix_world
.copy()
784 elif orient_slot
.type == 'LOCAL':
785 mat
= mathutils
.Matrix
.Identity(4)
786 elif orient_slot
.type == 'VIEW':
787 mat
= bpy
.context
.region_data
.view_matrix
.copy() @ \
788 object.matrix_world
.copy()
789 else: # orientation == 'GLOBAL'
790 mat
= object.matrix_world
.copy()
791 mat_inv
= mat
.inverted()
794 for index
, loc
in loop
:
796 if mapping
[index
] == -1:
799 index
= mapping
[index
]
801 delta
= (loc
- bm
.verts
[index
].co
) @ mat_inv
809 loc
= bm
.verts
[index
].co
+ delta
813 new_loc
= loc
* (influence
/ 100) + \
814 bm
.verts
[index
].co
* ((100 - influence
) / 100)
815 bm
.verts
[index
].co
= new_loc
819 bm
.verts
.ensure_lookup_table()
820 bm
.edges
.ensure_lookup_table()
821 bm
.faces
.ensure_lookup_table()
824 # load custom tool settings
825 def settings_load(self
):
826 lt
= bpy
.context
.window_manager
.looptools
827 tool
= self
.name
.split()[0].lower()
828 keys
= self
.as_keywords().keys()
830 setattr(self
, key
, getattr(lt
, tool
+ "_" + key
))
833 # store custom tool settings
834 def settings_write(self
):
835 lt
= bpy
.context
.window_manager
.looptools
836 tool
= self
.name
.split()[0].lower()
837 keys
= self
.as_keywords().keys()
839 setattr(lt
, tool
+ "_" + key
, getattr(self
, key
))
842 # clean up and set settings back to original state
844 # update editmesh cached data
845 obj
= bpy
.context
.active_object
846 if obj
.mode
== 'EDIT':
847 bmesh
.update_edit_mesh(obj
.data
, loop_triangles
=True, destructive
=True)
850 # ########################################
851 # ##### Bridge functions #################
852 # ########################################
854 # calculate a cubic spline through the middle section of 4 given coordinates
855 def bridge_calculate_cubic_spline(bm
, coordinates
):
861 for i
in coordinates
:
862 a
.append(float(i
[j
]))
865 h
.append(x
[i
+ 1] - x
[i
])
867 for i
in range(1, 3):
868 q
.append(3.0 / h
[i
] * (a
[i
+ 1] - a
[i
]) - 3.0 / h
[i
- 1] * (a
[i
] - a
[i
- 1]))
872 for i
in range(1, 3):
873 l
.append(2.0 * (x
[i
+ 1] - x
[i
- 1]) - h
[i
- 1] * u
[i
- 1])
874 u
.append(h
[i
] / l
[i
])
875 z
.append((q
[i
] - h
[i
- 1] * z
[i
- 1]) / l
[i
])
878 b
= [False for i
in range(3)]
879 c
= [False for i
in range(4)]
880 d
= [False for i
in range(3)]
882 for i
in range(2, -1, -1):
883 c
[i
] = z
[i
] - u
[i
] * c
[i
+ 1]
884 b
[i
] = (a
[i
+ 1] - a
[i
]) / h
[i
] - h
[i
] * (c
[i
+ 1] + 2.0 * c
[i
]) / 3.0
885 d
[i
] = (c
[i
+ 1] - c
[i
]) / (3.0 * h
[i
])
887 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
888 spline
= [result
[1], result
[4], result
[7]]
893 # return a list with new vertex location vectors, a list with face vertex
894 # integers, and the highest vertex integer in the virtual mesh
895 def bridge_calculate_geometry(bm
, lines
, vertex_normals
, segments
,
896 interpolation
, cubic_strength
, min_width
, max_vert_index
):
900 # calculate location based on interpolation method
901 def get_location(line
, segment
, splines
):
902 v1
= bm
.verts
[lines
[line
][0]].co
903 v2
= bm
.verts
[lines
[line
][1]].co
904 if interpolation
== 'linear':
905 return v1
+ (segment
/ segments
) * (v2
- v1
)
906 else: # interpolation == 'cubic'
907 m
= (segment
/ segments
)
908 ax
, bx
, cx
, dx
, tx
= splines
[line
][0]
909 x
= ax
+ bx
* m
+ cx
* m
** 2 + dx
* m
** 3
910 ay
, by
, cy
, dy
, ty
= splines
[line
][1]
911 y
= ay
+ by
* m
+ cy
* m
** 2 + dy
* m
** 3
912 az
, bz
, cz
, dz
, tz
= splines
[line
][2]
913 z
= az
+ bz
* m
+ cz
* m
** 2 + dz
* m
** 3
914 return mathutils
.Vector((x
, y
, z
))
916 # no interpolation needed
918 for i
, line
in enumerate(lines
):
919 if i
< len(lines
) - 1:
920 faces
.append([line
[0], lines
[i
+ 1][0], lines
[i
+ 1][1], line
[1]])
921 # more than 1 segment, interpolate
923 # calculate splines (if necessary) once, so no recalculations needed
924 if interpolation
== 'cubic':
927 v1
= bm
.verts
[line
[0]].co
928 v2
= bm
.verts
[line
[1]].co
929 size
= (v2
- v1
).length
* cubic_strength
930 splines
.append(bridge_calculate_cubic_spline(bm
,
931 [v1
+ size
* vertex_normals
[line
[0]], v1
, v2
,
932 v2
+ size
* vertex_normals
[line
[1]]]))
936 # create starting situation
937 virtual_width
= [(bm
.verts
[lines
[i
][0]].co
-
938 bm
.verts
[lines
[i
+ 1][0]].co
).length
for i
939 in range(len(lines
) - 1)]
940 new_verts
= [get_location(0, seg
, splines
) for seg
in range(1,
942 first_line_indices
= [i
for i
in range(max_vert_index
+ 1,
943 max_vert_index
+ segments
)]
945 prev_verts
= new_verts
[:] # vertex locations of verts on previous line
946 prev_vert_indices
= first_line_indices
[:]
947 max_vert_index
+= segments
- 1 # highest vertex index in virtual mesh
948 next_verts
= [] # vertex locations of verts on current line
949 next_vert_indices
= []
951 for i
, line
in enumerate(lines
):
952 if i
< len(lines
) - 1:
956 for seg
in range(1, segments
):
957 loc1
= prev_verts
[seg
- 1]
958 loc2
= get_location(i
+ 1, seg
, splines
)
959 if (loc1
- loc2
).length
< (min_width
/ 100) * virtual_width
[i
] \
960 and line
[1] == lines
[i
+ 1][1]:
961 # triangle, no new vertex
962 faces
.append([v1
, v2
, prev_vert_indices
[seg
- 1],
963 prev_vert_indices
[seg
- 1]])
964 next_verts
+= prev_verts
[seg
- 1:]
965 next_vert_indices
+= prev_vert_indices
[seg
- 1:]
969 if i
== len(lines
) - 2 and lines
[0] == lines
[-1]:
970 # quad with first line, no new vertex
971 faces
.append([v1
, v2
, first_line_indices
[seg
- 1],
972 prev_vert_indices
[seg
- 1]])
973 v2
= first_line_indices
[seg
- 1]
974 v1
= prev_vert_indices
[seg
- 1]
976 # quad, add new vertex
978 faces
.append([v1
, v2
, max_vert_index
,
979 prev_vert_indices
[seg
- 1]])
981 v1
= prev_vert_indices
[seg
- 1]
982 new_verts
.append(loc2
)
983 next_verts
.append(loc2
)
984 next_vert_indices
.append(max_vert_index
)
986 faces
.append([v1
, v2
, lines
[i
+ 1][1], line
[1]])
988 prev_verts
= next_verts
[:]
989 prev_vert_indices
= next_vert_indices
[:]
991 next_vert_indices
= []
993 return(new_verts
, faces
, max_vert_index
)
996 # calculate lines (list of lists, vertex indices) that are used for bridging
997 def bridge_calculate_lines(bm
, loops
, mode
, twist
, reverse
):
999 loop1
, loop2
= [i
[0] for i
in loops
]
1000 loop1_circular
, loop2_circular
= [i
[1] for i
in loops
]
1001 circular
= loop1_circular
or loop2_circular
1004 # calculate loop centers
1006 for loop
in [loop1
, loop2
]:
1007 center
= mathutils
.Vector()
1009 center
+= bm
.verts
[vertex
].co
1011 centers
.append(center
)
1012 for i
, loop
in enumerate([loop1
, loop2
]):
1014 if bm
.verts
[vertex
].co
== centers
[i
]:
1015 # prevent zero-length vectors in angle comparisons
1016 centers
[i
] += mathutils
.Vector((0.01, 0, 0))
1018 center1
, center2
= centers
1020 # calculate the normals of the virtual planes that the loops are on
1022 normal_plurity
= False
1023 for i
, loop
in enumerate([loop1
, loop2
]):
1025 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1028 x
, y
, z
= centers
[i
]
1029 for loc
in [bm
.verts
[vertex
].co
for vertex
in loop
]:
1030 mat
[0][0] += (loc
[0] - x
) ** 2
1031 mat
[1][0] += (loc
[0] - x
) * (loc
[1] - y
)
1032 mat
[2][0] += (loc
[0] - x
) * (loc
[2] - z
)
1033 mat
[0][1] += (loc
[1] - y
) * (loc
[0] - x
)
1034 mat
[1][1] += (loc
[1] - y
) ** 2
1035 mat
[2][1] += (loc
[1] - y
) * (loc
[2] - z
)
1036 mat
[0][2] += (loc
[2] - z
) * (loc
[0] - x
)
1037 mat
[1][2] += (loc
[2] - z
) * (loc
[1] - y
)
1038 mat
[2][2] += (loc
[2] - z
) ** 2
1041 if sum(mat
[0]) < 1e-6 or sum(mat
[1]) < 1e-6 or sum(mat
[2]) < 1e-6:
1042 normal_plurity
= True
1046 if sum(mat
[0]) == 0:
1047 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
1048 elif sum(mat
[1]) == 0:
1049 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
1050 elif sum(mat
[2]) == 0:
1051 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
1053 # warning! this is different from .normalize()
1056 vec
= mathutils
.Vector((1.0, 1.0, 1.0))
1057 vec2
= (mat
@ vec
) / (mat
@ vec
).length
1058 while vec
!= vec2
and iter < itermax
:
1062 if vec2
.length
!= 0:
1064 if vec2
.length
== 0:
1065 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
1067 normals
.append(normal
)
1068 # have plane normals face in the same direction (maximum angle: 90 degrees)
1069 if ((center1
+ normals
[0]) - center2
).length
< \
1070 ((center1
- normals
[0]) - center2
).length
:
1072 if ((center2
+ normals
[1]) - center1
).length
> \
1073 ((center2
- normals
[1]) - center1
).length
:
1076 # rotation matrix, representing the difference between the plane normals
1077 axis
= normals
[0].cross(normals
[1])
1078 axis
= mathutils
.Vector([loc
if abs(loc
) > 1e-8 else 0 for loc
in axis
])
1079 if axis
.angle(mathutils
.Vector((0, 0, 1)), 0) > 1.5707964:
1081 angle
= normals
[0].dot(normals
[1])
1082 rotation_matrix
= mathutils
.Matrix
.Rotation(angle
, 4, axis
)
1084 # if circular, rotate loops so they are aligned
1086 # make sure loop1 is the circular one (or both are circular)
1087 if loop2_circular
and not loop1_circular
:
1088 loop1_circular
, loop2_circular
= True, False
1089 loop1
, loop2
= loop2
, loop1
1091 # match start vertex of loop1 with loop2
1092 target_vector
= bm
.verts
[loop2
[0]].co
- center2
1093 dif_angles
= [[(rotation_matrix
@ (bm
.verts
[vertex
].co
- center1
)
1094 ).angle(target_vector
, 0), False, i
] for
1095 i
, vertex
in enumerate(loop1
)]
1097 if len(loop1
) != len(loop2
):
1098 angle_limit
= dif_angles
[0][0] * 1.2 # 20% margin
1100 [(bm
.verts
[loop2
[0]].co
-
1101 bm
.verts
[loop1
[index
]].co
).length
, angle
, index
] for
1102 angle
, distance
, index
in dif_angles
if angle
<= angle_limit
1105 loop1
= loop1
[dif_angles
[0][2]:] + loop1
[:dif_angles
[0][2]]
1107 # have both loops face the same way
1108 if normal_plurity
and not circular
:
1109 second_to_first
, second_to_second
, second_to_last
= [
1110 (bm
.verts
[loop1
[1]].co
- center1
).angle(
1111 bm
.verts
[loop2
[i
]].co
- center2
) for i
in [0, 1, -1]
1113 last_to_first
, last_to_second
= [
1114 (bm
.verts
[loop1
[-1]].co
-
1115 center1
).angle(bm
.verts
[loop2
[i
]].co
- center2
) for
1118 if (min(last_to_first
, last_to_second
) * 1.1 < min(second_to_first
,
1119 second_to_second
)) or (loop2_circular
and second_to_last
* 1.1 <
1120 min(second_to_first
, second_to_second
)):
1123 loop1
= [loop1
[-1]] + loop1
[:-1]
1125 angle
= (bm
.verts
[loop1
[0]].co
- center1
).\
1126 cross(bm
.verts
[loop1
[1]].co
- center1
).angle(normals
[0], 0)
1127 target_angle
= (bm
.verts
[loop2
[0]].co
- center2
).\
1128 cross(bm
.verts
[loop2
[1]].co
- center2
).angle(normals
[1], 0)
1129 limit
= 1.5707964 # 0.5*pi, 90 degrees
1130 if not ((angle
> limit
and target_angle
> limit
) or
1131 (angle
< limit
and target_angle
< limit
)):
1134 loop1
= [loop1
[-1]] + loop1
[:-1]
1135 elif normals
[0].angle(normals
[1]) > limit
:
1138 loop1
= [loop1
[-1]] + loop1
[:-1]
1140 # both loops have the same length
1141 if len(loop1
) == len(loop2
):
1144 if abs(twist
) < len(loop1
):
1145 loop1
= loop1
[twist
:] + loop1
[:twist
]
1149 lines
.append([loop1
[0], loop2
[0]])
1150 for i
in range(1, len(loop1
)):
1151 lines
.append([loop1
[i
], loop2
[i
]])
1153 # loops of different lengths
1155 # make loop1 longest loop
1156 if len(loop2
) > len(loop1
):
1157 loop1
, loop2
= loop2
, loop1
1158 loop1_circular
, loop2_circular
= loop2_circular
, loop1_circular
1162 if abs(twist
) < len(loop1
):
1163 loop1
= loop1
[twist
:] + loop1
[:twist
]
1167 # shortest angle difference doesn't always give correct start vertex
1168 if loop1_circular
and not loop2_circular
:
1171 if len(loop1
) - shifting
< len(loop2
):
1174 to_last
, to_first
= [
1175 (rotation_matrix
@ (bm
.verts
[loop1
[-1]].co
- center1
)).angle(
1176 (bm
.verts
[loop2
[i
]].co
- center2
), 0) for i
in [-1, 0]
1178 if to_first
< to_last
:
1179 loop1
= [loop1
[-1]] + loop1
[:-1]
1185 # basic shortest side first
1187 lines
.append([loop1
[0], loop2
[0]])
1188 for i
in range(1, len(loop1
)):
1189 if i
>= len(loop2
) - 1:
1191 lines
.append([loop1
[i
], loop2
[-1]])
1194 lines
.append([loop1
[i
], loop2
[i
]])
1196 # shortest edge algorithm
1197 else: # mode == 'shortest'
1198 lines
.append([loop1
[0], loop2
[0]])
1200 for i
in range(len(loop1
) - 1):
1201 if prev_vert2
== len(loop2
) - 1 and not loop2_circular
:
1202 # force triangles, reached end of loop2
1204 elif prev_vert2
== len(loop2
) - 1 and loop2_circular
:
1205 # at end of loop2, but circular, so check with first vert
1206 tri
, quad
= [(bm
.verts
[loop1
[i
+ 1]].co
-
1207 bm
.verts
[loop2
[j
]].co
).length
1208 for j
in [prev_vert2
, 0]]
1210 elif len(loop1
) - 1 - i
== len(loop2
) - 1 - prev_vert2
and \
1212 # force quads, otherwise won't make it to end of loop2
1215 # calculate if tri or quad gives shortest edge
1216 tri
, quad
= [(bm
.verts
[loop1
[i
+ 1]].co
-
1217 bm
.verts
[loop2
[j
]].co
).length
1218 for j
in range(prev_vert2
, prev_vert2
+ 2)]
1222 lines
.append([loop1
[i
+ 1], loop2
[prev_vert2
]])
1223 if circle_full
== 2:
1226 elif not circle_full
:
1227 lines
.append([loop1
[i
+ 1], loop2
[prev_vert2
+ 1]])
1229 # quad to first vertex of loop2
1231 lines
.append([loop1
[i
+ 1], loop2
[0]])
1235 # final face for circular loops
1236 if loop1_circular
and loop2_circular
:
1237 lines
.append([loop1
[0], loop2
[0]])
1242 # calculate number of segments needed
1243 def bridge_calculate_segments(bm
, lines
, loops
, segments
):
1244 # return if amount of segments is set by user
1249 average_edge_length
= [
1250 (bm
.verts
[vertex
].co
-
1251 bm
.verts
[loop
[0][i
+ 1]].co
).length
for loop
in loops
for
1252 i
, vertex
in enumerate(loop
[0][:-1])
1254 # closing edges of circular loops
1255 average_edge_length
+= [
1256 (bm
.verts
[loop
[0][-1]].co
-
1257 bm
.verts
[loop
[0][0]].co
).length
for loop
in loops
if loop
[1]
1261 average_edge_length
= sum(average_edge_length
) / len(average_edge_length
)
1262 average_bridge_length
= sum(
1264 bm
.verts
[v2
].co
).length
for v1
, v2
in lines
]
1267 segments
= max(1, round(average_bridge_length
/ average_edge_length
))
1272 # return dictionary with vertex index as key, and the normal vector as value
1273 def bridge_calculate_virtual_vertex_normals(bm
, lines
, loops
, edge_faces
,
1275 if not edge_faces
: # interpolation isn't set to cubic
1278 # pity reduce() isn't one of the basic functions in python anymore
1279 def average_vector_dictionary(dic
):
1280 for key
, vectors
in dic
.items():
1281 # if type(vectors) == type([]) and len(vectors) > 1:
1282 if len(vectors
) > 1:
1283 average
= mathutils
.Vector()
1284 for vector
in vectors
:
1286 average
/= len(vectors
)
1287 dic
[key
] = [average
]
1290 # get all edges of the loop
1292 [edgekey_to_edge
[tuple(sorted([loops
[j
][0][i
],
1293 loops
[j
][0][i
+ 1]]))] for i
in range(len(loops
[j
][0]) - 1)] for
1296 edges
= edges
[0] + edges
[1]
1298 if loops
[j
][1]: # circular
1299 edges
.append(edgekey_to_edge
[tuple(sorted([loops
[j
][0][0],
1300 loops
[j
][0][-1]]))])
1303 calculation based on face topology (assign edge-normals to vertices)
1305 edge_normal = face_normal x edge_vector
1306 vertex_normal = average(edge_normals)
1308 vertex_normals
= dict([(vertex
, []) for vertex
in loops
[0][0] + loops
[1][0]])
1310 faces
= edge_faces
[edgekey(edge
)] # valid faces connected to edge
1313 # get edge coordinates
1314 v1
, v2
= [bm
.verts
[edgekey(edge
)[i
]].co
for i
in [0, 1]]
1315 edge_vector
= v1
- v2
1316 if edge_vector
.length
< 1e-4:
1317 # zero-length edge, vertices at same location
1319 edge_center
= (v1
+ v2
) / 2
1321 # average face coordinates, if connected to more than 1 valid face
1323 face_normal
= mathutils
.Vector()
1324 face_center
= mathutils
.Vector()
1326 face_normal
+= face
.normal
1327 face_center
+= face
.calc_center_median()
1328 face_normal
/= len(faces
)
1329 face_center
/= len(faces
)
1331 face_normal
= faces
[0].normal
1332 face_center
= faces
[0].calc_center_median()
1333 if face_normal
.length
< 1e-4:
1334 # faces with a surface of 0 have no face normal
1337 # calculate virtual edge normal
1338 edge_normal
= edge_vector
.cross(face_normal
)
1339 edge_normal
.length
= 0.01
1340 if (face_center
- (edge_center
+ edge_normal
)).length
> \
1341 (face_center
- (edge_center
- edge_normal
)).length
:
1342 # make normal face the correct way
1343 edge_normal
.negate()
1344 edge_normal
.normalize()
1345 # add virtual edge normal as entry for both vertices it connects
1346 for vertex
in edgekey(edge
):
1347 vertex_normals
[vertex
].append(edge_normal
)
1350 calculation based on connection with other loop (vertex focused method)
1351 - used for vertices that aren't connected to any valid faces
1353 plane_normal = edge_vector x connection_vector
1354 vertex_normal = plane_normal x edge_vector
1357 vertex
for vertex
, normal
in vertex_normals
.items() if not normal
1361 # edge vectors connected to vertices
1362 edge_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1364 for v
in edgekey(edge
):
1365 if v
in edge_vectors
:
1366 edge_vector
= bm
.verts
[edgekey(edge
)[0]].co
- \
1367 bm
.verts
[edgekey(edge
)[1]].co
1368 if edge_vector
.length
< 1e-4:
1369 # zero-length edge, vertices at same location
1371 edge_vectors
[v
].append(edge_vector
)
1373 # connection vectors between vertices of both loops
1374 connection_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1375 connections
= dict([[vertex
, []] for vertex
in vertices
])
1376 for v1
, v2
in lines
:
1377 if v1
in connection_vectors
or v2
in connection_vectors
:
1378 new_vector
= bm
.verts
[v1
].co
- bm
.verts
[v2
].co
1379 if new_vector
.length
< 1e-4:
1380 # zero-length connection vector,
1381 # vertices in different loops at same location
1383 if v1
in connection_vectors
:
1384 connection_vectors
[v1
].append(new_vector
)
1385 connections
[v1
].append(v2
)
1386 if v2
in connection_vectors
:
1387 connection_vectors
[v2
].append(new_vector
)
1388 connections
[v2
].append(v1
)
1389 connection_vectors
= average_vector_dictionary(connection_vectors
)
1390 connection_vectors
= dict(
1391 [[vertex
, vector
[0]] if vector
else
1392 [vertex
, []] for vertex
, vector
in connection_vectors
.items()]
1395 for vertex
, values
in edge_vectors
.items():
1396 # vertex normal doesn't matter, just assign a random vector to it
1397 if not connection_vectors
[vertex
]:
1398 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1401 # calculate to what location the vertex is connected,
1402 # used to determine what way to flip the normal
1403 connected_center
= mathutils
.Vector()
1404 for v
in connections
[vertex
]:
1405 connected_center
+= bm
.verts
[v
].co
1406 if len(connections
[vertex
]) > 1:
1407 connected_center
/= len(connections
[vertex
])
1408 if len(connections
[vertex
]) == 0:
1409 # shouldn't be possible, but better safe than sorry
1410 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1413 # can't do proper calculations, because of zero-length vector
1415 if (connected_center
- (bm
.verts
[vertex
].co
+
1416 connection_vectors
[vertex
])).length
< (connected_center
-
1417 (bm
.verts
[vertex
].co
- connection_vectors
[vertex
])).length
:
1418 connection_vectors
[vertex
].negate()
1419 vertex_normals
[vertex
] = [connection_vectors
[vertex
].normalized()]
1422 # calculate vertex normals using edge-vectors,
1423 # connection-vectors and the derived plane normal
1424 for edge_vector
in values
:
1425 plane_normal
= edge_vector
.cross(connection_vectors
[vertex
])
1426 vertex_normal
= edge_vector
.cross(plane_normal
)
1427 vertex_normal
.length
= 0.1
1428 if (connected_center
- (bm
.verts
[vertex
].co
+
1429 vertex_normal
)).length
< (connected_center
-
1430 (bm
.verts
[vertex
].co
- vertex_normal
)).length
:
1431 # make normal face the correct way
1432 vertex_normal
.negate()
1433 vertex_normal
.normalize()
1434 vertex_normals
[vertex
].append(vertex_normal
)
1436 # average virtual vertex normals, based on all edges it's connected to
1437 vertex_normals
= average_vector_dictionary(vertex_normals
)
1438 vertex_normals
= dict([[vertex
, vector
[0]] for vertex
, vector
in vertex_normals
.items()])
1440 return(vertex_normals
)
1443 # add vertices to mesh
1444 def bridge_create_vertices(bm
, vertices
):
1445 for i
in range(len(vertices
)):
1446 bm
.verts
.new(vertices
[i
])
1447 bm
.verts
.ensure_lookup_table()
1451 def bridge_create_faces(object, bm
, faces
, twist
):
1452 # have the normal point the correct way
1454 [face
.reverse() for face
in faces
]
1455 faces
= [face
[2:] + face
[:2] if face
[0] == face
[1] else face
for face
in faces
]
1457 # eekadoodle prevention
1458 for i
in range(len(faces
)):
1459 if not faces
[i
][-1]:
1460 if faces
[i
][0] == faces
[i
][-1]:
1461 faces
[i
] = [faces
[i
][1], faces
[i
][2], faces
[i
][3], faces
[i
][1]]
1463 faces
[i
] = [faces
[i
][-1]] + faces
[i
][:-1]
1464 # result of converting from pre-bmesh period
1465 if faces
[i
][-1] == faces
[i
][-2]:
1466 faces
[i
] = faces
[i
][:-1]
1469 for i
in range(len(faces
)):
1470 new_faces
.append(bm
.faces
.new([bm
.verts
[v
] for v
in faces
[i
]]))
1472 object.data
.update(calc_edges
=True) # calc_edges prevents memory-corruption
1474 bm
.verts
.ensure_lookup_table()
1475 bm
.edges
.ensure_lookup_table()
1476 bm
.faces
.ensure_lookup_table()
1481 # calculate input loops
1482 def bridge_get_input(bm
):
1483 # create list of internal edges, which should be skipped
1484 eks_of_selected_faces
= [
1485 item
for sublist
in [face_edgekeys(face
) for
1486 face
in bm
.faces
if face
.select
and not face
.hide
] for item
in sublist
1489 for ek
in eks_of_selected_faces
:
1490 if ek
in edge_count
:
1494 internal_edges
= [ek
for ek
in edge_count
if edge_count
[ek
] > 1]
1496 # sort correct edges into loops
1498 edgekey(edge
) for edge
in bm
.edges
if edge
.select
and
1499 not edge
.hide
and edgekey(edge
) not in internal_edges
1501 loops
= get_connected_selections(selected_edges
)
1506 # return values needed by the bridge operator
1507 def bridge_initialise(bm
, interpolation
):
1508 if interpolation
== 'cubic':
1509 # dict with edge-key as key and list of connected valid faces as value
1511 face
.index
for face
in bm
.faces
if face
.select
or
1515 [[edgekey(edge
), []] for edge
in bm
.edges
if not edge
.hide
]
1517 for face
in bm
.faces
:
1518 if face
.index
in face_blacklist
:
1520 for key
in face_edgekeys(face
):
1521 edge_faces
[key
].append(face
)
1522 # dictionary with the edge-key as key and edge as value
1523 edgekey_to_edge
= dict(
1524 [[edgekey(edge
), edge
] for edge
in bm
.edges
if edge
.select
and not edge
.hide
]
1528 edgekey_to_edge
= False
1530 # selected faces input
1531 old_selected_faces
= [
1532 face
.index
for face
in bm
.faces
if face
.select
and not face
.hide
1535 # find out if faces created by bridging should be smoothed
1538 if sum([face
.smooth
for face
in bm
.faces
]) / len(bm
.faces
) >= 0.5:
1541 return(edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
)
1544 # return a string with the input method
1545 def bridge_input_method(loft
, loft_loop
):
1549 method
= "Loft loop"
1551 method
= "Loft no-loop"
1558 # match up loops in pairs, used for multi-input bridging
1559 def bridge_match_loops(bm
, loops
):
1560 # calculate average loop normals and centers
1563 for vertices
, circular
in loops
:
1564 normal
= mathutils
.Vector()
1565 center
= mathutils
.Vector()
1566 for vertex
in vertices
:
1567 normal
+= bm
.verts
[vertex
].normal
1568 center
+= bm
.verts
[vertex
].co
1569 normals
.append(normal
/ len(vertices
) / 10)
1570 centers
.append(center
/ len(vertices
))
1572 # possible matches if loop normals are faced towards the center
1574 matches
= dict([[i
, []] for i
in range(len(loops
))])
1576 for i
in range(len(loops
) + 1):
1577 for j
in range(i
+ 1, len(loops
)):
1578 if (centers
[i
] - centers
[j
]).length
> \
1579 (centers
[i
] - (centers
[j
] + normals
[j
])).length
and \
1580 (centers
[j
] - centers
[i
]).length
> \
1581 (centers
[j
] - (centers
[i
] + normals
[i
])).length
:
1583 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1584 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1585 # if no loops face each other, just make matches between all the loops
1586 if matches_amount
== 0:
1587 for i
in range(len(loops
) + 1):
1588 for j
in range(i
+ 1, len(loops
)):
1589 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1590 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1591 for key
, value
in matches
.items():
1594 # matches based on distance between centers and number of vertices in loops
1596 for loop_index
in range(len(loops
)):
1597 if loop_index
in new_order
:
1599 loop_matches
= matches
[loop_index
]
1600 if not loop_matches
:
1602 shortest_distance
= loop_matches
[0][0]
1603 shortest_distance
*= 1.1
1605 [abs(len(loops
[loop_index
][0]) -
1606 len(loops
[loop
[2]][0])), loop
[0], loop
[1], loop
[2]] for loop
in
1607 loop_matches
if loop
[0] < shortest_distance
1610 for match
in loop_matches
:
1611 if match
[3] not in new_order
:
1612 new_order
+= [loop_index
, match
[3]]
1615 # reorder loops based on matches
1616 if len(new_order
) >= 2:
1617 loops
= [loops
[i
] for i
in new_order
]
1622 # remove old_selected_faces
1623 def bridge_remove_internal_faces(bm
, old_selected_faces
):
1624 # collect bmesh faces and internal bmesh edges
1625 remove_faces
= [bm
.faces
[face
] for face
in old_selected_faces
]
1626 edges
= collections
.Counter(
1627 [edge
.index
for face
in remove_faces
for edge
in face
.edges
]
1629 remove_edges
= [bm
.edges
[edge
] for edge
in edges
if edges
[edge
] > 1]
1631 # remove internal faces and edges
1632 for face
in remove_faces
:
1633 bm
.faces
.remove(face
)
1634 for edge
in remove_edges
:
1635 bm
.edges
.remove(edge
)
1637 bm
.faces
.ensure_lookup_table()
1638 bm
.edges
.ensure_lookup_table()
1639 bm
.verts
.ensure_lookup_table()
1642 # update list of internal faces that are flagged for removal
1643 def bridge_save_unused_faces(bm
, old_selected_faces
, loops
):
1644 # key: vertex index, value: lists of selected faces using it
1646 vertex_to_face
= dict([[i
, []] for i
in range(len(bm
.verts
))])
1647 [[vertex_to_face
[vertex
.index
].append(face
) for vertex
in
1648 bm
.faces
[face
].verts
] for face
in old_selected_faces
]
1650 # group selected faces that are connected
1653 for face
in old_selected_faces
:
1654 if face
in grouped_faces
:
1656 grouped_faces
.append(face
)
1660 grow_face
= new_faces
[0]
1661 for vertex
in bm
.faces
[grow_face
].verts
:
1662 vertex_face_group
= [
1663 face
for face
in vertex_to_face
[vertex
.index
] if
1664 face
not in grouped_faces
1666 new_faces
+= vertex_face_group
1667 grouped_faces
+= vertex_face_group
1668 group
+= vertex_face_group
1670 groups
.append(group
)
1672 # key: vertex index, value: True/False (is it in a loop that is used)
1673 used_vertices
= dict([[i
, 0] for i
in range(len(bm
.verts
))])
1675 for vertex
in loop
[0]:
1676 used_vertices
[vertex
] = True
1678 # check if group is bridged, if not remove faces from internal faces list
1679 for group
in groups
:
1684 for vertex
in bm
.faces
[face
].verts
:
1685 if used_vertices
[vertex
.index
]:
1690 old_selected_faces
.remove(face
)
1693 # add the newly created faces to the selection
1694 def bridge_select_new_faces(new_faces
, smooth
):
1695 for face
in new_faces
:
1696 face
.select_set(True)
1697 face
.smooth
= smooth
1700 # sort loops, so they are connected in the correct order when lofting
1701 def bridge_sort_loops(bm
, loops
, loft_loop
):
1702 # simplify loops to single points, and prepare for pathfinding
1704 [sum([bm
.verts
[i
].co
[j
] for i
in loop
[0]]) /
1705 len(loop
[0]) for loop
in loops
] for j
in range(3)
1707 nodes
= [mathutils
.Vector((x
[i
], y
[i
], z
[i
])) for i
in range(len(loops
))]
1710 open = [i
for i
in range(1, len(loops
))]
1712 # connect node to path, that is shortest to active_node
1713 while len(open) > 0:
1714 distances
= [(nodes
[active_node
] - nodes
[i
]).length
for i
in open]
1715 active_node
= open[distances
.index(min(distances
))]
1716 open.remove(active_node
)
1717 path
.append([active_node
, min(distances
)])
1718 # check if we didn't start in the middle of the path
1719 for i
in range(2, len(path
)):
1720 if (nodes
[path
[i
][0]] - nodes
[0]).length
< path
[i
][1]:
1723 path
= path
[:-i
] + temp
1727 loops
= [loops
[i
[0]] for i
in path
]
1728 # if requested, duplicate first loop at last position, so loft can loop
1730 loops
= loops
+ [loops
[0]]
1735 # remapping old indices to new position in list
1736 def bridge_update_old_selection(bm
, old_selected_faces
):
1738 old_indices = old_selected_faces[:]
1739 old_selected_faces = []
1740 for i, face in enumerate(bm.faces):
1741 if face.index in old_indices:
1742 old_selected_faces.append(i)
1744 old_selected_faces
= [
1745 i
for i
, face
in enumerate(bm
.faces
) if face
.index
in old_selected_faces
1748 return(old_selected_faces
)
1751 # ########################################
1752 # ##### Circle functions #################
1753 # ########################################
1755 # convert 3d coordinates to 2d coordinates on plane
1756 def circle_3d_to_2d(bm_mod
, loop
, com
, normal
):
1757 # project vertices onto the plane
1758 verts
= [bm_mod
.verts
[v
] for v
in loop
[0]]
1759 verts_projected
= [[v
.co
- (v
.co
- com
).dot(normal
) * normal
, v
.index
]
1762 # calculate two vectors (p and q) along the plane
1763 m
= mathutils
.Vector((normal
[0] + 1.0, normal
[1], normal
[2]))
1764 p
= m
- (m
.dot(normal
) * normal
)
1766 m
= mathutils
.Vector((normal
[0], normal
[1] + 1.0, normal
[2]))
1767 p
= m
- (m
.dot(normal
) * normal
)
1770 # change to 2d coordinates using perpendicular projection
1772 for loc
, vert
in verts_projected
:
1774 x
= p
.dot(vloc
) / p
.dot(p
)
1775 y
= q
.dot(vloc
) / q
.dot(q
)
1776 locs_2d
.append([x
, y
, vert
])
1778 return(locs_2d
, p
, q
)
1781 # calculate a best-fit circle to the 2d locations on the plane
1782 def circle_calculate_best_fit(locs_2d
):
1788 # calculate center and radius (non-linear least squares solution)
1789 for iter in range(500):
1793 d
= (v
[0] ** 2 - 2.0 * x0
* v
[0] + v
[1] ** 2 - 2.0 * y0
* v
[1] + x0
** 2 + y0
** 2) ** 0.5
1794 jmat
.append([(x0
- v
[0]) / d
, (y0
- v
[1]) / d
, -1.0])
1795 k
.append(-(((v
[0] - x0
) ** 2 + (v
[1] - y0
) ** 2) ** 0.5 - r
))
1796 jmat2
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1800 k2
= mathutils
.Vector((0.0, 0.0, 0.0))
1801 for i
in range(len(jmat
)):
1802 k2
+= mathutils
.Vector(jmat
[i
]) * k
[i
]
1803 jmat2
[0][0] += jmat
[i
][0] ** 2
1804 jmat2
[1][0] += jmat
[i
][0] * jmat
[i
][1]
1805 jmat2
[2][0] += jmat
[i
][0] * jmat
[i
][2]
1806 jmat2
[1][1] += jmat
[i
][1] ** 2
1807 jmat2
[2][1] += jmat
[i
][1] * jmat
[i
][2]
1808 jmat2
[2][2] += jmat
[i
][2] ** 2
1809 jmat2
[0][1] = jmat2
[1][0]
1810 jmat2
[0][2] = jmat2
[2][0]
1811 jmat2
[1][2] = jmat2
[2][1]
1816 dx0
, dy0
, dr
= jmat2
@ k2
1820 # stop iterating if we're close enough to optimal solution
1821 if abs(dx0
) < 1e-6 and abs(dy0
) < 1e-6 and abs(dr
) < 1e-6:
1824 # return center of circle and radius
1828 # calculate circle so no vertices have to be moved away from the center
1829 def circle_calculate_min_fit(locs_2d
):
1831 x0
= (min([i
[0] for i
in locs_2d
]) + max([i
[0] for i
in locs_2d
])) / 2.0
1832 y0
= (min([i
[1] for i
in locs_2d
]) + max([i
[1] for i
in locs_2d
])) / 2.0
1833 center
= mathutils
.Vector([x0
, y0
])
1835 r
= min([(mathutils
.Vector([i
[0], i
[1]]) - center
).length
for i
in locs_2d
])
1837 # return center of circle and radius
1841 # calculate the new locations of the vertices that need to be moved
1842 def circle_calculate_verts(flatten
, bm_mod
, locs_2d
, com
, p
, q
, normal
):
1843 # changing 2d coordinates back to 3d coordinates
1846 locs_3d
.append([loc
[2], loc
[0] * p
+ loc
[1] * q
+ com
])
1848 if flatten
: # flat circle
1851 else: # project the locations on the existing mesh
1852 vert_edges
= dict_vert_edges(bm_mod
)
1853 vert_faces
= dict_vert_faces(bm_mod
)
1854 faces
= [f
for f
in bm_mod
.faces
if not f
.hide
]
1855 rays
= [normal
, -normal
]
1859 if bm_mod
.verts
[loc
[0]].co
== loc
[1]: # vertex hasn't moved
1862 dif
= normal
.angle(loc
[1] - bm_mod
.verts
[loc
[0]].co
)
1863 if -1e-6 < dif
< 1e-6 or math
.pi
- 1e-6 < dif
< math
.pi
+ 1e-6:
1864 # original location is already along projection normal
1865 projection
= bm_mod
.verts
[loc
[0]].co
1867 # quick search through adjacent faces
1868 for face
in vert_faces
[loc
[0]]:
1869 verts
= [v
.co
for v
in bm_mod
.faces
[face
].verts
]
1870 if len(verts
) == 3: # triangle
1874 v1
, v2
, v3
, v4
= verts
[:4]
1876 intersect
= mathutils
.geometry
.\
1877 intersect_ray_tri(v1
, v2
, v3
, ray
, loc
[1])
1879 projection
= intersect
1882 intersect
= mathutils
.geometry
.\
1883 intersect_ray_tri(v1
, v3
, v4
, ray
, loc
[1])
1885 projection
= intersect
1890 # check if projection is on adjacent edges
1891 for edgekey
in vert_edges
[loc
[0]]:
1892 line1
= bm_mod
.verts
[edgekey
[0]].co
1893 line2
= bm_mod
.verts
[edgekey
[1]].co
1894 intersect
, dist
= mathutils
.geometry
.intersect_point_line(
1895 loc
[1], line1
, line2
1897 if 1e-6 < dist
< 1 - 1e-6:
1898 projection
= intersect
1901 # full search through the entire mesh
1904 verts
= [v
.co
for v
in face
.verts
]
1905 if len(verts
) == 3: # triangle
1909 v1
, v2
, v3
, v4
= verts
[:4]
1911 intersect
= mathutils
.geometry
.intersect_ray_tri(
1912 v1
, v2
, v3
, ray
, loc
[1]
1915 hits
.append([(loc
[1] - intersect
).length
,
1919 intersect
= mathutils
.geometry
.intersect_ray_tri(
1920 v1
, v3
, v4
, ray
, loc
[1]
1923 hits
.append([(loc
[1] - intersect
).length
,
1927 # if more than 1 hit with mesh, closest hit is new loc
1929 projection
= hits
[0][1]
1931 # nothing to project on, remain at flat location
1933 new_locs
.append([loc
[0], projection
])
1935 # return new positions of projected circle
1939 # check loops and only return valid ones
1940 def circle_check_loops(single_loops
, loops
, mapping
, bm_mod
):
1941 valid_single_loops
= {}
1943 for i
, [loop
, circular
] in enumerate(loops
):
1944 # loop needs to have at least 3 vertices
1947 # loop needs at least 1 vertex in the original, non-mirrored mesh
1951 if mapping
[vert
] > -1:
1956 # loop has to be non-collinear
1958 loc0
= mathutils
.Vector(bm_mod
.verts
[loop
[0]].co
[:])
1959 loc1
= mathutils
.Vector(bm_mod
.verts
[loop
[1]].co
[:])
1961 locn
= mathutils
.Vector(bm_mod
.verts
[v
].co
[:])
1962 if loc0
== loc1
or loc1
== locn
:
1968 if -1e-6 < d1
.angle(d2
, 0) < 1e-6:
1976 # passed all tests, loop is valid
1977 valid_loops
.append([loop
, circular
])
1978 valid_single_loops
[len(valid_loops
) - 1] = single_loops
[i
]
1980 return(valid_single_loops
, valid_loops
)
1983 # calculate the location of single input vertices that need to be flattened
1984 def circle_flatten_singles(bm_mod
, com
, p
, q
, normal
, single_loop
):
1986 for vert
in single_loop
:
1987 loc
= mathutils
.Vector(bm_mod
.verts
[vert
].co
[:])
1988 new_locs
.append([vert
, loc
- (loc
- com
).dot(normal
) * normal
])
1993 # calculate input loops
1994 def circle_get_input(object, bm
):
1995 # get mesh with modifiers applied
1996 derived
, bm_mod
= get_derived_bmesh(object, bm
)
1998 # create list of edge-keys based on selection state
2000 for face
in bm
.faces
:
2001 if face
.select
and not face
.hide
:
2005 # get selected, non-hidden , non-internal edge-keys
2007 key
for keys
in [face_edgekeys(face
) for face
in
2008 bm_mod
.faces
if face
.select
and not face
.hide
] for key
in keys
2011 for ek
in eks_selected
:
2012 if ek
in edge_count
:
2017 edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and
2018 not edge
.hide
and edge_count
.get(edgekey(edge
), 1) == 1
2021 # no faces, so no internal edges either
2023 edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and not edge
.hide
2026 # add edge-keys around single vertices
2027 verts_connected
= dict(
2028 [[vert
, 1] for edge
in [edge
for edge
in
2029 bm_mod
.edges
if edge
.select
and not edge
.hide
] for vert
in
2033 vert
.index
for vert
in bm_mod
.verts
if
2034 vert
.select
and not vert
.hide
and
2035 not verts_connected
.get(vert
.index
, False)
2038 if single_vertices
and len(bm
.faces
) > 0:
2039 vert_to_single
= dict(
2040 [[v
.index
, []] for v
in bm_mod
.verts
if not v
.hide
]
2042 for face
in [face
for face
in bm_mod
.faces
if not face
.select
and not face
.hide
]:
2043 for vert
in face
.verts
:
2045 if vert
in single_vertices
:
2046 for ek
in face_edgekeys(face
):
2048 edge_keys
.append(ek
)
2049 if vert
not in vert_to_single
[ek
[0]]:
2050 vert_to_single
[ek
[0]].append(vert
)
2051 if vert
not in vert_to_single
[ek
[1]]:
2052 vert_to_single
[ek
[1]].append(vert
)
2055 # sort edge-keys into loops
2056 loops
= get_connected_selections(edge_keys
)
2058 # find out to which loops the single vertices belong
2059 single_loops
= dict([[i
, []] for i
in range(len(loops
))])
2060 if single_vertices
and len(bm
.faces
) > 0:
2061 for i
, [loop
, circular
] in enumerate(loops
):
2063 if vert_to_single
[vert
]:
2064 for single
in vert_to_single
[vert
]:
2065 if single
not in single_loops
[i
]:
2066 single_loops
[i
].append(single
)
2068 return(derived
, bm_mod
, single_vertices
, single_loops
, loops
)
2071 # recalculate positions based on the influence of the circle shape
2072 def circle_influence_locs(locs_2d
, new_locs_2d
, influence
):
2073 for i
in range(len(locs_2d
)):
2074 oldx
, oldy
, j
= locs_2d
[i
]
2075 newx
, newy
, k
= new_locs_2d
[i
]
2076 altx
= newx
* (influence
/ 100) + oldx
* ((100 - influence
) / 100)
2077 alty
= newy
* (influence
/ 100) + oldy
* ((100 - influence
) / 100)
2078 locs_2d
[i
] = [altx
, alty
, j
]
2083 # project 2d locations on circle, respecting distance relations between verts
2084 def circle_project_non_regular(locs_2d
, x0
, y0
, r
):
2085 for i
in range(len(locs_2d
)):
2086 x
, y
, j
= locs_2d
[i
]
2087 loc
= mathutils
.Vector([x
- x0
, y
- y0
])
2089 locs_2d
[i
] = [loc
[0], loc
[1], j
]
2094 # project 2d locations on circle, with equal distance between all vertices
2095 def circle_project_regular(locs_2d
, x0
, y0
, r
):
2096 # find offset angle and circling direction
2097 x
, y
, i
= locs_2d
[0]
2098 loc
= mathutils
.Vector([x
- x0
, y
- y0
])
2100 offset_angle
= loc
.angle(mathutils
.Vector([1.0, 0.0]), 0.0)
2101 loca
= mathutils
.Vector([x
- x0
, y
- y0
, 0.0])
2104 x
, y
, j
= locs_2d
[1]
2105 locb
= mathutils
.Vector([x
- x0
, y
- y0
, 0.0])
2106 if loca
.cross(locb
)[2] >= 0:
2110 # distribute vertices along the circle
2111 for i
in range(len(locs_2d
)):
2112 t
= offset_angle
+ ccw
* (i
/ len(locs_2d
) * 2 * math
.pi
)
2115 locs_2d
[i
] = [x
, y
, locs_2d
[i
][2]]
2120 # shift loop, so the first vertex is closest to the center
2121 def circle_shift_loop(bm_mod
, loop
, com
):
2122 verts
, circular
= loop
2124 [(bm_mod
.verts
[vert
].co
- com
).length
, i
] for i
, vert
in enumerate(verts
)
2127 shift
= distances
[0][1]
2128 loop
= [verts
[shift
:] + verts
[:shift
], circular
]
2133 # ########################################
2134 # ##### Curve functions ##################
2135 # ########################################
2137 # create lists with knots and points, all correctly sorted
2138 def curve_calculate_knots(loop
, verts_selected
):
2139 knots
= [v
for v
in loop
[0] if v
in verts_selected
]
2141 # circular loop, potential for weird splines
2143 offset
= int(len(loop
[0]) / 4)
2146 kpos
.append(loop
[0].index(k
))
2148 for i
in range(len(kpos
) - 1):
2149 kdif
.append(kpos
[i
+ 1] - kpos
[i
])
2150 kdif
.append(len(loop
[0]) - kpos
[-1] + kpos
[0])
2154 kadd
.append([kdif
.index(k
), True])
2155 # next 2 lines are optional, they insert
2156 # an extra control point in small gaps
2158 # kadd.append([kdif.index(k), False])
2161 for k
in kadd
: # extra knots to be added
2162 if k
[1]: # big gap (break circular spline)
2163 kpos
= loop
[0].index(knots
[k
[0]]) + offset
2164 if kpos
> len(loop
[0]) - 1:
2165 kpos
-= len(loop
[0])
2166 kins
.append([knots
[k
[0]], loop
[0][kpos
]])
2168 if kpos2
> len(knots
) - 1:
2170 kpos2
= loop
[0].index(knots
[kpos2
]) - offset
2172 kpos2
+= len(loop
[0])
2173 kins
.append([loop
[0][kpos
], loop
[0][kpos2
]])
2174 krot
= loop
[0][kpos2
]
2175 else: # small gap (keep circular spline)
2176 k1
= loop
[0].index(knots
[k
[0]])
2178 if k2
> len(knots
) - 1:
2180 k2
= loop
[0].index(knots
[k2
])
2182 dif
= len(loop
[0]) - 1 - k1
+ k2
2185 kn
= k1
+ int(dif
/ 2)
2186 if kn
> len(loop
[0]) - 1:
2188 kins
.append([loop
[0][k1
], loop
[0][kn
]])
2189 for j
in kins
: # insert new knots
2190 knots
.insert(knots
.index(j
[0]) + 1, j
[1])
2191 if not krot
: # circular loop
2192 knots
.append(knots
[0])
2193 points
= loop
[0][loop
[0].index(knots
[0]):]
2194 points
+= loop
[0][0:loop
[0].index(knots
[0]) + 1]
2195 else: # non-circular loop (broken by script)
2196 krot
= knots
.index(krot
)
2197 knots
= knots
[krot
:] + knots
[0:krot
]
2198 if loop
[0].index(knots
[0]) > loop
[0].index(knots
[-1]):
2199 points
= loop
[0][loop
[0].index(knots
[0]):]
2200 points
+= loop
[0][0:loop
[0].index(knots
[-1]) + 1]
2202 points
= loop
[0][loop
[0].index(knots
[0]):loop
[0].index(knots
[-1]) + 1]
2203 # non-circular loop, add first and last point as knots
2205 if loop
[0][0] not in knots
:
2206 knots
.insert(0, loop
[0][0])
2207 if loop
[0][-1] not in knots
:
2208 knots
.append(loop
[0][-1])
2210 return(knots
, points
)
2213 # calculate relative positions compared to first knot
2214 def curve_calculate_t(bm_mod
, knots
, points
, pknots
, regular
, circular
):
2221 loc
= pknots
[knots
.index(p
)] # use projected knot location
2223 loc
= mathutils
.Vector(bm_mod
.verts
[p
].co
[:])
2226 len_total
+= (loc
- loc_prev
).length
2227 tpoints
.append(len_total
)
2232 tknots
.append(tpoints
[points
.index(p
)])
2234 tknots
[-1] = tpoints
[-1]
2238 tpoints_average
= tpoints
[-1] / (len(tpoints
) - 1)
2239 for i
in range(1, len(tpoints
) - 1):
2240 tpoints
[i
] = i
* tpoints_average
2241 for i
in range(len(knots
)):
2242 tknots
[i
] = tpoints
[points
.index(knots
[i
])]
2244 tknots
[-1] = tpoints
[-1]
2246 return(tknots
, tpoints
)
2249 # change the location of non-selected points to their place on the spline
2250 def curve_calculate_vertices(bm_mod
, knots
, tknots
, points
, tpoints
, splines
,
2251 interpolation
, restriction
):
2258 m
= tpoints
[points
.index(p
)]
2266 if n
> len(splines
) - 1:
2267 n
= len(splines
) - 1
2271 if interpolation
== 'cubic':
2272 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
2273 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
2274 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
2275 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
2276 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
2277 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
2278 newloc
= mathutils
.Vector([x
, y
, z
])
2279 else: # interpolation == 'linear'
2280 a
, d
, t
, u
= splines
[n
]
2281 newloc
= ((m
- t
) / u
) * d
+ a
2283 if restriction
!= 'none': # vertex movement is restricted
2285 else: # set the vertex to its new location
2286 move
.append([p
, newloc
])
2288 if restriction
!= 'none': # vertex movement is restricted
2293 move
.append([p
, bm_mod
.verts
[p
].co
])
2295 oldloc
= bm_mod
.verts
[p
].co
2296 normal
= bm_mod
.verts
[p
].normal
2297 dloc
= newloc
- oldloc
2298 if dloc
.length
< 1e-6:
2299 move
.append([p
, newloc
])
2300 elif restriction
== 'extrude': # only extrusions
2301 if dloc
.angle(normal
, 0) < 0.5 * math
.pi
+ 1e-6:
2302 move
.append([p
, newloc
])
2303 else: # restriction == 'indent' only indentations
2304 if dloc
.angle(normal
) > 0.5 * math
.pi
- 1e-6:
2305 move
.append([p
, newloc
])
2310 # trim loops to part between first and last selected vertices (including)
2311 def curve_cut_boundaries(bm_mod
, loops
):
2313 for loop
, circular
in loops
:
2316 cut_loops
.append([loop
, circular
])
2318 selected
= [bm_mod
.verts
[v
].select
for v
in loop
]
2319 first
= selected
.index(True)
2321 last
= -selected
.index(True)
2323 cut_loops
.append([loop
[first
:], circular
])
2325 cut_loops
.append([loop
[first
:last
], circular
])
2330 # calculate input loops
2331 def curve_get_input(object, bm
, boundaries
):
2332 # get mesh with modifiers applied
2333 derived
, bm_mod
= get_derived_bmesh(object, bm
)
2335 # vertices that still need a loop to run through it
2337 v
.index
for v
in bm_mod
.verts
if v
.select
and not v
.hide
2339 # necessary dictionaries
2340 vert_edges
= dict_vert_edges(bm_mod
)
2341 edge_faces
= dict_edge_faces(bm_mod
)
2343 # find loops through each selected vertex
2344 while len(verts_unsorted
) > 0:
2345 loops
= curve_vertex_loops(bm_mod
, verts_unsorted
[0], vert_edges
,
2347 verts_unsorted
.pop(0)
2349 # check if loop is fully selected
2350 search_perpendicular
= False
2352 for loop
, circular
in loops
:
2354 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2355 if len(selected
) < 2:
2356 # only one selected vertex on loop, don't use
2359 elif len(selected
) == len(loop
):
2360 search_perpendicular
= loop
2362 # entire loop is selected, find perpendicular loops
2363 if search_perpendicular
:
2365 if vert
in verts_unsorted
:
2366 verts_unsorted
.remove(vert
)
2367 perp_loops
= curve_perpendicular_loops(bm_mod
, loop
,
2368 vert_edges
, edge_faces
)
2369 for perp_loop
in perp_loops
:
2370 correct_loops
.append(perp_loop
)
2373 for loop
, circular
in loops
:
2374 correct_loops
.append([loop
, circular
])
2378 correct_loops
= curve_cut_boundaries(bm_mod
, correct_loops
)
2380 return(derived
, bm_mod
, correct_loops
)
2383 # return all loops that are perpendicular to the given one
2384 def curve_perpendicular_loops(bm_mod
, start_loop
, vert_edges
, edge_faces
):
2385 # find perpendicular loops
2387 for start_vert
in start_loop
:
2388 loops
= curve_vertex_loops(bm_mod
, start_vert
, vert_edges
,
2390 for loop
, circular
in loops
:
2391 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2392 if len(selected
) == len(loop
):
2395 perp_loops
.append([loop
, circular
, loop
.index(start_vert
)])
2397 # trim loops to same lengths
2399 [len(loop
[0]), i
] for i
, loop
in enumerate(perp_loops
) if not loop
[1]
2402 # all loops are circular, not trimming
2403 return([[loop
[0], loop
[1]] for loop
in perp_loops
])
2405 shortest
= min(shortest
)
2406 shortest_start
= perp_loops
[shortest
[1]][2]
2407 before_start
= shortest_start
2408 after_start
= shortest
[0] - shortest_start
- 1
2409 bigger_before
= before_start
> after_start
2411 for loop
in perp_loops
:
2412 # have the loop face the same direction as the shortest one
2414 if loop
[2] < len(loop
[0]) / 2:
2416 loop
[2] = len(loop
[0]) - loop
[2] - 1
2418 if loop
[2] > len(loop
[0]) / 2:
2420 loop
[2] = len(loop
[0]) - loop
[2] - 1
2421 # circular loops can shift, to prevent wrong trimming
2423 shift
= shortest_start
- loop
[2]
2424 if loop
[2] + shift
> 0 and loop
[2] + shift
< len(loop
[0]):
2425 loop
[0] = loop
[0][-shift
:] + loop
[0][:-shift
]
2428 loop
[2] += len(loop
[0])
2429 elif loop
[2] > len(loop
[0]) - 1:
2430 loop
[2] -= len(loop
[0])
2432 start
= max(0, loop
[2] - before_start
)
2433 end
= min(len(loop
[0]), loop
[2] + after_start
+ 1)
2434 trimmed_loops
.append([loop
[0][start
:end
], False])
2436 return(trimmed_loops
)
2439 # project knots on non-selected geometry
2440 def curve_project_knots(bm_mod
, verts_selected
, knots
, points
, circular
):
2441 # function to project vertex on edge
2442 def project(v1
, v2
, v3
):
2443 # v1 and v2 are part of a line
2444 # v3 is projected onto it
2450 if circular
: # project all knots
2454 else: # first and last knot shouldn't be projected
2457 pknots
= [mathutils
.Vector(bm_mod
.verts
[knots
[0]].co
[:])]
2458 for knot
in knots
[start
:end
]:
2459 if knot
in verts_selected
:
2460 knot_left
= knot_right
= False
2461 for i
in range(points
.index(knot
) - 1, -1 * len(points
), -1):
2462 if points
[i
] not in knots
:
2463 knot_left
= points
[i
]
2465 for i
in range(points
.index(knot
) + 1, 2 * len(points
)):
2466 if i
> len(points
) - 1:
2468 if points
[i
] not in knots
:
2469 knot_right
= points
[i
]
2471 if knot_left
and knot_right
and knot_left
!= knot_right
:
2472 knot_left
= mathutils
.Vector(bm_mod
.verts
[knot_left
].co
[:])
2473 knot_right
= mathutils
.Vector(bm_mod
.verts
[knot_right
].co
[:])
2474 knot
= mathutils
.Vector(bm_mod
.verts
[knot
].co
[:])
2475 pknots
.append(project(knot_left
, knot_right
, knot
))
2477 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2478 else: # knot isn't selected, so shouldn't be changed
2479 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2481 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knots
[-1]].co
[:]))
2486 # find all loops through a given vertex
2487 def curve_vertex_loops(bm_mod
, start_vert
, vert_edges
, edge_faces
):
2491 for edge
in vert_edges
[start_vert
]:
2492 if edge
in edges_used
:
2497 active_faces
= edge_faces
[edge
]
2502 new_edges
= vert_edges
[new_vert
]
2503 loop
.append(new_vert
)
2505 edges_used
.append(tuple(sorted([loop
[-1], loop
[-2]])))
2506 if len(new_edges
) < 3 or len(new_edges
) > 4:
2511 for new_edge
in new_edges
:
2512 if new_edge
in edges_used
:
2515 for new_face
in edge_faces
[new_edge
]:
2516 if new_face
in active_faces
:
2521 # found correct new edge
2522 active_faces
= edge_faces
[new_edge
]
2528 if new_vert
== loop
[0]:
2536 loops
.append([loop
, circular
])
2541 # ########################################
2542 # ##### Flatten functions ################
2543 # ########################################
2545 # sort input into loops
2546 def flatten_get_input(bm
):
2547 vert_verts
= dict_vert_verts(
2548 [edgekey(edge
) for edge
in bm
.edges
if edge
.select
and not edge
.hide
]
2550 verts
= [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
2552 # no connected verts, consider all selected verts as a single input
2554 return([[verts
, False]])
2557 while len(verts
) > 0:
2561 if loop
[-1] in vert_verts
:
2562 to_grow
= vert_verts
[loop
[-1]]
2566 while len(to_grow
) > 0:
2567 new_vert
= to_grow
[0]
2569 if new_vert
in loop
:
2571 loop
.append(new_vert
)
2572 verts
.remove(new_vert
)
2573 to_grow
+= vert_verts
[new_vert
]
2575 loops
.append([loop
, False])
2580 # calculate position of vertex projections on plane
2581 def flatten_project(bm
, loop
, com
, normal
):
2582 verts
= [bm
.verts
[v
] for v
in loop
[0]]
2584 [v
.index
, mathutils
.Vector(v
.co
[:]) -
2585 (mathutils
.Vector(v
.co
[:]) - com
).dot(normal
) * normal
] for v
in verts
2588 return(verts_projected
)
2591 # ########################################
2592 # ##### Gstretch functions ###############
2593 # ########################################
2595 # fake stroke class, used to create custom strokes if no GP data is found
2596 class gstretch_fake_stroke():
2597 def __init__(self
, points
):
2598 self
.points
= [gstretch_fake_stroke_point(p
) for p
in points
]
2601 # fake stroke point class, used in fake strokes
2602 class gstretch_fake_stroke_point():
2603 def __init__(self
, loc
):
2607 # flips loops, if necessary, to obtain maximum alignment to stroke
2608 def gstretch_align_pairs(ls_pairs
, object, bm_mod
, method
):
2609 # returns total distance between all verts in loop and corresponding stroke
2610 def distance_loop_stroke(loop
, stroke
, object, bm_mod
, method
):
2611 stroke_lengths_cache
= False
2612 loop_length
= len(loop
[0])
2615 if method
!= 'regular':
2616 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2618 for i
, v_index
in enumerate(loop
[0]):
2619 if method
== 'regular':
2620 relative_distance
= i
/ (loop_length
- 1)
2622 relative_distance
= relative_lengths
[i
]
2624 loc1
= object.matrix_world
@ bm_mod
.verts
[v_index
].co
2625 loc2
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2626 relative_distance
, stroke_lengths_cache
)
2627 total_distance
+= (loc2
- loc1
).length
2629 return(total_distance
)
2632 for (loop
, stroke
) in ls_pairs
:
2633 total_dist
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2636 total_dist_rev
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2638 if total_dist_rev
> total_dist
:
2644 # calculate vertex positions on stroke
2645 def gstretch_calculate_verts(loop
, stroke
, object, bm_mod
, method
):
2647 stroke_lengths_cache
= False
2648 loop_length
= len(loop
[0])
2649 matrix_inverse
= object.matrix_world
.inverted()
2651 # return intersection of line with stroke, or None
2652 def intersect_line_stroke(vec1
, vec2
, stroke
):
2653 for i
, p
in enumerate(stroke
.points
[1:]):
2654 intersections
= mathutils
.geometry
.intersect_line_line(vec1
, vec2
,
2655 p
.co
, stroke
.points
[i
].co
)
2656 if intersections
and \
2657 (intersections
[0] - intersections
[1]).length
< 1e-2:
2658 x
, dist
= mathutils
.geometry
.intersect_point_line(
2659 intersections
[0], p
.co
, stroke
.points
[i
].co
)
2661 return(intersections
[0])
2664 if method
== 'project':
2665 vert_edges
= dict_vert_edges(bm_mod
)
2667 for v_index
in loop
[0]:
2669 for ek
in vert_edges
[v_index
]:
2671 v1
= bm_mod
.verts
[v1
]
2672 v2
= bm_mod
.verts
[v2
]
2673 if v1
.select
+ v2
.select
== 1 and not v1
.hide
and not v2
.hide
:
2674 vec1
= object.matrix_world
@ v1
.co
2675 vec2
= object.matrix_world
@ v2
.co
2676 intersection
= intersect_line_stroke(vec1
, vec2
, stroke
)
2679 if not intersection
:
2680 v
= bm_mod
.verts
[v_index
]
2681 intersection
= intersect_line_stroke(v
.co
, v
.co
+ v
.normal
,
2684 move
.append([v_index
, matrix_inverse
@ intersection
])
2687 if method
== 'irregular':
2688 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2690 for i
, v_index
in enumerate(loop
[0]):
2691 if method
== 'regular':
2692 relative_distance
= i
/ (loop_length
- 1)
2693 else: # method == 'irregular'
2694 relative_distance
= relative_lengths
[i
]
2695 loc
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2696 relative_distance
, stroke_lengths_cache
)
2697 loc
= matrix_inverse
@ loc
2698 move
.append([v_index
, loc
])
2703 # create new vertices, based on GP strokes
2704 def gstretch_create_verts(object, bm_mod
, strokes
, method
, conversion
,
2705 conversion_distance
, conversion_max
, conversion_min
, conversion_vertices
):
2708 mat_world
= object.matrix_world
.inverted()
2709 singles
= gstretch_match_single_verts(bm_mod
, strokes
, mat_world
)
2711 for stroke
in strokes
:
2712 stroke_verts
.append([stroke
, []])
2714 if conversion
== 'vertices':
2715 min_end_point
= conversion_vertices
2716 end_point
= conversion_vertices
2717 elif conversion
== 'limit_vertices':
2718 min_end_point
= conversion_min
2719 end_point
= conversion_max
2721 end_point
= len(stroke
.points
)
2722 # creation of new vertices at fixed user-defined distances
2723 if conversion
== 'distance':
2725 prev_point
= stroke
.points
[0]
2726 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
@ prev_point
.co
))
2728 limit
= conversion_distance
2729 for point
in stroke
.points
:
2730 new_distance
= distance
+ (point
.co
- prev_point
.co
).length
2732 while new_distance
> limit
:
2733 to_cover
= limit
- distance
+ (limit
* iteration
)
2734 new_loc
= prev_point
.co
+ to_cover
* \
2735 (point
.co
- prev_point
.co
).normalized()
2736 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* new_loc
))
2737 new_distance
-= limit
2739 distance
= new_distance
2741 # creation of new vertices for other methods
2743 # add vertices at stroke points
2744 for point
in stroke
.points
[:end_point
]:
2745 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
@ point
.co
))
2746 # add more vertices, beyond the points that are available
2747 if min_end_point
> min(len(stroke
.points
), end_point
):
2748 for i
in range(min_end_point
-
2749 (min(len(stroke
.points
), end_point
))):
2750 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
@ point
.co
))
2751 # force even spreading of points, so they are placed on stroke
2753 bm_mod
.verts
.ensure_lookup_table()
2754 bm_mod
.verts
.index_update()
2755 for stroke
, verts_seq
in stroke_verts
:
2756 if len(verts_seq
) < 2:
2758 # spread vertices evenly over the stroke
2759 if method
== 'regular':
2760 loop
= [[vert
.index
for vert
in verts_seq
], False]
2761 move
+= gstretch_calculate_verts(loop
, stroke
, object, bm_mod
,
2764 for i
, vert
in enumerate(verts_seq
):
2766 bm_mod
.edges
.new((verts_seq
[i
- 1], verts_seq
[i
]))
2768 # connect single vertices to the closest stroke
2770 for vert
, m_stroke
, point
in singles
:
2771 if m_stroke
!= stroke
:
2773 bm_mod
.edges
.new((vert
, verts_seq
[point
]))
2774 bm_mod
.edges
.ensure_lookup_table()
2775 bmesh
.update_edit_mesh(object.data
)
2780 # erases the grease pencil stroke
2781 def gstretch_erase_stroke(stroke
, context
):
2782 # change 3d coordinate into a stroke-point
2783 def sp(loc
, context
):
2787 'location': (0, 0, 0),
2789 view3d_utils
.location_3d_to_region_2d(
2790 context
.region
, context
.space_data
.region_3d
, loc
)
2797 if type(stroke
) != bpy
.types
.GPencilStroke
:
2798 # fake stroke, there is nothing to delete
2801 erase_stroke
= [sp(p
.co
, context
) for p
in stroke
.points
]
2803 erase_stroke
[0]['is_start'] = True
2804 #bpy.ops.gpencil.draw(mode='ERASER', stroke=erase_stroke)
2805 bpy
.ops
.gpencil
.data_unlink()
2809 # get point on stroke, given by relative distance (0.0 - 1.0)
2810 def gstretch_eval_stroke(stroke
, distance
, stroke_lengths_cache
=False):
2811 # use cache if available
2812 if not stroke_lengths_cache
:
2814 for i
, p
in enumerate(stroke
.points
[1:]):
2815 lengths
.append((p
.co
- stroke
.points
[i
].co
).length
+ lengths
[-1])
2816 total_length
= max(lengths
[-1], 1e-7)
2817 stroke_lengths_cache
= [length
/ total_length
for length
in
2819 stroke_lengths
= stroke_lengths_cache
[:]
2821 if distance
in stroke_lengths
:
2822 loc
= stroke
.points
[stroke_lengths
.index(distance
)].co
2823 elif distance
> stroke_lengths
[-1]:
2824 # should be impossible, but better safe than sorry
2825 loc
= stroke
.points
[-1].co
2827 stroke_lengths
.append(distance
)
2828 stroke_lengths
.sort()
2829 stroke_index
= stroke_lengths
.index(distance
)
2830 interval_length
= stroke_lengths
[
2831 stroke_index
+ 1] - stroke_lengths
[stroke_index
- 1
2833 distance_relative
= (distance
- stroke_lengths
[stroke_index
- 1]) / interval_length
2834 interval_vector
= stroke
.points
[stroke_index
].co
- stroke
.points
[stroke_index
- 1].co
2835 loc
= stroke
.points
[stroke_index
- 1].co
+ distance_relative
* interval_vector
2837 return(loc
, stroke_lengths_cache
)
2840 # create fake grease pencil strokes for the active object
2841 def gstretch_get_fake_strokes(object, bm_mod
, loops
):
2844 p1
= object.matrix_world
@ bm_mod
.verts
[loop
[0][0]].co
2845 p2
= object.matrix_world
@ bm_mod
.verts
[loop
[0][-1]].co
2846 strokes
.append(gstretch_fake_stroke([p1
, p2
]))
2851 def gstretch_get_strokes(self
, context
):
2852 looptools
= context
.window_manager
.looptools
2853 gp
= get_strokes(self
, context
)
2856 if looptools
.gstretch_use_guide
== "Annotation":
2857 layer
= bpy
.data
.grease_pencils
[0].layers
.active
2858 if looptools
.gstretch_use_guide
== "GPencil" and not looptools
.gstretch_guide
== None:
2859 layer
= looptools
.gstretch_guide
.data
.layers
.active
2862 frame
= layer
.active_frame
2865 strokes
= frame
.strokes
2866 if len(strokes
) < 1:
2871 # returns a list with loop-stroke pairs
2872 def gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
):
2873 if not loops
or not strokes
:
2876 # calculate loop centers
2878 bm_mod
.verts
.ensure_lookup_table()
2880 center
= mathutils
.Vector()
2881 for v_index
in loop
[0]:
2882 center
+= bm_mod
.verts
[v_index
].co
2883 center
/= len(loop
[0])
2884 center
= object.matrix_world
@ center
2885 loop_centers
.append([center
, loop
])
2887 # calculate stroke centers
2889 for stroke
in strokes
:
2890 center
= mathutils
.Vector()
2891 for p
in stroke
.points
:
2893 center
/= len(stroke
.points
)
2894 stroke_centers
.append([center
, stroke
, 0])
2896 # match, first by stroke use count, then by distance
2898 for lc
in loop_centers
:
2900 for i
, sc
in enumerate(stroke_centers
):
2901 distances
.append([sc
[2], (lc
[0] - sc
[0]).length
, i
])
2903 best_stroke
= distances
[0][2]
2904 ls_pairs
.append([lc
[1], stroke_centers
[best_stroke
][1]])
2905 stroke_centers
[best_stroke
][2] += 1 # increase stroke use count
2910 # match single selected vertices to the closest stroke endpoint
2911 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2912 def gstretch_match_single_verts(bm_mod
, strokes
, mat_world
):
2913 # calculate stroke endpoints in object space
2915 for stroke
in strokes
:
2916 endpoints
.append((mat_world
@ stroke
.points
[0].co
, stroke
, 0))
2917 endpoints
.append((mat_world
@ stroke
.points
[-1].co
, stroke
, -1))
2920 # find single vertices (not connected to other selected verts)
2921 for vert
in bm_mod
.verts
:
2925 for edge
in vert
.link_edges
:
2926 if edge
.other_vert(vert
).select
:
2931 # calculate distances from vertex to endpoints
2932 distance
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2933 endpoint_index
) for endpoint_index
, (loc
, stroke
, stroke_point
) in
2934 enumerate(endpoints
)]
2936 distances
.append(distance
[0])
2938 # create matches, based on shortest distance first
2942 singles
.append((distances
[0][1], distances
[0][2], distances
[0][3]))
2943 endpoints
.pop(distances
[0][4])
2946 for (i
, vert
, j
, k
, l
) in distances
:
2947 distance_new
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2948 endpoint_index
) for endpoint_index
, (loc
, stroke
,
2949 stroke_point
) in enumerate(endpoints
)]
2951 distances_new
.append(distance_new
[0])
2952 distances
= distances_new
2957 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2958 def gstretch_relative_lengths(loop
, bm_mod
):
2960 for i
, v_index
in enumerate(loop
[0][1:]):
2962 (bm_mod
.verts
[v_index
].co
-
2963 bm_mod
.verts
[loop
[0][i
]].co
).length
+ lengths
[-1]
2965 total_length
= max(lengths
[-1], 1e-7)
2966 relative_lengths
= [length
/ total_length
for length
in
2969 return(relative_lengths
)
2972 # convert cache-stored strokes into usable (fake) GP strokes
2973 def gstretch_safe_to_true_strokes(safe_strokes
):
2975 for safe_stroke
in safe_strokes
:
2976 strokes
.append(gstretch_fake_stroke(safe_stroke
))
2981 # convert a GP stroke into a list of points which can be stored in cache
2982 def gstretch_true_to_safe_strokes(strokes
):
2984 for stroke
in strokes
:
2985 safe_strokes
.append([p
.co
.copy() for p
in stroke
.points
])
2987 return(safe_strokes
)
2990 # force consistency in GUI, max value can never be lower than min value
2991 def gstretch_update_max(self
, context
):
2992 # called from operator settings (after execution)
2993 if 'conversion_min' in self
.keys():
2994 if self
.conversion_min
> self
.conversion_max
:
2995 self
.conversion_max
= self
.conversion_min
2996 # called from toolbar
2998 lt
= context
.window_manager
.looptools
2999 if lt
.gstretch_conversion_min
> lt
.gstretch_conversion_max
:
3000 lt
.gstretch_conversion_max
= lt
.gstretch_conversion_min
3003 # force consistency in GUI, min value can never be higher than max value
3004 def gstretch_update_min(self
, context
):
3005 # called from operator settings (after execution)
3006 if 'conversion_max' in self
.keys():
3007 if self
.conversion_max
< self
.conversion_min
:
3008 self
.conversion_min
= self
.conversion_max
3009 # called from toolbar
3011 lt
= context
.window_manager
.looptools
3012 if lt
.gstretch_conversion_max
< lt
.gstretch_conversion_min
:
3013 lt
.gstretch_conversion_min
= lt
.gstretch_conversion_max
3016 # ########################################
3017 # ##### Relax functions ##################
3018 # ########################################
3020 # create lists with knots and points, all correctly sorted
3021 def relax_calculate_knots(loops
):
3024 for loop
, circular
in loops
:
3028 if len(loop
) % 2 == 1: # odd
3029 extend
= [False, True, 0, 1, 0, 1]
3031 extend
= [True, False, 0, 1, 1, 2]
3033 if len(loop
) % 2 == 1: # odd
3034 extend
= [False, False, 0, 1, 1, 2]
3036 extend
= [False, False, 0, 1, 1, 2]
3039 loop
= [loop
[-1]] + loop
+ [loop
[0]]
3040 for i
in range(extend
[2 + 2 * j
], len(loop
), 2):
3041 knots
[j
].append(loop
[i
])
3042 for i
in range(extend
[3 + 2 * j
], len(loop
), 2):
3043 if loop
[i
] == loop
[-1] and not circular
:
3045 if len(points
[j
]) == 0:
3046 points
[j
].append(loop
[i
])
3047 elif loop
[i
] != points
[j
][0]:
3048 points
[j
].append(loop
[i
])
3050 if knots
[j
][0] != knots
[j
][-1]:
3051 knots
[j
].append(knots
[j
][0])
3052 if len(points
[1]) == 0:
3058 all_points
.append(p
)
3060 return(all_knots
, all_points
)
3063 # calculate relative positions compared to first knot
3064 def relax_calculate_t(bm_mod
, knots
, points
, regular
):
3067 for i
in range(len(knots
)):
3068 amount
= len(knots
[i
]) + len(points
[i
])
3070 for j
in range(amount
):
3072 mix
.append([True, knots
[i
][round(j
/ 2)]])
3073 elif j
== amount
- 1:
3074 mix
.append([True, knots
[i
][-1]])
3076 mix
.append([False, points
[i
][int(j
/ 2)]])
3082 loc
= mathutils
.Vector(bm_mod
.verts
[m
[1]].co
[:])
3085 len_total
+= (loc
- loc_prev
).length
3087 tknots
.append(len_total
)
3089 tpoints
.append(len_total
)
3093 for p
in range(len(points
[i
])):
3094 tpoints
.append((tknots
[p
] + tknots
[p
+ 1]) / 2)
3095 all_tknots
.append(tknots
)
3096 all_tpoints
.append(tpoints
)
3098 return(all_tknots
, all_tpoints
)
3101 # change the location of the points to their place on the spline
3102 def relax_calculate_verts(bm_mod
, interpolation
, tknots
, knots
, tpoints
,
3106 for i
in range(len(knots
)):
3108 m
= tpoints
[i
][points
[i
].index(p
)]
3110 n
= tknots
[i
].index(m
)
3116 if n
> len(splines
[i
]) - 1:
3117 n
= len(splines
[i
]) - 1
3121 if interpolation
== 'cubic':
3122 ax
, bx
, cx
, dx
, tx
= splines
[i
][n
][0]
3123 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
3124 ay
, by
, cy
, dy
, ty
= splines
[i
][n
][1]
3125 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
3126 az
, bz
, cz
, dz
, tz
= splines
[i
][n
][2]
3127 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
3128 change
.append([p
, mathutils
.Vector([x
, y
, z
])])
3129 else: # interpolation == 'linear'
3130 a
, d
, t
, u
= splines
[i
][n
]
3133 change
.append([p
, ((m
- t
) / u
) * d
+ a
])
3135 move
.append([c
[0], (bm_mod
.verts
[c
[0]].co
+ c
[1]) / 2])
3140 # ########################################
3141 # ##### Space functions ##################
3142 # ########################################
3144 # calculate relative positions compared to first knot
3145 def space_calculate_t(bm_mod
, knots
):
3150 loc
= mathutils
.Vector(bm_mod
.verts
[k
].co
[:])
3153 len_total
+= (loc
- loc_prev
).length
3154 tknots
.append(len_total
)
3157 t_per_segment
= len_total
/ (amount
- 1)
3158 tpoints
= [i
* t_per_segment
for i
in range(amount
)]
3160 return(tknots
, tpoints
)
3163 # change the location of the points to their place on the spline
3164 def space_calculate_verts(bm_mod
, interpolation
, tknots
, tpoints
, points
,
3168 m
= tpoints
[points
.index(p
)]
3176 if n
> len(splines
) - 1:
3177 n
= len(splines
) - 1
3181 if interpolation
== 'cubic':
3182 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
3183 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
3184 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
3185 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
3186 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
3187 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
3188 move
.append([p
, mathutils
.Vector([x
, y
, z
])])
3189 else: # interpolation == 'linear'
3190 a
, d
, t
, u
= splines
[n
]
3191 move
.append([p
, ((m
- t
) / u
) * d
+ a
])
3196 # ########################################
3197 # ##### Operators ########################
3198 # ########################################
3201 class Bridge(Operator
):
3202 bl_idname
= 'mesh.looptools_bridge'
3203 bl_label
= "Bridge / Loft"
3204 bl_description
= "Bridge two, or loft several, loops of vertices"
3205 bl_options
= {'REGISTER', 'UNDO'}
3207 cubic_strength
: FloatProperty(
3209 description
="Higher strength results in more fluid curves",
3214 interpolation
: EnumProperty(
3215 name
="Interpolation mode",
3216 items
=(('cubic', "Cubic", "Gives curved results"),
3217 ('linear', "Linear", "Basic, fast, straight interpolation")),
3218 description
="Interpolation mode: algorithm used when creating "
3224 description
="Loft multiple loops, instead of considering them as "
3225 "a multi-input for bridging",
3228 loft_loop
: BoolProperty(
3230 description
="Connect the first and the last loop with each other",
3233 min_width
: IntProperty(
3234 name
="Minimum width",
3235 description
="Segments with an edge smaller than this are merged "
3236 "(compared to base edge)",
3240 subtype
='PERCENTAGE'
3244 items
=(('basic', "Basic", "Fast algorithm"),
3245 ('shortest', "Shortest edge", "Slower algorithm with better vertex matching")),
3246 description
="Algorithm used for bridging",
3249 remove_faces
: BoolProperty(
3250 name
="Remove faces",
3251 description
="Remove faces that are internal after bridging",
3254 reverse
: BoolProperty(
3256 description
="Manually override the direction in which the loops "
3257 "are bridged. Only use if the tool gives the wrong result",
3260 segments
: IntProperty(
3262 description
="Number of segments used to bridge the gap (0=automatic)",
3269 description
="Twist what vertices are connected to each other",
3274 def poll(cls
, context
):
3275 ob
= context
.active_object
3276 return (ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3278 def draw(self
, context
):
3279 layout
= self
.layout
3280 # layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3283 col_top
= layout
.column(align
=True)
3284 row
= col_top
.row(align
=True)
3285 col_left
= row
.column(align
=True)
3286 col_right
= row
.column(align
=True)
3287 col_right
.active
= self
.segments
!= 1
3288 col_left
.prop(self
, "segments")
3289 col_right
.prop(self
, "min_width", text
="")
3291 bottom_left
= col_left
.row()
3292 bottom_left
.active
= self
.segments
!= 1
3293 bottom_left
.prop(self
, "interpolation", text
="")
3294 bottom_right
= col_right
.row()
3295 bottom_right
.active
= self
.interpolation
== 'cubic'
3296 bottom_right
.prop(self
, "cubic_strength")
3297 # boolean properties
3298 col_top
.prop(self
, "remove_faces")
3300 col_top
.prop(self
, "loft_loop")
3302 # override properties
3304 row
= layout
.row(align
=True)
3305 row
.prop(self
, "twist")
3306 row
.prop(self
, "reverse")
3308 def invoke(self
, context
, event
):
3309 # load custom settings
3310 context
.window_manager
.looptools
.bridge_loft
= self
.loft
3312 return self
.execute(context
)
3314 def execute(self
, context
):
3316 object, bm
= initialise()
3317 edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
= \
3318 bridge_initialise(bm
, self
.interpolation
)
3319 settings_write(self
)
3321 # check cache to see if we can save time
3322 input_method
= bridge_input_method(self
.loft
, self
.loft_loop
)
3323 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Bridge",
3324 object, bm
, input_method
, False)
3327 loops
= bridge_get_input(bm
)
3329 # reorder loops if there are more than 2
3332 loops
= bridge_sort_loops(bm
, loops
, self
.loft_loop
)
3334 loops
= bridge_match_loops(bm
, loops
)
3336 # saving cache for faster execution next time
3338 cache_write("Bridge", object, bm
, input_method
, False, False,
3339 loops
, False, False)
3342 # calculate new geometry
3345 max_vert_index
= len(bm
.verts
) - 1
3346 for i
in range(1, len(loops
)):
3347 if not self
.loft
and i
% 2 == 0:
3349 lines
= bridge_calculate_lines(bm
, loops
[i
- 1:i
+ 1],
3350 self
.mode
, self
.twist
, self
.reverse
)
3351 vertex_normals
= bridge_calculate_virtual_vertex_normals(bm
,
3352 lines
, loops
[i
- 1:i
+ 1], edge_faces
, edgekey_to_edge
)
3353 segments
= bridge_calculate_segments(bm
, lines
,
3354 loops
[i
- 1:i
+ 1], self
.segments
)
3355 new_verts
, new_faces
, max_vert_index
= \
3356 bridge_calculate_geometry(
3357 bm
, lines
, vertex_normals
,
3358 segments
, self
.interpolation
, self
.cubic_strength
,
3359 self
.min_width
, max_vert_index
3362 vertices
+= new_verts
3365 # make sure faces in loops that aren't used, aren't removed
3366 if self
.remove_faces
and old_selected_faces
:
3367 bridge_save_unused_faces(bm
, old_selected_faces
, loops
)
3370 bridge_create_vertices(bm
, vertices
)
3373 new_faces
= bridge_create_faces(object, bm
, faces
, self
.twist
)
3374 old_selected_faces
= [
3375 i
for i
, face
in enumerate(bm
.faces
) if face
.index
in old_selected_faces
3377 bridge_select_new_faces(new_faces
, smooth
)
3378 # edge-data could have changed, can't use cache next run
3379 if faces
and not vertices
:
3380 cache_delete("Bridge")
3381 # delete internal faces
3382 if self
.remove_faces
and old_selected_faces
:
3383 bridge_remove_internal_faces(bm
, old_selected_faces
)
3384 # make sure normals are facing outside
3385 bmesh
.update_edit_mesh(object.data
, loop_triangles
=False,
3387 bpy
.ops
.mesh
.normals_make_consistent()
3396 class Circle(Operator
):
3397 bl_idname
= "mesh.looptools_circle"
3399 bl_description
= "Move selected vertices into a circle shape"
3400 bl_options
= {'REGISTER', 'UNDO'}
3402 custom_radius
: BoolProperty(
3404 description
="Force a custom radius",
3409 items
=(("best", "Best fit", "Non-linear least squares"),
3410 ("inside", "Fit inside", "Only move vertices towards the center")),
3411 description
="Method used for fitting a circle to the vertices",
3414 flatten
: BoolProperty(
3416 description
="Flatten the circle, instead of projecting it on the mesh",
3419 influence
: FloatProperty(
3421 description
="Force of the tool",
3426 subtype
='PERCENTAGE'
3428 lock_x
: BoolProperty(
3430 description
="Lock editing of the x-coordinate",
3433 lock_y
: BoolProperty(
3435 description
="Lock editing of the y-coordinate",
3438 lock_z
: BoolProperty(name
="Lock Z",
3439 description
="Lock editing of the z-coordinate",
3442 radius
: FloatProperty(
3444 description
="Custom radius for circle",
3449 regular
: BoolProperty(
3451 description
="Distribute vertices at constant distances along the circle",
3456 def poll(cls
, context
):
3457 ob
= context
.active_object
3458 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3460 def draw(self
, context
):
3461 layout
= self
.layout
3462 col
= layout
.column()
3464 col
.prop(self
, "fit")
3467 col
.prop(self
, "flatten")
3468 row
= col
.row(align
=True)
3469 row
.prop(self
, "custom_radius")
3470 row_right
= row
.row(align
=True)
3471 row_right
.active
= self
.custom_radius
3472 row_right
.prop(self
, "radius", text
="")
3473 col
.prop(self
, "regular")
3476 col_move
= col
.column(align
=True)
3477 row
= col_move
.row(align
=True)
3479 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3481 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3483 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3485 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3487 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3489 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3490 col_move
.prop(self
, "influence")
3492 def invoke(self
, context
, event
):
3493 # load custom settings
3495 return self
.execute(context
)
3497 def execute(self
, context
):
3499 object, bm
= initialise()
3500 settings_write(self
)
3501 # check cache to see if we can save time
3502 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Circle",
3503 object, bm
, False, False)
3505 derived
, bm_mod
= get_derived_bmesh(object, bm
)
3508 derived
, bm_mod
, single_vertices
, single_loops
, loops
= \
3509 circle_get_input(object, bm
)
3510 mapping
= get_mapping(derived
, bm
, bm_mod
, single_vertices
,
3512 single_loops
, loops
= circle_check_loops(single_loops
, loops
,
3515 # saving cache for faster execution next time
3517 cache_write("Circle", object, bm
, False, False, single_loops
,
3518 loops
, derived
, mapping
)
3521 for i
, loop
in enumerate(loops
):
3522 # best fitting flat plane
3523 com
, normal
= calculate_plane(bm_mod
, loop
)
3524 # if circular, shift loop so we get a good starting vertex
3526 loop
= circle_shift_loop(bm_mod
, loop
, com
)
3527 # flatten vertices on plane
3528 locs_2d
, p
, q
= circle_3d_to_2d(bm_mod
, loop
, com
, normal
)
3530 if self
.fit
== 'best':
3531 x0
, y0
, r
= circle_calculate_best_fit(locs_2d
)
3532 else: # self.fit == 'inside'
3533 x0
, y0
, r
= circle_calculate_min_fit(locs_2d
)
3535 if self
.custom_radius
:
3536 r
= self
.radius
/ p
.length
3537 # calculate positions on circle
3539 new_locs_2d
= circle_project_regular(locs_2d
[:], x0
, y0
, r
)
3541 new_locs_2d
= circle_project_non_regular(locs_2d
[:], x0
, y0
, r
)
3542 # take influence into account
3543 locs_2d
= circle_influence_locs(locs_2d
, new_locs_2d
,
3545 # calculate 3d positions of the created 2d input
3546 move
.append(circle_calculate_verts(self
.flatten
, bm_mod
,
3547 locs_2d
, com
, p
, q
, normal
))
3548 # flatten single input vertices on plane defined by loop
3549 if self
.flatten
and single_loops
:
3550 move
.append(circle_flatten_singles(bm_mod
, com
, p
, q
,
3551 normal
, single_loops
[i
]))
3553 # move vertices to new locations
3554 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3555 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3558 move_verts(object, bm
, mapping
, move
, lock
, -1)
3569 class Curve(Operator
):
3570 bl_idname
= "mesh.looptools_curve"
3572 bl_description
= "Turn a loop into a smooth curve"
3573 bl_options
= {'REGISTER', 'UNDO'}
3575 boundaries
: BoolProperty(
3577 description
="Limit the tool to work within the boundaries of the selected vertices",
3580 influence
: FloatProperty(
3582 description
="Force of the tool",
3587 subtype
='PERCENTAGE'
3589 interpolation
: EnumProperty(
3590 name
="Interpolation",
3591 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
3592 ("linear", "Linear", "Simple and fast linear algorithm")),
3593 description
="Algorithm used for interpolation",
3596 lock_x
: BoolProperty(
3598 description
="Lock editing of the x-coordinate",
3601 lock_y
: BoolProperty(
3603 description
="Lock editing of the y-coordinate",
3606 lock_z
: BoolProperty(
3608 description
="Lock editing of the z-coordinate",
3611 regular
: BoolProperty(
3613 description
="Distribute vertices at constant distances along the curve",
3616 restriction
: EnumProperty(
3618 items
=(("none", "None", "No restrictions on vertex movement"),
3619 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
3620 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
3621 description
="Restrictions on how the vertices can be moved",
3626 def poll(cls
, context
):
3627 ob
= context
.active_object
3628 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3630 def draw(self
, context
):
3631 layout
= self
.layout
3632 col
= layout
.column()
3634 col
.prop(self
, "interpolation")
3635 col
.prop(self
, "restriction")
3636 col
.prop(self
, "boundaries")
3637 col
.prop(self
, "regular")
3640 col_move
= col
.column(align
=True)
3641 row
= col_move
.row(align
=True)
3643 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3645 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3647 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3649 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3651 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3653 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3654 col_move
.prop(self
, "influence")
3656 def invoke(self
, context
, event
):
3657 # load custom settings
3659 return self
.execute(context
)
3661 def execute(self
, context
):
3663 object, bm
= initialise()
3664 settings_write(self
)
3665 # check cache to see if we can save time
3666 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Curve",
3667 object, bm
, False, self
.boundaries
)
3669 derived
, bm_mod
= get_derived_bmesh(object, bm
)
3672 derived
, bm_mod
, loops
= curve_get_input(object, bm
, self
.boundaries
)
3673 mapping
= get_mapping(derived
, bm
, bm_mod
, False, True, loops
)
3674 loops
= check_loops(loops
, mapping
, bm_mod
)
3676 v
.index
for v
in bm_mod
.verts
if v
.select
and not v
.hide
3679 # saving cache for faster execution next time
3681 cache_write("Curve", object, bm
, False, self
.boundaries
, False,
3682 loops
, derived
, mapping
)
3686 knots
, points
= curve_calculate_knots(loop
, verts_selected
)
3687 pknots
= curve_project_knots(bm_mod
, verts_selected
, knots
,
3689 tknots
, tpoints
= curve_calculate_t(bm_mod
, knots
, points
,
3690 pknots
, self
.regular
, loop
[1])
3691 splines
= calculate_splines(self
.interpolation
, bm_mod
,
3693 move
.append(curve_calculate_vertices(bm_mod
, knots
, tknots
,
3694 points
, tpoints
, splines
, self
.interpolation
,
3697 # move vertices to new locations
3698 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3699 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3702 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
3713 class Flatten(Operator
):
3714 bl_idname
= "mesh.looptools_flatten"
3715 bl_label
= "Flatten"
3716 bl_description
= "Flatten vertices on a best-fitting plane"
3717 bl_options
= {'REGISTER', 'UNDO'}
3719 influence
: FloatProperty(
3721 description
="Force of the tool",
3726 subtype
='PERCENTAGE'
3728 lock_x
: BoolProperty(
3730 description
="Lock editing of the x-coordinate",
3733 lock_y
: BoolProperty(
3735 description
="Lock editing of the y-coordinate",
3738 lock_z
: BoolProperty(name
="Lock Z",
3739 description
="Lock editing of the z-coordinate",
3742 plane
: EnumProperty(
3744 items
=(("best_fit", "Best fit", "Calculate a best fitting plane"),
3745 ("normal", "Normal", "Derive plane from averaging vertex normals"),
3746 ("view", "View", "Flatten on a plane perpendicular to the viewing angle")),
3747 description
="Plane on which vertices are flattened",
3750 restriction
: EnumProperty(
3752 items
=(("none", "None", "No restrictions on vertex movement"),
3753 ("bounding_box", "Bounding box", "Vertices are restricted to "
3754 "movement inside the bounding box of the selection")),
3755 description
="Restrictions on how the vertices can be moved",
3760 def poll(cls
, context
):
3761 ob
= context
.active_object
3762 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3764 def draw(self
, context
):
3765 layout
= self
.layout
3766 col
= layout
.column()
3768 col
.prop(self
, "plane")
3769 # col.prop(self, "restriction")
3772 col_move
= col
.column(align
=True)
3773 row
= col_move
.row(align
=True)
3775 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3777 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3779 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3781 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3783 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3785 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3786 col_move
.prop(self
, "influence")
3788 def invoke(self
, context
, event
):
3789 # load custom settings
3791 return self
.execute(context
)
3793 def execute(self
, context
):
3795 object, bm
= initialise()
3796 settings_write(self
)
3797 # check cache to see if we can save time
3798 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Flatten",
3799 object, bm
, False, False)
3801 # order input into virtual loops
3802 loops
= flatten_get_input(bm
)
3803 loops
= check_loops(loops
, mapping
, bm
)
3805 # saving cache for faster execution next time
3807 cache_write("Flatten", object, bm
, False, False, False, loops
,
3812 # calculate plane and position of vertices on them
3813 com
, normal
= calculate_plane(bm
, loop
, method
=self
.plane
,
3815 to_move
= flatten_project(bm
, loop
, com
, normal
)
3816 if self
.restriction
== 'none':
3817 move
.append(to_move
)
3819 move
.append(to_move
)
3821 # move vertices to new locations
3822 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3823 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3826 move_verts(object, bm
, False, move
, lock
, self
.influence
)
3834 # Annotation operator
3835 class RemoveAnnotation(Operator
):
3836 bl_idname
= "remove.annotation"
3837 bl_label
= "Remove Annotation"
3838 bl_description
= "Remove all Annotation Strokes"
3839 bl_options
= {'REGISTER', 'UNDO'}
3841 def execute(self
, context
):
3844 bpy
.data
.grease_pencils
[0].layers
.active
.clear()
3846 self
.report({'INFO'}, "No Annotation data to Unlink")
3847 return {'CANCELLED'}
3852 class RemoveGPencil(Operator
):
3853 bl_idname
= "remove.gp"
3854 bl_label
= "Remove GPencil"
3855 bl_description
= "Remove all GPencil Strokes"
3856 bl_options
= {'REGISTER', 'UNDO'}
3858 def execute(self
, context
):
3861 looptools
= context
.window_manager
.looptools
3862 looptools
.gstretch_guide
.data
.layers
.data
.clear()
3863 looptools
.gstretch_guide
.data
.update_tag()
3865 self
.report({'INFO'}, "No GPencil data to Unlink")
3866 return {'CANCELLED'}
3871 class GStretch(Operator
):
3872 bl_idname
= "mesh.looptools_gstretch"
3873 bl_label
= "Gstretch"
3874 bl_description
= "Stretch selected vertices to active stroke"
3875 bl_options
= {'REGISTER', 'UNDO'}
3877 conversion
: EnumProperty(
3879 items
=(("distance", "Distance", "Set the distance between vertices "
3880 "of the converted stroke"),
3881 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
3882 "number of vertices that converted strokes will have"),
3883 ("vertices", "Exact vertices", "Set the exact number of vertices "
3884 "that converted strokes will have. Short strokes "
3885 "with few points may contain less vertices than this number."),
3886 ("none", "No simplification", "Convert each point "
3888 description
="If strokes are converted to geometry, "
3889 "use this simplification method",
3890 default
='limit_vertices'
3892 conversion_distance
: FloatProperty(
3894 description
="Absolute distance between vertices along the converted "
3901 conversion_max
: IntProperty(
3902 name
="Max Vertices",
3903 description
="Maximum number of vertices strokes will "
3904 "have, when they are converted to geomtery",
3908 update
=gstretch_update_min
3910 conversion_min
: IntProperty(
3911 name
="Min Vertices",
3912 description
="Minimum number of vertices strokes will "
3913 "have, when they are converted to geomtery",
3917 update
=gstretch_update_max
3919 conversion_vertices
: IntProperty(
3921 description
="Number of vertices strokes will "
3922 "have, when they are converted to geometry. If strokes have less "
3923 "points than required, the 'Spread evenly' method is used",
3928 delete_strokes
: BoolProperty(
3929 name
="Delete strokes",
3930 description
="Remove strokes if they have been used."
3931 "WARNING: DOES NOT SUPPORT UNDO",
3934 influence
: FloatProperty(
3936 description
="Force of the tool",
3941 subtype
='PERCENTAGE'
3943 lock_x
: BoolProperty(
3945 description
="Lock editing of the x-coordinate",
3948 lock_y
: BoolProperty(
3950 description
="Lock editing of the y-coordinate",
3953 lock_z
: BoolProperty(
3955 description
="Lock editing of the z-coordinate",
3958 method
: EnumProperty(
3960 items
=(("project", "Project", "Project vertices onto the stroke, "
3961 "using vertex normals and connected edges"),
3962 ("irregular", "Spread", "Distribute vertices along the full "
3963 "stroke, retaining relative distances between the vertices"),
3964 ("regular", "Spread evenly", "Distribute vertices at regular "
3965 "distances along the full stroke")),
3966 description
="Method of distributing the vertices over the "
3972 def poll(cls
, context
):
3973 ob
= context
.active_object
3974 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3976 def draw(self
, context
):
3977 looptools
= context
.window_manager
.looptools
3978 layout
= self
.layout
3979 col
= layout
.column()
3981 col
.prop(self
, "method")
3984 col_conv
= col
.column(align
=True)
3985 col_conv
.prop(self
, "conversion", text
="")
3986 if self
.conversion
== 'distance':
3987 col_conv
.prop(self
, "conversion_distance")
3988 elif self
.conversion
== 'limit_vertices':
3989 row
= col_conv
.row(align
=True)
3990 row
.prop(self
, "conversion_min", text
="Min")
3991 row
.prop(self
, "conversion_max", text
="Max")
3992 elif self
.conversion
== 'vertices':
3993 col_conv
.prop(self
, "conversion_vertices")
3996 col_move
= col
.column(align
=True)
3997 row
= col_move
.row(align
=True)
3999 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
4001 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
4003 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
4005 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
4007 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
4009 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
4010 col_move
.prop(self
, "influence")
4012 if looptools
.gstretch_use_guide
== "Annotation":
4013 col
.operator("remove.annotation", text
="Delete annotation strokes")
4014 if looptools
.gstretch_use_guide
== "GPencil":
4015 col
.operator("remove.gp", text
="Delete GPencil strokes")
4017 def invoke(self
, context
, event
):
4018 # flush cached strokes
4019 if 'Gstretch' in looptools_cache
:
4020 looptools_cache
['Gstretch']['single_loops'] = []
4021 # load custom settings
4023 return self
.execute(context
)
4025 def execute(self
, context
):
4027 object, bm
= initialise()
4028 settings_write(self
)
4030 # check cache to see if we can save time
4031 cached
, safe_strokes
, loops
, derived
, mapping
= cache_read("Gstretch",
4032 object, bm
, False, False)
4034 straightening
= False
4036 strokes
= gstretch_safe_to_true_strokes(safe_strokes
)
4037 # cached strokes were flushed (see operator's invoke function)
4038 elif get_strokes(self
, context
):
4039 strokes
= gstretch_get_strokes(self
, context
)
4041 # straightening function (no GP) -> loops ignore modifiers
4042 straightening
= True
4045 bm_mod
.verts
.ensure_lookup_table()
4046 bm_mod
.edges
.ensure_lookup_table()
4047 bm_mod
.faces
.ensure_lookup_table()
4048 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
4049 if not straightening
:
4050 derived
, bm_mod
= get_derived_bmesh(object, bm
)
4052 # get loops and strokes
4053 if get_strokes(self
, context
):
4055 derived
, bm_mod
, loops
= get_connected_input(object, bm
, input='selected')
4056 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4057 loops
= check_loops(loops
, mapping
, bm_mod
)
4059 strokes
= gstretch_get_strokes(self
, context
)
4061 # straightening function (no GP) -> loops ignore modifiers
4065 bm_mod
.verts
.ensure_lookup_table()
4066 bm_mod
.edges
.ensure_lookup_table()
4067 bm_mod
.faces
.ensure_lookup_table()
4069 edgekey(edge
) for edge
in bm_mod
.edges
if
4070 edge
.select
and not edge
.hide
4072 loops
= get_connected_selections(edge_keys
)
4073 loops
= check_loops(loops
, mapping
, bm_mod
)
4074 # create fake strokes
4075 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
4077 # saving cache for faster execution next time
4080 safe_strokes
= gstretch_true_to_safe_strokes(strokes
)
4083 cache_write("Gstretch", object, bm
, False, False,
4084 safe_strokes
, loops
, derived
, mapping
)
4086 # pair loops and strokes
4087 ls_pairs
= gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
)
4088 ls_pairs
= gstretch_align_pairs(ls_pairs
, object, bm_mod
, self
.method
)
4092 # no selected geometry, convert GP to verts
4094 move
.append(gstretch_create_verts(object, bm
, strokes
,
4095 self
.method
, self
.conversion
, self
.conversion_distance
,
4096 self
.conversion_max
, self
.conversion_min
,
4097 self
.conversion_vertices
))
4098 for stroke
in strokes
:
4099 gstretch_erase_stroke(stroke
, context
)
4101 for (loop
, stroke
) in ls_pairs
:
4102 move
.append(gstretch_calculate_verts(loop
, stroke
, object,
4103 bm_mod
, self
.method
))
4104 if self
.delete_strokes
:
4105 if type(stroke
) != bpy
.types
.GPencilStroke
:
4106 # in case of cached fake stroke, get the real one
4107 if get_strokes(self
, context
):
4108 strokes
= gstretch_get_strokes(self
, context
)
4109 if loops
and strokes
:
4110 ls_pairs
= gstretch_match_loops_strokes(loops
,
4111 strokes
, object, bm_mod
)
4112 ls_pairs
= gstretch_align_pairs(ls_pairs
,
4113 object, bm_mod
, self
.method
)
4114 for (l
, s
) in ls_pairs
:
4118 gstretch_erase_stroke(stroke
, context
)
4120 # move vertices to new locations
4121 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4122 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4125 bmesh
.update_edit_mesh(object.data
, loop_triangles
=True, destructive
=True)
4126 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4137 class Relax(Operator
):
4138 bl_idname
= "mesh.looptools_relax"
4140 bl_description
= "Relax the loop, so it is smoother"
4141 bl_options
= {'REGISTER', 'UNDO'}
4143 input: EnumProperty(
4145 items
=(("all", "Parallel (all)", "Also use non-selected "
4146 "parallel loops as input"),
4147 ("selected", "Selection", "Only use selected vertices as input")),
4148 description
="Loops that are relaxed",
4151 interpolation
: EnumProperty(
4152 name
="Interpolation",
4153 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4154 ("linear", "Linear", "Simple and fast linear algorithm")),
4155 description
="Algorithm used for interpolation",
4158 iterations
: EnumProperty(
4160 items
=(("1", "1", "One"),
4161 ("3", "3", "Three"),
4163 ("10", "10", "Ten"),
4164 ("25", "25", "Twenty-five")),
4165 description
="Number of times the loop is relaxed",
4168 regular
: BoolProperty(
4170 description
="Distribute vertices at constant distances along the loop",
4175 def poll(cls
, context
):
4176 ob
= context
.active_object
4177 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4179 def draw(self
, context
):
4180 layout
= self
.layout
4181 col
= layout
.column()
4183 col
.prop(self
, "interpolation")
4184 col
.prop(self
, "input")
4185 col
.prop(self
, "iterations")
4186 col
.prop(self
, "regular")
4188 def invoke(self
, context
, event
):
4189 # load custom settings
4191 return self
.execute(context
)
4193 def execute(self
, context
):
4195 object, bm
= initialise()
4196 settings_write(self
)
4197 # check cache to see if we can save time
4198 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Relax",
4199 object, bm
, self
.input, False)
4201 derived
, bm_mod
= get_derived_bmesh(object, bm
)
4204 derived
, bm_mod
, loops
= get_connected_input(object, bm
, self
.input)
4205 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4206 loops
= check_loops(loops
, mapping
, bm_mod
)
4207 knots
, points
= relax_calculate_knots(loops
)
4209 # saving cache for faster execution next time
4211 cache_write("Relax", object, bm
, self
.input, False, False, loops
,
4214 for iteration
in range(int(self
.iterations
)):
4215 # calculate splines and new positions
4216 tknots
, tpoints
= relax_calculate_t(bm_mod
, knots
, points
,
4219 for i
in range(len(knots
)):
4220 splines
.append(calculate_splines(self
.interpolation
, bm_mod
,
4221 tknots
[i
], knots
[i
]))
4222 move
= [relax_calculate_verts(bm_mod
, self
.interpolation
,
4223 tknots
, knots
, tpoints
, points
, splines
)]
4224 move_verts(object, bm
, mapping
, move
, False, -1)
4235 class Space(Operator
):
4236 bl_idname
= "mesh.looptools_space"
4238 bl_description
= "Space the vertices in a regular distribution on the loop"
4239 bl_options
= {'REGISTER', 'UNDO'}
4241 influence
: FloatProperty(
4243 description
="Force of the tool",
4248 subtype
='PERCENTAGE'
4250 input: EnumProperty(
4252 items
=(("all", "Parallel (all)", "Also use non-selected "
4253 "parallel loops as input"),
4254 ("selected", "Selection", "Only use selected vertices as input")),
4255 description
="Loops that are spaced",
4258 interpolation
: EnumProperty(
4259 name
="Interpolation",
4260 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4261 ("linear", "Linear", "Vertices are projected on existing edges")),
4262 description
="Algorithm used for interpolation",
4265 lock_x
: BoolProperty(
4267 description
="Lock editing of the x-coordinate",
4270 lock_y
: BoolProperty(
4272 description
="Lock editing of the y-coordinate",
4275 lock_z
: BoolProperty(
4277 description
="Lock editing of the z-coordinate",
4282 def poll(cls
, context
):
4283 ob
= context
.active_object
4284 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4286 def draw(self
, context
):
4287 layout
= self
.layout
4288 col
= layout
.column()
4290 col
.prop(self
, "interpolation")
4291 col
.prop(self
, "input")
4294 col_move
= col
.column(align
=True)
4295 row
= col_move
.row(align
=True)
4297 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
4299 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
4301 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
4303 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
4305 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
4307 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
4308 col_move
.prop(self
, "influence")
4310 def invoke(self
, context
, event
):
4311 # load custom settings
4313 return self
.execute(context
)
4315 def execute(self
, context
):
4317 object, bm
= initialise()
4318 settings_write(self
)
4319 # check cache to see if we can save time
4320 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Space",
4321 object, bm
, self
.input, False)
4323 derived
, bm_mod
= get_derived_bmesh(object, bm
)
4326 derived
, bm_mod
, loops
= get_connected_input(object, bm
, self
.input)
4327 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4328 loops
= check_loops(loops
, mapping
, bm_mod
)
4330 # saving cache for faster execution next time
4332 cache_write("Space", object, bm
, self
.input, False, False, loops
,
4337 # calculate splines and new positions
4338 if loop
[1]: # circular
4339 loop
[0].append(loop
[0][0])
4340 tknots
, tpoints
= space_calculate_t(bm_mod
, loop
[0][:])
4341 splines
= calculate_splines(self
.interpolation
, bm_mod
,
4343 move
.append(space_calculate_verts(bm_mod
, self
.interpolation
,
4344 tknots
, tpoints
, loop
[0][:-1], splines
))
4345 # move vertices to new locations
4346 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4347 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4350 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4360 # ########################################
4361 # ##### GUI and registration #############
4362 # ########################################
4364 # menu containing all tools
4365 class VIEW3D_MT_edit_mesh_looptools(Menu
):
4366 bl_label
= "LoopTools"
4368 def draw(self
, context
):
4369 layout
= self
.layout
4371 layout
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4372 layout
.operator("mesh.looptools_circle")
4373 layout
.operator("mesh.looptools_curve")
4374 layout
.operator("mesh.looptools_flatten")
4375 layout
.operator("mesh.looptools_gstretch")
4376 layout
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4377 layout
.operator("mesh.looptools_relax")
4378 layout
.operator("mesh.looptools_space")
4381 # panel containing all tools
4382 class VIEW3D_PT_tools_looptools(Panel
):
4383 bl_space_type
= 'VIEW_3D'
4384 bl_region_type
= 'UI'
4385 bl_category
= 'Edit'
4386 bl_context
= "mesh_edit"
4387 bl_label
= "LoopTools"
4388 bl_options
= {'DEFAULT_CLOSED'}
4390 def draw(self
, context
):
4391 layout
= self
.layout
4392 col
= layout
.column(align
=True)
4393 lt
= context
.window_manager
.looptools
4395 # bridge - first line
4396 split
= col
.split(factor
=0.15, align
=True)
4397 if lt
.display_bridge
:
4398 split
.prop(lt
, "display_bridge", text
="", icon
='DOWNARROW_HLT')
4400 split
.prop(lt
, "display_bridge", text
="", icon
='RIGHTARROW')
4401 split
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4403 if lt
.display_bridge
:
4404 box
= col
.column(align
=True).box().column()
4405 # box.prop(self, "mode")
4408 col_top
= box
.column(align
=True)
4409 row
= col_top
.row(align
=True)
4410 col_left
= row
.column(align
=True)
4411 col_right
= row
.column(align
=True)
4412 col_right
.active
= lt
.bridge_segments
!= 1
4413 col_left
.prop(lt
, "bridge_segments")
4414 col_right
.prop(lt
, "bridge_min_width", text
="")
4416 bottom_left
= col_left
.row()
4417 bottom_left
.active
= lt
.bridge_segments
!= 1
4418 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4419 bottom_right
= col_right
.row()
4420 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4421 bottom_right
.prop(lt
, "bridge_cubic_strength")
4422 # boolean properties
4423 col_top
.prop(lt
, "bridge_remove_faces")
4425 # override properties
4427 row
= box
.row(align
=True)
4428 row
.prop(lt
, "bridge_twist")
4429 row
.prop(lt
, "bridge_reverse")
4431 # circle - first line
4432 split
= col
.split(factor
=0.15, align
=True)
4433 if lt
.display_circle
:
4434 split
.prop(lt
, "display_circle", text
="", icon
='DOWNARROW_HLT')
4436 split
.prop(lt
, "display_circle", text
="", icon
='RIGHTARROW')
4437 split
.operator("mesh.looptools_circle")
4439 if lt
.display_circle
:
4440 box
= col
.column(align
=True).box().column()
4441 box
.prop(lt
, "circle_fit")
4444 box
.prop(lt
, "circle_flatten")
4445 row
= box
.row(align
=True)
4446 row
.prop(lt
, "circle_custom_radius")
4447 row_right
= row
.row(align
=True)
4448 row_right
.active
= lt
.circle_custom_radius
4449 row_right
.prop(lt
, "circle_radius", text
="")
4450 box
.prop(lt
, "circle_regular")
4453 col_move
= box
.column(align
=True)
4454 row
= col_move
.row(align
=True)
4455 if lt
.circle_lock_x
:
4456 row
.prop(lt
, "circle_lock_x", text
="X", icon
='LOCKED')
4458 row
.prop(lt
, "circle_lock_x", text
="X", icon
='UNLOCKED')
4459 if lt
.circle_lock_y
:
4460 row
.prop(lt
, "circle_lock_y", text
="Y", icon
='LOCKED')
4462 row
.prop(lt
, "circle_lock_y", text
="Y", icon
='UNLOCKED')
4463 if lt
.circle_lock_z
:
4464 row
.prop(lt
, "circle_lock_z", text
="Z", icon
='LOCKED')
4466 row
.prop(lt
, "circle_lock_z", text
="Z", icon
='UNLOCKED')
4467 col_move
.prop(lt
, "circle_influence")
4469 # curve - first line
4470 split
= col
.split(factor
=0.15, align
=True)
4471 if lt
.display_curve
:
4472 split
.prop(lt
, "display_curve", text
="", icon
='DOWNARROW_HLT')
4474 split
.prop(lt
, "display_curve", text
="", icon
='RIGHTARROW')
4475 split
.operator("mesh.looptools_curve")
4477 if lt
.display_curve
:
4478 box
= col
.column(align
=True).box().column()
4479 box
.prop(lt
, "curve_interpolation")
4480 box
.prop(lt
, "curve_restriction")
4481 box
.prop(lt
, "curve_boundaries")
4482 box
.prop(lt
, "curve_regular")
4485 col_move
= box
.column(align
=True)
4486 row
= col_move
.row(align
=True)
4488 row
.prop(lt
, "curve_lock_x", text
="X", icon
='LOCKED')
4490 row
.prop(lt
, "curve_lock_x", text
="X", icon
='UNLOCKED')
4492 row
.prop(lt
, "curve_lock_y", text
="Y", icon
='LOCKED')
4494 row
.prop(lt
, "curve_lock_y", text
="Y", icon
='UNLOCKED')
4496 row
.prop(lt
, "curve_lock_z", text
="Z", icon
='LOCKED')
4498 row
.prop(lt
, "curve_lock_z", text
="Z", icon
='UNLOCKED')
4499 col_move
.prop(lt
, "curve_influence")
4501 # flatten - first line
4502 split
= col
.split(factor
=0.15, align
=True)
4503 if lt
.display_flatten
:
4504 split
.prop(lt
, "display_flatten", text
="", icon
='DOWNARROW_HLT')
4506 split
.prop(lt
, "display_flatten", text
="", icon
='RIGHTARROW')
4507 split
.operator("mesh.looptools_flatten")
4508 # flatten - settings
4509 if lt
.display_flatten
:
4510 box
= col
.column(align
=True).box().column()
4511 box
.prop(lt
, "flatten_plane")
4512 # box.prop(lt, "flatten_restriction")
4515 col_move
= box
.column(align
=True)
4516 row
= col_move
.row(align
=True)
4517 if lt
.flatten_lock_x
:
4518 row
.prop(lt
, "flatten_lock_x", text
="X", icon
='LOCKED')
4520 row
.prop(lt
, "flatten_lock_x", text
="X", icon
='UNLOCKED')
4521 if lt
.flatten_lock_y
:
4522 row
.prop(lt
, "flatten_lock_y", text
="Y", icon
='LOCKED')
4524 row
.prop(lt
, "flatten_lock_y", text
="Y", icon
='UNLOCKED')
4525 if lt
.flatten_lock_z
:
4526 row
.prop(lt
, "flatten_lock_z", text
="Z", icon
='LOCKED')
4528 row
.prop(lt
, "flatten_lock_z", text
="Z", icon
='UNLOCKED')
4529 col_move
.prop(lt
, "flatten_influence")
4531 # gstretch - first line
4532 split
= col
.split(factor
=0.15, align
=True)
4533 if lt
.display_gstretch
:
4534 split
.prop(lt
, "display_gstretch", text
="", icon
='DOWNARROW_HLT')
4536 split
.prop(lt
, "display_gstretch", text
="", icon
='RIGHTARROW')
4537 split
.operator("mesh.looptools_gstretch")
4539 if lt
.display_gstretch
:
4540 box
= col
.column(align
=True).box().column()
4541 box
.prop(lt
, "gstretch_use_guide")
4542 if lt
.gstretch_use_guide
== "GPencil":
4543 box
.prop(lt
, "gstretch_guide")
4544 box
.prop(lt
, "gstretch_method")
4546 col_conv
= box
.column(align
=True)
4547 col_conv
.prop(lt
, "gstretch_conversion", text
="")
4548 if lt
.gstretch_conversion
== 'distance':
4549 col_conv
.prop(lt
, "gstretch_conversion_distance")
4550 elif lt
.gstretch_conversion
== 'limit_vertices':
4551 row
= col_conv
.row(align
=True)
4552 row
.prop(lt
, "gstretch_conversion_min", text
="Min")
4553 row
.prop(lt
, "gstretch_conversion_max", text
="Max")
4554 elif lt
.gstretch_conversion
== 'vertices':
4555 col_conv
.prop(lt
, "gstretch_conversion_vertices")
4558 col_move
= box
.column(align
=True)
4559 row
= col_move
.row(align
=True)
4560 if lt
.gstretch_lock_x
:
4561 row
.prop(lt
, "gstretch_lock_x", text
="X", icon
='LOCKED')
4563 row
.prop(lt
, "gstretch_lock_x", text
="X", icon
='UNLOCKED')
4564 if lt
.gstretch_lock_y
:
4565 row
.prop(lt
, "gstretch_lock_y", text
="Y", icon
='LOCKED')
4567 row
.prop(lt
, "gstretch_lock_y", text
="Y", icon
='UNLOCKED')
4568 if lt
.gstretch_lock_z
:
4569 row
.prop(lt
, "gstretch_lock_z", text
="Z", icon
='LOCKED')
4571 row
.prop(lt
, "gstretch_lock_z", text
="Z", icon
='UNLOCKED')
4572 col_move
.prop(lt
, "gstretch_influence")
4573 if lt
.gstretch_use_guide
== "Annotation":
4574 box
.operator("remove.annotation", text
="Delete Annotation Strokes")
4575 if lt
.gstretch_use_guide
== "GPencil":
4576 box
.operator("remove.gp", text
="Delete GPencil Strokes")
4579 split
= col
.split(factor
=0.15, align
=True)
4581 split
.prop(lt
, "display_loft", text
="", icon
='DOWNARROW_HLT')
4583 split
.prop(lt
, "display_loft", text
="", icon
='RIGHTARROW')
4584 split
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4587 box
= col
.column(align
=True).box().column()
4588 # box.prop(self, "mode")
4591 col_top
= box
.column(align
=True)
4592 row
= col_top
.row(align
=True)
4593 col_left
= row
.column(align
=True)
4594 col_right
= row
.column(align
=True)
4595 col_right
.active
= lt
.bridge_segments
!= 1
4596 col_left
.prop(lt
, "bridge_segments")
4597 col_right
.prop(lt
, "bridge_min_width", text
="")
4599 bottom_left
= col_left
.row()
4600 bottom_left
.active
= lt
.bridge_segments
!= 1
4601 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4602 bottom_right
= col_right
.row()
4603 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4604 bottom_right
.prop(lt
, "bridge_cubic_strength")
4605 # boolean properties
4606 col_top
.prop(lt
, "bridge_remove_faces")
4607 col_top
.prop(lt
, "bridge_loft_loop")
4609 # override properties
4611 row
= box
.row(align
=True)
4612 row
.prop(lt
, "bridge_twist")
4613 row
.prop(lt
, "bridge_reverse")
4615 # relax - first line
4616 split
= col
.split(factor
=0.15, align
=True)
4617 if lt
.display_relax
:
4618 split
.prop(lt
, "display_relax", text
="", icon
='DOWNARROW_HLT')
4620 split
.prop(lt
, "display_relax", text
="", icon
='RIGHTARROW')
4621 split
.operator("mesh.looptools_relax")
4623 if lt
.display_relax
:
4624 box
= col
.column(align
=True).box().column()
4625 box
.prop(lt
, "relax_interpolation")
4626 box
.prop(lt
, "relax_input")
4627 box
.prop(lt
, "relax_iterations")
4628 box
.prop(lt
, "relax_regular")
4630 # space - first line
4631 split
= col
.split(factor
=0.15, align
=True)
4632 if lt
.display_space
:
4633 split
.prop(lt
, "display_space", text
="", icon
='DOWNARROW_HLT')
4635 split
.prop(lt
, "display_space", text
="", icon
='RIGHTARROW')
4636 split
.operator("mesh.looptools_space")
4638 if lt
.display_space
:
4639 box
= col
.column(align
=True).box().column()
4640 box
.prop(lt
, "space_interpolation")
4641 box
.prop(lt
, "space_input")
4644 col_move
= box
.column(align
=True)
4645 row
= col_move
.row(align
=True)
4647 row
.prop(lt
, "space_lock_x", text
="X", icon
='LOCKED')
4649 row
.prop(lt
, "space_lock_x", text
="X", icon
='UNLOCKED')
4651 row
.prop(lt
, "space_lock_y", text
="Y", icon
='LOCKED')
4653 row
.prop(lt
, "space_lock_y", text
="Y", icon
='UNLOCKED')
4655 row
.prop(lt
, "space_lock_z", text
="Z", icon
='LOCKED')
4657 row
.prop(lt
, "space_lock_z", text
="Z", icon
='UNLOCKED')
4658 col_move
.prop(lt
, "space_influence")
4661 # property group containing all properties for the gui in the panel
4662 class LoopToolsProps(PropertyGroup
):
4664 Fake module like class
4665 bpy.context.window_manager.looptools
4667 # general display properties
4668 display_bridge
: BoolProperty(
4669 name
="Bridge settings",
4670 description
="Display settings of the Bridge tool",
4673 display_circle
: BoolProperty(
4674 name
="Circle settings",
4675 description
="Display settings of the Circle tool",
4678 display_curve
: BoolProperty(
4679 name
="Curve settings",
4680 description
="Display settings of the Curve tool",
4683 display_flatten
: BoolProperty(
4684 name
="Flatten settings",
4685 description
="Display settings of the Flatten tool",
4688 display_gstretch
: BoolProperty(
4689 name
="Gstretch settings",
4690 description
="Display settings of the Gstretch tool",
4693 display_loft
: BoolProperty(
4694 name
="Loft settings",
4695 description
="Display settings of the Loft tool",
4698 display_relax
: BoolProperty(
4699 name
="Relax settings",
4700 description
="Display settings of the Relax tool",
4703 display_space
: BoolProperty(
4704 name
="Space settings",
4705 description
="Display settings of the Space tool",
4710 bridge_cubic_strength
: FloatProperty(
4712 description
="Higher strength results in more fluid curves",
4717 bridge_interpolation
: EnumProperty(
4718 name
="Interpolation mode",
4719 items
=(('cubic', "Cubic", "Gives curved results"),
4720 ('linear', "Linear", "Basic, fast, straight interpolation")),
4721 description
="Interpolation mode: algorithm used when creating segments",
4724 bridge_loft
: BoolProperty(
4726 description
="Loft multiple loops, instead of considering them as "
4727 "a multi-input for bridging",
4730 bridge_loft_loop
: BoolProperty(
4732 description
="Connect the first and the last loop with each other",
4735 bridge_min_width
: IntProperty(
4736 name
="Minimum width",
4737 description
="Segments with an edge smaller than this are merged "
4738 "(compared to base edge)",
4742 subtype
='PERCENTAGE'
4744 bridge_mode
: EnumProperty(
4746 items
=(('basic', "Basic", "Fast algorithm"),
4747 ('shortest', "Shortest edge", "Slower algorithm with "
4748 "better vertex matching")),
4749 description
="Algorithm used for bridging",
4752 bridge_remove_faces
: BoolProperty(
4753 name
="Remove faces",
4754 description
="Remove faces that are internal after bridging",
4757 bridge_reverse
: BoolProperty(
4759 description
="Manually override the direction in which the loops "
4760 "are bridged. Only use if the tool gives the wrong result",
4763 bridge_segments
: IntProperty(
4765 description
="Number of segments used to bridge the gap (0=automatic)",
4770 bridge_twist
: IntProperty(
4772 description
="Twist what vertices are connected to each other",
4777 circle_custom_radius
: BoolProperty(
4779 description
="Force a custom radius",
4782 circle_fit
: EnumProperty(
4784 items
=(("best", "Best fit", "Non-linear least squares"),
4785 ("inside", "Fit inside", "Only move vertices towards the center")),
4786 description
="Method used for fitting a circle to the vertices",
4789 circle_flatten
: BoolProperty(
4791 description
="Flatten the circle, instead of projecting it on the mesh",
4794 circle_influence
: FloatProperty(
4796 description
="Force of the tool",
4801 subtype
='PERCENTAGE'
4803 circle_lock_x
: BoolProperty(
4805 description
="Lock editing of the x-coordinate",
4808 circle_lock_y
: BoolProperty(
4810 description
="Lock editing of the y-coordinate",
4813 circle_lock_z
: BoolProperty(
4815 description
="Lock editing of the z-coordinate",
4818 circle_radius
: FloatProperty(
4820 description
="Custom radius for circle",
4825 circle_regular
: BoolProperty(
4827 description
="Distribute vertices at constant distances along the circle",
4831 curve_boundaries
: BoolProperty(
4833 description
="Limit the tool to work within the boundaries of the "
4834 "selected vertices",
4837 curve_influence
: FloatProperty(
4839 description
="Force of the tool",
4844 subtype
='PERCENTAGE'
4846 curve_interpolation
: EnumProperty(
4847 name
="Interpolation",
4848 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4849 ("linear", "Linear", "Simple and fast linear algorithm")),
4850 description
="Algorithm used for interpolation",
4853 curve_lock_x
: BoolProperty(
4855 description
="Lock editing of the x-coordinate",
4858 curve_lock_y
: BoolProperty(
4860 description
="Lock editing of the y-coordinate",
4863 curve_lock_z
: BoolProperty(
4865 description
="Lock editing of the z-coordinate",
4868 curve_regular
: BoolProperty(
4870 description
="Distribute vertices at constant distances along the curve",
4873 curve_restriction
: EnumProperty(
4875 items
=(("none", "None", "No restrictions on vertex movement"),
4876 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
4877 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
4878 description
="Restrictions on how the vertices can be moved",
4882 # flatten properties
4883 flatten_influence
: FloatProperty(
4885 description
="Force of the tool",
4890 subtype
='PERCENTAGE'
4892 flatten_lock_x
: BoolProperty(
4894 description
="Lock editing of the x-coordinate",
4896 flatten_lock_y
: BoolProperty(name
="Lock Y",
4897 description
="Lock editing of the y-coordinate",
4900 flatten_lock_z
: BoolProperty(
4902 description
="Lock editing of the z-coordinate",
4905 flatten_plane
: EnumProperty(
4907 items
=(("best_fit", "Best fit", "Calculate a best fitting plane"),
4908 ("normal", "Normal", "Derive plane from averaging vertex "
4910 ("view", "View", "Flatten on a plane perpendicular to the "
4912 description
="Plane on which vertices are flattened",
4915 flatten_restriction
: EnumProperty(
4917 items
=(("none", "None", "No restrictions on vertex movement"),
4918 ("bounding_box", "Bounding box", "Vertices are restricted to "
4919 "movement inside the bounding box of the selection")),
4920 description
="Restrictions on how the vertices can be moved",
4924 # gstretch properties
4925 gstretch_conversion
: EnumProperty(
4927 items
=(("distance", "Distance", "Set the distance between vertices "
4928 "of the converted stroke"),
4929 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
4930 "number of vertices that converted GP strokes will have"),
4931 ("vertices", "Exact vertices", "Set the exact number of vertices "
4932 "that converted strokes will have. Short strokes "
4933 "with few points may contain less vertices than this number."),
4934 ("none", "No simplification", "Convert each point "
4936 description
="If strokes are converted to geometry, "
4937 "use this simplification method",
4938 default
='limit_vertices'
4940 gstretch_conversion_distance
: FloatProperty(
4942 description
="Absolute distance between vertices along the converted "
4949 gstretch_conversion_max
: IntProperty(
4950 name
="Max Vertices",
4951 description
="Maximum number of vertices strokes will "
4952 "have, when they are converted to geomtery",
4956 update
=gstretch_update_min
4958 gstretch_conversion_min
: IntProperty(
4959 name
="Min Vertices",
4960 description
="Minimum number of vertices strokes will "
4961 "have, when they are converted to geomtery",
4965 update
=gstretch_update_max
4967 gstretch_conversion_vertices
: IntProperty(
4969 description
="Number of vertices strokes will "
4970 "have, when they are converted to geometry. If strokes have less "
4971 "points than required, the 'Spread evenly' method is used",
4976 gstretch_delete_strokes
: BoolProperty(
4977 name
="Delete strokes",
4978 description
="Remove Grease Pencil strokes if they have been used "
4979 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
4982 gstretch_influence
: FloatProperty(
4984 description
="Force of the tool",
4989 subtype
='PERCENTAGE'
4991 gstretch_lock_x
: BoolProperty(
4993 description
="Lock editing of the x-coordinate",
4996 gstretch_lock_y
: BoolProperty(
4998 description
="Lock editing of the y-coordinate",
5001 gstretch_lock_z
: BoolProperty(
5003 description
="Lock editing of the z-coordinate",
5006 gstretch_method
: EnumProperty(
5008 items
=(("project", "Project", "Project vertices onto the stroke, "
5009 "using vertex normals and connected edges"),
5010 ("irregular", "Spread", "Distribute vertices along the full "
5011 "stroke, retaining relative distances between the vertices"),
5012 ("regular", "Spread evenly", "Distribute vertices at regular "
5013 "distances along the full stroke")),
5014 description
="Method of distributing the vertices over the Grease "
5018 gstretch_use_guide
: EnumProperty(
5020 items
=(("None", "None", "None"),
5021 ("Annotation", "Annotation", "Annotation"),
5022 ("GPencil", "GPencil", "GPencil")),
5025 gstretch_guide
: PointerProperty(
5026 name
="GPencil object",
5027 description
="Set GPencil object",
5028 type=bpy
.types
.Object
5032 relax_input
: EnumProperty(name
="Input",
5033 items
=(("all", "Parallel (all)", "Also use non-selected "
5034 "parallel loops as input"),
5035 ("selected", "Selection", "Only use selected vertices as input")),
5036 description
="Loops that are relaxed",
5039 relax_interpolation
: EnumProperty(
5040 name
="Interpolation",
5041 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5042 ("linear", "Linear", "Simple and fast linear algorithm")),
5043 description
="Algorithm used for interpolation",
5046 relax_iterations
: EnumProperty(name
="Iterations",
5047 items
=(("1", "1", "One"),
5048 ("3", "3", "Three"),
5050 ("10", "10", "Ten"),
5051 ("25", "25", "Twenty-five")),
5052 description
="Number of times the loop is relaxed",
5055 relax_regular
: BoolProperty(
5057 description
="Distribute vertices at constant distances along the loop",
5062 space_influence
: FloatProperty(
5064 description
="Force of the tool",
5069 subtype
='PERCENTAGE'
5071 space_input
: EnumProperty(
5073 items
=(("all", "Parallel (all)", "Also use non-selected "
5074 "parallel loops as input"),
5075 ("selected", "Selection", "Only use selected vertices as input")),
5076 description
="Loops that are spaced",
5079 space_interpolation
: EnumProperty(
5080 name
="Interpolation",
5081 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5082 ("linear", "Linear", "Vertices are projected on existing edges")),
5083 description
="Algorithm used for interpolation",
5086 space_lock_x
: BoolProperty(
5088 description
="Lock editing of the x-coordinate",
5091 space_lock_y
: BoolProperty(
5093 description
="Lock editing of the y-coordinate",
5096 space_lock_z
: BoolProperty(
5098 description
="Lock editing of the z-coordinate",
5102 # draw function for integration in menus
5103 def menu_func(self
, context
):
5104 self
.layout
.menu("VIEW3D_MT_edit_mesh_looptools")
5105 self
.layout
.separator()
5108 # Add-ons Preferences Update Panel
5110 # Define Panel classes for updating
5112 VIEW3D_PT_tools_looptools
,
5116 def update_panel(self
, context
):
5117 message
= "LoopTools: Updating Panel locations has failed"
5119 for panel
in panels
:
5120 if "bl_rna" in panel
.__dict
__:
5121 bpy
.utils
.unregister_class(panel
)
5123 for panel
in panels
:
5124 panel
.bl_category
= context
.preferences
.addons
[__name__
].preferences
.category
5125 bpy
.utils
.register_class(panel
)
5127 except Exception as e
:
5128 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
5132 class LoopPreferences(AddonPreferences
):
5133 # this must match the addon name, use '__package__'
5134 # when defining this in a submodule of a python package.
5135 bl_idname
= __name__
5137 category
: StringProperty(
5138 name
="Tab Category",
5139 description
="Choose a name for the category of the panel",
5144 def draw(self
, context
):
5145 layout
= self
.layout
5149 col
.label(text
="Tab Category:")
5150 col
.prop(self
, "category", text
="")
5153 # define classes for registration
5155 VIEW3D_MT_edit_mesh_looptools
,
5156 VIEW3D_PT_tools_looptools
,
5171 # registering and menu integration
5174 bpy
.utils
.register_class(cls
)
5175 bpy
.types
.VIEW3D_MT_edit_mesh_context_menu
.prepend(menu_func
)
5176 bpy
.types
.WindowManager
.looptools
= PointerProperty(type=LoopToolsProps
)
5177 update_panel(None, bpy
.context
)
5180 # unregistering and removing menus
5182 for cls
in reversed(classes
):
5183 bpy
.utils
.unregister_class(cls
)
5184 bpy
.types
.VIEW3D_MT_edit_mesh_context_menu
.remove(menu_func
)
5186 del bpy
.types
.WindowManager
.looptools
5187 except Exception as e
:
5188 print('unregister fail:\n', e
)
5192 if __name__
== "__main__":