1 # SPDX-FileCopyrightText: 2011-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # Maintainer: Vladimir Spivak (cwolf3d)
6 # Originally an addon by Bart Crouch
10 "author": "Bart Crouch, Vladimir Spivak (cwolf3d)",
12 "blender": (2, 80, 0),
13 "location": "View3D > Sidebar > Edit Tab / Edit Mode Context Menu",
15 "description": "Mesh modelling toolkit. Several tools to aid modelling",
16 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/looptools.html",
26 from bpy_extras
import view3d_utils
27 from bpy
.types
import (
34 from bpy
.props
import (
43 # ########################################
44 # ##### General functions ################
45 # ########################################
47 # used by all tools to improve speed on reruns Unlink
51 def get_strokes(self
, context
):
52 looptools
= context
.window_manager
.looptools
53 if looptools
.gstretch_use_guide
== "Annotation":
55 strokes
= bpy
.data
.grease_pencils
[0].layers
.active
.active_frame
.strokes
58 self
.report({'WARNING'}, "active Annotation strokes not found")
60 if looptools
.gstretch_use_guide
== "GPencil" and not looptools
.gstretch_guide
== None:
62 strokes
= looptools
.gstretch_guide
.data
.layers
.active
.active_frame
.strokes
65 self
.report({'WARNING'}, "active GPencil strokes not found")
70 # force a full recalculation next time
71 def cache_delete(tool
):
72 if tool
in looptools_cache
:
73 del looptools_cache
[tool
]
76 # check cache for stored information
77 def cache_read(tool
, object, bm
, input_method
, boundaries
):
78 # current tool not cached yet
79 if tool
not in looptools_cache
:
80 return(False, False, False, False, False)
81 # check if selected object didn't change
82 if object.name
!= looptools_cache
[tool
]["object"]:
83 return(False, False, False, False, False)
84 # check if input didn't change
85 if input_method
!= looptools_cache
[tool
]["input_method"]:
86 return(False, False, False, False, False)
87 if boundaries
!= looptools_cache
[tool
]["boundaries"]:
88 return(False, False, False, False, False)
89 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
and
91 if modifiers
!= looptools_cache
[tool
]["modifiers"]:
92 return(False, False, False, False, False)
93 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
94 if input != looptools_cache
[tool
]["input"]:
95 return(False, False, False, False, False)
97 single_loops
= looptools_cache
[tool
]["single_loops"]
98 loops
= looptools_cache
[tool
]["loops"]
99 derived
= looptools_cache
[tool
]["derived"]
100 mapping
= looptools_cache
[tool
]["mapping"]
102 return(True, single_loops
, loops
, derived
, mapping
)
105 # store information in the cache
106 def cache_write(tool
, object, bm
, input_method
, boundaries
, single_loops
,
107 loops
, derived
, mapping
):
108 # clear cache of current tool
109 if tool
in looptools_cache
:
110 del looptools_cache
[tool
]
111 # prepare values to be saved to cache
112 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
113 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
114 and mod
.type == 'MIRROR']
116 looptools_cache
[tool
] = {
117 "input": input, "object": object.name
,
118 "input_method": input_method
, "boundaries": boundaries
,
119 "single_loops": single_loops
, "loops": loops
,
120 "derived": derived
, "mapping": mapping
, "modifiers": modifiers
}
123 # calculates natural cubic splines through all given knots
124 def calculate_cubic_splines(bm_mod
, tknots
, knots
):
125 # hack for circular loops
126 if knots
[0] == knots
[-1] and len(knots
) > 1:
129 for k
in range(-1, -5, -1):
130 if k
- 1 < -len(knots
):
132 k_new1
.append(knots
[k
- 1])
135 if k
+ 1 > len(knots
) - 1:
137 k_new2
.append(knots
[k
+ 1])
144 for t
in range(-1, -5, -1):
145 if t
- 1 < -len(tknots
):
147 total1
+= tknots
[t
] - tknots
[t
- 1]
148 t_new1
.append(tknots
[0] - total1
)
152 if t
+ 1 > len(tknots
) - 1:
154 total2
+= tknots
[t
+ 1] - tknots
[t
]
155 t_new2
.append(tknots
[-1] + total2
)
168 locs
= [bm_mod
.verts
[k
].co
[:] for k
in knots
]
175 for i
in range(n
- 1):
176 if x
[i
+ 1] - x
[i
] == 0:
179 h
.append(x
[i
+ 1] - x
[i
])
181 for i
in range(1, n
- 1):
182 q
.append(3 / h
[i
] * (a
[i
+ 1] - a
[i
]) - 3 / h
[i
- 1] * (a
[i
] - a
[i
- 1]))
186 for i
in range(1, n
- 1):
187 l
.append(2 * (x
[i
+ 1] - x
[i
- 1]) - h
[i
- 1] * u
[i
- 1])
190 u
.append(h
[i
] / l
[i
])
191 z
.append((q
[i
] - h
[i
- 1] * z
[i
- 1]) / l
[i
])
194 b
= [False for i
in range(n
- 1)]
195 c
= [False for i
in range(n
)]
196 d
= [False for i
in range(n
- 1)]
198 for i
in range(n
- 2, -1, -1):
199 c
[i
] = z
[i
] - u
[i
] * c
[i
+ 1]
200 b
[i
] = (a
[i
+ 1] - a
[i
]) / h
[i
] - h
[i
] * (c
[i
+ 1] + 2 * c
[i
]) / 3
201 d
[i
] = (c
[i
+ 1] - c
[i
]) / (3 * h
[i
])
202 for i
in range(n
- 1):
203 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
205 for i
in range(len(knots
) - 1):
206 splines
.append([result
[i
], result
[i
+ n
- 1], result
[i
+ (n
- 1) * 2]])
207 if circular
: # cleaning up after hack
209 tknots
= tknots
[4:-4]
214 # calculates linear splines through all given knots
215 def calculate_linear_splines(bm_mod
, tknots
, knots
):
217 for i
in range(len(knots
) - 1):
218 a
= bm_mod
.verts
[knots
[i
]].co
219 b
= bm_mod
.verts
[knots
[i
+ 1]].co
222 u
= tknots
[i
+ 1] - t
223 splines
.append([a
, d
, t
, u
]) # [locStart, locDif, tStart, tDif]
228 # calculate a best-fit plane to the given vertices
229 def calculate_plane(bm_mod
, loop
, method
="best_fit", object=False):
230 # getting the vertex locations
231 locs
= [bm_mod
.verts
[v
].co
.copy() for v
in loop
[0]]
233 # calculating the center of masss
234 com
= mathutils
.Vector()
240 if method
== 'best_fit':
241 # creating the covariance matrix
242 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
247 mat
[0][0] += (loc
[0] - x
) ** 2
248 mat
[1][0] += (loc
[0] - x
) * (loc
[1] - y
)
249 mat
[2][0] += (loc
[0] - x
) * (loc
[2] - z
)
250 mat
[0][1] += (loc
[1] - y
) * (loc
[0] - x
)
251 mat
[1][1] += (loc
[1] - y
) ** 2
252 mat
[2][1] += (loc
[1] - y
) * (loc
[2] - z
)
253 mat
[0][2] += (loc
[2] - z
) * (loc
[0] - x
)
254 mat
[1][2] += (loc
[2] - z
) * (loc
[1] - y
)
255 mat
[2][2] += (loc
[2] - z
) ** 2
257 # calculating the normal to the plane
260 mat
= matrix_invert(mat
)
263 if math
.fabs(sum(mat
[0])) < math
.fabs(sum(mat
[1])):
264 if math
.fabs(sum(mat
[0])) < math
.fabs(sum(mat
[2])):
266 elif math
.fabs(sum(mat
[1])) < math
.fabs(sum(mat
[2])):
269 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
271 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
273 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
275 # warning! this is different from .normalize()
277 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
278 for i
in range(itermax
):
281 # Calculate length with double precision to avoid problems with `inf`
282 vec2_length
= math
.sqrt(vec2
[0] ** 2 + vec2
[1] ** 2 + vec2
[2] ** 2)
288 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
291 elif method
== 'normal':
292 # averaging the vertex normals
293 v_normals
= [bm_mod
.verts
[v
].normal
for v
in loop
[0]]
294 normal
= mathutils
.Vector()
295 for v_normal
in v_normals
:
297 normal
/= len(v_normals
)
300 elif method
== 'view':
301 # calculate view normal
302 rotation
= bpy
.context
.space_data
.region_3d
.view_matrix
.to_3x3().\
304 normal
= rotation
@ mathutils
.Vector((0.0, 0.0, 1.0))
306 normal
= object.matrix_world
.inverted().to_euler().to_matrix() @ \
312 # calculate splines based on given interpolation method (controller function)
313 def calculate_splines(interpolation
, bm_mod
, tknots
, knots
):
314 if interpolation
== 'cubic':
315 splines
= calculate_cubic_splines(bm_mod
, tknots
, knots
[:])
316 else: # interpolations == 'linear'
317 splines
= calculate_linear_splines(bm_mod
, tknots
, knots
[:])
322 # check loops and only return valid ones
323 def check_loops(loops
, mapping
, bm_mod
):
325 for loop
, circular
in loops
:
326 # loop needs to have at least 3 vertices
329 # loop needs at least 1 vertex in the original, non-mirrored mesh
333 if mapping
[vert
] > -1:
338 # vertices can not all be at the same location
340 for i
in range(len(loop
) - 1):
341 if (bm_mod
.verts
[loop
[i
]].co
- bm_mod
.verts
[loop
[i
+ 1]].co
).length
> 1e-6:
346 # passed all tests, loop is valid
347 valid_loops
.append([loop
, circular
])
352 # input: bmesh, output: dict with the edge-key as key and face-index as value
353 def dict_edge_faces(bm
):
354 edge_faces
= dict([[edgekey(edge
), []] for edge
in bm
.edges
if not edge
.hide
])
355 for face
in bm
.faces
:
358 for key
in face_edgekeys(face
):
359 edge_faces
[key
].append(face
.index
)
364 # input: bmesh (edge-faces optional), output: dict with face-face connections
365 def dict_face_faces(bm
, edge_faces
=False):
367 edge_faces
= dict_edge_faces(bm
)
369 connected_faces
= dict([[face
.index
, []] for face
in bm
.faces
if not face
.hide
])
370 for face
in bm
.faces
:
373 for edge_key
in face_edgekeys(face
):
374 for connected_face
in edge_faces
[edge_key
]:
375 if connected_face
== face
.index
:
377 connected_faces
[face
.index
].append(connected_face
)
379 return(connected_faces
)
382 # input: bmesh, output: dict with the vert index as key and edge-keys as value
383 def dict_vert_edges(bm
):
384 vert_edges
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
385 for edge
in bm
.edges
:
390 vert_edges
[vert
].append(ek
)
395 # input: bmesh, output: dict with the vert index as key and face index as value
396 def dict_vert_faces(bm
):
397 vert_faces
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
398 for face
in bm
.faces
:
400 for vert
in face
.verts
:
401 vert_faces
[vert
.index
].append(face
.index
)
406 # input: list of edge-keys, output: dictionary with vertex-vertex connections
407 def dict_vert_verts(edge_keys
):
408 # create connection data
412 if ek
[i
] in vert_verts
:
413 vert_verts
[ek
[i
]].append(ek
[1 - i
])
415 vert_verts
[ek
[i
]] = [ek
[1 - i
]]
420 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
422 return(tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])))
425 # returns the edgekeys of a bmesh face
426 def face_edgekeys(face
):
427 return([tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])) for edge
in face
.edges
])
430 # calculate input loops
431 def get_connected_input(object, bm
, not_use_mirror
, input):
432 # get mesh with modifiers applied
433 derived
, bm_mod
= get_derived_bmesh(object, bm
, not_use_mirror
)
435 # calculate selected loops
436 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and not edge
.hide
]
437 loops
= get_connected_selections(edge_keys
)
439 # if only selected loops are needed, we're done
440 if input == 'selected':
441 return(derived
, bm_mod
, loops
)
442 # elif input == 'all':
443 loops
= get_parallel_loops(bm_mod
, loops
)
445 return(derived
, bm_mod
, loops
)
448 # sorts all edge-keys into a list of loops
449 def get_connected_selections(edge_keys
):
450 # create connection data
451 vert_verts
= dict_vert_verts(edge_keys
)
453 # find loops consisting of connected selected edges
455 while len(vert_verts
) > 0:
456 loop
= [iter(vert_verts
.keys()).__next
__()]
462 # no more connection data for current vertex
463 if loop
[-1] not in vert_verts
:
471 for i
, next_vert
in enumerate(vert_verts
[loop
[-1]]):
472 if next_vert
not in loop
:
473 vert_verts
[loop
[-1]].pop(i
)
474 if len(vert_verts
[loop
[-1]]) == 0:
475 del vert_verts
[loop
[-1]]
476 # remove connection both ways
477 if next_vert
in vert_verts
:
478 if len(vert_verts
[next_vert
]) == 1:
479 del vert_verts
[next_vert
]
481 vert_verts
[next_vert
].remove(loop
[-1])
482 loop
.append(next_vert
)
486 # found one end of the loop, continue with next
490 # found both ends of the loop, stop growing
494 # check if loop is circular
495 if loop
[0] in vert_verts
:
496 if loop
[-1] in vert_verts
[loop
[0]]:
498 if len(vert_verts
[loop
[0]]) == 1:
499 del vert_verts
[loop
[0]]
501 vert_verts
[loop
[0]].remove(loop
[-1])
502 if len(vert_verts
[loop
[-1]]) == 1:
503 del vert_verts
[loop
[-1]]
505 vert_verts
[loop
[-1]].remove(loop
[0])
519 # get the derived mesh data, if there is a mirror modifier
520 def get_derived_bmesh(object, bm
, not_use_mirror
):
521 # check for mirror modifiers
522 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
524 # disable other modifiers
525 show_viewport
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
]
527 for mod
in object.modifiers
:
528 if mod
.type != 'MIRROR':
529 mod
.show_viewport
= False
530 #leave the merge points untouched
531 if mod
.type == 'MIRROR':
532 merge
.append(mod
.use_mirror_merge
)
534 mod
.use_mirror_merge
= False
537 depsgraph
= bpy
.context
.evaluated_depsgraph_get()
538 object_eval
= object.evaluated_get(depsgraph
)
539 mesh_mod
= object_eval
.to_mesh()
540 bm_mod
.from_mesh(mesh_mod
)
541 object_eval
.to_mesh_clear()
542 # re-enable other modifiers
543 for mod_name
in show_viewport
:
544 object.modifiers
[mod_name
].show_viewport
= True
546 for mod
in object.modifiers
:
547 if mod
.type == 'MIRROR':
548 mod
.use_mirror_merge
= merge
.pop()
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()
793 # get all mirror vectors
795 if object.data
.use_mirror_x
:
796 mirror_Vectors
.append(mathutils
.Vector((-1, 1, 1)))
797 if object.data
.use_mirror_y
:
798 mirror_Vectors
.append(mathutils
.Vector((1, -1, 1)))
799 if object.data
.use_mirror_x
and object.data
.use_mirror_y
:
800 mirror_Vectors
.append(mathutils
.Vector((-1, -1, 1)))
801 z_mirror_Vectors
= []
802 if object.data
.use_mirror_z
:
803 for v
in mirror_Vectors
:
804 z_mirror_Vectors
.append(mathutils
.Vector((1, 1, -1)) * v
)
805 mirror_Vectors
.extend(z_mirror_Vectors
)
806 mirror_Vectors
.append(mathutils
.Vector((1, 1, -1)))
809 for index
, loc
in loop
:
811 if mapping
[index
] == -1:
814 index
= mapping
[index
]
816 delta
= (loc
- bm
.verts
[index
].co
) @ mat_inv
824 loc
= bm
.verts
[index
].co
+ delta
828 new_loc
= loc
* (influence
/ 100) + \
829 bm
.verts
[index
].co
* ((100 - influence
) / 100)
831 for mirror_Vector
in mirror_Vectors
:
832 for vert
in bm
.verts
:
833 if vert
.co
== mirror_Vector
* bm
.verts
[index
].co
:
834 vert
.co
= mirror_Vector
* new_loc
836 bm
.verts
[index
].co
= new_loc
841 bm
.verts
.ensure_lookup_table()
842 bm
.edges
.ensure_lookup_table()
843 bm
.faces
.ensure_lookup_table()
846 # load custom tool settings
847 def settings_load(self
):
848 lt
= bpy
.context
.window_manager
.looptools
849 tool
= self
.name
.split()[0].lower()
850 keys
= self
.as_keywords().keys()
852 setattr(self
, key
, getattr(lt
, tool
+ "_" + key
))
855 # store custom tool settings
856 def settings_write(self
):
857 lt
= bpy
.context
.window_manager
.looptools
858 tool
= self
.name
.split()[0].lower()
859 keys
= self
.as_keywords().keys()
861 setattr(lt
, tool
+ "_" + key
, getattr(self
, key
))
864 # clean up and set settings back to original state
866 # update editmesh cached data
867 obj
= bpy
.context
.active_object
868 if obj
.mode
== 'EDIT':
869 bmesh
.update_edit_mesh(obj
.data
, loop_triangles
=True, destructive
=True)
872 # ########################################
873 # ##### Bridge functions #################
874 # ########################################
876 # calculate a cubic spline through the middle section of 4 given coordinates
877 def bridge_calculate_cubic_spline(bm
, coordinates
):
883 for i
in coordinates
:
884 a
.append(float(i
[j
]))
887 h
.append(x
[i
+ 1] - x
[i
])
889 for i
in range(1, 3):
890 q
.append(3.0 / h
[i
] * (a
[i
+ 1] - a
[i
]) - 3.0 / h
[i
- 1] * (a
[i
] - a
[i
- 1]))
894 for i
in range(1, 3):
895 l
.append(2.0 * (x
[i
+ 1] - x
[i
- 1]) - h
[i
- 1] * u
[i
- 1])
896 u
.append(h
[i
] / l
[i
])
897 z
.append((q
[i
] - h
[i
- 1] * z
[i
- 1]) / l
[i
])
900 b
= [False for i
in range(3)]
901 c
= [False for i
in range(4)]
902 d
= [False for i
in range(3)]
904 for i
in range(2, -1, -1):
905 c
[i
] = z
[i
] - u
[i
] * c
[i
+ 1]
906 b
[i
] = (a
[i
+ 1] - a
[i
]) / h
[i
] - h
[i
] * (c
[i
+ 1] + 2.0 * c
[i
]) / 3.0
907 d
[i
] = (c
[i
+ 1] - c
[i
]) / (3.0 * h
[i
])
909 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
910 spline
= [result
[1], result
[4], result
[7]]
915 # return a list with new vertex location vectors, a list with face vertex
916 # integers, and the highest vertex integer in the virtual mesh
917 def bridge_calculate_geometry(bm
, lines
, vertex_normals
, segments
,
918 interpolation
, cubic_strength
, min_width
, max_vert_index
):
922 # calculate location based on interpolation method
923 def get_location(line
, segment
, splines
):
924 v1
= bm
.verts
[lines
[line
][0]].co
925 v2
= bm
.verts
[lines
[line
][1]].co
926 if interpolation
== 'linear':
927 return v1
+ (segment
/ segments
) * (v2
- v1
)
928 else: # interpolation == 'cubic'
929 m
= (segment
/ segments
)
930 ax
, bx
, cx
, dx
, tx
= splines
[line
][0]
931 x
= ax
+ bx
* m
+ cx
* m
** 2 + dx
* m
** 3
932 ay
, by
, cy
, dy
, ty
= splines
[line
][1]
933 y
= ay
+ by
* m
+ cy
* m
** 2 + dy
* m
** 3
934 az
, bz
, cz
, dz
, tz
= splines
[line
][2]
935 z
= az
+ bz
* m
+ cz
* m
** 2 + dz
* m
** 3
936 return mathutils
.Vector((x
, y
, z
))
938 # no interpolation needed
940 for i
, line
in enumerate(lines
):
941 if i
< len(lines
) - 1:
942 faces
.append([line
[0], lines
[i
+ 1][0], lines
[i
+ 1][1], line
[1]])
943 # more than 1 segment, interpolate
945 # calculate splines (if necessary) once, so no recalculations needed
946 if interpolation
== 'cubic':
949 v1
= bm
.verts
[line
[0]].co
950 v2
= bm
.verts
[line
[1]].co
951 size
= (v2
- v1
).length
* cubic_strength
952 splines
.append(bridge_calculate_cubic_spline(bm
,
953 [v1
+ size
* vertex_normals
[line
[0]], v1
, v2
,
954 v2
+ size
* vertex_normals
[line
[1]]]))
958 # create starting situation
959 virtual_width
= [(bm
.verts
[lines
[i
][0]].co
-
960 bm
.verts
[lines
[i
+ 1][0]].co
).length
for i
961 in range(len(lines
) - 1)]
962 new_verts
= [get_location(0, seg
, splines
) for seg
in range(1,
964 first_line_indices
= [i
for i
in range(max_vert_index
+ 1,
965 max_vert_index
+ segments
)]
967 prev_verts
= new_verts
[:] # vertex locations of verts on previous line
968 prev_vert_indices
= first_line_indices
[:]
969 max_vert_index
+= segments
- 1 # highest vertex index in virtual mesh
970 next_verts
= [] # vertex locations of verts on current line
971 next_vert_indices
= []
973 for i
, line
in enumerate(lines
):
974 if i
< len(lines
) - 1:
978 for seg
in range(1, segments
):
979 loc1
= prev_verts
[seg
- 1]
980 loc2
= get_location(i
+ 1, seg
, splines
)
981 if (loc1
- loc2
).length
< (min_width
/ 100) * virtual_width
[i
] \
982 and line
[1] == lines
[i
+ 1][1]:
983 # triangle, no new vertex
984 faces
.append([v1
, v2
, prev_vert_indices
[seg
- 1],
985 prev_vert_indices
[seg
- 1]])
986 next_verts
+= prev_verts
[seg
- 1:]
987 next_vert_indices
+= prev_vert_indices
[seg
- 1:]
991 if i
== len(lines
) - 2 and lines
[0] == lines
[-1]:
992 # quad with first line, no new vertex
993 faces
.append([v1
, v2
, first_line_indices
[seg
- 1],
994 prev_vert_indices
[seg
- 1]])
995 v2
= first_line_indices
[seg
- 1]
996 v1
= prev_vert_indices
[seg
- 1]
998 # quad, add new vertex
1000 faces
.append([v1
, v2
, max_vert_index
,
1001 prev_vert_indices
[seg
- 1]])
1003 v1
= prev_vert_indices
[seg
- 1]
1004 new_verts
.append(loc2
)
1005 next_verts
.append(loc2
)
1006 next_vert_indices
.append(max_vert_index
)
1008 faces
.append([v1
, v2
, lines
[i
+ 1][1], line
[1]])
1010 prev_verts
= next_verts
[:]
1011 prev_vert_indices
= next_vert_indices
[:]
1013 next_vert_indices
= []
1015 return(new_verts
, faces
, max_vert_index
)
1018 # calculate lines (list of lists, vertex indices) that are used for bridging
1019 def bridge_calculate_lines(bm
, loops
, mode
, twist
, reverse
):
1021 loop1
, loop2
= [i
[0] for i
in loops
]
1022 loop1_circular
, loop2_circular
= [i
[1] for i
in loops
]
1023 circular
= loop1_circular
or loop2_circular
1026 # calculate loop centers
1028 for loop
in [loop1
, loop2
]:
1029 center
= mathutils
.Vector()
1031 center
+= bm
.verts
[vertex
].co
1033 centers
.append(center
)
1034 for i
, loop
in enumerate([loop1
, loop2
]):
1036 if bm
.verts
[vertex
].co
== centers
[i
]:
1037 # prevent zero-length vectors in angle comparisons
1038 centers
[i
] += mathutils
.Vector((0.01, 0, 0))
1040 center1
, center2
= centers
1042 # calculate the normals of the virtual planes that the loops are on
1044 normal_plurity
= False
1045 for i
, loop
in enumerate([loop1
, loop2
]):
1047 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1050 x
, y
, z
= centers
[i
]
1051 for loc
in [bm
.verts
[vertex
].co
for vertex
in loop
]:
1052 mat
[0][0] += (loc
[0] - x
) ** 2
1053 mat
[1][0] += (loc
[0] - x
) * (loc
[1] - y
)
1054 mat
[2][0] += (loc
[0] - x
) * (loc
[2] - z
)
1055 mat
[0][1] += (loc
[1] - y
) * (loc
[0] - x
)
1056 mat
[1][1] += (loc
[1] - y
) ** 2
1057 mat
[2][1] += (loc
[1] - y
) * (loc
[2] - z
)
1058 mat
[0][2] += (loc
[2] - z
) * (loc
[0] - x
)
1059 mat
[1][2] += (loc
[2] - z
) * (loc
[1] - y
)
1060 mat
[2][2] += (loc
[2] - z
) ** 2
1063 if sum(mat
[0]) < 1e-6 or sum(mat
[1]) < 1e-6 or sum(mat
[2]) < 1e-6:
1064 normal_plurity
= True
1068 if sum(mat
[0]) == 0:
1069 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
1070 elif sum(mat
[1]) == 0:
1071 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
1072 elif sum(mat
[2]) == 0:
1073 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
1075 # warning! this is different from .normalize()
1078 vec
= mathutils
.Vector((1.0, 1.0, 1.0))
1079 vec2
= (mat
@ vec
) / (mat
@ vec
).length
1080 while vec
!= vec2
and iter < itermax
:
1084 if vec2
.length
!= 0:
1086 if vec2
.length
== 0:
1087 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
1089 normals
.append(normal
)
1090 # have plane normals face in the same direction (maximum angle: 90 degrees)
1091 if ((center1
+ normals
[0]) - center2
).length
< \
1092 ((center1
- normals
[0]) - center2
).length
:
1094 if ((center2
+ normals
[1]) - center1
).length
> \
1095 ((center2
- normals
[1]) - center1
).length
:
1098 # rotation matrix, representing the difference between the plane normals
1099 axis
= normals
[0].cross(normals
[1])
1100 axis
= mathutils
.Vector([loc
if abs(loc
) > 1e-8 else 0 for loc
in axis
])
1101 if axis
.angle(mathutils
.Vector((0, 0, 1)), 0) > 1.5707964:
1103 angle
= normals
[0].dot(normals
[1])
1104 rotation_matrix
= mathutils
.Matrix
.Rotation(angle
, 4, axis
)
1106 # if circular, rotate loops so they are aligned
1108 # make sure loop1 is the circular one (or both are circular)
1109 if loop2_circular
and not loop1_circular
:
1110 loop1_circular
, loop2_circular
= True, False
1111 loop1
, loop2
= loop2
, loop1
1113 # match start vertex of loop1 with loop2
1114 target_vector
= bm
.verts
[loop2
[0]].co
- center2
1115 dif_angles
= [[(rotation_matrix
@ (bm
.verts
[vertex
].co
- center1
)
1116 ).angle(target_vector
, 0), False, i
] for
1117 i
, vertex
in enumerate(loop1
)]
1119 if len(loop1
) != len(loop2
):
1120 angle_limit
= dif_angles
[0][0] * 1.2 # 20% margin
1122 [(bm
.verts
[loop2
[0]].co
-
1123 bm
.verts
[loop1
[index
]].co
).length
, angle
, index
] for
1124 angle
, distance
, index
in dif_angles
if angle
<= angle_limit
1127 loop1
= loop1
[dif_angles
[0][2]:] + loop1
[:dif_angles
[0][2]]
1129 # have both loops face the same way
1130 if normal_plurity
and not circular
:
1131 second_to_first
, second_to_second
, second_to_last
= [
1132 (bm
.verts
[loop1
[1]].co
- center1
).angle(
1133 bm
.verts
[loop2
[i
]].co
- center2
) for i
in [0, 1, -1]
1135 last_to_first
, last_to_second
= [
1136 (bm
.verts
[loop1
[-1]].co
-
1137 center1
).angle(bm
.verts
[loop2
[i
]].co
- center2
) for
1140 if (min(last_to_first
, last_to_second
) * 1.1 < min(second_to_first
,
1141 second_to_second
)) or (loop2_circular
and second_to_last
* 1.1 <
1142 min(second_to_first
, second_to_second
)):
1145 loop1
= [loop1
[-1]] + loop1
[:-1]
1147 angle
= (bm
.verts
[loop1
[0]].co
- center1
).\
1148 cross(bm
.verts
[loop1
[1]].co
- center1
).angle(normals
[0], 0)
1149 target_angle
= (bm
.verts
[loop2
[0]].co
- center2
).\
1150 cross(bm
.verts
[loop2
[1]].co
- center2
).angle(normals
[1], 0)
1151 limit
= 1.5707964 # 0.5*pi, 90 degrees
1152 if not ((angle
> limit
and target_angle
> limit
) or
1153 (angle
< limit
and target_angle
< limit
)):
1156 loop1
= [loop1
[-1]] + loop1
[:-1]
1157 elif normals
[0].angle(normals
[1]) > limit
:
1160 loop1
= [loop1
[-1]] + loop1
[:-1]
1162 # both loops have the same length
1163 if len(loop1
) == len(loop2
):
1166 if abs(twist
) < len(loop1
):
1167 loop1
= loop1
[twist
:] + loop1
[:twist
]
1171 lines
.append([loop1
[0], loop2
[0]])
1172 for i
in range(1, len(loop1
)):
1173 lines
.append([loop1
[i
], loop2
[i
]])
1175 # loops of different lengths
1177 # make loop1 longest loop
1178 if len(loop2
) > len(loop1
):
1179 loop1
, loop2
= loop2
, loop1
1180 loop1_circular
, loop2_circular
= loop2_circular
, loop1_circular
1184 if abs(twist
) < len(loop1
):
1185 loop1
= loop1
[twist
:] + loop1
[:twist
]
1189 # shortest angle difference doesn't always give correct start vertex
1190 if loop1_circular
and not loop2_circular
:
1193 if len(loop1
) - shifting
< len(loop2
):
1196 to_last
, to_first
= [
1197 (rotation_matrix
@ (bm
.verts
[loop1
[-1]].co
- center1
)).angle(
1198 (bm
.verts
[loop2
[i
]].co
- center2
), 0) for i
in [-1, 0]
1200 if to_first
< to_last
:
1201 loop1
= [loop1
[-1]] + loop1
[:-1]
1207 # basic shortest side first
1209 lines
.append([loop1
[0], loop2
[0]])
1210 for i
in range(1, len(loop1
)):
1211 if i
>= len(loop2
) - 1:
1213 lines
.append([loop1
[i
], loop2
[-1]])
1216 lines
.append([loop1
[i
], loop2
[i
]])
1218 # shortest edge algorithm
1219 else: # mode == 'shortest'
1220 lines
.append([loop1
[0], loop2
[0]])
1222 for i
in range(len(loop1
) - 1):
1223 if prev_vert2
== len(loop2
) - 1 and not loop2_circular
:
1224 # force triangles, reached end of loop2
1226 elif prev_vert2
== len(loop2
) - 1 and loop2_circular
:
1227 # at end of loop2, but circular, so check with first vert
1228 tri
, quad
= [(bm
.verts
[loop1
[i
+ 1]].co
-
1229 bm
.verts
[loop2
[j
]].co
).length
1230 for j
in [prev_vert2
, 0]]
1232 elif len(loop1
) - 1 - i
== len(loop2
) - 1 - prev_vert2
and \
1234 # force quads, otherwise won't make it to end of loop2
1237 # calculate if tri or quad gives shortest edge
1238 tri
, quad
= [(bm
.verts
[loop1
[i
+ 1]].co
-
1239 bm
.verts
[loop2
[j
]].co
).length
1240 for j
in range(prev_vert2
, prev_vert2
+ 2)]
1244 lines
.append([loop1
[i
+ 1], loop2
[prev_vert2
]])
1245 if circle_full
== 2:
1248 elif not circle_full
:
1249 lines
.append([loop1
[i
+ 1], loop2
[prev_vert2
+ 1]])
1251 # quad to first vertex of loop2
1253 lines
.append([loop1
[i
+ 1], loop2
[0]])
1257 # final face for circular loops
1258 if loop1_circular
and loop2_circular
:
1259 lines
.append([loop1
[0], loop2
[0]])
1264 # calculate number of segments needed
1265 def bridge_calculate_segments(bm
, lines
, loops
, segments
):
1266 # return if amount of segments is set by user
1271 average_edge_length
= [
1272 (bm
.verts
[vertex
].co
-
1273 bm
.verts
[loop
[0][i
+ 1]].co
).length
for loop
in loops
for
1274 i
, vertex
in enumerate(loop
[0][:-1])
1276 # closing edges of circular loops
1277 average_edge_length
+= [
1278 (bm
.verts
[loop
[0][-1]].co
-
1279 bm
.verts
[loop
[0][0]].co
).length
for loop
in loops
if loop
[1]
1283 average_edge_length
= sum(average_edge_length
) / len(average_edge_length
)
1284 average_bridge_length
= sum(
1286 bm
.verts
[v2
].co
).length
for v1
, v2
in lines
]
1289 segments
= max(1, round(average_bridge_length
/ average_edge_length
))
1294 # return dictionary with vertex index as key, and the normal vector as value
1295 def bridge_calculate_virtual_vertex_normals(bm
, lines
, loops
, edge_faces
,
1297 if not edge_faces
: # interpolation isn't set to cubic
1300 # pity reduce() isn't one of the basic functions in python anymore
1301 def average_vector_dictionary(dic
):
1302 for key
, vectors
in dic
.items():
1303 # if type(vectors) == type([]) and len(vectors) > 1:
1304 if len(vectors
) > 1:
1305 average
= mathutils
.Vector()
1306 for vector
in vectors
:
1308 average
/= len(vectors
)
1309 dic
[key
] = [average
]
1312 # get all edges of the loop
1314 [edgekey_to_edge
[tuple(sorted([loops
[j
][0][i
],
1315 loops
[j
][0][i
+ 1]]))] for i
in range(len(loops
[j
][0]) - 1)] for
1318 edges
= edges
[0] + edges
[1]
1320 if loops
[j
][1]: # circular
1321 edges
.append(edgekey_to_edge
[tuple(sorted([loops
[j
][0][0],
1322 loops
[j
][0][-1]]))])
1325 calculation based on face topology (assign edge-normals to vertices)
1327 edge_normal = face_normal x edge_vector
1328 vertex_normal = average(edge_normals)
1330 vertex_normals
= dict([(vertex
, []) for vertex
in loops
[0][0] + loops
[1][0]])
1332 faces
= edge_faces
[edgekey(edge
)] # valid faces connected to edge
1335 # get edge coordinates
1336 v1
, v2
= [bm
.verts
[edgekey(edge
)[i
]].co
for i
in [0, 1]]
1337 edge_vector
= v1
- v2
1338 if edge_vector
.length
< 1e-4:
1339 # zero-length edge, vertices at same location
1341 edge_center
= (v1
+ v2
) / 2
1343 # average face coordinates, if connected to more than 1 valid face
1345 face_normal
= mathutils
.Vector()
1346 face_center
= mathutils
.Vector()
1348 face_normal
+= face
.normal
1349 face_center
+= face
.calc_center_median()
1350 face_normal
/= len(faces
)
1351 face_center
/= len(faces
)
1353 face_normal
= faces
[0].normal
1354 face_center
= faces
[0].calc_center_median()
1355 if face_normal
.length
< 1e-4:
1356 # faces with a surface of 0 have no face normal
1359 # calculate virtual edge normal
1360 edge_normal
= edge_vector
.cross(face_normal
)
1361 edge_normal
.length
= 0.01
1362 if (face_center
- (edge_center
+ edge_normal
)).length
> \
1363 (face_center
- (edge_center
- edge_normal
)).length
:
1364 # make normal face the correct way
1365 edge_normal
.negate()
1366 edge_normal
.normalize()
1367 # add virtual edge normal as entry for both vertices it connects
1368 for vertex
in edgekey(edge
):
1369 vertex_normals
[vertex
].append(edge_normal
)
1372 calculation based on connection with other loop (vertex focused method)
1373 - used for vertices that aren't connected to any valid faces
1375 plane_normal = edge_vector x connection_vector
1376 vertex_normal = plane_normal x edge_vector
1379 vertex
for vertex
, normal
in vertex_normals
.items() if not normal
1383 # edge vectors connected to vertices
1384 edge_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1386 for v
in edgekey(edge
):
1387 if v
in edge_vectors
:
1388 edge_vector
= bm
.verts
[edgekey(edge
)[0]].co
- \
1389 bm
.verts
[edgekey(edge
)[1]].co
1390 if edge_vector
.length
< 1e-4:
1391 # zero-length edge, vertices at same location
1393 edge_vectors
[v
].append(edge_vector
)
1395 # connection vectors between vertices of both loops
1396 connection_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1397 connections
= dict([[vertex
, []] for vertex
in vertices
])
1398 for v1
, v2
in lines
:
1399 if v1
in connection_vectors
or v2
in connection_vectors
:
1400 new_vector
= bm
.verts
[v1
].co
- bm
.verts
[v2
].co
1401 if new_vector
.length
< 1e-4:
1402 # zero-length connection vector,
1403 # vertices in different loops at same location
1405 if v1
in connection_vectors
:
1406 connection_vectors
[v1
].append(new_vector
)
1407 connections
[v1
].append(v2
)
1408 if v2
in connection_vectors
:
1409 connection_vectors
[v2
].append(new_vector
)
1410 connections
[v2
].append(v1
)
1411 connection_vectors
= average_vector_dictionary(connection_vectors
)
1412 connection_vectors
= dict(
1413 [[vertex
, vector
[0]] if vector
else
1414 [vertex
, []] for vertex
, vector
in connection_vectors
.items()]
1417 for vertex
, values
in edge_vectors
.items():
1418 # vertex normal doesn't matter, just assign a random vector to it
1419 if not connection_vectors
[vertex
]:
1420 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1423 # calculate to what location the vertex is connected,
1424 # used to determine what way to flip the normal
1425 connected_center
= mathutils
.Vector()
1426 for v
in connections
[vertex
]:
1427 connected_center
+= bm
.verts
[v
].co
1428 if len(connections
[vertex
]) > 1:
1429 connected_center
/= len(connections
[vertex
])
1430 if len(connections
[vertex
]) == 0:
1431 # shouldn't be possible, but better safe than sorry
1432 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1435 # can't do proper calculations, because of zero-length vector
1437 if (connected_center
- (bm
.verts
[vertex
].co
+
1438 connection_vectors
[vertex
])).length
< (connected_center
-
1439 (bm
.verts
[vertex
].co
- connection_vectors
[vertex
])).length
:
1440 connection_vectors
[vertex
].negate()
1441 vertex_normals
[vertex
] = [connection_vectors
[vertex
].normalized()]
1444 # calculate vertex normals using edge-vectors,
1445 # connection-vectors and the derived plane normal
1446 for edge_vector
in values
:
1447 plane_normal
= edge_vector
.cross(connection_vectors
[vertex
])
1448 vertex_normal
= edge_vector
.cross(plane_normal
)
1449 vertex_normal
.length
= 0.1
1450 if (connected_center
- (bm
.verts
[vertex
].co
+
1451 vertex_normal
)).length
< (connected_center
-
1452 (bm
.verts
[vertex
].co
- vertex_normal
)).length
:
1453 # make normal face the correct way
1454 vertex_normal
.negate()
1455 vertex_normal
.normalize()
1456 vertex_normals
[vertex
].append(vertex_normal
)
1458 # average virtual vertex normals, based on all edges it's connected to
1459 vertex_normals
= average_vector_dictionary(vertex_normals
)
1460 vertex_normals
= dict([[vertex
, vector
[0]] for vertex
, vector
in vertex_normals
.items()])
1462 return(vertex_normals
)
1465 # add vertices to mesh
1466 def bridge_create_vertices(bm
, vertices
):
1467 for i
in range(len(vertices
)):
1468 bm
.verts
.new(vertices
[i
])
1469 bm
.verts
.ensure_lookup_table()
1473 def bridge_create_faces(object, bm
, faces
, twist
):
1474 # have the normal point the correct way
1476 [face
.reverse() for face
in faces
]
1477 faces
= [face
[2:] + face
[:2] if face
[0] == face
[1] else face
for face
in faces
]
1479 # eekadoodle prevention
1480 for i
in range(len(faces
)):
1481 if not faces
[i
][-1]:
1482 if faces
[i
][0] == faces
[i
][-1]:
1483 faces
[i
] = [faces
[i
][1], faces
[i
][2], faces
[i
][3], faces
[i
][1]]
1485 faces
[i
] = [faces
[i
][-1]] + faces
[i
][:-1]
1486 # result of converting from pre-bmesh period
1487 if faces
[i
][-1] == faces
[i
][-2]:
1488 faces
[i
] = faces
[i
][:-1]
1491 for i
in range(len(faces
)):
1493 new_faces
.append(bm
.faces
.new([bm
.verts
[v
] for v
in faces
[i
]]))
1495 # face already exists
1498 object.data
.update(calc_edges
=True) # calc_edges prevents memory-corruption
1500 bm
.verts
.ensure_lookup_table()
1501 bm
.edges
.ensure_lookup_table()
1502 bm
.faces
.ensure_lookup_table()
1507 # calculate input loops
1508 def bridge_get_input(bm
):
1509 # create list of internal edges, which should be skipped
1510 eks_of_selected_faces
= [
1511 item
for sublist
in [face_edgekeys(face
) for
1512 face
in bm
.faces
if face
.select
and not face
.hide
] for item
in sublist
1515 for ek
in eks_of_selected_faces
:
1516 if ek
in edge_count
:
1520 internal_edges
= [ek
for ek
in edge_count
if edge_count
[ek
] > 1]
1522 # sort correct edges into loops
1524 edgekey(edge
) for edge
in bm
.edges
if edge
.select
and
1525 not edge
.hide
and edgekey(edge
) not in internal_edges
1527 loops
= get_connected_selections(selected_edges
)
1532 # return values needed by the bridge operator
1533 def bridge_initialise(bm
, interpolation
):
1534 if interpolation
== 'cubic':
1535 # dict with edge-key as key and list of connected valid faces as value
1537 face
.index
for face
in bm
.faces
if face
.select
or
1541 [[edgekey(edge
), []] for edge
in bm
.edges
if not edge
.hide
]
1543 for face
in bm
.faces
:
1544 if face
.index
in face_blacklist
:
1546 for key
in face_edgekeys(face
):
1547 edge_faces
[key
].append(face
)
1548 # dictionary with the edge-key as key and edge as value
1549 edgekey_to_edge
= dict(
1550 [[edgekey(edge
), edge
] for edge
in bm
.edges
if edge
.select
and not edge
.hide
]
1554 edgekey_to_edge
= False
1556 # selected faces input
1557 old_selected_faces
= [
1558 face
.index
for face
in bm
.faces
if face
.select
and not face
.hide
1561 # find out if faces created by bridging should be smoothed
1564 if sum([face
.smooth
for face
in bm
.faces
]) / len(bm
.faces
) >= 0.5:
1567 return(edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
)
1570 # return a string with the input method
1571 def bridge_input_method(loft
, loft_loop
):
1575 method
= "Loft loop"
1577 method
= "Loft no-loop"
1584 # match up loops in pairs, used for multi-input bridging
1585 def bridge_match_loops(bm
, loops
):
1586 # calculate average loop normals and centers
1589 for vertices
, circular
in loops
:
1590 normal
= mathutils
.Vector()
1591 center
= mathutils
.Vector()
1592 for vertex
in vertices
:
1593 normal
+= bm
.verts
[vertex
].normal
1594 center
+= bm
.verts
[vertex
].co
1595 normals
.append(normal
/ len(vertices
) / 10)
1596 centers
.append(center
/ len(vertices
))
1598 # possible matches if loop normals are faced towards the center
1600 matches
= dict([[i
, []] for i
in range(len(loops
))])
1602 for i
in range(len(loops
) + 1):
1603 for j
in range(i
+ 1, len(loops
)):
1604 if (centers
[i
] - centers
[j
]).length
> \
1605 (centers
[i
] - (centers
[j
] + normals
[j
])).length
and \
1606 (centers
[j
] - centers
[i
]).length
> \
1607 (centers
[j
] - (centers
[i
] + normals
[i
])).length
:
1609 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1610 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1611 # if no loops face each other, just make matches between all the loops
1612 if matches_amount
== 0:
1613 for i
in range(len(loops
) + 1):
1614 for j
in range(i
+ 1, len(loops
)):
1615 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1616 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1617 for key
, value
in matches
.items():
1620 # matches based on distance between centers and number of vertices in loops
1622 for loop_index
in range(len(loops
)):
1623 if loop_index
in new_order
:
1625 loop_matches
= matches
[loop_index
]
1626 if not loop_matches
:
1628 shortest_distance
= loop_matches
[0][0]
1629 shortest_distance
*= 1.1
1631 [abs(len(loops
[loop_index
][0]) -
1632 len(loops
[loop
[2]][0])), loop
[0], loop
[1], loop
[2]] for loop
in
1633 loop_matches
if loop
[0] < shortest_distance
1636 for match
in loop_matches
:
1637 if match
[3] not in new_order
:
1638 new_order
+= [loop_index
, match
[3]]
1641 # reorder loops based on matches
1642 if len(new_order
) >= 2:
1643 loops
= [loops
[i
] for i
in new_order
]
1648 # remove old_selected_faces
1649 def bridge_remove_internal_faces(bm
, old_selected_faces
):
1650 # collect bmesh faces and internal bmesh edges
1651 remove_faces
= [bm
.faces
[face
] for face
in old_selected_faces
]
1652 edges
= collections
.Counter(
1653 [edge
.index
for face
in remove_faces
for edge
in face
.edges
]
1655 remove_edges
= [bm
.edges
[edge
] for edge
in edges
if edges
[edge
] > 1]
1657 # remove internal faces and edges
1658 for face
in remove_faces
:
1659 bm
.faces
.remove(face
)
1660 for edge
in remove_edges
:
1661 bm
.edges
.remove(edge
)
1663 bm
.faces
.ensure_lookup_table()
1664 bm
.edges
.ensure_lookup_table()
1665 bm
.verts
.ensure_lookup_table()
1668 # update list of internal faces that are flagged for removal
1669 def bridge_save_unused_faces(bm
, old_selected_faces
, loops
):
1670 # key: vertex index, value: lists of selected faces using it
1672 vertex_to_face
= dict([[i
, []] for i
in range(len(bm
.verts
))])
1673 [[vertex_to_face
[vertex
.index
].append(face
) for vertex
in
1674 bm
.faces
[face
].verts
] for face
in old_selected_faces
]
1676 # group selected faces that are connected
1679 for face
in old_selected_faces
:
1680 if face
in grouped_faces
:
1682 grouped_faces
.append(face
)
1686 grow_face
= new_faces
[0]
1687 for vertex
in bm
.faces
[grow_face
].verts
:
1688 vertex_face_group
= [
1689 face
for face
in vertex_to_face
[vertex
.index
] if
1690 face
not in grouped_faces
1692 new_faces
+= vertex_face_group
1693 grouped_faces
+= vertex_face_group
1694 group
+= vertex_face_group
1696 groups
.append(group
)
1698 # key: vertex index, value: True/False (is it in a loop that is used)
1699 used_vertices
= dict([[i
, 0] for i
in range(len(bm
.verts
))])
1701 for vertex
in loop
[0]:
1702 used_vertices
[vertex
] = True
1704 # check if group is bridged, if not remove faces from internal faces list
1705 for group
in groups
:
1710 for vertex
in bm
.faces
[face
].verts
:
1711 if used_vertices
[vertex
.index
]:
1716 old_selected_faces
.remove(face
)
1719 # add the newly created faces to the selection
1720 def bridge_select_new_faces(new_faces
, smooth
):
1721 for face
in new_faces
:
1722 face
.select_set(True)
1723 face
.smooth
= smooth
1726 # sort loops, so they are connected in the correct order when lofting
1727 def bridge_sort_loops(bm
, loops
, loft_loop
):
1728 # simplify loops to single points, and prepare for pathfinding
1730 [sum([bm
.verts
[i
].co
[j
] for i
in loop
[0]]) /
1731 len(loop
[0]) for loop
in loops
] for j
in range(3)
1733 nodes
= [mathutils
.Vector((x
[i
], y
[i
], z
[i
])) for i
in range(len(loops
))]
1736 open = [i
for i
in range(1, len(loops
))]
1738 # connect node to path, that is shortest to active_node
1739 while len(open) > 0:
1740 distances
= [(nodes
[active_node
] - nodes
[i
]).length
for i
in open]
1741 active_node
= open[distances
.index(min(distances
))]
1742 open.remove(active_node
)
1743 path
.append([active_node
, min(distances
)])
1744 # check if we didn't start in the middle of the path
1745 for i
in range(2, len(path
)):
1746 if (nodes
[path
[i
][0]] - nodes
[0]).length
< path
[i
][1]:
1749 path
= path
[:-i
] + temp
1753 loops
= [loops
[i
[0]] for i
in path
]
1754 # if requested, duplicate first loop at last position, so loft can loop
1756 loops
= loops
+ [loops
[0]]
1761 # remapping old indices to new position in list
1762 def bridge_update_old_selection(bm
, old_selected_faces
):
1764 old_indices = old_selected_faces[:]
1765 old_selected_faces = []
1766 for i, face in enumerate(bm.faces):
1767 if face.index in old_indices:
1768 old_selected_faces.append(i)
1770 old_selected_faces
= [
1771 i
for i
, face
in enumerate(bm
.faces
) if face
.index
in old_selected_faces
1774 return(old_selected_faces
)
1777 # ########################################
1778 # ##### Circle functions #################
1779 # ########################################
1781 # convert 3d coordinates to 2d coordinates on plane
1782 def circle_3d_to_2d(bm_mod
, loop
, com
, normal
):
1783 # project vertices onto the plane
1784 verts
= [bm_mod
.verts
[v
] for v
in loop
[0]]
1785 verts_projected
= [[v
.co
- (v
.co
- com
).dot(normal
) * normal
, v
.index
]
1788 # calculate two vectors (p and q) along the plane
1789 m
= mathutils
.Vector((normal
[0] + 1.0, normal
[1], normal
[2]))
1790 p
= m
- (m
.dot(normal
) * normal
)
1792 m
= mathutils
.Vector((normal
[0], normal
[1] + 1.0, normal
[2]))
1793 p
= m
- (m
.dot(normal
) * normal
)
1796 # change to 2d coordinates using perpendicular projection
1798 for loc
, vert
in verts_projected
:
1800 x
= p
.dot(vloc
) / p
.dot(p
)
1801 y
= q
.dot(vloc
) / q
.dot(q
)
1802 locs_2d
.append([x
, y
, vert
])
1804 return(locs_2d
, p
, q
)
1807 # calculate a best-fit circle to the 2d locations on the plane
1808 def circle_calculate_best_fit(locs_2d
):
1814 # calculate center and radius (non-linear least squares solution)
1815 for iter in range(500):
1819 d
= (v
[0] ** 2 - 2.0 * x0
* v
[0] + v
[1] ** 2 - 2.0 * y0
* v
[1] + x0
** 2 + y0
** 2) ** 0.5
1820 jmat
.append([(x0
- v
[0]) / d
, (y0
- v
[1]) / d
, -1.0])
1821 k
.append(-(((v
[0] - x0
) ** 2 + (v
[1] - y0
) ** 2) ** 0.5 - r
))
1822 jmat2
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1826 k2
= mathutils
.Vector((0.0, 0.0, 0.0))
1827 for i
in range(len(jmat
)):
1828 k2
+= mathutils
.Vector(jmat
[i
]) * k
[i
]
1829 jmat2
[0][0] += jmat
[i
][0] ** 2
1830 jmat2
[1][0] += jmat
[i
][0] * jmat
[i
][1]
1831 jmat2
[2][0] += jmat
[i
][0] * jmat
[i
][2]
1832 jmat2
[1][1] += jmat
[i
][1] ** 2
1833 jmat2
[2][1] += jmat
[i
][1] * jmat
[i
][2]
1834 jmat2
[2][2] += jmat
[i
][2] ** 2
1835 jmat2
[0][1] = jmat2
[1][0]
1836 jmat2
[0][2] = jmat2
[2][0]
1837 jmat2
[1][2] = jmat2
[2][1]
1842 dx0
, dy0
, dr
= jmat2
@ k2
1846 # stop iterating if we're close enough to optimal solution
1847 if abs(dx0
) < 1e-6 and abs(dy0
) < 1e-6 and abs(dr
) < 1e-6:
1850 # return center of circle and radius
1854 # calculate circle so no vertices have to be moved away from the center
1855 def circle_calculate_min_fit(locs_2d
):
1857 x0
= (min([i
[0] for i
in locs_2d
]) + max([i
[0] for i
in locs_2d
])) / 2.0
1858 y0
= (min([i
[1] for i
in locs_2d
]) + max([i
[1] for i
in locs_2d
])) / 2.0
1859 center
= mathutils
.Vector([x0
, y0
])
1861 r
= min([(mathutils
.Vector([i
[0], i
[1]]) - center
).length
for i
in locs_2d
])
1863 # return center of circle and radius
1867 # calculate the new locations of the vertices that need to be moved
1868 def circle_calculate_verts(flatten
, bm_mod
, locs_2d
, com
, p
, q
, normal
):
1869 # changing 2d coordinates back to 3d coordinates
1872 locs_3d
.append([loc
[2], loc
[0] * p
+ loc
[1] * q
+ com
])
1874 if flatten
: # flat circle
1877 else: # project the locations on the existing mesh
1878 vert_edges
= dict_vert_edges(bm_mod
)
1879 vert_faces
= dict_vert_faces(bm_mod
)
1880 faces
= [f
for f
in bm_mod
.faces
if not f
.hide
]
1881 rays
= [normal
, -normal
]
1885 if bm_mod
.verts
[loc
[0]].co
== loc
[1]: # vertex hasn't moved
1888 dif
= normal
.angle(loc
[1] - bm_mod
.verts
[loc
[0]].co
)
1889 if -1e-6 < dif
< 1e-6 or math
.pi
- 1e-6 < dif
< math
.pi
+ 1e-6:
1890 # original location is already along projection normal
1891 projection
= bm_mod
.verts
[loc
[0]].co
1893 # quick search through adjacent faces
1894 for face
in vert_faces
[loc
[0]]:
1895 verts
= [v
.co
for v
in bm_mod
.faces
[face
].verts
]
1896 if len(verts
) == 3: # triangle
1900 v1
, v2
, v3
, v4
= verts
[:4]
1902 intersect
= mathutils
.geometry
.\
1903 intersect_ray_tri(v1
, v2
, v3
, ray
, loc
[1])
1905 projection
= intersect
1908 intersect
= mathutils
.geometry
.\
1909 intersect_ray_tri(v1
, v3
, v4
, ray
, loc
[1])
1911 projection
= intersect
1916 # check if projection is on adjacent edges
1917 for edgekey
in vert_edges
[loc
[0]]:
1918 line1
= bm_mod
.verts
[edgekey
[0]].co
1919 line2
= bm_mod
.verts
[edgekey
[1]].co
1920 intersect
, dist
= mathutils
.geometry
.intersect_point_line(
1921 loc
[1], line1
, line2
1923 if 1e-6 < dist
< 1 - 1e-6:
1924 projection
= intersect
1927 # full search through the entire mesh
1930 verts
= [v
.co
for v
in face
.verts
]
1931 if len(verts
) == 3: # triangle
1935 v1
, v2
, v3
, v4
= verts
[:4]
1937 intersect
= mathutils
.geometry
.intersect_ray_tri(
1938 v1
, v2
, v3
, ray
, loc
[1]
1941 hits
.append([(loc
[1] - intersect
).length
,
1945 intersect
= mathutils
.geometry
.intersect_ray_tri(
1946 v1
, v3
, v4
, ray
, loc
[1]
1949 hits
.append([(loc
[1] - intersect
).length
,
1953 # if more than 1 hit with mesh, closest hit is new loc
1955 projection
= hits
[0][1]
1957 # nothing to project on, remain at flat location
1959 new_locs
.append([loc
[0], projection
])
1961 # return new positions of projected circle
1965 # check loops and only return valid ones
1966 def circle_check_loops(single_loops
, loops
, mapping
, bm_mod
):
1967 valid_single_loops
= {}
1969 for i
, [loop
, circular
] in enumerate(loops
):
1970 # loop needs to have at least 3 vertices
1973 # loop needs at least 1 vertex in the original, non-mirrored mesh
1977 if mapping
[vert
] > -1:
1982 # loop has to be non-collinear
1984 loc0
= mathutils
.Vector(bm_mod
.verts
[loop
[0]].co
[:])
1985 loc1
= mathutils
.Vector(bm_mod
.verts
[loop
[1]].co
[:])
1987 locn
= mathutils
.Vector(bm_mod
.verts
[v
].co
[:])
1988 if loc0
== loc1
or loc1
== locn
:
1994 if -1e-6 < d1
.angle(d2
, 0) < 1e-6:
2002 # passed all tests, loop is valid
2003 valid_loops
.append([loop
, circular
])
2004 valid_single_loops
[len(valid_loops
) - 1] = single_loops
[i
]
2006 return(valid_single_loops
, valid_loops
)
2009 # calculate the location of single input vertices that need to be flattened
2010 def circle_flatten_singles(bm_mod
, com
, p
, q
, normal
, single_loop
):
2012 for vert
in single_loop
:
2013 loc
= mathutils
.Vector(bm_mod
.verts
[vert
].co
[:])
2014 new_locs
.append([vert
, loc
- (loc
- com
).dot(normal
) * normal
])
2019 # calculate input loops
2020 def circle_get_input(object, bm
):
2021 # get mesh with modifiers applied
2022 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
2024 # create list of edge-keys based on selection state
2026 for face
in bm
.faces
:
2027 if face
.select
and not face
.hide
:
2031 # get selected, non-hidden , non-internal edge-keys
2033 key
for keys
in [face_edgekeys(face
) for face
in
2034 bm_mod
.faces
if face
.select
and not face
.hide
] for key
in keys
2037 for ek
in eks_selected
:
2038 if ek
in edge_count
:
2043 edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and
2044 not edge
.hide
and edge_count
.get(edgekey(edge
), 1) == 1
2047 # no faces, so no internal edges either
2049 edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and not edge
.hide
2052 # add edge-keys around single vertices
2053 verts_connected
= dict(
2054 [[vert
, 1] for edge
in [edge
for edge
in
2055 bm_mod
.edges
if edge
.select
and not edge
.hide
] for vert
in
2059 vert
.index
for vert
in bm_mod
.verts
if
2060 vert
.select
and not vert
.hide
and
2061 not verts_connected
.get(vert
.index
, False)
2064 if single_vertices
and len(bm
.faces
) > 0:
2065 vert_to_single
= dict(
2066 [[v
.index
, []] for v
in bm_mod
.verts
if not v
.hide
]
2068 for face
in [face
for face
in bm_mod
.faces
if not face
.select
and not face
.hide
]:
2069 for vert
in face
.verts
:
2071 if vert
in single_vertices
:
2072 for ek
in face_edgekeys(face
):
2074 edge_keys
.append(ek
)
2075 if vert
not in vert_to_single
[ek
[0]]:
2076 vert_to_single
[ek
[0]].append(vert
)
2077 if vert
not in vert_to_single
[ek
[1]]:
2078 vert_to_single
[ek
[1]].append(vert
)
2081 # sort edge-keys into loops
2082 loops
= get_connected_selections(edge_keys
)
2084 # find out to which loops the single vertices belong
2085 single_loops
= dict([[i
, []] for i
in range(len(loops
))])
2086 if single_vertices
and len(bm
.faces
) > 0:
2087 for i
, [loop
, circular
] in enumerate(loops
):
2089 if vert_to_single
[vert
]:
2090 for single
in vert_to_single
[vert
]:
2091 if single
not in single_loops
[i
]:
2092 single_loops
[i
].append(single
)
2094 return(derived
, bm_mod
, single_vertices
, single_loops
, loops
)
2097 # recalculate positions based on the influence of the circle shape
2098 def circle_influence_locs(locs_2d
, new_locs_2d
, influence
):
2099 for i
in range(len(locs_2d
)):
2100 oldx
, oldy
, j
= locs_2d
[i
]
2101 newx
, newy
, k
= new_locs_2d
[i
]
2102 altx
= newx
* (influence
/ 100) + oldx
* ((100 - influence
) / 100)
2103 alty
= newy
* (influence
/ 100) + oldy
* ((100 - influence
) / 100)
2104 locs_2d
[i
] = [altx
, alty
, j
]
2109 # project 2d locations on circle, respecting distance relations between verts
2110 def circle_project_non_regular(locs_2d
, x0
, y0
, r
, angle
):
2111 for i
in range(len(locs_2d
)):
2112 x
, y
, j
= locs_2d
[i
]
2113 loc
= mathutils
.Vector([x
- x0
, y
- y0
])
2114 mat_rot
= mathutils
.Matrix
.Rotation(angle
, 2, 'X')
2117 locs_2d
[i
] = [loc
[0], loc
[1], j
]
2122 # project 2d locations on circle, with equal distance between all vertices
2123 def circle_project_regular(locs_2d
, x0
, y0
, r
, angle
):
2124 # find offset angle and circling direction
2125 x
, y
, i
= locs_2d
[0]
2126 loc
= mathutils
.Vector([x
- x0
, y
- y0
])
2128 offset_angle
= loc
.angle(mathutils
.Vector([1.0, 0.0]), 0.0)
2129 loca
= mathutils
.Vector([x
- x0
, y
- y0
, 0.0])
2132 x
, y
, j
= locs_2d
[1]
2133 locb
= mathutils
.Vector([x
- x0
, y
- y0
, 0.0])
2134 if loca
.cross(locb
)[2] >= 0:
2138 # distribute vertices along the circle
2139 for i
in range(len(locs_2d
)):
2140 t
= offset_angle
+ ccw
* (i
/ len(locs_2d
) * 2 * math
.pi
)
2141 x
= math
.cos(t
+ angle
) * r
2142 y
= math
.sin(t
+ angle
) * r
2143 locs_2d
[i
] = [x
, y
, locs_2d
[i
][2]]
2148 # shift loop, so the first vertex is closest to the center
2149 def circle_shift_loop(bm_mod
, loop
, com
):
2150 verts
, circular
= loop
2152 [(bm_mod
.verts
[vert
].co
- com
).length
, i
] for i
, vert
in enumerate(verts
)
2155 shift
= distances
[0][1]
2156 loop
= [verts
[shift
:] + verts
[:shift
], circular
]
2161 # ########################################
2162 # ##### Curve functions ##################
2163 # ########################################
2165 # create lists with knots and points, all correctly sorted
2166 def curve_calculate_knots(loop
, verts_selected
):
2167 knots
= [v
for v
in loop
[0] if v
in verts_selected
]
2169 # circular loop, potential for weird splines
2171 offset
= int(len(loop
[0]) / 4)
2174 kpos
.append(loop
[0].index(k
))
2176 for i
in range(len(kpos
) - 1):
2177 kdif
.append(kpos
[i
+ 1] - kpos
[i
])
2178 kdif
.append(len(loop
[0]) - kpos
[-1] + kpos
[0])
2182 kadd
.append([kdif
.index(k
), True])
2183 # next 2 lines are optional, they insert
2184 # an extra control point in small gaps
2186 # kadd.append([kdif.index(k), False])
2189 for k
in kadd
: # extra knots to be added
2190 if k
[1]: # big gap (break circular spline)
2191 kpos
= loop
[0].index(knots
[k
[0]]) + offset
2192 if kpos
> len(loop
[0]) - 1:
2193 kpos
-= len(loop
[0])
2194 kins
.append([knots
[k
[0]], loop
[0][kpos
]])
2196 if kpos2
> len(knots
) - 1:
2198 kpos2
= loop
[0].index(knots
[kpos2
]) - offset
2200 kpos2
+= len(loop
[0])
2201 kins
.append([loop
[0][kpos
], loop
[0][kpos2
]])
2202 krot
= loop
[0][kpos2
]
2203 else: # small gap (keep circular spline)
2204 k1
= loop
[0].index(knots
[k
[0]])
2206 if k2
> len(knots
) - 1:
2208 k2
= loop
[0].index(knots
[k2
])
2210 dif
= len(loop
[0]) - 1 - k1
+ k2
2213 kn
= k1
+ int(dif
/ 2)
2214 if kn
> len(loop
[0]) - 1:
2216 kins
.append([loop
[0][k1
], loop
[0][kn
]])
2217 for j
in kins
: # insert new knots
2218 knots
.insert(knots
.index(j
[0]) + 1, j
[1])
2219 if not krot
: # circular loop
2220 knots
.append(knots
[0])
2221 points
= loop
[0][loop
[0].index(knots
[0]):]
2222 points
+= loop
[0][0:loop
[0].index(knots
[0]) + 1]
2223 else: # non-circular loop (broken by script)
2224 krot
= knots
.index(krot
)
2225 knots
= knots
[krot
:] + knots
[0:krot
]
2226 if loop
[0].index(knots
[0]) > loop
[0].index(knots
[-1]):
2227 points
= loop
[0][loop
[0].index(knots
[0]):]
2228 points
+= loop
[0][0:loop
[0].index(knots
[-1]) + 1]
2230 points
= loop
[0][loop
[0].index(knots
[0]):loop
[0].index(knots
[-1]) + 1]
2231 # non-circular loop, add first and last point as knots
2233 if loop
[0][0] not in knots
:
2234 knots
.insert(0, loop
[0][0])
2235 if loop
[0][-1] not in knots
:
2236 knots
.append(loop
[0][-1])
2238 return(knots
, points
)
2241 # calculate relative positions compared to first knot
2242 def curve_calculate_t(bm_mod
, knots
, points
, pknots
, regular
, circular
):
2249 loc
= pknots
[knots
.index(p
)] # use projected knot location
2251 loc
= mathutils
.Vector(bm_mod
.verts
[p
].co
[:])
2254 len_total
+= (loc
- loc_prev
).length
2255 tpoints
.append(len_total
)
2260 tknots
.append(tpoints
[points
.index(p
)])
2262 tknots
[-1] = tpoints
[-1]
2266 tpoints_average
= tpoints
[-1] / (len(tpoints
) - 1)
2267 for i
in range(1, len(tpoints
) - 1):
2268 tpoints
[i
] = i
* tpoints_average
2269 for i
in range(len(knots
)):
2270 tknots
[i
] = tpoints
[points
.index(knots
[i
])]
2272 tknots
[-1] = tpoints
[-1]
2274 return(tknots
, tpoints
)
2277 # change the location of non-selected points to their place on the spline
2278 def curve_calculate_vertices(bm_mod
, knots
, tknots
, points
, tpoints
, splines
,
2279 interpolation
, restriction
):
2286 m
= tpoints
[points
.index(p
)]
2294 if n
> len(splines
) - 1:
2295 n
= len(splines
) - 1
2299 if interpolation
== 'cubic':
2300 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
2301 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
2302 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
2303 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
2304 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
2305 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
2306 newloc
= mathutils
.Vector([x
, y
, z
])
2307 else: # interpolation == 'linear'
2308 a
, d
, t
, u
= splines
[n
]
2309 newloc
= ((m
- t
) / u
) * d
+ a
2311 if restriction
!= 'none': # vertex movement is restricted
2313 else: # set the vertex to its new location
2314 move
.append([p
, newloc
])
2316 if restriction
!= 'none': # vertex movement is restricted
2321 move
.append([p
, bm_mod
.verts
[p
].co
])
2323 oldloc
= bm_mod
.verts
[p
].co
2324 normal
= bm_mod
.verts
[p
].normal
2325 dloc
= newloc
- oldloc
2326 if dloc
.length
< 1e-6:
2327 move
.append([p
, newloc
])
2328 elif restriction
== 'extrude': # only extrusions
2329 if dloc
.angle(normal
, 0) < 0.5 * math
.pi
+ 1e-6:
2330 move
.append([p
, newloc
])
2331 else: # restriction == 'indent' only indentations
2332 if dloc
.angle(normal
) > 0.5 * math
.pi
- 1e-6:
2333 move
.append([p
, newloc
])
2338 # trim loops to part between first and last selected vertices (including)
2339 def curve_cut_boundaries(bm_mod
, loops
):
2341 for loop
, circular
in loops
:
2343 selected
= [bm_mod
.verts
[v
].select
for v
in loop
]
2344 first
= selected
.index(True)
2346 last
= -selected
.index(True)
2348 if len(loop
[first
:]) < len(loop
)/2:
2349 cut_loops
.append([loop
[first
:], False])
2351 if len(loop
[first
:last
]) < len(loop
)/2:
2352 cut_loops
.append([loop
[first
:last
], False])
2354 selected
= [bm_mod
.verts
[v
].select
for v
in loop
]
2355 first
= selected
.index(True)
2357 last
= -selected
.index(True)
2359 cut_loops
.append([loop
[first
:], circular
])
2361 cut_loops
.append([loop
[first
:last
], circular
])
2366 # calculate input loops
2367 def curve_get_input(object, bm
, boundaries
):
2368 # get mesh with modifiers applied
2369 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
2371 # vertices that still need a loop to run through it
2373 v
.index
for v
in bm_mod
.verts
if v
.select
and not v
.hide
2375 # necessary dictionaries
2376 vert_edges
= dict_vert_edges(bm_mod
)
2377 edge_faces
= dict_edge_faces(bm_mod
)
2379 # find loops through each selected vertex
2380 while len(verts_unsorted
) > 0:
2381 loops
= curve_vertex_loops(bm_mod
, verts_unsorted
[0], vert_edges
,
2383 verts_unsorted
.pop(0)
2385 # check if loop is fully selected
2386 search_perpendicular
= False
2388 for loop
, circular
in loops
:
2390 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2391 if len(selected
) < 2:
2392 # only one selected vertex on loop, don't use
2395 elif len(selected
) == len(loop
):
2396 search_perpendicular
= loop
2398 # entire loop is selected, find perpendicular loops
2399 if search_perpendicular
:
2401 if vert
in verts_unsorted
:
2402 verts_unsorted
.remove(vert
)
2403 perp_loops
= curve_perpendicular_loops(bm_mod
, loop
,
2404 vert_edges
, edge_faces
)
2405 for perp_loop
in perp_loops
:
2406 correct_loops
.append(perp_loop
)
2409 for loop
, circular
in loops
:
2410 correct_loops
.append([loop
, circular
])
2414 correct_loops
= curve_cut_boundaries(bm_mod
, correct_loops
)
2416 return(derived
, bm_mod
, correct_loops
)
2419 # return all loops that are perpendicular to the given one
2420 def curve_perpendicular_loops(bm_mod
, start_loop
, vert_edges
, edge_faces
):
2421 # find perpendicular loops
2423 for start_vert
in start_loop
:
2424 loops
= curve_vertex_loops(bm_mod
, start_vert
, vert_edges
,
2426 for loop
, circular
in loops
:
2427 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2428 if len(selected
) == len(loop
):
2431 perp_loops
.append([loop
, circular
, loop
.index(start_vert
)])
2433 # trim loops to same lengths
2435 [len(loop
[0]), i
] for i
, loop
in enumerate(perp_loops
) if not loop
[1]
2438 # all loops are circular, not trimming
2439 return([[loop
[0], loop
[1]] for loop
in perp_loops
])
2441 shortest
= min(shortest
)
2442 shortest_start
= perp_loops
[shortest
[1]][2]
2443 before_start
= shortest_start
2444 after_start
= shortest
[0] - shortest_start
- 1
2445 bigger_before
= before_start
> after_start
2447 for loop
in perp_loops
:
2448 # have the loop face the same direction as the shortest one
2450 if loop
[2] < len(loop
[0]) / 2:
2452 loop
[2] = len(loop
[0]) - loop
[2] - 1
2454 if loop
[2] > len(loop
[0]) / 2:
2456 loop
[2] = len(loop
[0]) - loop
[2] - 1
2457 # circular loops can shift, to prevent wrong trimming
2459 shift
= shortest_start
- loop
[2]
2460 if loop
[2] + shift
> 0 and loop
[2] + shift
< len(loop
[0]):
2461 loop
[0] = loop
[0][-shift
:] + loop
[0][:-shift
]
2464 loop
[2] += len(loop
[0])
2465 elif loop
[2] > len(loop
[0]) - 1:
2466 loop
[2] -= len(loop
[0])
2468 start
= max(0, loop
[2] - before_start
)
2469 end
= min(len(loop
[0]), loop
[2] + after_start
+ 1)
2470 trimmed_loops
.append([loop
[0][start
:end
], False])
2472 return(trimmed_loops
)
2475 # project knots on non-selected geometry
2476 def curve_project_knots(bm_mod
, verts_selected
, knots
, points
, circular
):
2477 # function to project vertex on edge
2478 def project(v1
, v2
, v3
):
2479 # v1 and v2 are part of a line
2480 # v3 is projected onto it
2486 if circular
: # project all knots
2490 else: # first and last knot shouldn't be projected
2493 pknots
= [mathutils
.Vector(bm_mod
.verts
[knots
[0]].co
[:])]
2494 for knot
in knots
[start
:end
]:
2495 if knot
in verts_selected
:
2496 knot_left
= knot_right
= False
2497 for i
in range(points
.index(knot
) - 1, -1 * len(points
), -1):
2498 if points
[i
] not in knots
:
2499 knot_left
= points
[i
]
2501 for i
in range(points
.index(knot
) + 1, 2 * len(points
)):
2502 if i
> len(points
) - 1:
2504 if points
[i
] not in knots
:
2505 knot_right
= points
[i
]
2507 if knot_left
and knot_right
and knot_left
!= knot_right
:
2508 knot_left
= mathutils
.Vector(bm_mod
.verts
[knot_left
].co
[:])
2509 knot_right
= mathutils
.Vector(bm_mod
.verts
[knot_right
].co
[:])
2510 knot
= mathutils
.Vector(bm_mod
.verts
[knot
].co
[:])
2511 pknots
.append(project(knot_left
, knot_right
, knot
))
2513 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2514 else: # knot isn't selected, so shouldn't be changed
2515 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2517 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knots
[-1]].co
[:]))
2522 # find all loops through a given vertex
2523 def curve_vertex_loops(bm_mod
, start_vert
, vert_edges
, edge_faces
):
2527 for edge
in vert_edges
[start_vert
]:
2528 if edge
in edges_used
:
2533 active_faces
= edge_faces
[edge
]
2538 new_edges
= vert_edges
[new_vert
]
2539 loop
.append(new_vert
)
2541 edges_used
.append(tuple(sorted([loop
[-1], loop
[-2]])))
2542 if len(new_edges
) < 3 or len(new_edges
) > 4:
2547 for new_edge
in new_edges
:
2548 if new_edge
in edges_used
:
2551 for new_face
in edge_faces
[new_edge
]:
2552 if new_face
in active_faces
:
2557 # found correct new edge
2558 active_faces
= edge_faces
[new_edge
]
2564 if new_vert
== loop
[0]:
2572 loops
.append([loop
, circular
])
2577 # ########################################
2578 # ##### Flatten functions ################
2579 # ########################################
2581 # sort input into loops
2582 def flatten_get_input(bm
):
2583 vert_verts
= dict_vert_verts(
2584 [edgekey(edge
) for edge
in bm
.edges
if edge
.select
and not edge
.hide
]
2586 verts
= [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
2588 # no connected verts, consider all selected verts as a single input
2590 return([[verts
, False]])
2593 while len(verts
) > 0:
2597 if loop
[-1] in vert_verts
:
2598 to_grow
= vert_verts
[loop
[-1]]
2602 while len(to_grow
) > 0:
2603 new_vert
= to_grow
[0]
2605 if new_vert
in loop
:
2607 loop
.append(new_vert
)
2608 verts
.remove(new_vert
)
2609 to_grow
+= vert_verts
[new_vert
]
2611 loops
.append([loop
, False])
2616 # calculate position of vertex projections on plane
2617 def flatten_project(bm
, loop
, com
, normal
):
2618 verts
= [bm
.verts
[v
] for v
in loop
[0]]
2620 [v
.index
, mathutils
.Vector(v
.co
[:]) -
2621 (mathutils
.Vector(v
.co
[:]) - com
).dot(normal
) * normal
] for v
in verts
2624 return(verts_projected
)
2627 # ########################################
2628 # ##### Gstretch functions ###############
2629 # ########################################
2631 # fake stroke class, used to create custom strokes if no GP data is found
2632 class gstretch_fake_stroke():
2633 def __init__(self
, points
):
2634 self
.points
= [gstretch_fake_stroke_point(p
) for p
in points
]
2637 # fake stroke point class, used in fake strokes
2638 class gstretch_fake_stroke_point():
2639 def __init__(self
, loc
):
2643 # flips loops, if necessary, to obtain maximum alignment to stroke
2644 def gstretch_align_pairs(ls_pairs
, object, bm_mod
, method
):
2645 # returns total distance between all verts in loop and corresponding stroke
2646 def distance_loop_stroke(loop
, stroke
, object, bm_mod
, method
):
2647 stroke_lengths_cache
= False
2648 loop_length
= len(loop
[0])
2651 if method
!= 'regular':
2652 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2654 for i
, v_index
in enumerate(loop
[0]):
2655 if method
== 'regular':
2656 relative_distance
= i
/ (loop_length
- 1)
2658 relative_distance
= relative_lengths
[i
]
2660 loc1
= object.matrix_world
@ bm_mod
.verts
[v_index
].co
2661 loc2
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2662 relative_distance
, stroke_lengths_cache
)
2663 total_distance
+= (loc2
- loc1
).length
2665 return(total_distance
)
2668 for (loop
, stroke
) in ls_pairs
:
2669 total_dist
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2672 total_dist_rev
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2674 if total_dist_rev
> total_dist
:
2680 # calculate vertex positions on stroke
2681 def gstretch_calculate_verts(loop
, stroke
, object, bm_mod
, method
):
2683 stroke_lengths_cache
= False
2684 loop_length
= len(loop
[0])
2685 matrix_inverse
= object.matrix_world
.inverted()
2687 # return intersection of line with stroke, or None
2688 def intersect_line_stroke(vec1
, vec2
, stroke
):
2689 for i
, p
in enumerate(stroke
.points
[1:]):
2690 intersections
= mathutils
.geometry
.intersect_line_line(vec1
, vec2
,
2691 p
.co
, stroke
.points
[i
].co
)
2692 if intersections
and \
2693 (intersections
[0] - intersections
[1]).length
< 1e-2:
2694 x
, dist
= mathutils
.geometry
.intersect_point_line(
2695 intersections
[0], p
.co
, stroke
.points
[i
].co
)
2697 return(intersections
[0])
2700 if method
== 'project':
2701 vert_edges
= dict_vert_edges(bm_mod
)
2703 for v_index
in loop
[0]:
2705 for ek
in vert_edges
[v_index
]:
2707 v1
= bm_mod
.verts
[v1
]
2708 v2
= bm_mod
.verts
[v2
]
2709 if v1
.select
+ v2
.select
== 1 and not v1
.hide
and not v2
.hide
:
2710 vec1
= object.matrix_world
@ v1
.co
2711 vec2
= object.matrix_world
@ v2
.co
2712 intersection
= intersect_line_stroke(vec1
, vec2
, stroke
)
2715 if not intersection
:
2716 v
= bm_mod
.verts
[v_index
]
2717 intersection
= intersect_line_stroke(v
.co
, v
.co
+ v
.normal
,
2720 move
.append([v_index
, matrix_inverse
@ intersection
])
2723 if method
== 'irregular':
2724 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2726 for i
, v_index
in enumerate(loop
[0]):
2727 if method
== 'regular':
2728 relative_distance
= i
/ (loop_length
- 1)
2729 else: # method == 'irregular'
2730 relative_distance
= relative_lengths
[i
]
2731 loc
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2732 relative_distance
, stroke_lengths_cache
)
2733 loc
= matrix_inverse
@ loc
2734 move
.append([v_index
, loc
])
2739 # create new vertices, based on GP strokes
2740 def gstretch_create_verts(object, bm_mod
, strokes
, method
, conversion
,
2741 conversion_distance
, conversion_max
, conversion_min
, conversion_vertices
):
2744 mat_world
= object.matrix_world
.inverted()
2745 singles
= gstretch_match_single_verts(bm_mod
, strokes
, mat_world
)
2747 for stroke
in strokes
:
2748 stroke_verts
.append([stroke
, []])
2750 if conversion
== 'vertices':
2751 min_end_point
= conversion_vertices
2752 end_point
= conversion_vertices
2753 elif conversion
== 'limit_vertices':
2754 min_end_point
= conversion_min
2755 end_point
= conversion_max
2757 end_point
= len(stroke
.points
)
2758 # creation of new vertices at fixed user-defined distances
2759 if conversion
== 'distance':
2761 prev_point
= stroke
.points
[0]
2762 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
@ prev_point
.co
))
2764 limit
= conversion_distance
2765 for point
in stroke
.points
:
2766 new_distance
= distance
+ (point
.co
- prev_point
.co
).length
2768 while new_distance
> limit
:
2769 to_cover
= limit
- distance
+ (limit
* iteration
)
2770 new_loc
= prev_point
.co
+ to_cover
* \
2771 (point
.co
- prev_point
.co
).normalized()
2772 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* new_loc
))
2773 new_distance
-= limit
2775 distance
= new_distance
2777 # creation of new vertices for other methods
2779 # add vertices at stroke points
2780 for point
in stroke
.points
[:end_point
]:
2781 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
@ point
.co
))
2782 # add more vertices, beyond the points that are available
2783 if min_end_point
> min(len(stroke
.points
), end_point
):
2784 for i
in range(min_end_point
-
2785 (min(len(stroke
.points
), end_point
))):
2786 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
@ point
.co
))
2787 # force even spreading of points, so they are placed on stroke
2789 bm_mod
.verts
.ensure_lookup_table()
2790 bm_mod
.verts
.index_update()
2791 for stroke
, verts_seq
in stroke_verts
:
2792 if len(verts_seq
) < 2:
2794 # spread vertices evenly over the stroke
2795 if method
== 'regular':
2796 loop
= [[vert
.index
for vert
in verts_seq
], False]
2797 move
+= gstretch_calculate_verts(loop
, stroke
, object, bm_mod
,
2800 for i
, vert
in enumerate(verts_seq
):
2802 bm_mod
.edges
.new((verts_seq
[i
- 1], verts_seq
[i
]))
2804 # connect single vertices to the closest stroke
2806 for vert
, m_stroke
, point
in singles
:
2807 if m_stroke
!= stroke
:
2809 bm_mod
.edges
.new((vert
, verts_seq
[point
]))
2810 bm_mod
.edges
.ensure_lookup_table()
2811 bmesh
.update_edit_mesh(object.data
)
2816 # erases the grease pencil stroke
2817 def gstretch_erase_stroke(stroke
, context
):
2818 # change 3d coordinate into a stroke-point
2819 def sp(loc
, context
):
2823 'location': (0, 0, 0),
2825 view3d_utils
.location_3d_to_region_2d(
2826 context
.region
, context
.space_data
.region_3d
, loc
)
2833 if type(stroke
) != bpy
.types
.GPencilStroke
:
2834 # fake stroke, there is nothing to delete
2837 erase_stroke
= [sp(p
.co
, context
) for p
in stroke
.points
]
2839 erase_stroke
[0]['is_start'] = True
2840 #bpy.ops.gpencil.draw(mode='ERASER', stroke=erase_stroke)
2841 bpy
.ops
.gpencil
.data_unlink()
2845 # get point on stroke, given by relative distance (0.0 - 1.0)
2846 def gstretch_eval_stroke(stroke
, distance
, stroke_lengths_cache
=False):
2847 # use cache if available
2848 if not stroke_lengths_cache
:
2850 for i
, p
in enumerate(stroke
.points
[1:]):
2851 lengths
.append((p
.co
- stroke
.points
[i
].co
).length
+ lengths
[-1])
2852 total_length
= max(lengths
[-1], 1e-7)
2853 stroke_lengths_cache
= [length
/ total_length
for length
in
2855 stroke_lengths
= stroke_lengths_cache
[:]
2857 if distance
in stroke_lengths
:
2858 loc
= stroke
.points
[stroke_lengths
.index(distance
)].co
2859 elif distance
> stroke_lengths
[-1]:
2860 # should be impossible, but better safe than sorry
2861 loc
= stroke
.points
[-1].co
2863 stroke_lengths
.append(distance
)
2864 stroke_lengths
.sort()
2865 stroke_index
= stroke_lengths
.index(distance
)
2866 interval_length
= stroke_lengths
[
2867 stroke_index
+ 1] - stroke_lengths
[stroke_index
- 1
2869 distance_relative
= (distance
- stroke_lengths
[stroke_index
- 1]) / interval_length
2870 interval_vector
= stroke
.points
[stroke_index
].co
- stroke
.points
[stroke_index
- 1].co
2871 loc
= stroke
.points
[stroke_index
- 1].co
+ distance_relative
* interval_vector
2873 return(loc
, stroke_lengths_cache
)
2876 # create fake grease pencil strokes for the active object
2877 def gstretch_get_fake_strokes(object, bm_mod
, loops
):
2880 p1
= object.matrix_world
@ bm_mod
.verts
[loop
[0][0]].co
2881 p2
= object.matrix_world
@ bm_mod
.verts
[loop
[0][-1]].co
2882 strokes
.append(gstretch_fake_stroke([p1
, p2
]))
2887 def gstretch_get_strokes(self
, context
):
2888 looptools
= context
.window_manager
.looptools
2889 gp
= get_strokes(self
, context
)
2892 if looptools
.gstretch_use_guide
== "Annotation":
2893 layer
= bpy
.data
.grease_pencils
[0].layers
.active
2894 if looptools
.gstretch_use_guide
== "GPencil" and not looptools
.gstretch_guide
== None:
2895 layer
= looptools
.gstretch_guide
.data
.layers
.active
2898 frame
= layer
.active_frame
2901 strokes
= frame
.strokes
2902 if len(strokes
) < 1:
2907 # returns a list with loop-stroke pairs
2908 def gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
):
2909 if not loops
or not strokes
:
2912 # calculate loop centers
2914 bm_mod
.verts
.ensure_lookup_table()
2916 center
= mathutils
.Vector()
2917 for v_index
in loop
[0]:
2918 center
+= bm_mod
.verts
[v_index
].co
2919 center
/= len(loop
[0])
2920 center
= object.matrix_world
@ center
2921 loop_centers
.append([center
, loop
])
2923 # calculate stroke centers
2925 for stroke
in strokes
:
2926 center
= mathutils
.Vector()
2927 for p
in stroke
.points
:
2929 center
/= len(stroke
.points
)
2930 stroke_centers
.append([center
, stroke
, 0])
2932 # match, first by stroke use count, then by distance
2934 for lc
in loop_centers
:
2936 for i
, sc
in enumerate(stroke_centers
):
2937 distances
.append([sc
[2], (lc
[0] - sc
[0]).length
, i
])
2939 best_stroke
= distances
[0][2]
2940 ls_pairs
.append([lc
[1], stroke_centers
[best_stroke
][1]])
2941 stroke_centers
[best_stroke
][2] += 1 # increase stroke use count
2946 # match single selected vertices to the closest stroke endpoint
2947 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2948 def gstretch_match_single_verts(bm_mod
, strokes
, mat_world
):
2949 # calculate stroke endpoints in object space
2951 for stroke
in strokes
:
2952 endpoints
.append((mat_world
@ stroke
.points
[0].co
, stroke
, 0))
2953 endpoints
.append((mat_world
@ stroke
.points
[-1].co
, stroke
, -1))
2956 # find single vertices (not connected to other selected verts)
2957 for vert
in bm_mod
.verts
:
2961 for edge
in vert
.link_edges
:
2962 if edge
.other_vert(vert
).select
:
2967 # calculate distances from vertex to endpoints
2968 distance
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2969 endpoint_index
) for endpoint_index
, (loc
, stroke
, stroke_point
) in
2970 enumerate(endpoints
)]
2972 distances
.append(distance
[0])
2974 # create matches, based on shortest distance first
2978 singles
.append((distances
[0][1], distances
[0][2], distances
[0][3]))
2979 endpoints
.pop(distances
[0][4])
2982 for (i
, vert
, j
, k
, l
) in distances
:
2983 distance_new
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2984 endpoint_index
) for endpoint_index
, (loc
, stroke
,
2985 stroke_point
) in enumerate(endpoints
)]
2987 distances_new
.append(distance_new
[0])
2988 distances
= distances_new
2993 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2994 def gstretch_relative_lengths(loop
, bm_mod
):
2996 for i
, v_index
in enumerate(loop
[0][1:]):
2998 (bm_mod
.verts
[v_index
].co
-
2999 bm_mod
.verts
[loop
[0][i
]].co
).length
+ lengths
[-1]
3001 total_length
= max(lengths
[-1], 1e-7)
3002 relative_lengths
= [length
/ total_length
for length
in
3005 return(relative_lengths
)
3008 # convert cache-stored strokes into usable (fake) GP strokes
3009 def gstretch_safe_to_true_strokes(safe_strokes
):
3011 for safe_stroke
in safe_strokes
:
3012 strokes
.append(gstretch_fake_stroke(safe_stroke
))
3017 # convert a GP stroke into a list of points which can be stored in cache
3018 def gstretch_true_to_safe_strokes(strokes
):
3020 for stroke
in strokes
:
3021 safe_strokes
.append([p
.co
.copy() for p
in stroke
.points
])
3023 return(safe_strokes
)
3026 # force consistency in GUI, max value can never be lower than min value
3027 def gstretch_update_max(self
, context
):
3028 # called from operator settings (after execution)
3029 if 'conversion_min' in self
.keys():
3030 if self
.conversion_min
> self
.conversion_max
:
3031 self
.conversion_max
= self
.conversion_min
3032 # called from toolbar
3034 lt
= context
.window_manager
.looptools
3035 if lt
.gstretch_conversion_min
> lt
.gstretch_conversion_max
:
3036 lt
.gstretch_conversion_max
= lt
.gstretch_conversion_min
3039 # force consistency in GUI, min value can never be higher than max value
3040 def gstretch_update_min(self
, context
):
3041 # called from operator settings (after execution)
3042 if 'conversion_max' in self
.keys():
3043 if self
.conversion_max
< self
.conversion_min
:
3044 self
.conversion_min
= self
.conversion_max
3045 # called from toolbar
3047 lt
= context
.window_manager
.looptools
3048 if lt
.gstretch_conversion_max
< lt
.gstretch_conversion_min
:
3049 lt
.gstretch_conversion_min
= lt
.gstretch_conversion_max
3052 # ########################################
3053 # ##### Relax functions ##################
3054 # ########################################
3056 # create lists with knots and points, all correctly sorted
3057 def relax_calculate_knots(loops
):
3060 for loop
, circular
in loops
:
3064 if len(loop
) % 2 == 1: # odd
3065 extend
= [False, True, 0, 1, 0, 1]
3067 extend
= [True, False, 0, 1, 1, 2]
3069 if len(loop
) % 2 == 1: # odd
3070 extend
= [False, False, 0, 1, 1, 2]
3072 extend
= [False, False, 0, 1, 1, 2]
3075 loop
= [loop
[-1]] + loop
+ [loop
[0]]
3076 for i
in range(extend
[2 + 2 * j
], len(loop
), 2):
3077 knots
[j
].append(loop
[i
])
3078 for i
in range(extend
[3 + 2 * j
], len(loop
), 2):
3079 if loop
[i
] == loop
[-1] and not circular
:
3081 if len(points
[j
]) == 0:
3082 points
[j
].append(loop
[i
])
3083 elif loop
[i
] != points
[j
][0]:
3084 points
[j
].append(loop
[i
])
3086 if knots
[j
][0] != knots
[j
][-1]:
3087 knots
[j
].append(knots
[j
][0])
3088 if len(points
[1]) == 0:
3094 all_points
.append(p
)
3096 return(all_knots
, all_points
)
3099 # calculate relative positions compared to first knot
3100 def relax_calculate_t(bm_mod
, knots
, points
, regular
):
3103 for i
in range(len(knots
)):
3104 amount
= len(knots
[i
]) + len(points
[i
])
3106 for j
in range(amount
):
3108 mix
.append([True, knots
[i
][round(j
/ 2)]])
3109 elif j
== amount
- 1:
3110 mix
.append([True, knots
[i
][-1]])
3112 mix
.append([False, points
[i
][int(j
/ 2)]])
3118 loc
= mathutils
.Vector(bm_mod
.verts
[m
[1]].co
[:])
3121 len_total
+= (loc
- loc_prev
).length
3123 tknots
.append(len_total
)
3125 tpoints
.append(len_total
)
3129 for p
in range(len(points
[i
])):
3130 tpoints
.append((tknots
[p
] + tknots
[p
+ 1]) / 2)
3131 all_tknots
.append(tknots
)
3132 all_tpoints
.append(tpoints
)
3134 return(all_tknots
, all_tpoints
)
3137 # change the location of the points to their place on the spline
3138 def relax_calculate_verts(bm_mod
, interpolation
, tknots
, knots
, tpoints
,
3142 for i
in range(len(knots
)):
3144 m
= tpoints
[i
][points
[i
].index(p
)]
3146 n
= tknots
[i
].index(m
)
3152 if n
> len(splines
[i
]) - 1:
3153 n
= len(splines
[i
]) - 1
3157 if interpolation
== 'cubic':
3158 ax
, bx
, cx
, dx
, tx
= splines
[i
][n
][0]
3159 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
3160 ay
, by
, cy
, dy
, ty
= splines
[i
][n
][1]
3161 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
3162 az
, bz
, cz
, dz
, tz
= splines
[i
][n
][2]
3163 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
3164 change
.append([p
, mathutils
.Vector([x
, y
, z
])])
3165 else: # interpolation == 'linear'
3166 a
, d
, t
, u
= splines
[i
][n
]
3169 change
.append([p
, ((m
- t
) / u
) * d
+ a
])
3171 move
.append([c
[0], (bm_mod
.verts
[c
[0]].co
+ c
[1]) / 2])
3176 # ########################################
3177 # ##### Space functions ##################
3178 # ########################################
3180 # calculate relative positions compared to first knot
3181 def space_calculate_t(bm_mod
, knots
):
3186 loc
= mathutils
.Vector(bm_mod
.verts
[k
].co
[:])
3189 len_total
+= (loc
- loc_prev
).length
3190 tknots
.append(len_total
)
3193 t_per_segment
= len_total
/ (amount
- 1)
3194 tpoints
= [i
* t_per_segment
for i
in range(amount
)]
3196 return(tknots
, tpoints
)
3199 # change the location of the points to their place on the spline
3200 def space_calculate_verts(bm_mod
, interpolation
, tknots
, tpoints
, points
,
3204 m
= tpoints
[points
.index(p
)]
3212 if n
> len(splines
) - 1:
3213 n
= len(splines
) - 1
3217 if interpolation
== 'cubic':
3218 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
3219 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
3220 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
3221 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
3222 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
3223 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
3224 move
.append([p
, mathutils
.Vector([x
, y
, z
])])
3225 else: # interpolation == 'linear'
3226 a
, d
, t
, u
= splines
[n
]
3227 move
.append([p
, ((m
- t
) / u
) * d
+ a
])
3232 # ########################################
3233 # ##### Operators ########################
3234 # ########################################
3237 class Bridge(Operator
):
3238 bl_idname
= 'mesh.looptools_bridge'
3239 bl_label
= "Bridge / Loft"
3240 bl_description
= "Bridge two, or loft several, loops of vertices"
3241 bl_options
= {'REGISTER', 'UNDO'}
3243 cubic_strength
: FloatProperty(
3245 description
="Higher strength results in more fluid curves",
3250 interpolation
: EnumProperty(
3251 name
="Interpolation mode",
3252 items
=(('cubic', "Cubic", "Gives curved results"),
3253 ('linear', "Linear", "Basic, fast, straight interpolation")),
3254 description
="Interpolation mode: algorithm used when creating "
3260 description
="Loft multiple loops, instead of considering them as "
3261 "a multi-input for bridging",
3264 loft_loop
: BoolProperty(
3266 description
="Connect the first and the last loop with each other",
3269 min_width
: IntProperty(
3270 name
="Minimum width",
3271 description
="Segments with an edge smaller than this are merged "
3272 "(compared to base edge)",
3276 subtype
='PERCENTAGE'
3280 items
=(('basic', "Basic", "Fast algorithm"),
3281 ('shortest', "Shortest edge", "Slower algorithm with better vertex matching")),
3282 description
="Algorithm used for bridging",
3285 remove_faces
: BoolProperty(
3286 name
="Remove faces",
3287 description
="Remove faces that are internal after bridging",
3290 reverse
: BoolProperty(
3292 description
="Manually override the direction in which the loops "
3293 "are bridged. Only use if the tool gives the wrong result",
3296 segments
: IntProperty(
3298 description
="Number of segments used to bridge the gap (0=automatic)",
3305 description
="Twist what vertices are connected to each other",
3310 def poll(cls
, context
):
3311 ob
= context
.active_object
3312 return (ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3314 def draw(self
, context
):
3315 layout
= self
.layout
3316 # layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3319 col_top
= layout
.column(align
=True)
3320 row
= col_top
.row(align
=True)
3321 col_left
= row
.column(align
=True)
3322 col_right
= row
.column(align
=True)
3323 col_right
.active
= self
.segments
!= 1
3324 col_left
.prop(self
, "segments")
3325 col_right
.prop(self
, "min_width", text
="")
3327 bottom_left
= col_left
.row()
3328 bottom_left
.active
= self
.segments
!= 1
3329 bottom_left
.prop(self
, "interpolation", text
="")
3330 bottom_right
= col_right
.row()
3331 bottom_right
.active
= self
.interpolation
== 'cubic'
3332 bottom_right
.prop(self
, "cubic_strength")
3333 # boolean properties
3334 col_top
.prop(self
, "remove_faces")
3336 col_top
.prop(self
, "loft_loop")
3338 # override properties
3340 row
= layout
.row(align
=True)
3341 row
.prop(self
, "twist")
3342 row
.prop(self
, "reverse")
3344 def invoke(self
, context
, event
):
3345 # load custom settings
3346 context
.window_manager
.looptools
.bridge_loft
= self
.loft
3348 return self
.execute(context
)
3350 def execute(self
, context
):
3352 object, bm
= initialise()
3353 edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
= \
3354 bridge_initialise(bm
, self
.interpolation
)
3355 settings_write(self
)
3357 # check cache to see if we can save time
3358 input_method
= bridge_input_method(self
.loft
, self
.loft_loop
)
3359 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Bridge",
3360 object, bm
, input_method
, False)
3363 loops
= bridge_get_input(bm
)
3365 # reorder loops if there are more than 2
3368 loops
= bridge_sort_loops(bm
, loops
, self
.loft_loop
)
3370 loops
= bridge_match_loops(bm
, loops
)
3372 # saving cache for faster execution next time
3374 cache_write("Bridge", object, bm
, input_method
, False, False,
3375 loops
, False, False)
3378 # calculate new geometry
3381 max_vert_index
= len(bm
.verts
) - 1
3382 for i
in range(1, len(loops
)):
3383 if not self
.loft
and i
% 2 == 0:
3385 lines
= bridge_calculate_lines(bm
, loops
[i
- 1:i
+ 1],
3386 self
.mode
, self
.twist
, self
.reverse
)
3387 vertex_normals
= bridge_calculate_virtual_vertex_normals(bm
,
3388 lines
, loops
[i
- 1:i
+ 1], edge_faces
, edgekey_to_edge
)
3389 segments
= bridge_calculate_segments(bm
, lines
,
3390 loops
[i
- 1:i
+ 1], self
.segments
)
3391 new_verts
, new_faces
, max_vert_index
= \
3392 bridge_calculate_geometry(
3393 bm
, lines
, vertex_normals
,
3394 segments
, self
.interpolation
, self
.cubic_strength
,
3395 self
.min_width
, max_vert_index
3398 vertices
+= new_verts
3401 # make sure faces in loops that aren't used, aren't removed
3402 if self
.remove_faces
and old_selected_faces
:
3403 bridge_save_unused_faces(bm
, old_selected_faces
, loops
)
3406 bridge_create_vertices(bm
, vertices
)
3407 # delete internal faces
3408 if self
.remove_faces
and old_selected_faces
:
3409 bridge_remove_internal_faces(bm
, old_selected_faces
)
3412 new_faces
= bridge_create_faces(object, bm
, faces
, self
.twist
)
3413 bridge_select_new_faces(new_faces
, smooth
)
3414 # edge-data could have changed, can't use cache next run
3415 if faces
and not vertices
:
3416 cache_delete("Bridge")
3417 # make sure normals are facing outside
3418 bmesh
.update_edit_mesh(object.data
, loop_triangles
=False, destructive
=True)
3419 bpy
.ops
.mesh
.normals_make_consistent()
3428 class Circle(Operator
):
3429 bl_idname
= "mesh.looptools_circle"
3431 bl_description
= "Move selected vertices into a circle shape"
3432 bl_options
= {'REGISTER', 'UNDO'}
3434 custom_radius
: BoolProperty(
3436 description
="Force a custom radius",
3441 items
=(("best", "Best fit", "Non-linear least squares"),
3442 ("inside", "Fit inside", "Only move vertices towards the center")),
3443 description
="Method used for fitting a circle to the vertices",
3446 flatten
: BoolProperty(
3448 description
="Flatten the circle, instead of projecting it on the mesh",
3451 influence
: FloatProperty(
3453 description
="Force of the tool",
3458 subtype
='PERCENTAGE'
3460 lock_x
: BoolProperty(
3462 description
="Lock editing of the x-coordinate",
3465 lock_y
: BoolProperty(
3467 description
="Lock editing of the y-coordinate",
3470 lock_z
: BoolProperty(name
="Lock Z",
3471 description
="Lock editing of the z-coordinate",
3474 radius
: FloatProperty(
3476 description
="Custom radius for circle",
3481 angle
: FloatProperty(
3483 description
="Rotate a circle by an angle",
3485 default
=math
.radians(0.0),
3486 soft_min
=math
.radians(-360.0),
3487 soft_max
=math
.radians(360.0)
3489 regular
: BoolProperty(
3491 description
="Distribute vertices at constant distances along the circle",
3496 def poll(cls
, context
):
3497 ob
= context
.active_object
3498 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3500 def draw(self
, context
):
3501 layout
= self
.layout
3502 col
= layout
.column()
3504 col
.prop(self
, "fit")
3507 col
.prop(self
, "flatten")
3508 row
= col
.row(align
=True)
3509 row
.prop(self
, "custom_radius")
3510 row_right
= row
.row(align
=True)
3511 row_right
.active
= self
.custom_radius
3512 row_right
.prop(self
, "radius", text
="")
3513 col
.prop(self
, "regular")
3514 col
.prop(self
, "angle")
3517 col_move
= col
.column(align
=True)
3518 row
= col_move
.row(align
=True)
3520 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3522 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3524 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3526 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3528 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3530 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3531 col_move
.prop(self
, "influence")
3533 def invoke(self
, context
, event
):
3534 # load custom settings
3536 return self
.execute(context
)
3538 def execute(self
, context
):
3540 object, bm
= initialise()
3541 settings_write(self
)
3542 # check cache to see if we can save time
3543 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Circle",
3544 object, bm
, False, False)
3546 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
3549 derived
, bm_mod
, single_vertices
, single_loops
, loops
= \
3550 circle_get_input(object, bm
)
3551 mapping
= get_mapping(derived
, bm
, bm_mod
, single_vertices
,
3553 single_loops
, loops
= circle_check_loops(single_loops
, loops
,
3556 # saving cache for faster execution next time
3558 cache_write("Circle", object, bm
, False, False, single_loops
,
3559 loops
, derived
, mapping
)
3562 for i
, loop
in enumerate(loops
):
3563 # best fitting flat plane
3564 com
, normal
= calculate_plane(bm_mod
, loop
)
3565 # if circular, shift loop so we get a good starting vertex
3567 loop
= circle_shift_loop(bm_mod
, loop
, com
)
3568 # flatten vertices on plane
3569 locs_2d
, p
, q
= circle_3d_to_2d(bm_mod
, loop
, com
, normal
)
3571 if self
.fit
== 'best':
3572 x0
, y0
, r
= circle_calculate_best_fit(locs_2d
)
3573 else: # self.fit == 'inside'
3574 x0
, y0
, r
= circle_calculate_min_fit(locs_2d
)
3576 if self
.custom_radius
:
3577 r
= self
.radius
/ p
.length
3578 # calculate positions on circle
3580 new_locs_2d
= circle_project_regular(locs_2d
[:], x0
, y0
, r
, self
.angle
)
3582 new_locs_2d
= circle_project_non_regular(locs_2d
[:], x0
, y0
, r
, self
.angle
)
3583 # take influence into account
3584 locs_2d
= circle_influence_locs(locs_2d
, new_locs_2d
,
3586 # calculate 3d positions of the created 2d input
3587 move
.append(circle_calculate_verts(self
.flatten
, bm_mod
,
3588 locs_2d
, com
, p
, q
, normal
))
3589 # flatten single input vertices on plane defined by loop
3590 if self
.flatten
and single_loops
:
3591 move
.append(circle_flatten_singles(bm_mod
, com
, p
, q
,
3592 normal
, single_loops
[i
]))
3594 # move vertices to new locations
3595 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3596 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3599 move_verts(object, bm
, mapping
, move
, lock
, -1)
3610 class Curve(Operator
):
3611 bl_idname
= "mesh.looptools_curve"
3613 bl_description
= "Turn a loop into a smooth curve"
3614 bl_options
= {'REGISTER', 'UNDO'}
3616 boundaries
: BoolProperty(
3618 description
="Limit the tool to work within the boundaries of the selected vertices",
3621 influence
: FloatProperty(
3623 description
="Force of the tool",
3628 subtype
='PERCENTAGE'
3630 interpolation
: EnumProperty(
3631 name
="Interpolation",
3632 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
3633 ("linear", "Linear", "Simple and fast linear algorithm")),
3634 description
="Algorithm used for interpolation",
3637 lock_x
: BoolProperty(
3639 description
="Lock editing of the x-coordinate",
3642 lock_y
: BoolProperty(
3644 description
="Lock editing of the y-coordinate",
3647 lock_z
: BoolProperty(
3649 description
="Lock editing of the z-coordinate",
3652 regular
: BoolProperty(
3654 description
="Distribute vertices at constant distances along the curve",
3657 restriction
: EnumProperty(
3659 items
=(("none", "None", "No restrictions on vertex movement"),
3660 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
3661 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
3662 description
="Restrictions on how the vertices can be moved",
3667 def poll(cls
, context
):
3668 ob
= context
.active_object
3669 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3671 def draw(self
, context
):
3672 layout
= self
.layout
3673 col
= layout
.column()
3675 col
.prop(self
, "interpolation")
3676 col
.prop(self
, "restriction")
3677 col
.prop(self
, "boundaries")
3678 col
.prop(self
, "regular")
3681 col_move
= col
.column(align
=True)
3682 row
= col_move
.row(align
=True)
3684 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3686 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3688 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3690 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3692 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3694 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3695 col_move
.prop(self
, "influence")
3697 def invoke(self
, context
, event
):
3698 # load custom settings
3700 return self
.execute(context
)
3702 def execute(self
, context
):
3704 object, bm
= initialise()
3705 settings_write(self
)
3706 # check cache to see if we can save time
3707 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Curve",
3708 object, bm
, False, self
.boundaries
)
3710 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
3713 derived
, bm_mod
, loops
= curve_get_input(object, bm
, self
.boundaries
)
3714 mapping
= get_mapping(derived
, bm
, bm_mod
, False, True, loops
)
3715 loops
= check_loops(loops
, mapping
, bm_mod
)
3717 v
.index
for v
in bm_mod
.verts
if v
.select
and not v
.hide
3720 # saving cache for faster execution next time
3722 cache_write("Curve", object, bm
, False, self
.boundaries
, False,
3723 loops
, derived
, mapping
)
3727 knots
, points
= curve_calculate_knots(loop
, verts_selected
)
3728 pknots
= curve_project_knots(bm_mod
, verts_selected
, knots
,
3730 tknots
, tpoints
= curve_calculate_t(bm_mod
, knots
, points
,
3731 pknots
, self
.regular
, loop
[1])
3732 splines
= calculate_splines(self
.interpolation
, bm_mod
,
3734 move
.append(curve_calculate_vertices(bm_mod
, knots
, tknots
,
3735 points
, tpoints
, splines
, self
.interpolation
,
3738 # move vertices to new locations
3739 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3740 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3743 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
3754 class Flatten(Operator
):
3755 bl_idname
= "mesh.looptools_flatten"
3756 bl_label
= "Flatten"
3757 bl_description
= "Flatten vertices on a best-fitting plane"
3758 bl_options
= {'REGISTER', 'UNDO'}
3760 influence
: FloatProperty(
3762 description
="Force of the tool",
3767 subtype
='PERCENTAGE'
3769 lock_x
: BoolProperty(
3771 description
="Lock editing of the x-coordinate",
3774 lock_y
: BoolProperty(
3776 description
="Lock editing of the y-coordinate",
3779 lock_z
: BoolProperty(name
="Lock Z",
3780 description
="Lock editing of the z-coordinate",
3783 plane
: EnumProperty(
3785 items
=(("best_fit", "Best fit", "Calculate a best fitting plane"),
3786 ("normal", "Normal", "Derive plane from averaging vertex normals"),
3787 ("view", "View", "Flatten on a plane perpendicular to the viewing angle")),
3788 description
="Plane on which vertices are flattened",
3791 restriction
: EnumProperty(
3793 items
=(("none", "None", "No restrictions on vertex movement"),
3794 ("bounding_box", "Bounding box", "Vertices are restricted to "
3795 "movement inside the bounding box of the selection")),
3796 description
="Restrictions on how the vertices can be moved",
3801 def poll(cls
, context
):
3802 ob
= context
.active_object
3803 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3805 def draw(self
, context
):
3806 layout
= self
.layout
3807 col
= layout
.column()
3809 col
.prop(self
, "plane")
3810 # col.prop(self, "restriction")
3813 col_move
= col
.column(align
=True)
3814 row
= col_move
.row(align
=True)
3816 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3818 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3820 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3822 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3824 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3826 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3827 col_move
.prop(self
, "influence")
3829 def invoke(self
, context
, event
):
3830 # load custom settings
3832 return self
.execute(context
)
3834 def execute(self
, context
):
3836 object, bm
= initialise()
3837 settings_write(self
)
3838 # check cache to see if we can save time
3839 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Flatten",
3840 object, bm
, False, False)
3842 # order input into virtual loops
3843 loops
= flatten_get_input(bm
)
3844 loops
= check_loops(loops
, mapping
, bm
)
3846 # saving cache for faster execution next time
3848 cache_write("Flatten", object, bm
, False, False, False, loops
,
3853 # calculate plane and position of vertices on them
3854 com
, normal
= calculate_plane(bm
, loop
, method
=self
.plane
,
3856 to_move
= flatten_project(bm
, loop
, com
, normal
)
3857 if self
.restriction
== 'none':
3858 move
.append(to_move
)
3860 move
.append(to_move
)
3862 # move vertices to new locations
3863 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3864 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3867 move_verts(object, bm
, False, move
, lock
, self
.influence
)
3875 # Annotation operator
3876 class RemoveAnnotation(Operator
):
3877 bl_idname
= "remove.annotation"
3878 bl_label
= "Remove Annotation"
3879 bl_description
= "Remove all Annotation Strokes"
3880 bl_options
= {'REGISTER', 'UNDO'}
3882 def execute(self
, context
):
3885 bpy
.data
.grease_pencils
[0].layers
.active
.clear()
3887 self
.report({'INFO'}, "No Annotation data to Unlink")
3888 return {'CANCELLED'}
3893 class RemoveGPencil(Operator
):
3894 bl_idname
= "remove.gp"
3895 bl_label
= "Remove GPencil"
3896 bl_description
= "Remove all GPencil Strokes"
3897 bl_options
= {'REGISTER', 'UNDO'}
3899 def execute(self
, context
):
3902 looptools
= context
.window_manager
.looptools
3903 looptools
.gstretch_guide
.data
.layers
.data
.clear()
3904 looptools
.gstretch_guide
.data
.update_tag()
3906 self
.report({'INFO'}, "No GPencil data to Unlink")
3907 return {'CANCELLED'}
3912 class GStretch(Operator
):
3913 bl_idname
= "mesh.looptools_gstretch"
3914 bl_label
= "Gstretch"
3915 bl_description
= "Stretch selected vertices to active stroke"
3916 bl_options
= {'REGISTER', 'UNDO'}
3918 conversion
: EnumProperty(
3920 items
=(("distance", "Distance", "Set the distance between vertices "
3921 "of the converted stroke"),
3922 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
3923 "number of vertices that converted strokes will have"),
3924 ("vertices", "Exact vertices", "Set the exact number of vertices "
3925 "that converted strokes will have. Short strokes "
3926 "with few points may contain less vertices than this number."),
3927 ("none", "No simplification", "Convert each point "
3929 description
="If strokes are converted to geometry, "
3930 "use this simplification method",
3931 default
='limit_vertices'
3933 conversion_distance
: FloatProperty(
3935 description
="Absolute distance between vertices along the converted "
3942 conversion_max
: IntProperty(
3943 name
="Max Vertices",
3944 description
="Maximum number of vertices strokes will "
3945 "have, when they are converted to geometry",
3949 update
=gstretch_update_min
3951 conversion_min
: IntProperty(
3952 name
="Min Vertices",
3953 description
="Minimum number of vertices strokes will "
3954 "have, when they are converted to geometry",
3958 update
=gstretch_update_max
3960 conversion_vertices
: IntProperty(
3962 description
="Number of vertices strokes will "
3963 "have, when they are converted to geometry. If strokes have less "
3964 "points than required, the 'Spread evenly' method is used",
3969 delete_strokes
: BoolProperty(
3970 name
="Delete strokes",
3971 description
="Remove strokes if they have been used."
3972 "WARNING: DOES NOT SUPPORT UNDO",
3975 influence
: FloatProperty(
3977 description
="Force of the tool",
3982 subtype
='PERCENTAGE'
3984 lock_x
: BoolProperty(
3986 description
="Lock editing of the x-coordinate",
3989 lock_y
: BoolProperty(
3991 description
="Lock editing of the y-coordinate",
3994 lock_z
: BoolProperty(
3996 description
="Lock editing of the z-coordinate",
3999 method
: EnumProperty(
4001 items
=(("project", "Project", "Project vertices onto the stroke, "
4002 "using vertex normals and connected edges"),
4003 ("irregular", "Spread", "Distribute vertices along the full "
4004 "stroke, retaining relative distances between the vertices"),
4005 ("regular", "Spread evenly", "Distribute vertices at regular "
4006 "distances along the full stroke")),
4007 description
="Method of distributing the vertices over the "
4013 def poll(cls
, context
):
4014 ob
= context
.active_object
4015 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4017 def draw(self
, context
):
4018 looptools
= context
.window_manager
.looptools
4019 layout
= self
.layout
4020 col
= layout
.column()
4022 col
.prop(self
, "method")
4025 col_conv
= col
.column(align
=True)
4026 col_conv
.prop(self
, "conversion", text
="")
4027 if self
.conversion
== 'distance':
4028 col_conv
.prop(self
, "conversion_distance")
4029 elif self
.conversion
== 'limit_vertices':
4030 row
= col_conv
.row(align
=True)
4031 row
.prop(self
, "conversion_min", text
="Min")
4032 row
.prop(self
, "conversion_max", text
="Max")
4033 elif self
.conversion
== 'vertices':
4034 col_conv
.prop(self
, "conversion_vertices")
4037 col_move
= col
.column(align
=True)
4038 row
= col_move
.row(align
=True)
4040 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
4042 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
4044 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
4046 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
4048 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
4050 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
4051 col_move
.prop(self
, "influence")
4053 if looptools
.gstretch_use_guide
== "Annotation":
4054 col
.operator("remove.annotation", text
="Delete annotation strokes")
4055 if looptools
.gstretch_use_guide
== "GPencil":
4056 col
.operator("remove.gp", text
="Delete GPencil strokes")
4058 def invoke(self
, context
, event
):
4059 # flush cached strokes
4060 if 'Gstretch' in looptools_cache
:
4061 looptools_cache
['Gstretch']['single_loops'] = []
4062 # load custom settings
4064 return self
.execute(context
)
4066 def execute(self
, context
):
4068 object, bm
= initialise()
4069 settings_write(self
)
4071 # check cache to see if we can save time
4072 cached
, safe_strokes
, loops
, derived
, mapping
= cache_read("Gstretch",
4073 object, bm
, False, False)
4075 straightening
= False
4077 strokes
= gstretch_safe_to_true_strokes(safe_strokes
)
4078 # cached strokes were flushed (see operator's invoke function)
4079 elif get_strokes(self
, context
):
4080 strokes
= gstretch_get_strokes(self
, context
)
4082 # straightening function (no GP) -> loops ignore modifiers
4083 straightening
= True
4086 bm_mod
.verts
.ensure_lookup_table()
4087 bm_mod
.edges
.ensure_lookup_table()
4088 bm_mod
.faces
.ensure_lookup_table()
4089 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
4090 if not straightening
:
4091 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
4093 # get loops and strokes
4094 if get_strokes(self
, context
):
4096 derived
, bm_mod
, loops
= get_connected_input(object, bm
, False, input='selected')
4097 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4098 loops
= check_loops(loops
, mapping
, bm_mod
)
4100 strokes
= gstretch_get_strokes(self
, context
)
4102 # straightening function (no GP) -> loops ignore modifiers
4106 bm_mod
.verts
.ensure_lookup_table()
4107 bm_mod
.edges
.ensure_lookup_table()
4108 bm_mod
.faces
.ensure_lookup_table()
4110 edgekey(edge
) for edge
in bm_mod
.edges
if
4111 edge
.select
and not edge
.hide
4113 loops
= get_connected_selections(edge_keys
)
4114 loops
= check_loops(loops
, mapping
, bm_mod
)
4115 # create fake strokes
4116 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
4118 # saving cache for faster execution next time
4121 safe_strokes
= gstretch_true_to_safe_strokes(strokes
)
4124 cache_write("Gstretch", object, bm
, False, False,
4125 safe_strokes
, loops
, derived
, mapping
)
4127 # pair loops and strokes
4128 ls_pairs
= gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
)
4129 ls_pairs
= gstretch_align_pairs(ls_pairs
, object, bm_mod
, self
.method
)
4133 # no selected geometry, convert GP to verts
4135 move
.append(gstretch_create_verts(object, bm
, strokes
,
4136 self
.method
, self
.conversion
, self
.conversion_distance
,
4137 self
.conversion_max
, self
.conversion_min
,
4138 self
.conversion_vertices
))
4139 for stroke
in strokes
:
4140 gstretch_erase_stroke(stroke
, context
)
4142 for (loop
, stroke
) in ls_pairs
:
4143 move
.append(gstretch_calculate_verts(loop
, stroke
, object,
4144 bm_mod
, self
.method
))
4145 if self
.delete_strokes
:
4146 if type(stroke
) != bpy
.types
.GPencilStroke
:
4147 # in case of cached fake stroke, get the real one
4148 if get_strokes(self
, context
):
4149 strokes
= gstretch_get_strokes(self
, context
)
4150 if loops
and strokes
:
4151 ls_pairs
= gstretch_match_loops_strokes(loops
,
4152 strokes
, object, bm_mod
)
4153 ls_pairs
= gstretch_align_pairs(ls_pairs
,
4154 object, bm_mod
, self
.method
)
4155 for (l
, s
) in ls_pairs
:
4159 gstretch_erase_stroke(stroke
, context
)
4161 # move vertices to new locations
4162 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4163 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4166 bmesh
.update_edit_mesh(object.data
, loop_triangles
=True, destructive
=True)
4167 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4178 class Relax(Operator
):
4179 bl_idname
= "mesh.looptools_relax"
4181 bl_description
= "Relax the loop, so it is smoother"
4182 bl_options
= {'REGISTER', 'UNDO'}
4184 input: EnumProperty(
4186 items
=(("all", "Parallel (all)", "Also use non-selected "
4187 "parallel loops as input"),
4188 ("selected", "Selection", "Only use selected vertices as input")),
4189 description
="Loops that are relaxed",
4192 interpolation
: EnumProperty(
4193 name
="Interpolation",
4194 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4195 ("linear", "Linear", "Simple and fast linear algorithm")),
4196 description
="Algorithm used for interpolation",
4199 iterations
: EnumProperty(
4201 items
=(("1", "1", "One"),
4202 ("3", "3", "Three"),
4204 ("10", "10", "Ten"),
4205 ("25", "25", "Twenty-five")),
4206 description
="Number of times the loop is relaxed",
4209 regular
: BoolProperty(
4211 description
="Distribute vertices at constant distances along the loop",
4216 def poll(cls
, context
):
4217 ob
= context
.active_object
4218 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4220 def draw(self
, context
):
4221 layout
= self
.layout
4222 col
= layout
.column()
4224 col
.prop(self
, "interpolation")
4225 col
.prop(self
, "input")
4226 col
.prop(self
, "iterations")
4227 col
.prop(self
, "regular")
4229 def invoke(self
, context
, event
):
4230 # load custom settings
4232 return self
.execute(context
)
4234 def execute(self
, context
):
4236 object, bm
= initialise()
4237 settings_write(self
)
4238 # check cache to see if we can save time
4239 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Relax",
4240 object, bm
, self
.input, False)
4242 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
4245 derived
, bm_mod
, loops
= get_connected_input(object, bm
, False, self
.input)
4246 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4247 loops
= check_loops(loops
, mapping
, bm_mod
)
4248 knots
, points
= relax_calculate_knots(loops
)
4250 # saving cache for faster execution next time
4252 cache_write("Relax", object, bm
, self
.input, False, False, loops
,
4255 for iteration
in range(int(self
.iterations
)):
4256 # calculate splines and new positions
4257 tknots
, tpoints
= relax_calculate_t(bm_mod
, knots
, points
,
4260 for i
in range(len(knots
)):
4261 splines
.append(calculate_splines(self
.interpolation
, bm_mod
,
4262 tknots
[i
], knots
[i
]))
4263 move
= [relax_calculate_verts(bm_mod
, self
.interpolation
,
4264 tknots
, knots
, tpoints
, points
, splines
)]
4265 move_verts(object, bm
, mapping
, move
, False, -1)
4276 class Space(Operator
):
4277 bl_idname
= "mesh.looptools_space"
4279 bl_description
= "Space the vertices in a regular distribution on the loop"
4280 bl_options
= {'REGISTER', 'UNDO'}
4282 influence
: FloatProperty(
4284 description
="Force of the tool",
4289 subtype
='PERCENTAGE'
4291 input: EnumProperty(
4293 items
=(("all", "Parallel (all)", "Also use non-selected "
4294 "parallel loops as input"),
4295 ("selected", "Selection", "Only use selected vertices as input")),
4296 description
="Loops that are spaced",
4299 interpolation
: EnumProperty(
4300 name
="Interpolation",
4301 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4302 ("linear", "Linear", "Vertices are projected on existing edges")),
4303 description
="Algorithm used for interpolation",
4306 lock_x
: BoolProperty(
4308 description
="Lock editing of the x-coordinate",
4311 lock_y
: BoolProperty(
4313 description
="Lock editing of the y-coordinate",
4316 lock_z
: BoolProperty(
4318 description
="Lock editing of the z-coordinate",
4323 def poll(cls
, context
):
4324 ob
= context
.active_object
4325 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4327 def draw(self
, context
):
4328 layout
= self
.layout
4329 col
= layout
.column()
4331 col
.prop(self
, "interpolation")
4332 col
.prop(self
, "input")
4335 col_move
= col
.column(align
=True)
4336 row
= col_move
.row(align
=True)
4338 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
4340 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
4342 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
4344 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
4346 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
4348 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
4349 col_move
.prop(self
, "influence")
4351 def invoke(self
, context
, event
):
4352 # load custom settings
4354 return self
.execute(context
)
4356 def execute(self
, context
):
4358 object, bm
= initialise()
4359 settings_write(self
)
4360 # check cache to see if we can save time
4361 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Space",
4362 object, bm
, self
.input, False)
4364 derived
, bm_mod
= get_derived_bmesh(object, bm
, True)
4367 derived
, bm_mod
, loops
= get_connected_input(object, bm
, True, self
.input)
4368 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4369 loops
= check_loops(loops
, mapping
, bm_mod
)
4371 # saving cache for faster execution next time
4373 cache_write("Space", object, bm
, self
.input, False, False, loops
,
4378 # calculate splines and new positions
4379 if loop
[1]: # circular
4380 loop
[0].append(loop
[0][0])
4381 tknots
, tpoints
= space_calculate_t(bm_mod
, loop
[0][:])
4382 splines
= calculate_splines(self
.interpolation
, bm_mod
,
4384 move
.append(space_calculate_verts(bm_mod
, self
.interpolation
,
4385 tknots
, tpoints
, loop
[0][:-1], splines
))
4386 # move vertices to new locations
4387 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4388 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4391 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4398 cache_delete("Space")
4403 # ########################################
4404 # ##### GUI and registration #############
4405 # ########################################
4407 # menu containing all tools
4408 class VIEW3D_MT_edit_mesh_looptools(Menu
):
4409 bl_label
= "LoopTools"
4411 def draw(self
, context
):
4412 layout
= self
.layout
4414 layout
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4415 layout
.operator("mesh.looptools_circle")
4416 layout
.operator("mesh.looptools_curve")
4417 layout
.operator("mesh.looptools_flatten")
4418 layout
.operator("mesh.looptools_gstretch")
4419 layout
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4420 layout
.operator("mesh.looptools_relax")
4421 layout
.operator("mesh.looptools_space")
4424 # panel containing all tools
4425 class VIEW3D_PT_tools_looptools(Panel
):
4426 bl_space_type
= 'VIEW_3D'
4427 bl_region_type
= 'UI'
4428 bl_category
= 'Edit'
4429 bl_context
= "mesh_edit"
4430 bl_label
= "LoopTools"
4431 bl_options
= {'DEFAULT_CLOSED'}
4433 def draw(self
, context
):
4434 layout
= self
.layout
4435 col
= layout
.column(align
=True)
4436 lt
= context
.window_manager
.looptools
4438 # bridge - first line
4439 split
= col
.split(factor
=0.15, align
=True)
4440 if lt
.display_bridge
:
4441 split
.prop(lt
, "display_bridge", text
="", icon
='DOWNARROW_HLT')
4443 split
.prop(lt
, "display_bridge", text
="", icon
='RIGHTARROW')
4444 split
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4446 if lt
.display_bridge
:
4447 box
= col
.column(align
=True).box().column()
4448 # box.prop(self, "mode")
4451 col_top
= box
.column(align
=True)
4452 row
= col_top
.row(align
=True)
4453 col_left
= row
.column(align
=True)
4454 col_right
= row
.column(align
=True)
4455 col_right
.active
= lt
.bridge_segments
!= 1
4456 col_left
.prop(lt
, "bridge_segments")
4457 col_right
.prop(lt
, "bridge_min_width", text
="")
4459 bottom_left
= col_left
.row()
4460 bottom_left
.active
= lt
.bridge_segments
!= 1
4461 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4462 bottom_right
= col_right
.row()
4463 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4464 bottom_right
.prop(lt
, "bridge_cubic_strength")
4465 # boolean properties
4466 col_top
.prop(lt
, "bridge_remove_faces")
4468 # override properties
4470 row
= box
.row(align
=True)
4471 row
.prop(lt
, "bridge_twist")
4472 row
.prop(lt
, "bridge_reverse")
4474 # circle - first line
4475 split
= col
.split(factor
=0.15, align
=True)
4476 if lt
.display_circle
:
4477 split
.prop(lt
, "display_circle", text
="", icon
='DOWNARROW_HLT')
4479 split
.prop(lt
, "display_circle", text
="", icon
='RIGHTARROW')
4480 split
.operator("mesh.looptools_circle")
4482 if lt
.display_circle
:
4483 box
= col
.column(align
=True).box().column()
4484 box
.prop(lt
, "circle_fit")
4487 box
.prop(lt
, "circle_flatten")
4488 row
= box
.row(align
=True)
4489 row
.prop(lt
, "circle_custom_radius")
4490 row_right
= row
.row(align
=True)
4491 row_right
.active
= lt
.circle_custom_radius
4492 row_right
.prop(lt
, "circle_radius", text
="")
4493 box
.prop(lt
, "circle_regular")
4496 col_move
= box
.column(align
=True)
4497 row
= col_move
.row(align
=True)
4498 if lt
.circle_lock_x
:
4499 row
.prop(lt
, "circle_lock_x", text
="X", icon
='LOCKED')
4501 row
.prop(lt
, "circle_lock_x", text
="X", icon
='UNLOCKED')
4502 if lt
.circle_lock_y
:
4503 row
.prop(lt
, "circle_lock_y", text
="Y", icon
='LOCKED')
4505 row
.prop(lt
, "circle_lock_y", text
="Y", icon
='UNLOCKED')
4506 if lt
.circle_lock_z
:
4507 row
.prop(lt
, "circle_lock_z", text
="Z", icon
='LOCKED')
4509 row
.prop(lt
, "circle_lock_z", text
="Z", icon
='UNLOCKED')
4510 col_move
.prop(lt
, "circle_influence")
4512 # curve - first line
4513 split
= col
.split(factor
=0.15, align
=True)
4514 if lt
.display_curve
:
4515 split
.prop(lt
, "display_curve", text
="", icon
='DOWNARROW_HLT')
4517 split
.prop(lt
, "display_curve", text
="", icon
='RIGHTARROW')
4518 split
.operator("mesh.looptools_curve")
4520 if lt
.display_curve
:
4521 box
= col
.column(align
=True).box().column()
4522 box
.prop(lt
, "curve_interpolation")
4523 box
.prop(lt
, "curve_restriction")
4524 box
.prop(lt
, "curve_boundaries")
4525 box
.prop(lt
, "curve_regular")
4528 col_move
= box
.column(align
=True)
4529 row
= col_move
.row(align
=True)
4531 row
.prop(lt
, "curve_lock_x", text
="X", icon
='LOCKED')
4533 row
.prop(lt
, "curve_lock_x", text
="X", icon
='UNLOCKED')
4535 row
.prop(lt
, "curve_lock_y", text
="Y", icon
='LOCKED')
4537 row
.prop(lt
, "curve_lock_y", text
="Y", icon
='UNLOCKED')
4539 row
.prop(lt
, "curve_lock_z", text
="Z", icon
='LOCKED')
4541 row
.prop(lt
, "curve_lock_z", text
="Z", icon
='UNLOCKED')
4542 col_move
.prop(lt
, "curve_influence")
4544 # flatten - first line
4545 split
= col
.split(factor
=0.15, align
=True)
4546 if lt
.display_flatten
:
4547 split
.prop(lt
, "display_flatten", text
="", icon
='DOWNARROW_HLT')
4549 split
.prop(lt
, "display_flatten", text
="", icon
='RIGHTARROW')
4550 split
.operator("mesh.looptools_flatten")
4551 # flatten - settings
4552 if lt
.display_flatten
:
4553 box
= col
.column(align
=True).box().column()
4554 box
.prop(lt
, "flatten_plane")
4555 # box.prop(lt, "flatten_restriction")
4558 col_move
= box
.column(align
=True)
4559 row
= col_move
.row(align
=True)
4560 if lt
.flatten_lock_x
:
4561 row
.prop(lt
, "flatten_lock_x", text
="X", icon
='LOCKED')
4563 row
.prop(lt
, "flatten_lock_x", text
="X", icon
='UNLOCKED')
4564 if lt
.flatten_lock_y
:
4565 row
.prop(lt
, "flatten_lock_y", text
="Y", icon
='LOCKED')
4567 row
.prop(lt
, "flatten_lock_y", text
="Y", icon
='UNLOCKED')
4568 if lt
.flatten_lock_z
:
4569 row
.prop(lt
, "flatten_lock_z", text
="Z", icon
='LOCKED')
4571 row
.prop(lt
, "flatten_lock_z", text
="Z", icon
='UNLOCKED')
4572 col_move
.prop(lt
, "flatten_influence")
4574 # gstretch - first line
4575 split
= col
.split(factor
=0.15, align
=True)
4576 if lt
.display_gstretch
:
4577 split
.prop(lt
, "display_gstretch", text
="", icon
='DOWNARROW_HLT')
4579 split
.prop(lt
, "display_gstretch", text
="", icon
='RIGHTARROW')
4580 split
.operator("mesh.looptools_gstretch")
4582 if lt
.display_gstretch
:
4583 box
= col
.column(align
=True).box().column()
4584 box
.prop(lt
, "gstretch_use_guide")
4585 if lt
.gstretch_use_guide
== "GPencil":
4586 box
.prop(lt
, "gstretch_guide")
4587 box
.prop(lt
, "gstretch_method")
4589 col_conv
= box
.column(align
=True)
4590 col_conv
.prop(lt
, "gstretch_conversion", text
="")
4591 if lt
.gstretch_conversion
== 'distance':
4592 col_conv
.prop(lt
, "gstretch_conversion_distance")
4593 elif lt
.gstretch_conversion
== 'limit_vertices':
4594 row
= col_conv
.row(align
=True)
4595 row
.prop(lt
, "gstretch_conversion_min", text
="Min")
4596 row
.prop(lt
, "gstretch_conversion_max", text
="Max")
4597 elif lt
.gstretch_conversion
== 'vertices':
4598 col_conv
.prop(lt
, "gstretch_conversion_vertices")
4601 col_move
= box
.column(align
=True)
4602 row
= col_move
.row(align
=True)
4603 if lt
.gstretch_lock_x
:
4604 row
.prop(lt
, "gstretch_lock_x", text
="X", icon
='LOCKED')
4606 row
.prop(lt
, "gstretch_lock_x", text
="X", icon
='UNLOCKED')
4607 if lt
.gstretch_lock_y
:
4608 row
.prop(lt
, "gstretch_lock_y", text
="Y", icon
='LOCKED')
4610 row
.prop(lt
, "gstretch_lock_y", text
="Y", icon
='UNLOCKED')
4611 if lt
.gstretch_lock_z
:
4612 row
.prop(lt
, "gstretch_lock_z", text
="Z", icon
='LOCKED')
4614 row
.prop(lt
, "gstretch_lock_z", text
="Z", icon
='UNLOCKED')
4615 col_move
.prop(lt
, "gstretch_influence")
4616 if lt
.gstretch_use_guide
== "Annotation":
4617 box
.operator("remove.annotation", text
="Delete Annotation Strokes")
4618 if lt
.gstretch_use_guide
== "GPencil":
4619 box
.operator("remove.gp", text
="Delete GPencil Strokes")
4622 split
= col
.split(factor
=0.15, align
=True)
4624 split
.prop(lt
, "display_loft", text
="", icon
='DOWNARROW_HLT')
4626 split
.prop(lt
, "display_loft", text
="", icon
='RIGHTARROW')
4627 split
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4630 box
= col
.column(align
=True).box().column()
4631 # box.prop(self, "mode")
4634 col_top
= box
.column(align
=True)
4635 row
= col_top
.row(align
=True)
4636 col_left
= row
.column(align
=True)
4637 col_right
= row
.column(align
=True)
4638 col_right
.active
= lt
.bridge_segments
!= 1
4639 col_left
.prop(lt
, "bridge_segments")
4640 col_right
.prop(lt
, "bridge_min_width", text
="")
4642 bottom_left
= col_left
.row()
4643 bottom_left
.active
= lt
.bridge_segments
!= 1
4644 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4645 bottom_right
= col_right
.row()
4646 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4647 bottom_right
.prop(lt
, "bridge_cubic_strength")
4648 # boolean properties
4649 col_top
.prop(lt
, "bridge_remove_faces")
4650 col_top
.prop(lt
, "bridge_loft_loop")
4652 # override properties
4654 row
= box
.row(align
=True)
4655 row
.prop(lt
, "bridge_twist")
4656 row
.prop(lt
, "bridge_reverse")
4658 # relax - first line
4659 split
= col
.split(factor
=0.15, align
=True)
4660 if lt
.display_relax
:
4661 split
.prop(lt
, "display_relax", text
="", icon
='DOWNARROW_HLT')
4663 split
.prop(lt
, "display_relax", text
="", icon
='RIGHTARROW')
4664 split
.operator("mesh.looptools_relax")
4666 if lt
.display_relax
:
4667 box
= col
.column(align
=True).box().column()
4668 box
.prop(lt
, "relax_interpolation")
4669 box
.prop(lt
, "relax_input")
4670 box
.prop(lt
, "relax_iterations")
4671 box
.prop(lt
, "relax_regular")
4673 # space - first line
4674 split
= col
.split(factor
=0.15, align
=True)
4675 if lt
.display_space
:
4676 split
.prop(lt
, "display_space", text
="", icon
='DOWNARROW_HLT')
4678 split
.prop(lt
, "display_space", text
="", icon
='RIGHTARROW')
4679 split
.operator("mesh.looptools_space")
4681 if lt
.display_space
:
4682 box
= col
.column(align
=True).box().column()
4683 box
.prop(lt
, "space_interpolation")
4684 box
.prop(lt
, "space_input")
4687 col_move
= box
.column(align
=True)
4688 row
= col_move
.row(align
=True)
4690 row
.prop(lt
, "space_lock_x", text
="X", icon
='LOCKED')
4692 row
.prop(lt
, "space_lock_x", text
="X", icon
='UNLOCKED')
4694 row
.prop(lt
, "space_lock_y", text
="Y", icon
='LOCKED')
4696 row
.prop(lt
, "space_lock_y", text
="Y", icon
='UNLOCKED')
4698 row
.prop(lt
, "space_lock_z", text
="Z", icon
='LOCKED')
4700 row
.prop(lt
, "space_lock_z", text
="Z", icon
='UNLOCKED')
4701 col_move
.prop(lt
, "space_influence")
4704 # property group containing all properties for the gui in the panel
4705 class LoopToolsProps(PropertyGroup
):
4707 Fake module like class
4708 bpy.context.window_manager.looptools
4710 # general display properties
4711 display_bridge
: BoolProperty(
4712 name
="Bridge settings",
4713 description
="Display settings of the Bridge tool",
4716 display_circle
: BoolProperty(
4717 name
="Circle settings",
4718 description
="Display settings of the Circle tool",
4721 display_curve
: BoolProperty(
4722 name
="Curve settings",
4723 description
="Display settings of the Curve tool",
4726 display_flatten
: BoolProperty(
4727 name
="Flatten settings",
4728 description
="Display settings of the Flatten tool",
4731 display_gstretch
: BoolProperty(
4732 name
="Gstretch settings",
4733 description
="Display settings of the Gstretch tool",
4736 display_loft
: BoolProperty(
4737 name
="Loft settings",
4738 description
="Display settings of the Loft tool",
4741 display_relax
: BoolProperty(
4742 name
="Relax settings",
4743 description
="Display settings of the Relax tool",
4746 display_space
: BoolProperty(
4747 name
="Space settings",
4748 description
="Display settings of the Space tool",
4753 bridge_cubic_strength
: FloatProperty(
4755 description
="Higher strength results in more fluid curves",
4760 bridge_interpolation
: EnumProperty(
4761 name
="Interpolation mode",
4762 items
=(('cubic', "Cubic", "Gives curved results"),
4763 ('linear', "Linear", "Basic, fast, straight interpolation")),
4764 description
="Interpolation mode: algorithm used when creating segments",
4767 bridge_loft
: BoolProperty(
4769 description
="Loft multiple loops, instead of considering them as "
4770 "a multi-input for bridging",
4773 bridge_loft_loop
: BoolProperty(
4775 description
="Connect the first and the last loop with each other",
4778 bridge_min_width
: IntProperty(
4779 name
="Minimum width",
4780 description
="Segments with an edge smaller than this are merged "
4781 "(compared to base edge)",
4785 subtype
='PERCENTAGE'
4787 bridge_mode
: EnumProperty(
4789 items
=(('basic', "Basic", "Fast algorithm"),
4790 ('shortest', "Shortest edge", "Slower algorithm with "
4791 "better vertex matching")),
4792 description
="Algorithm used for bridging",
4795 bridge_remove_faces
: BoolProperty(
4796 name
="Remove faces",
4797 description
="Remove faces that are internal after bridging",
4800 bridge_reverse
: BoolProperty(
4802 description
="Manually override the direction in which the loops "
4803 "are bridged. Only use if the tool gives the wrong result",
4806 bridge_segments
: IntProperty(
4808 description
="Number of segments used to bridge the gap (0=automatic)",
4813 bridge_twist
: IntProperty(
4815 description
="Twist what vertices are connected to each other",
4820 circle_custom_radius
: BoolProperty(
4822 description
="Force a custom radius",
4825 circle_fit
: EnumProperty(
4827 items
=(("best", "Best fit", "Non-linear least squares"),
4828 ("inside", "Fit inside", "Only move vertices towards the center")),
4829 description
="Method used for fitting a circle to the vertices",
4832 circle_flatten
: BoolProperty(
4834 description
="Flatten the circle, instead of projecting it on the mesh",
4837 circle_influence
: FloatProperty(
4839 description
="Force of the tool",
4844 subtype
='PERCENTAGE'
4846 circle_lock_x
: BoolProperty(
4848 description
="Lock editing of the x-coordinate",
4851 circle_lock_y
: BoolProperty(
4853 description
="Lock editing of the y-coordinate",
4856 circle_lock_z
: BoolProperty(
4858 description
="Lock editing of the z-coordinate",
4861 circle_radius
: FloatProperty(
4863 description
="Custom radius for circle",
4868 circle_regular
: BoolProperty(
4870 description
="Distribute vertices at constant distances along the circle",
4873 circle_angle
: FloatProperty(
4875 description
="Rotate a circle by an angle",
4877 default
=math
.radians(0.0),
4878 soft_min
=math
.radians(-360.0),
4879 soft_max
=math
.radians(360.0)
4882 curve_boundaries
: BoolProperty(
4884 description
="Limit the tool to work within the boundaries of the "
4885 "selected vertices",
4888 curve_influence
: FloatProperty(
4890 description
="Force of the tool",
4895 subtype
='PERCENTAGE'
4897 curve_interpolation
: EnumProperty(
4898 name
="Interpolation",
4899 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4900 ("linear", "Linear", "Simple and fast linear algorithm")),
4901 description
="Algorithm used for interpolation",
4904 curve_lock_x
: BoolProperty(
4906 description
="Lock editing of the x-coordinate",
4909 curve_lock_y
: BoolProperty(
4911 description
="Lock editing of the y-coordinate",
4914 curve_lock_z
: BoolProperty(
4916 description
="Lock editing of the z-coordinate",
4919 curve_regular
: BoolProperty(
4921 description
="Distribute vertices at constant distances along the curve",
4924 curve_restriction
: EnumProperty(
4926 items
=(("none", "None", "No restrictions on vertex movement"),
4927 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
4928 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
4929 description
="Restrictions on how the vertices can be moved",
4933 # flatten properties
4934 flatten_influence
: FloatProperty(
4936 description
="Force of the tool",
4941 subtype
='PERCENTAGE'
4943 flatten_lock_x
: BoolProperty(
4945 description
="Lock editing of the x-coordinate",
4947 flatten_lock_y
: BoolProperty(name
="Lock Y",
4948 description
="Lock editing of the y-coordinate",
4951 flatten_lock_z
: BoolProperty(
4953 description
="Lock editing of the z-coordinate",
4956 flatten_plane
: EnumProperty(
4958 items
=(("best_fit", "Best fit", "Calculate a best fitting plane"),
4959 ("normal", "Normal", "Derive plane from averaging vertex "
4961 ("view", "View", "Flatten on a plane perpendicular to the "
4963 description
="Plane on which vertices are flattened",
4966 flatten_restriction
: EnumProperty(
4968 items
=(("none", "None", "No restrictions on vertex movement"),
4969 ("bounding_box", "Bounding box", "Vertices are restricted to "
4970 "movement inside the bounding box of the selection")),
4971 description
="Restrictions on how the vertices can be moved",
4975 # gstretch properties
4976 gstretch_conversion
: EnumProperty(
4978 items
=(("distance", "Distance", "Set the distance between vertices "
4979 "of the converted stroke"),
4980 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
4981 "number of vertices that converted GP strokes will have"),
4982 ("vertices", "Exact vertices", "Set the exact number of vertices "
4983 "that converted strokes will have. Short strokes "
4984 "with few points may contain less vertices than this number."),
4985 ("none", "No simplification", "Convert each point "
4987 description
="If strokes are converted to geometry, "
4988 "use this simplification method",
4989 default
='limit_vertices'
4991 gstretch_conversion_distance
: FloatProperty(
4993 description
="Absolute distance between vertices along the converted "
5000 gstretch_conversion_max
: IntProperty(
5001 name
="Max Vertices",
5002 description
="Maximum number of vertices strokes will "
5003 "have, when they are converted to geometry",
5007 update
=gstretch_update_min
5009 gstretch_conversion_min
: IntProperty(
5010 name
="Min Vertices",
5011 description
="Minimum number of vertices strokes will "
5012 "have, when they are converted to geometry",
5016 update
=gstretch_update_max
5018 gstretch_conversion_vertices
: IntProperty(
5020 description
="Number of vertices strokes will "
5021 "have, when they are converted to geometry. If strokes have less "
5022 "points than required, the 'Spread evenly' method is used",
5027 gstretch_delete_strokes
: BoolProperty(
5028 name
="Delete strokes",
5029 description
="Remove Grease Pencil strokes if they have been used "
5030 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
5033 gstretch_influence
: FloatProperty(
5035 description
="Force of the tool",
5040 subtype
='PERCENTAGE'
5042 gstretch_lock_x
: BoolProperty(
5044 description
="Lock editing of the x-coordinate",
5047 gstretch_lock_y
: BoolProperty(
5049 description
="Lock editing of the y-coordinate",
5052 gstretch_lock_z
: BoolProperty(
5054 description
="Lock editing of the z-coordinate",
5057 gstretch_method
: EnumProperty(
5059 items
=(("project", "Project", "Project vertices onto the stroke, "
5060 "using vertex normals and connected edges"),
5061 ("irregular", "Spread", "Distribute vertices along the full "
5062 "stroke, retaining relative distances between the vertices"),
5063 ("regular", "Spread evenly", "Distribute vertices at regular "
5064 "distances along the full stroke")),
5065 description
="Method of distributing the vertices over the Grease "
5069 gstretch_use_guide
: EnumProperty(
5071 items
=(("None", "None", "None"),
5072 ("Annotation", "Annotation", "Annotation"),
5073 ("GPencil", "GPencil", "GPencil")),
5076 gstretch_guide
: PointerProperty(
5077 name
="GPencil object",
5078 description
="Set GPencil object",
5079 type=bpy
.types
.Object
5083 relax_input
: EnumProperty(name
="Input",
5084 items
=(("all", "Parallel (all)", "Also use non-selected "
5085 "parallel loops as input"),
5086 ("selected", "Selection", "Only use selected vertices as input")),
5087 description
="Loops that are relaxed",
5090 relax_interpolation
: EnumProperty(
5091 name
="Interpolation",
5092 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5093 ("linear", "Linear", "Simple and fast linear algorithm")),
5094 description
="Algorithm used for interpolation",
5097 relax_iterations
: EnumProperty(name
="Iterations",
5098 items
=(("1", "1", "One"),
5099 ("3", "3", "Three"),
5101 ("10", "10", "Ten"),
5102 ("25", "25", "Twenty-five")),
5103 description
="Number of times the loop is relaxed",
5106 relax_regular
: BoolProperty(
5108 description
="Distribute vertices at constant distances along the loop",
5113 space_influence
: FloatProperty(
5115 description
="Force of the tool",
5120 subtype
='PERCENTAGE'
5122 space_input
: EnumProperty(
5124 items
=(("all", "Parallel (all)", "Also use non-selected "
5125 "parallel loops as input"),
5126 ("selected", "Selection", "Only use selected vertices as input")),
5127 description
="Loops that are spaced",
5130 space_interpolation
: EnumProperty(
5131 name
="Interpolation",
5132 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5133 ("linear", "Linear", "Vertices are projected on existing edges")),
5134 description
="Algorithm used for interpolation",
5137 space_lock_x
: BoolProperty(
5139 description
="Lock editing of the x-coordinate",
5142 space_lock_y
: BoolProperty(
5144 description
="Lock editing of the y-coordinate",
5147 space_lock_z
: BoolProperty(
5149 description
="Lock editing of the z-coordinate",
5153 # draw function for integration in menus
5154 def menu_func(self
, context
):
5155 self
.layout
.menu("VIEW3D_MT_edit_mesh_looptools")
5156 self
.layout
.separator()
5159 # Add-ons Preferences Update Panel
5161 # Define Panel classes for updating
5163 VIEW3D_PT_tools_looptools
,
5167 def update_panel(self
, context
):
5168 message
= "LoopTools: Updating Panel locations has failed"
5170 for panel
in panels
:
5171 if "bl_rna" in panel
.__dict
__:
5172 bpy
.utils
.unregister_class(panel
)
5174 for panel
in panels
:
5175 panel
.bl_category
= context
.preferences
.addons
[__name__
].preferences
.category
5176 bpy
.utils
.register_class(panel
)
5178 except Exception as e
:
5179 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
5183 class LoopPreferences(AddonPreferences
):
5184 # this must match the addon name, use '__package__'
5185 # when defining this in a submodule of a python package.
5186 bl_idname
= __name__
5188 category
: StringProperty(
5189 name
="Tab Category",
5190 description
="Choose a name for the category of the panel",
5195 def draw(self
, context
):
5196 layout
= self
.layout
5200 col
.label(text
="Tab Category:")
5201 col
.prop(self
, "category", text
="")
5204 # define classes for registration
5206 VIEW3D_MT_edit_mesh_looptools
,
5207 VIEW3D_PT_tools_looptools
,
5222 # registering and menu integration
5225 bpy
.utils
.register_class(cls
)
5226 bpy
.types
.VIEW3D_MT_edit_mesh_context_menu
.prepend(menu_func
)
5227 bpy
.types
.WindowManager
.looptools
= PointerProperty(type=LoopToolsProps
)
5228 update_panel(None, bpy
.context
)
5231 # unregistering and removing menus
5233 for cls
in reversed(classes
):
5234 bpy
.utils
.unregister_class(cls
)
5235 bpy
.types
.VIEW3D_MT_edit_mesh_context_menu
.remove(menu_func
)
5237 del bpy
.types
.WindowManager
.looptools
5238 except Exception as e
:
5239 print('unregister fail:\n', e
)
5243 if __name__
== "__main__":