1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
18 # Contributed to Germano Cavalcante (mano-wii), Florian Meyer (testscreenings),
19 # Brendon Murphy (meta-androcto),
20 # Maintainer: Vladimir Spivak (cwolf3d)
21 # Originally an addon by Bart Crouch
25 "author": "Bart Crouch, Vladimir Spivak (cwolf3d)",
27 "blender": (2, 80, 0),
28 "location": "View3D > Sidebar > Edit Tab / Edit Mode Context Menu",
30 "description": "Mesh modelling toolkit. Several tools to aid modelling",
31 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/looptools.html",
41 from bpy_extras
import view3d_utils
42 from bpy
.types
import (
49 from bpy
.props
import (
58 # ########################################
59 # ##### General functions ################
60 # ########################################
62 # used by all tools to improve speed on reruns Unlink
66 def get_strokes(self
, context
):
67 looptools
= context
.window_manager
.looptools
68 if looptools
.gstretch_use_guide
== "Annotation":
70 strokes
= bpy
.data
.grease_pencils
[0].layers
.active
.active_frame
.strokes
73 self
.report({'WARNING'}, "active Annotation strokes not found")
75 if looptools
.gstretch_use_guide
== "GPencil" and not looptools
.gstretch_guide
== None:
77 strokes
= looptools
.gstretch_guide
.data
.layers
.active
.active_frame
.strokes
80 self
.report({'WARNING'}, "active GPencil strokes not found")
85 # force a full recalculation next time
86 def cache_delete(tool
):
87 if tool
in looptools_cache
:
88 del looptools_cache
[tool
]
91 # check cache for stored information
92 def cache_read(tool
, object, bm
, input_method
, boundaries
):
93 # current tool not cached yet
94 if tool
not in looptools_cache
:
95 return(False, False, False, False, False)
96 # check if selected object didn't change
97 if object.name
!= looptools_cache
[tool
]["object"]:
98 return(False, False, False, False, False)
99 # check if input didn't change
100 if input_method
!= looptools_cache
[tool
]["input_method"]:
101 return(False, False, False, False, False)
102 if boundaries
!= looptools_cache
[tool
]["boundaries"]:
103 return(False, False, False, False, False)
104 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
and
105 mod
.type == 'MIRROR']
106 if modifiers
!= looptools_cache
[tool
]["modifiers"]:
107 return(False, False, False, False, False)
108 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
109 if input != looptools_cache
[tool
]["input"]:
110 return(False, False, False, False, False)
112 single_loops
= looptools_cache
[tool
]["single_loops"]
113 loops
= looptools_cache
[tool
]["loops"]
114 derived
= looptools_cache
[tool
]["derived"]
115 mapping
= looptools_cache
[tool
]["mapping"]
117 return(True, single_loops
, loops
, derived
, mapping
)
120 # store information in the cache
121 def cache_write(tool
, object, bm
, input_method
, boundaries
, single_loops
,
122 loops
, derived
, mapping
):
123 # clear cache of current tool
124 if tool
in looptools_cache
:
125 del looptools_cache
[tool
]
126 # prepare values to be saved to cache
127 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
128 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
129 and mod
.type == 'MIRROR']
131 looptools_cache
[tool
] = {
132 "input": input, "object": object.name
,
133 "input_method": input_method
, "boundaries": boundaries
,
134 "single_loops": single_loops
, "loops": loops
,
135 "derived": derived
, "mapping": mapping
, "modifiers": modifiers
}
138 # calculates natural cubic splines through all given knots
139 def calculate_cubic_splines(bm_mod
, tknots
, knots
):
140 # hack for circular loops
141 if knots
[0] == knots
[-1] and len(knots
) > 1:
144 for k
in range(-1, -5, -1):
145 if k
- 1 < -len(knots
):
147 k_new1
.append(knots
[k
- 1])
150 if k
+ 1 > len(knots
) - 1:
152 k_new2
.append(knots
[k
+ 1])
159 for t
in range(-1, -5, -1):
160 if t
- 1 < -len(tknots
):
162 total1
+= tknots
[t
] - tknots
[t
- 1]
163 t_new1
.append(tknots
[0] - total1
)
167 if t
+ 1 > len(tknots
) - 1:
169 total2
+= tknots
[t
+ 1] - tknots
[t
]
170 t_new2
.append(tknots
[-1] + total2
)
183 locs
= [bm_mod
.verts
[k
].co
[:] for k
in knots
]
190 for i
in range(n
- 1):
191 if x
[i
+ 1] - x
[i
] == 0:
194 h
.append(x
[i
+ 1] - x
[i
])
196 for i
in range(1, n
- 1):
197 q
.append(3 / h
[i
] * (a
[i
+ 1] - a
[i
]) - 3 / h
[i
- 1] * (a
[i
] - a
[i
- 1]))
201 for i
in range(1, n
- 1):
202 l
.append(2 * (x
[i
+ 1] - x
[i
- 1]) - h
[i
- 1] * u
[i
- 1])
205 u
.append(h
[i
] / l
[i
])
206 z
.append((q
[i
] - h
[i
- 1] * z
[i
- 1]) / l
[i
])
209 b
= [False for i
in range(n
- 1)]
210 c
= [False for i
in range(n
)]
211 d
= [False for i
in range(n
- 1)]
213 for i
in range(n
- 2, -1, -1):
214 c
[i
] = z
[i
] - u
[i
] * c
[i
+ 1]
215 b
[i
] = (a
[i
+ 1] - a
[i
]) / h
[i
] - h
[i
] * (c
[i
+ 1] + 2 * c
[i
]) / 3
216 d
[i
] = (c
[i
+ 1] - c
[i
]) / (3 * h
[i
])
217 for i
in range(n
- 1):
218 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
220 for i
in range(len(knots
) - 1):
221 splines
.append([result
[i
], result
[i
+ n
- 1], result
[i
+ (n
- 1) * 2]])
222 if circular
: # cleaning up after hack
224 tknots
= tknots
[4:-4]
229 # calculates linear splines through all given knots
230 def calculate_linear_splines(bm_mod
, tknots
, knots
):
232 for i
in range(len(knots
) - 1):
233 a
= bm_mod
.verts
[knots
[i
]].co
234 b
= bm_mod
.verts
[knots
[i
+ 1]].co
237 u
= tknots
[i
+ 1] - t
238 splines
.append([a
, d
, t
, u
]) # [locStart, locDif, tStart, tDif]
243 # calculate a best-fit plane to the given vertices
244 def calculate_plane(bm_mod
, loop
, method
="best_fit", object=False):
245 # getting the vertex locations
246 locs
= [bm_mod
.verts
[v
].co
.copy() for v
in loop
[0]]
248 # calculating the center of masss
249 com
= mathutils
.Vector()
255 if method
== 'best_fit':
256 # creating the covariance matrix
257 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
262 mat
[0][0] += (loc
[0] - x
) ** 2
263 mat
[1][0] += (loc
[0] - x
) * (loc
[1] - y
)
264 mat
[2][0] += (loc
[0] - x
) * (loc
[2] - z
)
265 mat
[0][1] += (loc
[1] - y
) * (loc
[0] - x
)
266 mat
[1][1] += (loc
[1] - y
) ** 2
267 mat
[2][1] += (loc
[1] - y
) * (loc
[2] - z
)
268 mat
[0][2] += (loc
[2] - z
) * (loc
[0] - x
)
269 mat
[1][2] += (loc
[2] - z
) * (loc
[1] - y
)
270 mat
[2][2] += (loc
[2] - z
) ** 2
272 # calculating the normal to the plane
275 mat
= matrix_invert(mat
)
278 if math
.fabs(sum(mat
[0])) < math
.fabs(sum(mat
[1])):
279 if math
.fabs(sum(mat
[0])) < math
.fabs(sum(mat
[2])):
281 elif math
.fabs(sum(mat
[1])) < math
.fabs(sum(mat
[2])):
284 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
286 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
288 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
290 # warning! this is different from .normalize()
292 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
293 for i
in range(itermax
):
296 # Calculate length with double precision to avoid problems with `inf`
297 vec2_length
= math
.sqrt(vec2
[0] ** 2 + vec2
[1] ** 2 + vec2
[2] ** 2)
303 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
306 elif method
== 'normal':
307 # averaging the vertex normals
308 v_normals
= [bm_mod
.verts
[v
].normal
for v
in loop
[0]]
309 normal
= mathutils
.Vector()
310 for v_normal
in v_normals
:
312 normal
/= len(v_normals
)
315 elif method
== 'view':
316 # calculate view normal
317 rotation
= bpy
.context
.space_data
.region_3d
.view_matrix
.to_3x3().\
319 normal
= rotation
@ mathutils
.Vector((0.0, 0.0, 1.0))
321 normal
= object.matrix_world
.inverted().to_euler().to_matrix() @ \
327 # calculate splines based on given interpolation method (controller function)
328 def calculate_splines(interpolation
, bm_mod
, tknots
, knots
):
329 if interpolation
== 'cubic':
330 splines
= calculate_cubic_splines(bm_mod
, tknots
, knots
[:])
331 else: # interpolations == 'linear'
332 splines
= calculate_linear_splines(bm_mod
, tknots
, knots
[:])
337 # check loops and only return valid ones
338 def check_loops(loops
, mapping
, bm_mod
):
340 for loop
, circular
in loops
:
341 # loop needs to have at least 3 vertices
344 # loop needs at least 1 vertex in the original, non-mirrored mesh
348 if mapping
[vert
] > -1:
353 # vertices can not all be at the same location
355 for i
in range(len(loop
) - 1):
356 if (bm_mod
.verts
[loop
[i
]].co
- bm_mod
.verts
[loop
[i
+ 1]].co
).length
> 1e-6:
361 # passed all tests, loop is valid
362 valid_loops
.append([loop
, circular
])
367 # input: bmesh, output: dict with the edge-key as key and face-index as value
368 def dict_edge_faces(bm
):
369 edge_faces
= dict([[edgekey(edge
), []] for edge
in bm
.edges
if not edge
.hide
])
370 for face
in bm
.faces
:
373 for key
in face_edgekeys(face
):
374 edge_faces
[key
].append(face
.index
)
379 # input: bmesh (edge-faces optional), output: dict with face-face connections
380 def dict_face_faces(bm
, edge_faces
=False):
382 edge_faces
= dict_edge_faces(bm
)
384 connected_faces
= dict([[face
.index
, []] for face
in bm
.faces
if not face
.hide
])
385 for face
in bm
.faces
:
388 for edge_key
in face_edgekeys(face
):
389 for connected_face
in edge_faces
[edge_key
]:
390 if connected_face
== face
.index
:
392 connected_faces
[face
.index
].append(connected_face
)
394 return(connected_faces
)
397 # input: bmesh, output: dict with the vert index as key and edge-keys as value
398 def dict_vert_edges(bm
):
399 vert_edges
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
400 for edge
in bm
.edges
:
405 vert_edges
[vert
].append(ek
)
410 # input: bmesh, output: dict with the vert index as key and face index as value
411 def dict_vert_faces(bm
):
412 vert_faces
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
413 for face
in bm
.faces
:
415 for vert
in face
.verts
:
416 vert_faces
[vert
.index
].append(face
.index
)
421 # input: list of edge-keys, output: dictionary with vertex-vertex connections
422 def dict_vert_verts(edge_keys
):
423 # create connection data
427 if ek
[i
] in vert_verts
:
428 vert_verts
[ek
[i
]].append(ek
[1 - i
])
430 vert_verts
[ek
[i
]] = [ek
[1 - i
]]
435 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
437 return(tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])))
440 # returns the edgekeys of a bmesh face
441 def face_edgekeys(face
):
442 return([tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])) for edge
in face
.edges
])
445 # calculate input loops
446 def get_connected_input(object, bm
, not_use_mirror
, input):
447 # get mesh with modifiers applied
448 derived
, bm_mod
= get_derived_bmesh(object, bm
, not_use_mirror
)
450 # calculate selected loops
451 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and not edge
.hide
]
452 loops
= get_connected_selections(edge_keys
)
454 # if only selected loops are needed, we're done
455 if input == 'selected':
456 return(derived
, bm_mod
, loops
)
457 # elif input == 'all':
458 loops
= get_parallel_loops(bm_mod
, loops
)
460 return(derived
, bm_mod
, loops
)
463 # sorts all edge-keys into a list of loops
464 def get_connected_selections(edge_keys
):
465 # create connection data
466 vert_verts
= dict_vert_verts(edge_keys
)
468 # find loops consisting of connected selected edges
470 while len(vert_verts
) > 0:
471 loop
= [iter(vert_verts
.keys()).__next
__()]
477 # no more connection data for current vertex
478 if loop
[-1] not in vert_verts
:
486 for i
, next_vert
in enumerate(vert_verts
[loop
[-1]]):
487 if next_vert
not in loop
:
488 vert_verts
[loop
[-1]].pop(i
)
489 if len(vert_verts
[loop
[-1]]) == 0:
490 del vert_verts
[loop
[-1]]
491 # remove connection both ways
492 if next_vert
in vert_verts
:
493 if len(vert_verts
[next_vert
]) == 1:
494 del vert_verts
[next_vert
]
496 vert_verts
[next_vert
].remove(loop
[-1])
497 loop
.append(next_vert
)
501 # found one end of the loop, continue with next
505 # found both ends of the loop, stop growing
509 # check if loop is circular
510 if loop
[0] in vert_verts
:
511 if loop
[-1] in vert_verts
[loop
[0]]:
513 if len(vert_verts
[loop
[0]]) == 1:
514 del vert_verts
[loop
[0]]
516 vert_verts
[loop
[0]].remove(loop
[-1])
517 if len(vert_verts
[loop
[-1]]) == 1:
518 del vert_verts
[loop
[-1]]
520 vert_verts
[loop
[-1]].remove(loop
[0])
534 # get the derived mesh data, if there is a mirror modifier
535 def get_derived_bmesh(object, bm
, not_use_mirror
):
536 # check for mirror modifiers
537 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
539 # disable other modifiers
540 show_viewport
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
]
542 for mod
in object.modifiers
:
543 if mod
.type != 'MIRROR':
544 mod
.show_viewport
= False
545 #leave the merge points untouched
546 if mod
.type == 'MIRROR':
547 merge
.append(mod
.use_mirror_merge
)
549 mod
.use_mirror_merge
= False
552 depsgraph
= bpy
.context
.evaluated_depsgraph_get()
553 object_eval
= object.evaluated_get(depsgraph
)
554 mesh_mod
= object_eval
.to_mesh()
555 bm_mod
.from_mesh(mesh_mod
)
556 object_eval
.to_mesh_clear()
557 # re-enable other modifiers
558 for mod_name
in show_viewport
:
559 object.modifiers
[mod_name
].show_viewport
= True
561 for mod
in object.modifiers
:
562 if mod
.type == 'MIRROR':
563 mod
.use_mirror_merge
= merge
.pop()
564 # no mirror modifiers, so no derived mesh necessary
569 bm_mod
.verts
.ensure_lookup_table()
570 bm_mod
.edges
.ensure_lookup_table()
571 bm_mod
.faces
.ensure_lookup_table()
573 return(derived
, bm_mod
)
576 # return a mapping of derived indices to indices
577 def get_mapping(derived
, bm
, bm_mod
, single_vertices
, full_search
, loops
):
582 verts
= [v
for v
in bm
.verts
if not v
.hide
]
584 verts
= [v
for v
in bm
.verts
if v
.select
and not v
.hide
]
586 # non-selected vertices around single vertices also need to be mapped
588 mapping
= dict([[vert
, -1] for vert
in single_vertices
])
589 verts_mod
= [bm_mod
.verts
[vert
] for vert
in single_vertices
]
591 for v_mod
in verts_mod
:
592 if (v
.co
- v_mod
.co
).length
< 1e-6:
593 mapping
[v_mod
.index
] = v
.index
595 real_singles
= [v_real
for v_real
in mapping
.values() if v_real
> -1]
597 verts_indices
= [vert
.index
for vert
in verts
]
598 for face
in [face
for face
in bm
.faces
if not face
.select
and not face
.hide
]:
599 for vert
in face
.verts
:
600 if vert
.index
in real_singles
:
602 if v
.index
not in verts_indices
:
607 # create mapping of derived indices to indices
608 mapping
= dict([[vert
, -1] for loop
in loops
for vert
in loop
[0]])
610 for single
in single_vertices
:
612 verts_mod
= [bm_mod
.verts
[i
] for i
in mapping
.keys()]
614 for v_mod
in verts_mod
:
615 if (v
.co
- v_mod
.co
).length
< 1e-6:
616 mapping
[v_mod
.index
] = v
.index
617 verts_mod
.remove(v_mod
)
623 # calculate the determinant of a matrix
624 def matrix_determinant(m
):
625 determinant
= m
[0][0] * m
[1][1] * m
[2][2] + m
[0][1] * m
[1][2] * m
[2][0] \
626 + m
[0][2] * m
[1][0] * m
[2][1] - m
[0][2] * m
[1][1] * m
[2][0] \
627 - m
[0][1] * m
[1][0] * m
[2][2] - m
[0][0] * m
[1][2] * m
[2][1]
632 # custom matrix inversion, to provide higher precision than the built-in one
633 def matrix_invert(m
):
634 r
= mathutils
.Matrix((
635 (m
[1][1] * m
[2][2] - m
[1][2] * m
[2][1], m
[0][2] * m
[2][1] - m
[0][1] * m
[2][2],
636 m
[0][1] * m
[1][2] - m
[0][2] * m
[1][1]),
637 (m
[1][2] * m
[2][0] - m
[1][0] * m
[2][2], m
[0][0] * m
[2][2] - m
[0][2] * m
[2][0],
638 m
[0][2] * m
[1][0] - m
[0][0] * m
[1][2]),
639 (m
[1][0] * m
[2][1] - m
[1][1] * m
[2][0], m
[0][1] * m
[2][0] - m
[0][0] * m
[2][1],
640 m
[0][0] * m
[1][1] - m
[0][1] * m
[1][0])))
642 return (r
* (1 / matrix_determinant(m
)))
645 # returns a list of all loops parallel to the input, input included
646 def get_parallel_loops(bm_mod
, loops
):
647 # get required dictionaries
648 edge_faces
= dict_edge_faces(bm_mod
)
649 connected_faces
= dict_face_faces(bm_mod
, edge_faces
)
650 # turn vertex loops into edge loops
653 edgeloop
= [[sorted([loop
[0][i
], loop
[0][i
+ 1]]) for i
in
654 range(len(loop
[0]) - 1)], loop
[1]]
655 if loop
[1]: # circular
656 edgeloop
[0].append(sorted([loop
[0][-1], loop
[0][0]]))
657 edgeloops
.append(edgeloop
[:])
658 # variables to keep track while iterating
662 for loop
in edgeloops
:
663 # initialise with original loop
664 all_edgeloops
.append(loop
[0])
668 if edge
[0] not in verts_used
:
669 verts_used
.append(edge
[0])
670 if edge
[1] not in verts_used
:
671 verts_used
.append(edge
[1])
673 # find parallel loops
674 while len(newloops
) > 0:
677 for i
in newloops
[-1]:
679 forbidden_side
= False
680 if i
not in edge_faces
:
681 # weird input with branches
684 for face
in edge_faces
[i
]:
685 if len(side_a
) == 0 and forbidden_side
!= "a":
691 elif side_a
[-1] in connected_faces
[face
] and \
692 forbidden_side
!= "a":
698 if len(side_b
) == 0 and forbidden_side
!= "b":
704 elif side_b
[-1] in connected_faces
[face
] and \
705 forbidden_side
!= "b":
713 # weird input with branches
726 for key
in face_edgekeys(bm_mod
.faces
[fi
]):
727 if key
[0] not in verts_used
and key
[1] not in \
729 extraloop
.append(key
)
732 for key
in extraloop
:
734 if new_vert
not in verts_used
:
735 verts_used
.append(new_vert
)
736 newloops
.append(extraloop
)
737 all_edgeloops
.append(extraloop
)
739 # input contains branches, only return selected loop
743 # change edgeloops into normal loops
745 for edgeloop
in all_edgeloops
:
747 # grow loop by comparing vertices between consecutive edge-keys
748 for i
in range(len(edgeloop
) - 1):
749 for vert
in range(2):
750 if edgeloop
[i
][vert
] in edgeloop
[i
+ 1]:
751 loop
.append(edgeloop
[i
][vert
])
754 # add starting vertex
755 for vert
in range(2):
756 if edgeloop
[0][vert
] != loop
[0]:
757 loop
= [edgeloop
[0][vert
]] + loop
760 for vert
in range(2):
761 if edgeloop
[-1][vert
] != loop
[-1]:
762 loop
.append(edgeloop
[-1][vert
])
764 # check if loop is circular
765 if loop
[0] == loop
[-1]:
770 loops
.append([loop
, circular
])
775 # gather initial data
777 object = bpy
.context
.active_object
778 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
779 # ensure that selection is synced for the derived mesh
780 bpy
.ops
.object.mode_set(mode
='OBJECT')
781 bpy
.ops
.object.mode_set(mode
='EDIT')
782 bm
= bmesh
.from_edit_mesh(object.data
)
784 bm
.verts
.ensure_lookup_table()
785 bm
.edges
.ensure_lookup_table()
786 bm
.faces
.ensure_lookup_table()
791 # move the vertices to their new locations
792 def move_verts(object, bm
, mapping
, move
, lock
, influence
):
794 lock_x
, lock_y
, lock_z
= lock
795 orient_slot
= bpy
.context
.scene
.transform_orientation_slots
[0]
796 custom
= orient_slot
.custom_orientation
798 mat
= custom
.matrix
.to_4x4().inverted() @ object.matrix_world
.copy()
799 elif orient_slot
.type == 'LOCAL':
800 mat
= mathutils
.Matrix
.Identity(4)
801 elif orient_slot
.type == 'VIEW':
802 mat
= bpy
.context
.region_data
.view_matrix
.copy() @ \
803 object.matrix_world
.copy()
804 else: # orientation == 'GLOBAL'
805 mat
= object.matrix_world
.copy()
806 mat_inv
= mat
.inverted()
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)
830 bm
.verts
[index
].co
= new_loc
834 bm
.verts
.ensure_lookup_table()
835 bm
.edges
.ensure_lookup_table()
836 bm
.faces
.ensure_lookup_table()
839 # load custom tool settings
840 def settings_load(self
):
841 lt
= bpy
.context
.window_manager
.looptools
842 tool
= self
.name
.split()[0].lower()
843 keys
= self
.as_keywords().keys()
845 setattr(self
, key
, getattr(lt
, tool
+ "_" + key
))
848 # store custom tool settings
849 def settings_write(self
):
850 lt
= bpy
.context
.window_manager
.looptools
851 tool
= self
.name
.split()[0].lower()
852 keys
= self
.as_keywords().keys()
854 setattr(lt
, tool
+ "_" + key
, getattr(self
, key
))
857 # clean up and set settings back to original state
859 # update editmesh cached data
860 obj
= bpy
.context
.active_object
861 if obj
.mode
== 'EDIT':
862 bmesh
.update_edit_mesh(obj
.data
, loop_triangles
=True, destructive
=True)
865 # ########################################
866 # ##### Bridge functions #################
867 # ########################################
869 # calculate a cubic spline through the middle section of 4 given coordinates
870 def bridge_calculate_cubic_spline(bm
, coordinates
):
876 for i
in coordinates
:
877 a
.append(float(i
[j
]))
880 h
.append(x
[i
+ 1] - x
[i
])
882 for i
in range(1, 3):
883 q
.append(3.0 / h
[i
] * (a
[i
+ 1] - a
[i
]) - 3.0 / h
[i
- 1] * (a
[i
] - a
[i
- 1]))
887 for i
in range(1, 3):
888 l
.append(2.0 * (x
[i
+ 1] - x
[i
- 1]) - h
[i
- 1] * u
[i
- 1])
889 u
.append(h
[i
] / l
[i
])
890 z
.append((q
[i
] - h
[i
- 1] * z
[i
- 1]) / l
[i
])
893 b
= [False for i
in range(3)]
894 c
= [False for i
in range(4)]
895 d
= [False for i
in range(3)]
897 for i
in range(2, -1, -1):
898 c
[i
] = z
[i
] - u
[i
] * c
[i
+ 1]
899 b
[i
] = (a
[i
+ 1] - a
[i
]) / h
[i
] - h
[i
] * (c
[i
+ 1] + 2.0 * c
[i
]) / 3.0
900 d
[i
] = (c
[i
+ 1] - c
[i
]) / (3.0 * h
[i
])
902 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
903 spline
= [result
[1], result
[4], result
[7]]
908 # return a list with new vertex location vectors, a list with face vertex
909 # integers, and the highest vertex integer in the virtual mesh
910 def bridge_calculate_geometry(bm
, lines
, vertex_normals
, segments
,
911 interpolation
, cubic_strength
, min_width
, max_vert_index
):
915 # calculate location based on interpolation method
916 def get_location(line
, segment
, splines
):
917 v1
= bm
.verts
[lines
[line
][0]].co
918 v2
= bm
.verts
[lines
[line
][1]].co
919 if interpolation
== 'linear':
920 return v1
+ (segment
/ segments
) * (v2
- v1
)
921 else: # interpolation == 'cubic'
922 m
= (segment
/ segments
)
923 ax
, bx
, cx
, dx
, tx
= splines
[line
][0]
924 x
= ax
+ bx
* m
+ cx
* m
** 2 + dx
* m
** 3
925 ay
, by
, cy
, dy
, ty
= splines
[line
][1]
926 y
= ay
+ by
* m
+ cy
* m
** 2 + dy
* m
** 3
927 az
, bz
, cz
, dz
, tz
= splines
[line
][2]
928 z
= az
+ bz
* m
+ cz
* m
** 2 + dz
* m
** 3
929 return mathutils
.Vector((x
, y
, z
))
931 # no interpolation needed
933 for i
, line
in enumerate(lines
):
934 if i
< len(lines
) - 1:
935 faces
.append([line
[0], lines
[i
+ 1][0], lines
[i
+ 1][1], line
[1]])
936 # more than 1 segment, interpolate
938 # calculate splines (if necessary) once, so no recalculations needed
939 if interpolation
== 'cubic':
942 v1
= bm
.verts
[line
[0]].co
943 v2
= bm
.verts
[line
[1]].co
944 size
= (v2
- v1
).length
* cubic_strength
945 splines
.append(bridge_calculate_cubic_spline(bm
,
946 [v1
+ size
* vertex_normals
[line
[0]], v1
, v2
,
947 v2
+ size
* vertex_normals
[line
[1]]]))
951 # create starting situation
952 virtual_width
= [(bm
.verts
[lines
[i
][0]].co
-
953 bm
.verts
[lines
[i
+ 1][0]].co
).length
for i
954 in range(len(lines
) - 1)]
955 new_verts
= [get_location(0, seg
, splines
) for seg
in range(1,
957 first_line_indices
= [i
for i
in range(max_vert_index
+ 1,
958 max_vert_index
+ segments
)]
960 prev_verts
= new_verts
[:] # vertex locations of verts on previous line
961 prev_vert_indices
= first_line_indices
[:]
962 max_vert_index
+= segments
- 1 # highest vertex index in virtual mesh
963 next_verts
= [] # vertex locations of verts on current line
964 next_vert_indices
= []
966 for i
, line
in enumerate(lines
):
967 if i
< len(lines
) - 1:
971 for seg
in range(1, segments
):
972 loc1
= prev_verts
[seg
- 1]
973 loc2
= get_location(i
+ 1, seg
, splines
)
974 if (loc1
- loc2
).length
< (min_width
/ 100) * virtual_width
[i
] \
975 and line
[1] == lines
[i
+ 1][1]:
976 # triangle, no new vertex
977 faces
.append([v1
, v2
, prev_vert_indices
[seg
- 1],
978 prev_vert_indices
[seg
- 1]])
979 next_verts
+= prev_verts
[seg
- 1:]
980 next_vert_indices
+= prev_vert_indices
[seg
- 1:]
984 if i
== len(lines
) - 2 and lines
[0] == lines
[-1]:
985 # quad with first line, no new vertex
986 faces
.append([v1
, v2
, first_line_indices
[seg
- 1],
987 prev_vert_indices
[seg
- 1]])
988 v2
= first_line_indices
[seg
- 1]
989 v1
= prev_vert_indices
[seg
- 1]
991 # quad, add new vertex
993 faces
.append([v1
, v2
, max_vert_index
,
994 prev_vert_indices
[seg
- 1]])
996 v1
= prev_vert_indices
[seg
- 1]
997 new_verts
.append(loc2
)
998 next_verts
.append(loc2
)
999 next_vert_indices
.append(max_vert_index
)
1001 faces
.append([v1
, v2
, lines
[i
+ 1][1], line
[1]])
1003 prev_verts
= next_verts
[:]
1004 prev_vert_indices
= next_vert_indices
[:]
1006 next_vert_indices
= []
1008 return(new_verts
, faces
, max_vert_index
)
1011 # calculate lines (list of lists, vertex indices) that are used for bridging
1012 def bridge_calculate_lines(bm
, loops
, mode
, twist
, reverse
):
1014 loop1
, loop2
= [i
[0] for i
in loops
]
1015 loop1_circular
, loop2_circular
= [i
[1] for i
in loops
]
1016 circular
= loop1_circular
or loop2_circular
1019 # calculate loop centers
1021 for loop
in [loop1
, loop2
]:
1022 center
= mathutils
.Vector()
1024 center
+= bm
.verts
[vertex
].co
1026 centers
.append(center
)
1027 for i
, loop
in enumerate([loop1
, loop2
]):
1029 if bm
.verts
[vertex
].co
== centers
[i
]:
1030 # prevent zero-length vectors in angle comparisons
1031 centers
[i
] += mathutils
.Vector((0.01, 0, 0))
1033 center1
, center2
= centers
1035 # calculate the normals of the virtual planes that the loops are on
1037 normal_plurity
= False
1038 for i
, loop
in enumerate([loop1
, loop2
]):
1040 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1043 x
, y
, z
= centers
[i
]
1044 for loc
in [bm
.verts
[vertex
].co
for vertex
in loop
]:
1045 mat
[0][0] += (loc
[0] - x
) ** 2
1046 mat
[1][0] += (loc
[0] - x
) * (loc
[1] - y
)
1047 mat
[2][0] += (loc
[0] - x
) * (loc
[2] - z
)
1048 mat
[0][1] += (loc
[1] - y
) * (loc
[0] - x
)
1049 mat
[1][1] += (loc
[1] - y
) ** 2
1050 mat
[2][1] += (loc
[1] - y
) * (loc
[2] - z
)
1051 mat
[0][2] += (loc
[2] - z
) * (loc
[0] - x
)
1052 mat
[1][2] += (loc
[2] - z
) * (loc
[1] - y
)
1053 mat
[2][2] += (loc
[2] - z
) ** 2
1056 if sum(mat
[0]) < 1e-6 or sum(mat
[1]) < 1e-6 or sum(mat
[2]) < 1e-6:
1057 normal_plurity
= True
1061 if sum(mat
[0]) == 0:
1062 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
1063 elif sum(mat
[1]) == 0:
1064 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
1065 elif sum(mat
[2]) == 0:
1066 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
1068 # warning! this is different from .normalize()
1071 vec
= mathutils
.Vector((1.0, 1.0, 1.0))
1072 vec2
= (mat
@ vec
) / (mat
@ vec
).length
1073 while vec
!= vec2
and iter < itermax
:
1077 if vec2
.length
!= 0:
1079 if vec2
.length
== 0:
1080 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
1082 normals
.append(normal
)
1083 # have plane normals face in the same direction (maximum angle: 90 degrees)
1084 if ((center1
+ normals
[0]) - center2
).length
< \
1085 ((center1
- normals
[0]) - center2
).length
:
1087 if ((center2
+ normals
[1]) - center1
).length
> \
1088 ((center2
- normals
[1]) - center1
).length
:
1091 # rotation matrix, representing the difference between the plane normals
1092 axis
= normals
[0].cross(normals
[1])
1093 axis
= mathutils
.Vector([loc
if abs(loc
) > 1e-8 else 0 for loc
in axis
])
1094 if axis
.angle(mathutils
.Vector((0, 0, 1)), 0) > 1.5707964:
1096 angle
= normals
[0].dot(normals
[1])
1097 rotation_matrix
= mathutils
.Matrix
.Rotation(angle
, 4, axis
)
1099 # if circular, rotate loops so they are aligned
1101 # make sure loop1 is the circular one (or both are circular)
1102 if loop2_circular
and not loop1_circular
:
1103 loop1_circular
, loop2_circular
= True, False
1104 loop1
, loop2
= loop2
, loop1
1106 # match start vertex of loop1 with loop2
1107 target_vector
= bm
.verts
[loop2
[0]].co
- center2
1108 dif_angles
= [[(rotation_matrix
@ (bm
.verts
[vertex
].co
- center1
)
1109 ).angle(target_vector
, 0), False, i
] for
1110 i
, vertex
in enumerate(loop1
)]
1112 if len(loop1
) != len(loop2
):
1113 angle_limit
= dif_angles
[0][0] * 1.2 # 20% margin
1115 [(bm
.verts
[loop2
[0]].co
-
1116 bm
.verts
[loop1
[index
]].co
).length
, angle
, index
] for
1117 angle
, distance
, index
in dif_angles
if angle
<= angle_limit
1120 loop1
= loop1
[dif_angles
[0][2]:] + loop1
[:dif_angles
[0][2]]
1122 # have both loops face the same way
1123 if normal_plurity
and not circular
:
1124 second_to_first
, second_to_second
, second_to_last
= [
1125 (bm
.verts
[loop1
[1]].co
- center1
).angle(
1126 bm
.verts
[loop2
[i
]].co
- center2
) for i
in [0, 1, -1]
1128 last_to_first
, last_to_second
= [
1129 (bm
.verts
[loop1
[-1]].co
-
1130 center1
).angle(bm
.verts
[loop2
[i
]].co
- center2
) for
1133 if (min(last_to_first
, last_to_second
) * 1.1 < min(second_to_first
,
1134 second_to_second
)) or (loop2_circular
and second_to_last
* 1.1 <
1135 min(second_to_first
, second_to_second
)):
1138 loop1
= [loop1
[-1]] + loop1
[:-1]
1140 angle
= (bm
.verts
[loop1
[0]].co
- center1
).\
1141 cross(bm
.verts
[loop1
[1]].co
- center1
).angle(normals
[0], 0)
1142 target_angle
= (bm
.verts
[loop2
[0]].co
- center2
).\
1143 cross(bm
.verts
[loop2
[1]].co
- center2
).angle(normals
[1], 0)
1144 limit
= 1.5707964 # 0.5*pi, 90 degrees
1145 if not ((angle
> limit
and target_angle
> limit
) or
1146 (angle
< limit
and target_angle
< limit
)):
1149 loop1
= [loop1
[-1]] + loop1
[:-1]
1150 elif normals
[0].angle(normals
[1]) > limit
:
1153 loop1
= [loop1
[-1]] + loop1
[:-1]
1155 # both loops have the same length
1156 if len(loop1
) == len(loop2
):
1159 if abs(twist
) < len(loop1
):
1160 loop1
= loop1
[twist
:] + loop1
[:twist
]
1164 lines
.append([loop1
[0], loop2
[0]])
1165 for i
in range(1, len(loop1
)):
1166 lines
.append([loop1
[i
], loop2
[i
]])
1168 # loops of different lengths
1170 # make loop1 longest loop
1171 if len(loop2
) > len(loop1
):
1172 loop1
, loop2
= loop2
, loop1
1173 loop1_circular
, loop2_circular
= loop2_circular
, loop1_circular
1177 if abs(twist
) < len(loop1
):
1178 loop1
= loop1
[twist
:] + loop1
[:twist
]
1182 # shortest angle difference doesn't always give correct start vertex
1183 if loop1_circular
and not loop2_circular
:
1186 if len(loop1
) - shifting
< len(loop2
):
1189 to_last
, to_first
= [
1190 (rotation_matrix
@ (bm
.verts
[loop1
[-1]].co
- center1
)).angle(
1191 (bm
.verts
[loop2
[i
]].co
- center2
), 0) for i
in [-1, 0]
1193 if to_first
< to_last
:
1194 loop1
= [loop1
[-1]] + loop1
[:-1]
1200 # basic shortest side first
1202 lines
.append([loop1
[0], loop2
[0]])
1203 for i
in range(1, len(loop1
)):
1204 if i
>= len(loop2
) - 1:
1206 lines
.append([loop1
[i
], loop2
[-1]])
1209 lines
.append([loop1
[i
], loop2
[i
]])
1211 # shortest edge algorithm
1212 else: # mode == 'shortest'
1213 lines
.append([loop1
[0], loop2
[0]])
1215 for i
in range(len(loop1
) - 1):
1216 if prev_vert2
== len(loop2
) - 1 and not loop2_circular
:
1217 # force triangles, reached end of loop2
1219 elif prev_vert2
== len(loop2
) - 1 and loop2_circular
:
1220 # at end of loop2, but circular, so check with first vert
1221 tri
, quad
= [(bm
.verts
[loop1
[i
+ 1]].co
-
1222 bm
.verts
[loop2
[j
]].co
).length
1223 for j
in [prev_vert2
, 0]]
1225 elif len(loop1
) - 1 - i
== len(loop2
) - 1 - prev_vert2
and \
1227 # force quads, otherwise won't make it to end of loop2
1230 # calculate if tri or quad gives shortest edge
1231 tri
, quad
= [(bm
.verts
[loop1
[i
+ 1]].co
-
1232 bm
.verts
[loop2
[j
]].co
).length
1233 for j
in range(prev_vert2
, prev_vert2
+ 2)]
1237 lines
.append([loop1
[i
+ 1], loop2
[prev_vert2
]])
1238 if circle_full
== 2:
1241 elif not circle_full
:
1242 lines
.append([loop1
[i
+ 1], loop2
[prev_vert2
+ 1]])
1244 # quad to first vertex of loop2
1246 lines
.append([loop1
[i
+ 1], loop2
[0]])
1250 # final face for circular loops
1251 if loop1_circular
and loop2_circular
:
1252 lines
.append([loop1
[0], loop2
[0]])
1257 # calculate number of segments needed
1258 def bridge_calculate_segments(bm
, lines
, loops
, segments
):
1259 # return if amount of segments is set by user
1264 average_edge_length
= [
1265 (bm
.verts
[vertex
].co
-
1266 bm
.verts
[loop
[0][i
+ 1]].co
).length
for loop
in loops
for
1267 i
, vertex
in enumerate(loop
[0][:-1])
1269 # closing edges of circular loops
1270 average_edge_length
+= [
1271 (bm
.verts
[loop
[0][-1]].co
-
1272 bm
.verts
[loop
[0][0]].co
).length
for loop
in loops
if loop
[1]
1276 average_edge_length
= sum(average_edge_length
) / len(average_edge_length
)
1277 average_bridge_length
= sum(
1279 bm
.verts
[v2
].co
).length
for v1
, v2
in lines
]
1282 segments
= max(1, round(average_bridge_length
/ average_edge_length
))
1287 # return dictionary with vertex index as key, and the normal vector as value
1288 def bridge_calculate_virtual_vertex_normals(bm
, lines
, loops
, edge_faces
,
1290 if not edge_faces
: # interpolation isn't set to cubic
1293 # pity reduce() isn't one of the basic functions in python anymore
1294 def average_vector_dictionary(dic
):
1295 for key
, vectors
in dic
.items():
1296 # if type(vectors) == type([]) and len(vectors) > 1:
1297 if len(vectors
) > 1:
1298 average
= mathutils
.Vector()
1299 for vector
in vectors
:
1301 average
/= len(vectors
)
1302 dic
[key
] = [average
]
1305 # get all edges of the loop
1307 [edgekey_to_edge
[tuple(sorted([loops
[j
][0][i
],
1308 loops
[j
][0][i
+ 1]]))] for i
in range(len(loops
[j
][0]) - 1)] for
1311 edges
= edges
[0] + edges
[1]
1313 if loops
[j
][1]: # circular
1314 edges
.append(edgekey_to_edge
[tuple(sorted([loops
[j
][0][0],
1315 loops
[j
][0][-1]]))])
1318 calculation based on face topology (assign edge-normals to vertices)
1320 edge_normal = face_normal x edge_vector
1321 vertex_normal = average(edge_normals)
1323 vertex_normals
= dict([(vertex
, []) for vertex
in loops
[0][0] + loops
[1][0]])
1325 faces
= edge_faces
[edgekey(edge
)] # valid faces connected to edge
1328 # get edge coordinates
1329 v1
, v2
= [bm
.verts
[edgekey(edge
)[i
]].co
for i
in [0, 1]]
1330 edge_vector
= v1
- v2
1331 if edge_vector
.length
< 1e-4:
1332 # zero-length edge, vertices at same location
1334 edge_center
= (v1
+ v2
) / 2
1336 # average face coordinates, if connected to more than 1 valid face
1338 face_normal
= mathutils
.Vector()
1339 face_center
= mathutils
.Vector()
1341 face_normal
+= face
.normal
1342 face_center
+= face
.calc_center_median()
1343 face_normal
/= len(faces
)
1344 face_center
/= len(faces
)
1346 face_normal
= faces
[0].normal
1347 face_center
= faces
[0].calc_center_median()
1348 if face_normal
.length
< 1e-4:
1349 # faces with a surface of 0 have no face normal
1352 # calculate virtual edge normal
1353 edge_normal
= edge_vector
.cross(face_normal
)
1354 edge_normal
.length
= 0.01
1355 if (face_center
- (edge_center
+ edge_normal
)).length
> \
1356 (face_center
- (edge_center
- edge_normal
)).length
:
1357 # make normal face the correct way
1358 edge_normal
.negate()
1359 edge_normal
.normalize()
1360 # add virtual edge normal as entry for both vertices it connects
1361 for vertex
in edgekey(edge
):
1362 vertex_normals
[vertex
].append(edge_normal
)
1365 calculation based on connection with other loop (vertex focused method)
1366 - used for vertices that aren't connected to any valid faces
1368 plane_normal = edge_vector x connection_vector
1369 vertex_normal = plane_normal x edge_vector
1372 vertex
for vertex
, normal
in vertex_normals
.items() if not normal
1376 # edge vectors connected to vertices
1377 edge_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1379 for v
in edgekey(edge
):
1380 if v
in edge_vectors
:
1381 edge_vector
= bm
.verts
[edgekey(edge
)[0]].co
- \
1382 bm
.verts
[edgekey(edge
)[1]].co
1383 if edge_vector
.length
< 1e-4:
1384 # zero-length edge, vertices at same location
1386 edge_vectors
[v
].append(edge_vector
)
1388 # connection vectors between vertices of both loops
1389 connection_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1390 connections
= dict([[vertex
, []] for vertex
in vertices
])
1391 for v1
, v2
in lines
:
1392 if v1
in connection_vectors
or v2
in connection_vectors
:
1393 new_vector
= bm
.verts
[v1
].co
- bm
.verts
[v2
].co
1394 if new_vector
.length
< 1e-4:
1395 # zero-length connection vector,
1396 # vertices in different loops at same location
1398 if v1
in connection_vectors
:
1399 connection_vectors
[v1
].append(new_vector
)
1400 connections
[v1
].append(v2
)
1401 if v2
in connection_vectors
:
1402 connection_vectors
[v2
].append(new_vector
)
1403 connections
[v2
].append(v1
)
1404 connection_vectors
= average_vector_dictionary(connection_vectors
)
1405 connection_vectors
= dict(
1406 [[vertex
, vector
[0]] if vector
else
1407 [vertex
, []] for vertex
, vector
in connection_vectors
.items()]
1410 for vertex
, values
in edge_vectors
.items():
1411 # vertex normal doesn't matter, just assign a random vector to it
1412 if not connection_vectors
[vertex
]:
1413 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1416 # calculate to what location the vertex is connected,
1417 # used to determine what way to flip the normal
1418 connected_center
= mathutils
.Vector()
1419 for v
in connections
[vertex
]:
1420 connected_center
+= bm
.verts
[v
].co
1421 if len(connections
[vertex
]) > 1:
1422 connected_center
/= len(connections
[vertex
])
1423 if len(connections
[vertex
]) == 0:
1424 # shouldn't be possible, but better safe than sorry
1425 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1428 # can't do proper calculations, because of zero-length vector
1430 if (connected_center
- (bm
.verts
[vertex
].co
+
1431 connection_vectors
[vertex
])).length
< (connected_center
-
1432 (bm
.verts
[vertex
].co
- connection_vectors
[vertex
])).length
:
1433 connection_vectors
[vertex
].negate()
1434 vertex_normals
[vertex
] = [connection_vectors
[vertex
].normalized()]
1437 # calculate vertex normals using edge-vectors,
1438 # connection-vectors and the derived plane normal
1439 for edge_vector
in values
:
1440 plane_normal
= edge_vector
.cross(connection_vectors
[vertex
])
1441 vertex_normal
= edge_vector
.cross(plane_normal
)
1442 vertex_normal
.length
= 0.1
1443 if (connected_center
- (bm
.verts
[vertex
].co
+
1444 vertex_normal
)).length
< (connected_center
-
1445 (bm
.verts
[vertex
].co
- vertex_normal
)).length
:
1446 # make normal face the correct way
1447 vertex_normal
.negate()
1448 vertex_normal
.normalize()
1449 vertex_normals
[vertex
].append(vertex_normal
)
1451 # average virtual vertex normals, based on all edges it's connected to
1452 vertex_normals
= average_vector_dictionary(vertex_normals
)
1453 vertex_normals
= dict([[vertex
, vector
[0]] for vertex
, vector
in vertex_normals
.items()])
1455 return(vertex_normals
)
1458 # add vertices to mesh
1459 def bridge_create_vertices(bm
, vertices
):
1460 for i
in range(len(vertices
)):
1461 bm
.verts
.new(vertices
[i
])
1462 bm
.verts
.ensure_lookup_table()
1466 def bridge_create_faces(object, bm
, faces
, twist
):
1467 # have the normal point the correct way
1469 [face
.reverse() for face
in faces
]
1470 faces
= [face
[2:] + face
[:2] if face
[0] == face
[1] else face
for face
in faces
]
1472 # eekadoodle prevention
1473 for i
in range(len(faces
)):
1474 if not faces
[i
][-1]:
1475 if faces
[i
][0] == faces
[i
][-1]:
1476 faces
[i
] = [faces
[i
][1], faces
[i
][2], faces
[i
][3], faces
[i
][1]]
1478 faces
[i
] = [faces
[i
][-1]] + faces
[i
][:-1]
1479 # result of converting from pre-bmesh period
1480 if faces
[i
][-1] == faces
[i
][-2]:
1481 faces
[i
] = faces
[i
][:-1]
1484 for i
in range(len(faces
)):
1485 new_faces
.append(bm
.faces
.new([bm
.verts
[v
] for v
in faces
[i
]]))
1487 object.data
.update(calc_edges
=True) # calc_edges prevents memory-corruption
1489 bm
.verts
.ensure_lookup_table()
1490 bm
.edges
.ensure_lookup_table()
1491 bm
.faces
.ensure_lookup_table()
1496 # calculate input loops
1497 def bridge_get_input(bm
):
1498 # create list of internal edges, which should be skipped
1499 eks_of_selected_faces
= [
1500 item
for sublist
in [face_edgekeys(face
) for
1501 face
in bm
.faces
if face
.select
and not face
.hide
] for item
in sublist
1504 for ek
in eks_of_selected_faces
:
1505 if ek
in edge_count
:
1509 internal_edges
= [ek
for ek
in edge_count
if edge_count
[ek
] > 1]
1511 # sort correct edges into loops
1513 edgekey(edge
) for edge
in bm
.edges
if edge
.select
and
1514 not edge
.hide
and edgekey(edge
) not in internal_edges
1516 loops
= get_connected_selections(selected_edges
)
1521 # return values needed by the bridge operator
1522 def bridge_initialise(bm
, interpolation
):
1523 if interpolation
== 'cubic':
1524 # dict with edge-key as key and list of connected valid faces as value
1526 face
.index
for face
in bm
.faces
if face
.select
or
1530 [[edgekey(edge
), []] for edge
in bm
.edges
if not edge
.hide
]
1532 for face
in bm
.faces
:
1533 if face
.index
in face_blacklist
:
1535 for key
in face_edgekeys(face
):
1536 edge_faces
[key
].append(face
)
1537 # dictionary with the edge-key as key and edge as value
1538 edgekey_to_edge
= dict(
1539 [[edgekey(edge
), edge
] for edge
in bm
.edges
if edge
.select
and not edge
.hide
]
1543 edgekey_to_edge
= False
1545 # selected faces input
1546 old_selected_faces
= [
1547 face
.index
for face
in bm
.faces
if face
.select
and not face
.hide
1550 # find out if faces created by bridging should be smoothed
1553 if sum([face
.smooth
for face
in bm
.faces
]) / len(bm
.faces
) >= 0.5:
1556 return(edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
)
1559 # return a string with the input method
1560 def bridge_input_method(loft
, loft_loop
):
1564 method
= "Loft loop"
1566 method
= "Loft no-loop"
1573 # match up loops in pairs, used for multi-input bridging
1574 def bridge_match_loops(bm
, loops
):
1575 # calculate average loop normals and centers
1578 for vertices
, circular
in loops
:
1579 normal
= mathutils
.Vector()
1580 center
= mathutils
.Vector()
1581 for vertex
in vertices
:
1582 normal
+= bm
.verts
[vertex
].normal
1583 center
+= bm
.verts
[vertex
].co
1584 normals
.append(normal
/ len(vertices
) / 10)
1585 centers
.append(center
/ len(vertices
))
1587 # possible matches if loop normals are faced towards the center
1589 matches
= dict([[i
, []] for i
in range(len(loops
))])
1591 for i
in range(len(loops
) + 1):
1592 for j
in range(i
+ 1, len(loops
)):
1593 if (centers
[i
] - centers
[j
]).length
> \
1594 (centers
[i
] - (centers
[j
] + normals
[j
])).length
and \
1595 (centers
[j
] - centers
[i
]).length
> \
1596 (centers
[j
] - (centers
[i
] + normals
[i
])).length
:
1598 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1599 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1600 # if no loops face each other, just make matches between all the loops
1601 if matches_amount
== 0:
1602 for i
in range(len(loops
) + 1):
1603 for j
in range(i
+ 1, len(loops
)):
1604 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1605 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1606 for key
, value
in matches
.items():
1609 # matches based on distance between centers and number of vertices in loops
1611 for loop_index
in range(len(loops
)):
1612 if loop_index
in new_order
:
1614 loop_matches
= matches
[loop_index
]
1615 if not loop_matches
:
1617 shortest_distance
= loop_matches
[0][0]
1618 shortest_distance
*= 1.1
1620 [abs(len(loops
[loop_index
][0]) -
1621 len(loops
[loop
[2]][0])), loop
[0], loop
[1], loop
[2]] for loop
in
1622 loop_matches
if loop
[0] < shortest_distance
1625 for match
in loop_matches
:
1626 if match
[3] not in new_order
:
1627 new_order
+= [loop_index
, match
[3]]
1630 # reorder loops based on matches
1631 if len(new_order
) >= 2:
1632 loops
= [loops
[i
] for i
in new_order
]
1637 # remove old_selected_faces
1638 def bridge_remove_internal_faces(bm
, old_selected_faces
):
1639 # collect bmesh faces and internal bmesh edges
1640 remove_faces
= [bm
.faces
[face
] for face
in old_selected_faces
]
1641 edges
= collections
.Counter(
1642 [edge
.index
for face
in remove_faces
for edge
in face
.edges
]
1644 remove_edges
= [bm
.edges
[edge
] for edge
in edges
if edges
[edge
] > 1]
1646 # remove internal faces and edges
1647 for face
in remove_faces
:
1648 bm
.faces
.remove(face
)
1649 for edge
in remove_edges
:
1650 bm
.edges
.remove(edge
)
1652 bm
.faces
.ensure_lookup_table()
1653 bm
.edges
.ensure_lookup_table()
1654 bm
.verts
.ensure_lookup_table()
1657 # update list of internal faces that are flagged for removal
1658 def bridge_save_unused_faces(bm
, old_selected_faces
, loops
):
1659 # key: vertex index, value: lists of selected faces using it
1661 vertex_to_face
= dict([[i
, []] for i
in range(len(bm
.verts
))])
1662 [[vertex_to_face
[vertex
.index
].append(face
) for vertex
in
1663 bm
.faces
[face
].verts
] for face
in old_selected_faces
]
1665 # group selected faces that are connected
1668 for face
in old_selected_faces
:
1669 if face
in grouped_faces
:
1671 grouped_faces
.append(face
)
1675 grow_face
= new_faces
[0]
1676 for vertex
in bm
.faces
[grow_face
].verts
:
1677 vertex_face_group
= [
1678 face
for face
in vertex_to_face
[vertex
.index
] if
1679 face
not in grouped_faces
1681 new_faces
+= vertex_face_group
1682 grouped_faces
+= vertex_face_group
1683 group
+= vertex_face_group
1685 groups
.append(group
)
1687 # key: vertex index, value: True/False (is it in a loop that is used)
1688 used_vertices
= dict([[i
, 0] for i
in range(len(bm
.verts
))])
1690 for vertex
in loop
[0]:
1691 used_vertices
[vertex
] = True
1693 # check if group is bridged, if not remove faces from internal faces list
1694 for group
in groups
:
1699 for vertex
in bm
.faces
[face
].verts
:
1700 if used_vertices
[vertex
.index
]:
1705 old_selected_faces
.remove(face
)
1708 # add the newly created faces to the selection
1709 def bridge_select_new_faces(new_faces
, smooth
):
1710 for face
in new_faces
:
1711 face
.select_set(True)
1712 face
.smooth
= smooth
1715 # sort loops, so they are connected in the correct order when lofting
1716 def bridge_sort_loops(bm
, loops
, loft_loop
):
1717 # simplify loops to single points, and prepare for pathfinding
1719 [sum([bm
.verts
[i
].co
[j
] for i
in loop
[0]]) /
1720 len(loop
[0]) for loop
in loops
] for j
in range(3)
1722 nodes
= [mathutils
.Vector((x
[i
], y
[i
], z
[i
])) for i
in range(len(loops
))]
1725 open = [i
for i
in range(1, len(loops
))]
1727 # connect node to path, that is shortest to active_node
1728 while len(open) > 0:
1729 distances
= [(nodes
[active_node
] - nodes
[i
]).length
for i
in open]
1730 active_node
= open[distances
.index(min(distances
))]
1731 open.remove(active_node
)
1732 path
.append([active_node
, min(distances
)])
1733 # check if we didn't start in the middle of the path
1734 for i
in range(2, len(path
)):
1735 if (nodes
[path
[i
][0]] - nodes
[0]).length
< path
[i
][1]:
1738 path
= path
[:-i
] + temp
1742 loops
= [loops
[i
[0]] for i
in path
]
1743 # if requested, duplicate first loop at last position, so loft can loop
1745 loops
= loops
+ [loops
[0]]
1750 # remapping old indices to new position in list
1751 def bridge_update_old_selection(bm
, old_selected_faces
):
1753 old_indices = old_selected_faces[:]
1754 old_selected_faces = []
1755 for i, face in enumerate(bm.faces):
1756 if face.index in old_indices:
1757 old_selected_faces.append(i)
1759 old_selected_faces
= [
1760 i
for i
, face
in enumerate(bm
.faces
) if face
.index
in old_selected_faces
1763 return(old_selected_faces
)
1766 # ########################################
1767 # ##### Circle functions #################
1768 # ########################################
1770 # convert 3d coordinates to 2d coordinates on plane
1771 def circle_3d_to_2d(bm_mod
, loop
, com
, normal
):
1772 # project vertices onto the plane
1773 verts
= [bm_mod
.verts
[v
] for v
in loop
[0]]
1774 verts_projected
= [[v
.co
- (v
.co
- com
).dot(normal
) * normal
, v
.index
]
1777 # calculate two vectors (p and q) along the plane
1778 m
= mathutils
.Vector((normal
[0] + 1.0, normal
[1], normal
[2]))
1779 p
= m
- (m
.dot(normal
) * normal
)
1781 m
= mathutils
.Vector((normal
[0], normal
[1] + 1.0, normal
[2]))
1782 p
= m
- (m
.dot(normal
) * normal
)
1785 # change to 2d coordinates using perpendicular projection
1787 for loc
, vert
in verts_projected
:
1789 x
= p
.dot(vloc
) / p
.dot(p
)
1790 y
= q
.dot(vloc
) / q
.dot(q
)
1791 locs_2d
.append([x
, y
, vert
])
1793 return(locs_2d
, p
, q
)
1796 # calculate a best-fit circle to the 2d locations on the plane
1797 def circle_calculate_best_fit(locs_2d
):
1803 # calculate center and radius (non-linear least squares solution)
1804 for iter in range(500):
1808 d
= (v
[0] ** 2 - 2.0 * x0
* v
[0] + v
[1] ** 2 - 2.0 * y0
* v
[1] + x0
** 2 + y0
** 2) ** 0.5
1809 jmat
.append([(x0
- v
[0]) / d
, (y0
- v
[1]) / d
, -1.0])
1810 k
.append(-(((v
[0] - x0
) ** 2 + (v
[1] - y0
) ** 2) ** 0.5 - r
))
1811 jmat2
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1815 k2
= mathutils
.Vector((0.0, 0.0, 0.0))
1816 for i
in range(len(jmat
)):
1817 k2
+= mathutils
.Vector(jmat
[i
]) * k
[i
]
1818 jmat2
[0][0] += jmat
[i
][0] ** 2
1819 jmat2
[1][0] += jmat
[i
][0] * jmat
[i
][1]
1820 jmat2
[2][0] += jmat
[i
][0] * jmat
[i
][2]
1821 jmat2
[1][1] += jmat
[i
][1] ** 2
1822 jmat2
[2][1] += jmat
[i
][1] * jmat
[i
][2]
1823 jmat2
[2][2] += jmat
[i
][2] ** 2
1824 jmat2
[0][1] = jmat2
[1][0]
1825 jmat2
[0][2] = jmat2
[2][0]
1826 jmat2
[1][2] = jmat2
[2][1]
1831 dx0
, dy0
, dr
= jmat2
@ k2
1835 # stop iterating if we're close enough to optimal solution
1836 if abs(dx0
) < 1e-6 and abs(dy0
) < 1e-6 and abs(dr
) < 1e-6:
1839 # return center of circle and radius
1843 # calculate circle so no vertices have to be moved away from the center
1844 def circle_calculate_min_fit(locs_2d
):
1846 x0
= (min([i
[0] for i
in locs_2d
]) + max([i
[0] for i
in locs_2d
])) / 2.0
1847 y0
= (min([i
[1] for i
in locs_2d
]) + max([i
[1] for i
in locs_2d
])) / 2.0
1848 center
= mathutils
.Vector([x0
, y0
])
1850 r
= min([(mathutils
.Vector([i
[0], i
[1]]) - center
).length
for i
in locs_2d
])
1852 # return center of circle and radius
1856 # calculate the new locations of the vertices that need to be moved
1857 def circle_calculate_verts(flatten
, bm_mod
, locs_2d
, com
, p
, q
, normal
):
1858 # changing 2d coordinates back to 3d coordinates
1861 locs_3d
.append([loc
[2], loc
[0] * p
+ loc
[1] * q
+ com
])
1863 if flatten
: # flat circle
1866 else: # project the locations on the existing mesh
1867 vert_edges
= dict_vert_edges(bm_mod
)
1868 vert_faces
= dict_vert_faces(bm_mod
)
1869 faces
= [f
for f
in bm_mod
.faces
if not f
.hide
]
1870 rays
= [normal
, -normal
]
1874 if bm_mod
.verts
[loc
[0]].co
== loc
[1]: # vertex hasn't moved
1877 dif
= normal
.angle(loc
[1] - bm_mod
.verts
[loc
[0]].co
)
1878 if -1e-6 < dif
< 1e-6 or math
.pi
- 1e-6 < dif
< math
.pi
+ 1e-6:
1879 # original location is already along projection normal
1880 projection
= bm_mod
.verts
[loc
[0]].co
1882 # quick search through adjacent faces
1883 for face
in vert_faces
[loc
[0]]:
1884 verts
= [v
.co
for v
in bm_mod
.faces
[face
].verts
]
1885 if len(verts
) == 3: # triangle
1889 v1
, v2
, v3
, v4
= verts
[:4]
1891 intersect
= mathutils
.geometry
.\
1892 intersect_ray_tri(v1
, v2
, v3
, ray
, loc
[1])
1894 projection
= intersect
1897 intersect
= mathutils
.geometry
.\
1898 intersect_ray_tri(v1
, v3
, v4
, ray
, loc
[1])
1900 projection
= intersect
1905 # check if projection is on adjacent edges
1906 for edgekey
in vert_edges
[loc
[0]]:
1907 line1
= bm_mod
.verts
[edgekey
[0]].co
1908 line2
= bm_mod
.verts
[edgekey
[1]].co
1909 intersect
, dist
= mathutils
.geometry
.intersect_point_line(
1910 loc
[1], line1
, line2
1912 if 1e-6 < dist
< 1 - 1e-6:
1913 projection
= intersect
1916 # full search through the entire mesh
1919 verts
= [v
.co
for v
in face
.verts
]
1920 if len(verts
) == 3: # triangle
1924 v1
, v2
, v3
, v4
= verts
[:4]
1926 intersect
= mathutils
.geometry
.intersect_ray_tri(
1927 v1
, v2
, v3
, ray
, loc
[1]
1930 hits
.append([(loc
[1] - intersect
).length
,
1934 intersect
= mathutils
.geometry
.intersect_ray_tri(
1935 v1
, v3
, v4
, ray
, loc
[1]
1938 hits
.append([(loc
[1] - intersect
).length
,
1942 # if more than 1 hit with mesh, closest hit is new loc
1944 projection
= hits
[0][1]
1946 # nothing to project on, remain at flat location
1948 new_locs
.append([loc
[0], projection
])
1950 # return new positions of projected circle
1954 # check loops and only return valid ones
1955 def circle_check_loops(single_loops
, loops
, mapping
, bm_mod
):
1956 valid_single_loops
= {}
1958 for i
, [loop
, circular
] in enumerate(loops
):
1959 # loop needs to have at least 3 vertices
1962 # loop needs at least 1 vertex in the original, non-mirrored mesh
1966 if mapping
[vert
] > -1:
1971 # loop has to be non-collinear
1973 loc0
= mathutils
.Vector(bm_mod
.verts
[loop
[0]].co
[:])
1974 loc1
= mathutils
.Vector(bm_mod
.verts
[loop
[1]].co
[:])
1976 locn
= mathutils
.Vector(bm_mod
.verts
[v
].co
[:])
1977 if loc0
== loc1
or loc1
== locn
:
1983 if -1e-6 < d1
.angle(d2
, 0) < 1e-6:
1991 # passed all tests, loop is valid
1992 valid_loops
.append([loop
, circular
])
1993 valid_single_loops
[len(valid_loops
) - 1] = single_loops
[i
]
1995 return(valid_single_loops
, valid_loops
)
1998 # calculate the location of single input vertices that need to be flattened
1999 def circle_flatten_singles(bm_mod
, com
, p
, q
, normal
, single_loop
):
2001 for vert
in single_loop
:
2002 loc
= mathutils
.Vector(bm_mod
.verts
[vert
].co
[:])
2003 new_locs
.append([vert
, loc
- (loc
- com
).dot(normal
) * normal
])
2008 # calculate input loops
2009 def circle_get_input(object, bm
):
2010 # get mesh with modifiers applied
2011 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
2013 # create list of edge-keys based on selection state
2015 for face
in bm
.faces
:
2016 if face
.select
and not face
.hide
:
2020 # get selected, non-hidden , non-internal edge-keys
2022 key
for keys
in [face_edgekeys(face
) for face
in
2023 bm_mod
.faces
if face
.select
and not face
.hide
] for key
in keys
2026 for ek
in eks_selected
:
2027 if ek
in edge_count
:
2032 edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and
2033 not edge
.hide
and edge_count
.get(edgekey(edge
), 1) == 1
2036 # no faces, so no internal edges either
2038 edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and not edge
.hide
2041 # add edge-keys around single vertices
2042 verts_connected
= dict(
2043 [[vert
, 1] for edge
in [edge
for edge
in
2044 bm_mod
.edges
if edge
.select
and not edge
.hide
] for vert
in
2048 vert
.index
for vert
in bm_mod
.verts
if
2049 vert
.select
and not vert
.hide
and
2050 not verts_connected
.get(vert
.index
, False)
2053 if single_vertices
and len(bm
.faces
) > 0:
2054 vert_to_single
= dict(
2055 [[v
.index
, []] for v
in bm_mod
.verts
if not v
.hide
]
2057 for face
in [face
for face
in bm_mod
.faces
if not face
.select
and not face
.hide
]:
2058 for vert
in face
.verts
:
2060 if vert
in single_vertices
:
2061 for ek
in face_edgekeys(face
):
2063 edge_keys
.append(ek
)
2064 if vert
not in vert_to_single
[ek
[0]]:
2065 vert_to_single
[ek
[0]].append(vert
)
2066 if vert
not in vert_to_single
[ek
[1]]:
2067 vert_to_single
[ek
[1]].append(vert
)
2070 # sort edge-keys into loops
2071 loops
= get_connected_selections(edge_keys
)
2073 # find out to which loops the single vertices belong
2074 single_loops
= dict([[i
, []] for i
in range(len(loops
))])
2075 if single_vertices
and len(bm
.faces
) > 0:
2076 for i
, [loop
, circular
] in enumerate(loops
):
2078 if vert_to_single
[vert
]:
2079 for single
in vert_to_single
[vert
]:
2080 if single
not in single_loops
[i
]:
2081 single_loops
[i
].append(single
)
2083 return(derived
, bm_mod
, single_vertices
, single_loops
, loops
)
2086 # recalculate positions based on the influence of the circle shape
2087 def circle_influence_locs(locs_2d
, new_locs_2d
, influence
):
2088 for i
in range(len(locs_2d
)):
2089 oldx
, oldy
, j
= locs_2d
[i
]
2090 newx
, newy
, k
= new_locs_2d
[i
]
2091 altx
= newx
* (influence
/ 100) + oldx
* ((100 - influence
) / 100)
2092 alty
= newy
* (influence
/ 100) + oldy
* ((100 - influence
) / 100)
2093 locs_2d
[i
] = [altx
, alty
, j
]
2098 # project 2d locations on circle, respecting distance relations between verts
2099 def circle_project_non_regular(locs_2d
, x0
, y0
, r
):
2100 for i
in range(len(locs_2d
)):
2101 x
, y
, j
= locs_2d
[i
]
2102 loc
= mathutils
.Vector([x
- x0
, y
- y0
])
2104 locs_2d
[i
] = [loc
[0], loc
[1], j
]
2109 # project 2d locations on circle, with equal distance between all vertices
2110 def circle_project_regular(locs_2d
, x0
, y0
, r
):
2111 # find offset angle and circling direction
2112 x
, y
, i
= locs_2d
[0]
2113 loc
= mathutils
.Vector([x
- x0
, y
- y0
])
2115 offset_angle
= loc
.angle(mathutils
.Vector([1.0, 0.0]), 0.0)
2116 loca
= mathutils
.Vector([x
- x0
, y
- y0
, 0.0])
2119 x
, y
, j
= locs_2d
[1]
2120 locb
= mathutils
.Vector([x
- x0
, y
- y0
, 0.0])
2121 if loca
.cross(locb
)[2] >= 0:
2125 # distribute vertices along the circle
2126 for i
in range(len(locs_2d
)):
2127 t
= offset_angle
+ ccw
* (i
/ len(locs_2d
) * 2 * math
.pi
)
2130 locs_2d
[i
] = [x
, y
, locs_2d
[i
][2]]
2135 # shift loop, so the first vertex is closest to the center
2136 def circle_shift_loop(bm_mod
, loop
, com
):
2137 verts
, circular
= loop
2139 [(bm_mod
.verts
[vert
].co
- com
).length
, i
] for i
, vert
in enumerate(verts
)
2142 shift
= distances
[0][1]
2143 loop
= [verts
[shift
:] + verts
[:shift
], circular
]
2148 # ########################################
2149 # ##### Curve functions ##################
2150 # ########################################
2152 # create lists with knots and points, all correctly sorted
2153 def curve_calculate_knots(loop
, verts_selected
):
2154 knots
= [v
for v
in loop
[0] if v
in verts_selected
]
2156 # circular loop, potential for weird splines
2158 offset
= int(len(loop
[0]) / 4)
2161 kpos
.append(loop
[0].index(k
))
2163 for i
in range(len(kpos
) - 1):
2164 kdif
.append(kpos
[i
+ 1] - kpos
[i
])
2165 kdif
.append(len(loop
[0]) - kpos
[-1] + kpos
[0])
2169 kadd
.append([kdif
.index(k
), True])
2170 # next 2 lines are optional, they insert
2171 # an extra control point in small gaps
2173 # kadd.append([kdif.index(k), False])
2176 for k
in kadd
: # extra knots to be added
2177 if k
[1]: # big gap (break circular spline)
2178 kpos
= loop
[0].index(knots
[k
[0]]) + offset
2179 if kpos
> len(loop
[0]) - 1:
2180 kpos
-= len(loop
[0])
2181 kins
.append([knots
[k
[0]], loop
[0][kpos
]])
2183 if kpos2
> len(knots
) - 1:
2185 kpos2
= loop
[0].index(knots
[kpos2
]) - offset
2187 kpos2
+= len(loop
[0])
2188 kins
.append([loop
[0][kpos
], loop
[0][kpos2
]])
2189 krot
= loop
[0][kpos2
]
2190 else: # small gap (keep circular spline)
2191 k1
= loop
[0].index(knots
[k
[0]])
2193 if k2
> len(knots
) - 1:
2195 k2
= loop
[0].index(knots
[k2
])
2197 dif
= len(loop
[0]) - 1 - k1
+ k2
2200 kn
= k1
+ int(dif
/ 2)
2201 if kn
> len(loop
[0]) - 1:
2203 kins
.append([loop
[0][k1
], loop
[0][kn
]])
2204 for j
in kins
: # insert new knots
2205 knots
.insert(knots
.index(j
[0]) + 1, j
[1])
2206 if not krot
: # circular loop
2207 knots
.append(knots
[0])
2208 points
= loop
[0][loop
[0].index(knots
[0]):]
2209 points
+= loop
[0][0:loop
[0].index(knots
[0]) + 1]
2210 else: # non-circular loop (broken by script)
2211 krot
= knots
.index(krot
)
2212 knots
= knots
[krot
:] + knots
[0:krot
]
2213 if loop
[0].index(knots
[0]) > loop
[0].index(knots
[-1]):
2214 points
= loop
[0][loop
[0].index(knots
[0]):]
2215 points
+= loop
[0][0:loop
[0].index(knots
[-1]) + 1]
2217 points
= loop
[0][loop
[0].index(knots
[0]):loop
[0].index(knots
[-1]) + 1]
2218 # non-circular loop, add first and last point as knots
2220 if loop
[0][0] not in knots
:
2221 knots
.insert(0, loop
[0][0])
2222 if loop
[0][-1] not in knots
:
2223 knots
.append(loop
[0][-1])
2225 return(knots
, points
)
2228 # calculate relative positions compared to first knot
2229 def curve_calculate_t(bm_mod
, knots
, points
, pknots
, regular
, circular
):
2236 loc
= pknots
[knots
.index(p
)] # use projected knot location
2238 loc
= mathutils
.Vector(bm_mod
.verts
[p
].co
[:])
2241 len_total
+= (loc
- loc_prev
).length
2242 tpoints
.append(len_total
)
2247 tknots
.append(tpoints
[points
.index(p
)])
2249 tknots
[-1] = tpoints
[-1]
2253 tpoints_average
= tpoints
[-1] / (len(tpoints
) - 1)
2254 for i
in range(1, len(tpoints
) - 1):
2255 tpoints
[i
] = i
* tpoints_average
2256 for i
in range(len(knots
)):
2257 tknots
[i
] = tpoints
[points
.index(knots
[i
])]
2259 tknots
[-1] = tpoints
[-1]
2261 return(tknots
, tpoints
)
2264 # change the location of non-selected points to their place on the spline
2265 def curve_calculate_vertices(bm_mod
, knots
, tknots
, points
, tpoints
, splines
,
2266 interpolation
, restriction
):
2273 m
= tpoints
[points
.index(p
)]
2281 if n
> len(splines
) - 1:
2282 n
= len(splines
) - 1
2286 if interpolation
== 'cubic':
2287 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
2288 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
2289 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
2290 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
2291 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
2292 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
2293 newloc
= mathutils
.Vector([x
, y
, z
])
2294 else: # interpolation == 'linear'
2295 a
, d
, t
, u
= splines
[n
]
2296 newloc
= ((m
- t
) / u
) * d
+ a
2298 if restriction
!= 'none': # vertex movement is restricted
2300 else: # set the vertex to its new location
2301 move
.append([p
, newloc
])
2303 if restriction
!= 'none': # vertex movement is restricted
2308 move
.append([p
, bm_mod
.verts
[p
].co
])
2310 oldloc
= bm_mod
.verts
[p
].co
2311 normal
= bm_mod
.verts
[p
].normal
2312 dloc
= newloc
- oldloc
2313 if dloc
.length
< 1e-6:
2314 move
.append([p
, newloc
])
2315 elif restriction
== 'extrude': # only extrusions
2316 if dloc
.angle(normal
, 0) < 0.5 * math
.pi
+ 1e-6:
2317 move
.append([p
, newloc
])
2318 else: # restriction == 'indent' only indentations
2319 if dloc
.angle(normal
) > 0.5 * math
.pi
- 1e-6:
2320 move
.append([p
, newloc
])
2325 # trim loops to part between first and last selected vertices (including)
2326 def curve_cut_boundaries(bm_mod
, loops
):
2328 for loop
, circular
in loops
:
2330 selected
= [bm_mod
.verts
[v
].select
for v
in loop
]
2331 first
= selected
.index(True)
2333 last
= -selected
.index(True)
2335 if len(loop
[first
:]) < len(loop
)/2:
2336 cut_loops
.append([loop
[first
:], False])
2338 if len(loop
[first
:last
]) < len(loop
)/2:
2339 cut_loops
.append([loop
[first
:last
], False])
2341 selected
= [bm_mod
.verts
[v
].select
for v
in loop
]
2342 first
= selected
.index(True)
2344 last
= -selected
.index(True)
2346 cut_loops
.append([loop
[first
:], circular
])
2348 cut_loops
.append([loop
[first
:last
], circular
])
2353 # calculate input loops
2354 def curve_get_input(object, bm
, boundaries
):
2355 # get mesh with modifiers applied
2356 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
2358 # vertices that still need a loop to run through it
2360 v
.index
for v
in bm_mod
.verts
if v
.select
and not v
.hide
2362 # necessary dictionaries
2363 vert_edges
= dict_vert_edges(bm_mod
)
2364 edge_faces
= dict_edge_faces(bm_mod
)
2366 # find loops through each selected vertex
2367 while len(verts_unsorted
) > 0:
2368 loops
= curve_vertex_loops(bm_mod
, verts_unsorted
[0], vert_edges
,
2370 verts_unsorted
.pop(0)
2372 # check if loop is fully selected
2373 search_perpendicular
= False
2375 for loop
, circular
in loops
:
2377 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2378 if len(selected
) < 2:
2379 # only one selected vertex on loop, don't use
2382 elif len(selected
) == len(loop
):
2383 search_perpendicular
= loop
2385 # entire loop is selected, find perpendicular loops
2386 if search_perpendicular
:
2388 if vert
in verts_unsorted
:
2389 verts_unsorted
.remove(vert
)
2390 perp_loops
= curve_perpendicular_loops(bm_mod
, loop
,
2391 vert_edges
, edge_faces
)
2392 for perp_loop
in perp_loops
:
2393 correct_loops
.append(perp_loop
)
2396 for loop
, circular
in loops
:
2397 correct_loops
.append([loop
, circular
])
2401 correct_loops
= curve_cut_boundaries(bm_mod
, correct_loops
)
2403 return(derived
, bm_mod
, correct_loops
)
2406 # return all loops that are perpendicular to the given one
2407 def curve_perpendicular_loops(bm_mod
, start_loop
, vert_edges
, edge_faces
):
2408 # find perpendicular loops
2410 for start_vert
in start_loop
:
2411 loops
= curve_vertex_loops(bm_mod
, start_vert
, vert_edges
,
2413 for loop
, circular
in loops
:
2414 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2415 if len(selected
) == len(loop
):
2418 perp_loops
.append([loop
, circular
, loop
.index(start_vert
)])
2420 # trim loops to same lengths
2422 [len(loop
[0]), i
] for i
, loop
in enumerate(perp_loops
) if not loop
[1]
2425 # all loops are circular, not trimming
2426 return([[loop
[0], loop
[1]] for loop
in perp_loops
])
2428 shortest
= min(shortest
)
2429 shortest_start
= perp_loops
[shortest
[1]][2]
2430 before_start
= shortest_start
2431 after_start
= shortest
[0] - shortest_start
- 1
2432 bigger_before
= before_start
> after_start
2434 for loop
in perp_loops
:
2435 # have the loop face the same direction as the shortest one
2437 if loop
[2] < len(loop
[0]) / 2:
2439 loop
[2] = len(loop
[0]) - loop
[2] - 1
2441 if loop
[2] > len(loop
[0]) / 2:
2443 loop
[2] = len(loop
[0]) - loop
[2] - 1
2444 # circular loops can shift, to prevent wrong trimming
2446 shift
= shortest_start
- loop
[2]
2447 if loop
[2] + shift
> 0 and loop
[2] + shift
< len(loop
[0]):
2448 loop
[0] = loop
[0][-shift
:] + loop
[0][:-shift
]
2451 loop
[2] += len(loop
[0])
2452 elif loop
[2] > len(loop
[0]) - 1:
2453 loop
[2] -= len(loop
[0])
2455 start
= max(0, loop
[2] - before_start
)
2456 end
= min(len(loop
[0]), loop
[2] + after_start
+ 1)
2457 trimmed_loops
.append([loop
[0][start
:end
], False])
2459 return(trimmed_loops
)
2462 # project knots on non-selected geometry
2463 def curve_project_knots(bm_mod
, verts_selected
, knots
, points
, circular
):
2464 # function to project vertex on edge
2465 def project(v1
, v2
, v3
):
2466 # v1 and v2 are part of a line
2467 # v3 is projected onto it
2473 if circular
: # project all knots
2477 else: # first and last knot shouldn't be projected
2480 pknots
= [mathutils
.Vector(bm_mod
.verts
[knots
[0]].co
[:])]
2481 for knot
in knots
[start
:end
]:
2482 if knot
in verts_selected
:
2483 knot_left
= knot_right
= False
2484 for i
in range(points
.index(knot
) - 1, -1 * len(points
), -1):
2485 if points
[i
] not in knots
:
2486 knot_left
= points
[i
]
2488 for i
in range(points
.index(knot
) + 1, 2 * len(points
)):
2489 if i
> len(points
) - 1:
2491 if points
[i
] not in knots
:
2492 knot_right
= points
[i
]
2494 if knot_left
and knot_right
and knot_left
!= knot_right
:
2495 knot_left
= mathutils
.Vector(bm_mod
.verts
[knot_left
].co
[:])
2496 knot_right
= mathutils
.Vector(bm_mod
.verts
[knot_right
].co
[:])
2497 knot
= mathutils
.Vector(bm_mod
.verts
[knot
].co
[:])
2498 pknots
.append(project(knot_left
, knot_right
, knot
))
2500 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2501 else: # knot isn't selected, so shouldn't be changed
2502 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2504 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knots
[-1]].co
[:]))
2509 # find all loops through a given vertex
2510 def curve_vertex_loops(bm_mod
, start_vert
, vert_edges
, edge_faces
):
2514 for edge
in vert_edges
[start_vert
]:
2515 if edge
in edges_used
:
2520 active_faces
= edge_faces
[edge
]
2525 new_edges
= vert_edges
[new_vert
]
2526 loop
.append(new_vert
)
2528 edges_used
.append(tuple(sorted([loop
[-1], loop
[-2]])))
2529 if len(new_edges
) < 3 or len(new_edges
) > 4:
2534 for new_edge
in new_edges
:
2535 if new_edge
in edges_used
:
2538 for new_face
in edge_faces
[new_edge
]:
2539 if new_face
in active_faces
:
2544 # found correct new edge
2545 active_faces
= edge_faces
[new_edge
]
2551 if new_vert
== loop
[0]:
2559 loops
.append([loop
, circular
])
2564 # ########################################
2565 # ##### Flatten functions ################
2566 # ########################################
2568 # sort input into loops
2569 def flatten_get_input(bm
):
2570 vert_verts
= dict_vert_verts(
2571 [edgekey(edge
) for edge
in bm
.edges
if edge
.select
and not edge
.hide
]
2573 verts
= [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
2575 # no connected verts, consider all selected verts as a single input
2577 return([[verts
, False]])
2580 while len(verts
) > 0:
2584 if loop
[-1] in vert_verts
:
2585 to_grow
= vert_verts
[loop
[-1]]
2589 while len(to_grow
) > 0:
2590 new_vert
= to_grow
[0]
2592 if new_vert
in loop
:
2594 loop
.append(new_vert
)
2595 verts
.remove(new_vert
)
2596 to_grow
+= vert_verts
[new_vert
]
2598 loops
.append([loop
, False])
2603 # calculate position of vertex projections on plane
2604 def flatten_project(bm
, loop
, com
, normal
):
2605 verts
= [bm
.verts
[v
] for v
in loop
[0]]
2607 [v
.index
, mathutils
.Vector(v
.co
[:]) -
2608 (mathutils
.Vector(v
.co
[:]) - com
).dot(normal
) * normal
] for v
in verts
2611 return(verts_projected
)
2614 # ########################################
2615 # ##### Gstretch functions ###############
2616 # ########################################
2618 # fake stroke class, used to create custom strokes if no GP data is found
2619 class gstretch_fake_stroke():
2620 def __init__(self
, points
):
2621 self
.points
= [gstretch_fake_stroke_point(p
) for p
in points
]
2624 # fake stroke point class, used in fake strokes
2625 class gstretch_fake_stroke_point():
2626 def __init__(self
, loc
):
2630 # flips loops, if necessary, to obtain maximum alignment to stroke
2631 def gstretch_align_pairs(ls_pairs
, object, bm_mod
, method
):
2632 # returns total distance between all verts in loop and corresponding stroke
2633 def distance_loop_stroke(loop
, stroke
, object, bm_mod
, method
):
2634 stroke_lengths_cache
= False
2635 loop_length
= len(loop
[0])
2638 if method
!= 'regular':
2639 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2641 for i
, v_index
in enumerate(loop
[0]):
2642 if method
== 'regular':
2643 relative_distance
= i
/ (loop_length
- 1)
2645 relative_distance
= relative_lengths
[i
]
2647 loc1
= object.matrix_world
@ bm_mod
.verts
[v_index
].co
2648 loc2
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2649 relative_distance
, stroke_lengths_cache
)
2650 total_distance
+= (loc2
- loc1
).length
2652 return(total_distance
)
2655 for (loop
, stroke
) in ls_pairs
:
2656 total_dist
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2659 total_dist_rev
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2661 if total_dist_rev
> total_dist
:
2667 # calculate vertex positions on stroke
2668 def gstretch_calculate_verts(loop
, stroke
, object, bm_mod
, method
):
2670 stroke_lengths_cache
= False
2671 loop_length
= len(loop
[0])
2672 matrix_inverse
= object.matrix_world
.inverted()
2674 # return intersection of line with stroke, or None
2675 def intersect_line_stroke(vec1
, vec2
, stroke
):
2676 for i
, p
in enumerate(stroke
.points
[1:]):
2677 intersections
= mathutils
.geometry
.intersect_line_line(vec1
, vec2
,
2678 p
.co
, stroke
.points
[i
].co
)
2679 if intersections
and \
2680 (intersections
[0] - intersections
[1]).length
< 1e-2:
2681 x
, dist
= mathutils
.geometry
.intersect_point_line(
2682 intersections
[0], p
.co
, stroke
.points
[i
].co
)
2684 return(intersections
[0])
2687 if method
== 'project':
2688 vert_edges
= dict_vert_edges(bm_mod
)
2690 for v_index
in loop
[0]:
2692 for ek
in vert_edges
[v_index
]:
2694 v1
= bm_mod
.verts
[v1
]
2695 v2
= bm_mod
.verts
[v2
]
2696 if v1
.select
+ v2
.select
== 1 and not v1
.hide
and not v2
.hide
:
2697 vec1
= object.matrix_world
@ v1
.co
2698 vec2
= object.matrix_world
@ v2
.co
2699 intersection
= intersect_line_stroke(vec1
, vec2
, stroke
)
2702 if not intersection
:
2703 v
= bm_mod
.verts
[v_index
]
2704 intersection
= intersect_line_stroke(v
.co
, v
.co
+ v
.normal
,
2707 move
.append([v_index
, matrix_inverse
@ intersection
])
2710 if method
== 'irregular':
2711 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2713 for i
, v_index
in enumerate(loop
[0]):
2714 if method
== 'regular':
2715 relative_distance
= i
/ (loop_length
- 1)
2716 else: # method == 'irregular'
2717 relative_distance
= relative_lengths
[i
]
2718 loc
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2719 relative_distance
, stroke_lengths_cache
)
2720 loc
= matrix_inverse
@ loc
2721 move
.append([v_index
, loc
])
2726 # create new vertices, based on GP strokes
2727 def gstretch_create_verts(object, bm_mod
, strokes
, method
, conversion
,
2728 conversion_distance
, conversion_max
, conversion_min
, conversion_vertices
):
2731 mat_world
= object.matrix_world
.inverted()
2732 singles
= gstretch_match_single_verts(bm_mod
, strokes
, mat_world
)
2734 for stroke
in strokes
:
2735 stroke_verts
.append([stroke
, []])
2737 if conversion
== 'vertices':
2738 min_end_point
= conversion_vertices
2739 end_point
= conversion_vertices
2740 elif conversion
== 'limit_vertices':
2741 min_end_point
= conversion_min
2742 end_point
= conversion_max
2744 end_point
= len(stroke
.points
)
2745 # creation of new vertices at fixed user-defined distances
2746 if conversion
== 'distance':
2748 prev_point
= stroke
.points
[0]
2749 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
@ prev_point
.co
))
2751 limit
= conversion_distance
2752 for point
in stroke
.points
:
2753 new_distance
= distance
+ (point
.co
- prev_point
.co
).length
2755 while new_distance
> limit
:
2756 to_cover
= limit
- distance
+ (limit
* iteration
)
2757 new_loc
= prev_point
.co
+ to_cover
* \
2758 (point
.co
- prev_point
.co
).normalized()
2759 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* new_loc
))
2760 new_distance
-= limit
2762 distance
= new_distance
2764 # creation of new vertices for other methods
2766 # add vertices at stroke points
2767 for point
in stroke
.points
[:end_point
]:
2768 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
@ point
.co
))
2769 # add more vertices, beyond the points that are available
2770 if min_end_point
> min(len(stroke
.points
), end_point
):
2771 for i
in range(min_end_point
-
2772 (min(len(stroke
.points
), end_point
))):
2773 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
@ point
.co
))
2774 # force even spreading of points, so they are placed on stroke
2776 bm_mod
.verts
.ensure_lookup_table()
2777 bm_mod
.verts
.index_update()
2778 for stroke
, verts_seq
in stroke_verts
:
2779 if len(verts_seq
) < 2:
2781 # spread vertices evenly over the stroke
2782 if method
== 'regular':
2783 loop
= [[vert
.index
for vert
in verts_seq
], False]
2784 move
+= gstretch_calculate_verts(loop
, stroke
, object, bm_mod
,
2787 for i
, vert
in enumerate(verts_seq
):
2789 bm_mod
.edges
.new((verts_seq
[i
- 1], verts_seq
[i
]))
2791 # connect single vertices to the closest stroke
2793 for vert
, m_stroke
, point
in singles
:
2794 if m_stroke
!= stroke
:
2796 bm_mod
.edges
.new((vert
, verts_seq
[point
]))
2797 bm_mod
.edges
.ensure_lookup_table()
2798 bmesh
.update_edit_mesh(object.data
)
2803 # erases the grease pencil stroke
2804 def gstretch_erase_stroke(stroke
, context
):
2805 # change 3d coordinate into a stroke-point
2806 def sp(loc
, context
):
2810 'location': (0, 0, 0),
2812 view3d_utils
.location_3d_to_region_2d(
2813 context
.region
, context
.space_data
.region_3d
, loc
)
2820 if type(stroke
) != bpy
.types
.GPencilStroke
:
2821 # fake stroke, there is nothing to delete
2824 erase_stroke
= [sp(p
.co
, context
) for p
in stroke
.points
]
2826 erase_stroke
[0]['is_start'] = True
2827 #bpy.ops.gpencil.draw(mode='ERASER', stroke=erase_stroke)
2828 bpy
.ops
.gpencil
.data_unlink()
2832 # get point on stroke, given by relative distance (0.0 - 1.0)
2833 def gstretch_eval_stroke(stroke
, distance
, stroke_lengths_cache
=False):
2834 # use cache if available
2835 if not stroke_lengths_cache
:
2837 for i
, p
in enumerate(stroke
.points
[1:]):
2838 lengths
.append((p
.co
- stroke
.points
[i
].co
).length
+ lengths
[-1])
2839 total_length
= max(lengths
[-1], 1e-7)
2840 stroke_lengths_cache
= [length
/ total_length
for length
in
2842 stroke_lengths
= stroke_lengths_cache
[:]
2844 if distance
in stroke_lengths
:
2845 loc
= stroke
.points
[stroke_lengths
.index(distance
)].co
2846 elif distance
> stroke_lengths
[-1]:
2847 # should be impossible, but better safe than sorry
2848 loc
= stroke
.points
[-1].co
2850 stroke_lengths
.append(distance
)
2851 stroke_lengths
.sort()
2852 stroke_index
= stroke_lengths
.index(distance
)
2853 interval_length
= stroke_lengths
[
2854 stroke_index
+ 1] - stroke_lengths
[stroke_index
- 1
2856 distance_relative
= (distance
- stroke_lengths
[stroke_index
- 1]) / interval_length
2857 interval_vector
= stroke
.points
[stroke_index
].co
- stroke
.points
[stroke_index
- 1].co
2858 loc
= stroke
.points
[stroke_index
- 1].co
+ distance_relative
* interval_vector
2860 return(loc
, stroke_lengths_cache
)
2863 # create fake grease pencil strokes for the active object
2864 def gstretch_get_fake_strokes(object, bm_mod
, loops
):
2867 p1
= object.matrix_world
@ bm_mod
.verts
[loop
[0][0]].co
2868 p2
= object.matrix_world
@ bm_mod
.verts
[loop
[0][-1]].co
2869 strokes
.append(gstretch_fake_stroke([p1
, p2
]))
2874 def gstretch_get_strokes(self
, context
):
2875 looptools
= context
.window_manager
.looptools
2876 gp
= get_strokes(self
, context
)
2879 if looptools
.gstretch_use_guide
== "Annotation":
2880 layer
= bpy
.data
.grease_pencils
[0].layers
.active
2881 if looptools
.gstretch_use_guide
== "GPencil" and not looptools
.gstretch_guide
== None:
2882 layer
= looptools
.gstretch_guide
.data
.layers
.active
2885 frame
= layer
.active_frame
2888 strokes
= frame
.strokes
2889 if len(strokes
) < 1:
2894 # returns a list with loop-stroke pairs
2895 def gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
):
2896 if not loops
or not strokes
:
2899 # calculate loop centers
2901 bm_mod
.verts
.ensure_lookup_table()
2903 center
= mathutils
.Vector()
2904 for v_index
in loop
[0]:
2905 center
+= bm_mod
.verts
[v_index
].co
2906 center
/= len(loop
[0])
2907 center
= object.matrix_world
@ center
2908 loop_centers
.append([center
, loop
])
2910 # calculate stroke centers
2912 for stroke
in strokes
:
2913 center
= mathutils
.Vector()
2914 for p
in stroke
.points
:
2916 center
/= len(stroke
.points
)
2917 stroke_centers
.append([center
, stroke
, 0])
2919 # match, first by stroke use count, then by distance
2921 for lc
in loop_centers
:
2923 for i
, sc
in enumerate(stroke_centers
):
2924 distances
.append([sc
[2], (lc
[0] - sc
[0]).length
, i
])
2926 best_stroke
= distances
[0][2]
2927 ls_pairs
.append([lc
[1], stroke_centers
[best_stroke
][1]])
2928 stroke_centers
[best_stroke
][2] += 1 # increase stroke use count
2933 # match single selected vertices to the closest stroke endpoint
2934 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2935 def gstretch_match_single_verts(bm_mod
, strokes
, mat_world
):
2936 # calculate stroke endpoints in object space
2938 for stroke
in strokes
:
2939 endpoints
.append((mat_world
@ stroke
.points
[0].co
, stroke
, 0))
2940 endpoints
.append((mat_world
@ stroke
.points
[-1].co
, stroke
, -1))
2943 # find single vertices (not connected to other selected verts)
2944 for vert
in bm_mod
.verts
:
2948 for edge
in vert
.link_edges
:
2949 if edge
.other_vert(vert
).select
:
2954 # calculate distances from vertex to endpoints
2955 distance
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2956 endpoint_index
) for endpoint_index
, (loc
, stroke
, stroke_point
) in
2957 enumerate(endpoints
)]
2959 distances
.append(distance
[0])
2961 # create matches, based on shortest distance first
2965 singles
.append((distances
[0][1], distances
[0][2], distances
[0][3]))
2966 endpoints
.pop(distances
[0][4])
2969 for (i
, vert
, j
, k
, l
) in distances
:
2970 distance_new
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2971 endpoint_index
) for endpoint_index
, (loc
, stroke
,
2972 stroke_point
) in enumerate(endpoints
)]
2974 distances_new
.append(distance_new
[0])
2975 distances
= distances_new
2980 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2981 def gstretch_relative_lengths(loop
, bm_mod
):
2983 for i
, v_index
in enumerate(loop
[0][1:]):
2985 (bm_mod
.verts
[v_index
].co
-
2986 bm_mod
.verts
[loop
[0][i
]].co
).length
+ lengths
[-1]
2988 total_length
= max(lengths
[-1], 1e-7)
2989 relative_lengths
= [length
/ total_length
for length
in
2992 return(relative_lengths
)
2995 # convert cache-stored strokes into usable (fake) GP strokes
2996 def gstretch_safe_to_true_strokes(safe_strokes
):
2998 for safe_stroke
in safe_strokes
:
2999 strokes
.append(gstretch_fake_stroke(safe_stroke
))
3004 # convert a GP stroke into a list of points which can be stored in cache
3005 def gstretch_true_to_safe_strokes(strokes
):
3007 for stroke
in strokes
:
3008 safe_strokes
.append([p
.co
.copy() for p
in stroke
.points
])
3010 return(safe_strokes
)
3013 # force consistency in GUI, max value can never be lower than min value
3014 def gstretch_update_max(self
, context
):
3015 # called from operator settings (after execution)
3016 if 'conversion_min' in self
.keys():
3017 if self
.conversion_min
> self
.conversion_max
:
3018 self
.conversion_max
= self
.conversion_min
3019 # called from toolbar
3021 lt
= context
.window_manager
.looptools
3022 if lt
.gstretch_conversion_min
> lt
.gstretch_conversion_max
:
3023 lt
.gstretch_conversion_max
= lt
.gstretch_conversion_min
3026 # force consistency in GUI, min value can never be higher than max value
3027 def gstretch_update_min(self
, context
):
3028 # called from operator settings (after execution)
3029 if 'conversion_max' in self
.keys():
3030 if self
.conversion_max
< self
.conversion_min
:
3031 self
.conversion_min
= self
.conversion_max
3032 # called from toolbar
3034 lt
= context
.window_manager
.looptools
3035 if lt
.gstretch_conversion_max
< lt
.gstretch_conversion_min
:
3036 lt
.gstretch_conversion_min
= lt
.gstretch_conversion_max
3039 # ########################################
3040 # ##### Relax functions ##################
3041 # ########################################
3043 # create lists with knots and points, all correctly sorted
3044 def relax_calculate_knots(loops
):
3047 for loop
, circular
in loops
:
3051 if len(loop
) % 2 == 1: # odd
3052 extend
= [False, True, 0, 1, 0, 1]
3054 extend
= [True, False, 0, 1, 1, 2]
3056 if len(loop
) % 2 == 1: # odd
3057 extend
= [False, False, 0, 1, 1, 2]
3059 extend
= [False, False, 0, 1, 1, 2]
3062 loop
= [loop
[-1]] + loop
+ [loop
[0]]
3063 for i
in range(extend
[2 + 2 * j
], len(loop
), 2):
3064 knots
[j
].append(loop
[i
])
3065 for i
in range(extend
[3 + 2 * j
], len(loop
), 2):
3066 if loop
[i
] == loop
[-1] and not circular
:
3068 if len(points
[j
]) == 0:
3069 points
[j
].append(loop
[i
])
3070 elif loop
[i
] != points
[j
][0]:
3071 points
[j
].append(loop
[i
])
3073 if knots
[j
][0] != knots
[j
][-1]:
3074 knots
[j
].append(knots
[j
][0])
3075 if len(points
[1]) == 0:
3081 all_points
.append(p
)
3083 return(all_knots
, all_points
)
3086 # calculate relative positions compared to first knot
3087 def relax_calculate_t(bm_mod
, knots
, points
, regular
):
3090 for i
in range(len(knots
)):
3091 amount
= len(knots
[i
]) + len(points
[i
])
3093 for j
in range(amount
):
3095 mix
.append([True, knots
[i
][round(j
/ 2)]])
3096 elif j
== amount
- 1:
3097 mix
.append([True, knots
[i
][-1]])
3099 mix
.append([False, points
[i
][int(j
/ 2)]])
3105 loc
= mathutils
.Vector(bm_mod
.verts
[m
[1]].co
[:])
3108 len_total
+= (loc
- loc_prev
).length
3110 tknots
.append(len_total
)
3112 tpoints
.append(len_total
)
3116 for p
in range(len(points
[i
])):
3117 tpoints
.append((tknots
[p
] + tknots
[p
+ 1]) / 2)
3118 all_tknots
.append(tknots
)
3119 all_tpoints
.append(tpoints
)
3121 return(all_tknots
, all_tpoints
)
3124 # change the location of the points to their place on the spline
3125 def relax_calculate_verts(bm_mod
, interpolation
, tknots
, knots
, tpoints
,
3129 for i
in range(len(knots
)):
3131 m
= tpoints
[i
][points
[i
].index(p
)]
3133 n
= tknots
[i
].index(m
)
3139 if n
> len(splines
[i
]) - 1:
3140 n
= len(splines
[i
]) - 1
3144 if interpolation
== 'cubic':
3145 ax
, bx
, cx
, dx
, tx
= splines
[i
][n
][0]
3146 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
3147 ay
, by
, cy
, dy
, ty
= splines
[i
][n
][1]
3148 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
3149 az
, bz
, cz
, dz
, tz
= splines
[i
][n
][2]
3150 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
3151 change
.append([p
, mathutils
.Vector([x
, y
, z
])])
3152 else: # interpolation == 'linear'
3153 a
, d
, t
, u
= splines
[i
][n
]
3156 change
.append([p
, ((m
- t
) / u
) * d
+ a
])
3158 move
.append([c
[0], (bm_mod
.verts
[c
[0]].co
+ c
[1]) / 2])
3163 # ########################################
3164 # ##### Space functions ##################
3165 # ########################################
3167 # calculate relative positions compared to first knot
3168 def space_calculate_t(bm_mod
, knots
):
3173 loc
= mathutils
.Vector(bm_mod
.verts
[k
].co
[:])
3176 len_total
+= (loc
- loc_prev
).length
3177 tknots
.append(len_total
)
3180 t_per_segment
= len_total
/ (amount
- 1)
3181 tpoints
= [i
* t_per_segment
for i
in range(amount
)]
3183 return(tknots
, tpoints
)
3186 # change the location of the points to their place on the spline
3187 def space_calculate_verts(bm_mod
, interpolation
, tknots
, tpoints
, points
,
3191 m
= tpoints
[points
.index(p
)]
3199 if n
> len(splines
) - 1:
3200 n
= len(splines
) - 1
3204 if interpolation
== 'cubic':
3205 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
3206 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
3207 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
3208 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
3209 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
3210 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
3211 move
.append([p
, mathutils
.Vector([x
, y
, z
])])
3212 else: # interpolation == 'linear'
3213 a
, d
, t
, u
= splines
[n
]
3214 move
.append([p
, ((m
- t
) / u
) * d
+ a
])
3219 # ########################################
3220 # ##### Operators ########################
3221 # ########################################
3224 class Bridge(Operator
):
3225 bl_idname
= 'mesh.looptools_bridge'
3226 bl_label
= "Bridge / Loft"
3227 bl_description
= "Bridge two, or loft several, loops of vertices"
3228 bl_options
= {'REGISTER', 'UNDO'}
3230 cubic_strength
: FloatProperty(
3232 description
="Higher strength results in more fluid curves",
3237 interpolation
: EnumProperty(
3238 name
="Interpolation mode",
3239 items
=(('cubic', "Cubic", "Gives curved results"),
3240 ('linear', "Linear", "Basic, fast, straight interpolation")),
3241 description
="Interpolation mode: algorithm used when creating "
3247 description
="Loft multiple loops, instead of considering them as "
3248 "a multi-input for bridging",
3251 loft_loop
: BoolProperty(
3253 description
="Connect the first and the last loop with each other",
3256 min_width
: IntProperty(
3257 name
="Minimum width",
3258 description
="Segments with an edge smaller than this are merged "
3259 "(compared to base edge)",
3263 subtype
='PERCENTAGE'
3267 items
=(('basic', "Basic", "Fast algorithm"),
3268 ('shortest', "Shortest edge", "Slower algorithm with better vertex matching")),
3269 description
="Algorithm used for bridging",
3272 remove_faces
: BoolProperty(
3273 name
="Remove faces",
3274 description
="Remove faces that are internal after bridging",
3277 reverse
: BoolProperty(
3279 description
="Manually override the direction in which the loops "
3280 "are bridged. Only use if the tool gives the wrong result",
3283 segments
: IntProperty(
3285 description
="Number of segments used to bridge the gap (0=automatic)",
3292 description
="Twist what vertices are connected to each other",
3297 def poll(cls
, context
):
3298 ob
= context
.active_object
3299 return (ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3301 def draw(self
, context
):
3302 layout
= self
.layout
3303 # layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3306 col_top
= layout
.column(align
=True)
3307 row
= col_top
.row(align
=True)
3308 col_left
= row
.column(align
=True)
3309 col_right
= row
.column(align
=True)
3310 col_right
.active
= self
.segments
!= 1
3311 col_left
.prop(self
, "segments")
3312 col_right
.prop(self
, "min_width", text
="")
3314 bottom_left
= col_left
.row()
3315 bottom_left
.active
= self
.segments
!= 1
3316 bottom_left
.prop(self
, "interpolation", text
="")
3317 bottom_right
= col_right
.row()
3318 bottom_right
.active
= self
.interpolation
== 'cubic'
3319 bottom_right
.prop(self
, "cubic_strength")
3320 # boolean properties
3321 col_top
.prop(self
, "remove_faces")
3323 col_top
.prop(self
, "loft_loop")
3325 # override properties
3327 row
= layout
.row(align
=True)
3328 row
.prop(self
, "twist")
3329 row
.prop(self
, "reverse")
3331 def invoke(self
, context
, event
):
3332 # load custom settings
3333 context
.window_manager
.looptools
.bridge_loft
= self
.loft
3335 return self
.execute(context
)
3337 def execute(self
, context
):
3339 object, bm
= initialise()
3340 edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
= \
3341 bridge_initialise(bm
, self
.interpolation
)
3342 settings_write(self
)
3344 # check cache to see if we can save time
3345 input_method
= bridge_input_method(self
.loft
, self
.loft_loop
)
3346 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Bridge",
3347 object, bm
, input_method
, False)
3350 loops
= bridge_get_input(bm
)
3352 # reorder loops if there are more than 2
3355 loops
= bridge_sort_loops(bm
, loops
, self
.loft_loop
)
3357 loops
= bridge_match_loops(bm
, loops
)
3359 # saving cache for faster execution next time
3361 cache_write("Bridge", object, bm
, input_method
, False, False,
3362 loops
, False, False)
3365 # calculate new geometry
3368 max_vert_index
= len(bm
.verts
) - 1
3369 for i
in range(1, len(loops
)):
3370 if not self
.loft
and i
% 2 == 0:
3372 lines
= bridge_calculate_lines(bm
, loops
[i
- 1:i
+ 1],
3373 self
.mode
, self
.twist
, self
.reverse
)
3374 vertex_normals
= bridge_calculate_virtual_vertex_normals(bm
,
3375 lines
, loops
[i
- 1:i
+ 1], edge_faces
, edgekey_to_edge
)
3376 segments
= bridge_calculate_segments(bm
, lines
,
3377 loops
[i
- 1:i
+ 1], self
.segments
)
3378 new_verts
, new_faces
, max_vert_index
= \
3379 bridge_calculate_geometry(
3380 bm
, lines
, vertex_normals
,
3381 segments
, self
.interpolation
, self
.cubic_strength
,
3382 self
.min_width
, max_vert_index
3385 vertices
+= new_verts
3388 # make sure faces in loops that aren't used, aren't removed
3389 if self
.remove_faces
and old_selected_faces
:
3390 bridge_save_unused_faces(bm
, old_selected_faces
, loops
)
3393 bridge_create_vertices(bm
, vertices
)
3396 new_faces
= bridge_create_faces(object, bm
, faces
, self
.twist
)
3397 old_selected_faces
= [
3398 i
for i
, face
in enumerate(bm
.faces
) if face
.index
in old_selected_faces
3400 bridge_select_new_faces(new_faces
, smooth
)
3401 # edge-data could have changed, can't use cache next run
3402 if faces
and not vertices
:
3403 cache_delete("Bridge")
3404 # delete internal faces
3405 if self
.remove_faces
and old_selected_faces
:
3406 bridge_remove_internal_faces(bm
, old_selected_faces
)
3407 # make sure normals are facing outside
3408 bmesh
.update_edit_mesh(object.data
, loop_triangles
=False,
3410 bpy
.ops
.mesh
.normals_make_consistent()
3419 class Circle(Operator
):
3420 bl_idname
= "mesh.looptools_circle"
3422 bl_description
= "Move selected vertices into a circle shape"
3423 bl_options
= {'REGISTER', 'UNDO'}
3425 custom_radius
: BoolProperty(
3427 description
="Force a custom radius",
3432 items
=(("best", "Best fit", "Non-linear least squares"),
3433 ("inside", "Fit inside", "Only move vertices towards the center")),
3434 description
="Method used for fitting a circle to the vertices",
3437 flatten
: BoolProperty(
3439 description
="Flatten the circle, instead of projecting it on the mesh",
3442 influence
: FloatProperty(
3444 description
="Force of the tool",
3449 subtype
='PERCENTAGE'
3451 lock_x
: BoolProperty(
3453 description
="Lock editing of the x-coordinate",
3456 lock_y
: BoolProperty(
3458 description
="Lock editing of the y-coordinate",
3461 lock_z
: BoolProperty(name
="Lock Z",
3462 description
="Lock editing of the z-coordinate",
3465 radius
: FloatProperty(
3467 description
="Custom radius for circle",
3472 regular
: BoolProperty(
3474 description
="Distribute vertices at constant distances along the circle",
3479 def poll(cls
, context
):
3480 ob
= context
.active_object
3481 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3483 def draw(self
, context
):
3484 layout
= self
.layout
3485 col
= layout
.column()
3487 col
.prop(self
, "fit")
3490 col
.prop(self
, "flatten")
3491 row
= col
.row(align
=True)
3492 row
.prop(self
, "custom_radius")
3493 row_right
= row
.row(align
=True)
3494 row_right
.active
= self
.custom_radius
3495 row_right
.prop(self
, "radius", text
="")
3496 col
.prop(self
, "regular")
3499 col_move
= col
.column(align
=True)
3500 row
= col_move
.row(align
=True)
3502 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3504 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3506 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3508 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3510 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3512 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3513 col_move
.prop(self
, "influence")
3515 def invoke(self
, context
, event
):
3516 # load custom settings
3518 return self
.execute(context
)
3520 def execute(self
, context
):
3522 object, bm
= initialise()
3523 settings_write(self
)
3524 # check cache to see if we can save time
3525 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Circle",
3526 object, bm
, False, False)
3528 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
3531 derived
, bm_mod
, single_vertices
, single_loops
, loops
= \
3532 circle_get_input(object, bm
)
3533 mapping
= get_mapping(derived
, bm
, bm_mod
, single_vertices
,
3535 single_loops
, loops
= circle_check_loops(single_loops
, loops
,
3538 # saving cache for faster execution next time
3540 cache_write("Circle", object, bm
, False, False, single_loops
,
3541 loops
, derived
, mapping
)
3544 for i
, loop
in enumerate(loops
):
3545 # best fitting flat plane
3546 com
, normal
= calculate_plane(bm_mod
, loop
)
3547 # if circular, shift loop so we get a good starting vertex
3549 loop
= circle_shift_loop(bm_mod
, loop
, com
)
3550 # flatten vertices on plane
3551 locs_2d
, p
, q
= circle_3d_to_2d(bm_mod
, loop
, com
, normal
)
3553 if self
.fit
== 'best':
3554 x0
, y0
, r
= circle_calculate_best_fit(locs_2d
)
3555 else: # self.fit == 'inside'
3556 x0
, y0
, r
= circle_calculate_min_fit(locs_2d
)
3558 if self
.custom_radius
:
3559 r
= self
.radius
/ p
.length
3560 # calculate positions on circle
3562 new_locs_2d
= circle_project_regular(locs_2d
[:], x0
, y0
, r
)
3564 new_locs_2d
= circle_project_non_regular(locs_2d
[:], x0
, y0
, r
)
3565 # take influence into account
3566 locs_2d
= circle_influence_locs(locs_2d
, new_locs_2d
,
3568 # calculate 3d positions of the created 2d input
3569 move
.append(circle_calculate_verts(self
.flatten
, bm_mod
,
3570 locs_2d
, com
, p
, q
, normal
))
3571 # flatten single input vertices on plane defined by loop
3572 if self
.flatten
and single_loops
:
3573 move
.append(circle_flatten_singles(bm_mod
, com
, p
, q
,
3574 normal
, single_loops
[i
]))
3576 # move vertices to new locations
3577 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3578 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3581 move_verts(object, bm
, mapping
, move
, lock
, -1)
3592 class Curve(Operator
):
3593 bl_idname
= "mesh.looptools_curve"
3595 bl_description
= "Turn a loop into a smooth curve"
3596 bl_options
= {'REGISTER', 'UNDO'}
3598 boundaries
: BoolProperty(
3600 description
="Limit the tool to work within the boundaries of the selected vertices",
3603 influence
: FloatProperty(
3605 description
="Force of the tool",
3610 subtype
='PERCENTAGE'
3612 interpolation
: EnumProperty(
3613 name
="Interpolation",
3614 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
3615 ("linear", "Linear", "Simple and fast linear algorithm")),
3616 description
="Algorithm used for interpolation",
3619 lock_x
: BoolProperty(
3621 description
="Lock editing of the x-coordinate",
3624 lock_y
: BoolProperty(
3626 description
="Lock editing of the y-coordinate",
3629 lock_z
: BoolProperty(
3631 description
="Lock editing of the z-coordinate",
3634 regular
: BoolProperty(
3636 description
="Distribute vertices at constant distances along the curve",
3639 restriction
: EnumProperty(
3641 items
=(("none", "None", "No restrictions on vertex movement"),
3642 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
3643 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
3644 description
="Restrictions on how the vertices can be moved",
3649 def poll(cls
, context
):
3650 ob
= context
.active_object
3651 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3653 def draw(self
, context
):
3654 layout
= self
.layout
3655 col
= layout
.column()
3657 col
.prop(self
, "interpolation")
3658 col
.prop(self
, "restriction")
3659 col
.prop(self
, "boundaries")
3660 col
.prop(self
, "regular")
3663 col_move
= col
.column(align
=True)
3664 row
= col_move
.row(align
=True)
3666 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3668 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3670 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3672 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3674 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3676 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3677 col_move
.prop(self
, "influence")
3679 def invoke(self
, context
, event
):
3680 # load custom settings
3682 return self
.execute(context
)
3684 def execute(self
, context
):
3686 object, bm
= initialise()
3687 settings_write(self
)
3688 # check cache to see if we can save time
3689 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Curve",
3690 object, bm
, False, self
.boundaries
)
3692 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
3695 derived
, bm_mod
, loops
= curve_get_input(object, bm
, self
.boundaries
)
3696 mapping
= get_mapping(derived
, bm
, bm_mod
, False, True, loops
)
3697 loops
= check_loops(loops
, mapping
, bm_mod
)
3699 v
.index
for v
in bm_mod
.verts
if v
.select
and not v
.hide
3702 # saving cache for faster execution next time
3704 cache_write("Curve", object, bm
, False, self
.boundaries
, False,
3705 loops
, derived
, mapping
)
3709 knots
, points
= curve_calculate_knots(loop
, verts_selected
)
3710 pknots
= curve_project_knots(bm_mod
, verts_selected
, knots
,
3712 tknots
, tpoints
= curve_calculate_t(bm_mod
, knots
, points
,
3713 pknots
, self
.regular
, loop
[1])
3714 splines
= calculate_splines(self
.interpolation
, bm_mod
,
3716 move
.append(curve_calculate_vertices(bm_mod
, knots
, tknots
,
3717 points
, tpoints
, splines
, self
.interpolation
,
3720 # move vertices to new locations
3721 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3722 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3725 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
3736 class Flatten(Operator
):
3737 bl_idname
= "mesh.looptools_flatten"
3738 bl_label
= "Flatten"
3739 bl_description
= "Flatten vertices on a best-fitting plane"
3740 bl_options
= {'REGISTER', 'UNDO'}
3742 influence
: FloatProperty(
3744 description
="Force of the tool",
3749 subtype
='PERCENTAGE'
3751 lock_x
: BoolProperty(
3753 description
="Lock editing of the x-coordinate",
3756 lock_y
: BoolProperty(
3758 description
="Lock editing of the y-coordinate",
3761 lock_z
: BoolProperty(name
="Lock Z",
3762 description
="Lock editing of the z-coordinate",
3765 plane
: EnumProperty(
3767 items
=(("best_fit", "Best fit", "Calculate a best fitting plane"),
3768 ("normal", "Normal", "Derive plane from averaging vertex normals"),
3769 ("view", "View", "Flatten on a plane perpendicular to the viewing angle")),
3770 description
="Plane on which vertices are flattened",
3773 restriction
: EnumProperty(
3775 items
=(("none", "None", "No restrictions on vertex movement"),
3776 ("bounding_box", "Bounding box", "Vertices are restricted to "
3777 "movement inside the bounding box of the selection")),
3778 description
="Restrictions on how the vertices can be moved",
3783 def poll(cls
, context
):
3784 ob
= context
.active_object
3785 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3787 def draw(self
, context
):
3788 layout
= self
.layout
3789 col
= layout
.column()
3791 col
.prop(self
, "plane")
3792 # col.prop(self, "restriction")
3795 col_move
= col
.column(align
=True)
3796 row
= col_move
.row(align
=True)
3798 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3800 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3802 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3804 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3806 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3808 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3809 col_move
.prop(self
, "influence")
3811 def invoke(self
, context
, event
):
3812 # load custom settings
3814 return self
.execute(context
)
3816 def execute(self
, context
):
3818 object, bm
= initialise()
3819 settings_write(self
)
3820 # check cache to see if we can save time
3821 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Flatten",
3822 object, bm
, False, False)
3824 # order input into virtual loops
3825 loops
= flatten_get_input(bm
)
3826 loops
= check_loops(loops
, mapping
, bm
)
3828 # saving cache for faster execution next time
3830 cache_write("Flatten", object, bm
, False, False, False, loops
,
3835 # calculate plane and position of vertices on them
3836 com
, normal
= calculate_plane(bm
, loop
, method
=self
.plane
,
3838 to_move
= flatten_project(bm
, loop
, com
, normal
)
3839 if self
.restriction
== 'none':
3840 move
.append(to_move
)
3842 move
.append(to_move
)
3844 # move vertices to new locations
3845 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3846 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3849 move_verts(object, bm
, False, move
, lock
, self
.influence
)
3857 # Annotation operator
3858 class RemoveAnnotation(Operator
):
3859 bl_idname
= "remove.annotation"
3860 bl_label
= "Remove Annotation"
3861 bl_description
= "Remove all Annotation Strokes"
3862 bl_options
= {'REGISTER', 'UNDO'}
3864 def execute(self
, context
):
3867 bpy
.data
.grease_pencils
[0].layers
.active
.clear()
3869 self
.report({'INFO'}, "No Annotation data to Unlink")
3870 return {'CANCELLED'}
3875 class RemoveGPencil(Operator
):
3876 bl_idname
= "remove.gp"
3877 bl_label
= "Remove GPencil"
3878 bl_description
= "Remove all GPencil Strokes"
3879 bl_options
= {'REGISTER', 'UNDO'}
3881 def execute(self
, context
):
3884 looptools
= context
.window_manager
.looptools
3885 looptools
.gstretch_guide
.data
.layers
.data
.clear()
3886 looptools
.gstretch_guide
.data
.update_tag()
3888 self
.report({'INFO'}, "No GPencil data to Unlink")
3889 return {'CANCELLED'}
3894 class GStretch(Operator
):
3895 bl_idname
= "mesh.looptools_gstretch"
3896 bl_label
= "Gstretch"
3897 bl_description
= "Stretch selected vertices to active stroke"
3898 bl_options
= {'REGISTER', 'UNDO'}
3900 conversion
: EnumProperty(
3902 items
=(("distance", "Distance", "Set the distance between vertices "
3903 "of the converted stroke"),
3904 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
3905 "number of vertices that converted strokes will have"),
3906 ("vertices", "Exact vertices", "Set the exact number of vertices "
3907 "that converted strokes will have. Short strokes "
3908 "with few points may contain less vertices than this number."),
3909 ("none", "No simplification", "Convert each point "
3911 description
="If strokes are converted to geometry, "
3912 "use this simplification method",
3913 default
='limit_vertices'
3915 conversion_distance
: FloatProperty(
3917 description
="Absolute distance between vertices along the converted "
3924 conversion_max
: IntProperty(
3925 name
="Max Vertices",
3926 description
="Maximum number of vertices strokes will "
3927 "have, when they are converted to geomtery",
3931 update
=gstretch_update_min
3933 conversion_min
: IntProperty(
3934 name
="Min Vertices",
3935 description
="Minimum number of vertices strokes will "
3936 "have, when they are converted to geomtery",
3940 update
=gstretch_update_max
3942 conversion_vertices
: IntProperty(
3944 description
="Number of vertices strokes will "
3945 "have, when they are converted to geometry. If strokes have less "
3946 "points than required, the 'Spread evenly' method is used",
3951 delete_strokes
: BoolProperty(
3952 name
="Delete strokes",
3953 description
="Remove strokes if they have been used."
3954 "WARNING: DOES NOT SUPPORT UNDO",
3957 influence
: FloatProperty(
3959 description
="Force of the tool",
3964 subtype
='PERCENTAGE'
3966 lock_x
: BoolProperty(
3968 description
="Lock editing of the x-coordinate",
3971 lock_y
: BoolProperty(
3973 description
="Lock editing of the y-coordinate",
3976 lock_z
: BoolProperty(
3978 description
="Lock editing of the z-coordinate",
3981 method
: EnumProperty(
3983 items
=(("project", "Project", "Project vertices onto the stroke, "
3984 "using vertex normals and connected edges"),
3985 ("irregular", "Spread", "Distribute vertices along the full "
3986 "stroke, retaining relative distances between the vertices"),
3987 ("regular", "Spread evenly", "Distribute vertices at regular "
3988 "distances along the full stroke")),
3989 description
="Method of distributing the vertices over the "
3995 def poll(cls
, context
):
3996 ob
= context
.active_object
3997 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3999 def draw(self
, context
):
4000 looptools
= context
.window_manager
.looptools
4001 layout
= self
.layout
4002 col
= layout
.column()
4004 col
.prop(self
, "method")
4007 col_conv
= col
.column(align
=True)
4008 col_conv
.prop(self
, "conversion", text
="")
4009 if self
.conversion
== 'distance':
4010 col_conv
.prop(self
, "conversion_distance")
4011 elif self
.conversion
== 'limit_vertices':
4012 row
= col_conv
.row(align
=True)
4013 row
.prop(self
, "conversion_min", text
="Min")
4014 row
.prop(self
, "conversion_max", text
="Max")
4015 elif self
.conversion
== 'vertices':
4016 col_conv
.prop(self
, "conversion_vertices")
4019 col_move
= col
.column(align
=True)
4020 row
= col_move
.row(align
=True)
4022 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
4024 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
4026 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
4028 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
4030 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
4032 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
4033 col_move
.prop(self
, "influence")
4035 if looptools
.gstretch_use_guide
== "Annotation":
4036 col
.operator("remove.annotation", text
="Delete annotation strokes")
4037 if looptools
.gstretch_use_guide
== "GPencil":
4038 col
.operator("remove.gp", text
="Delete GPencil strokes")
4040 def invoke(self
, context
, event
):
4041 # flush cached strokes
4042 if 'Gstretch' in looptools_cache
:
4043 looptools_cache
['Gstretch']['single_loops'] = []
4044 # load custom settings
4046 return self
.execute(context
)
4048 def execute(self
, context
):
4050 object, bm
= initialise()
4051 settings_write(self
)
4053 # check cache to see if we can save time
4054 cached
, safe_strokes
, loops
, derived
, mapping
= cache_read("Gstretch",
4055 object, bm
, False, False)
4057 straightening
= False
4059 strokes
= gstretch_safe_to_true_strokes(safe_strokes
)
4060 # cached strokes were flushed (see operator's invoke function)
4061 elif get_strokes(self
, context
):
4062 strokes
= gstretch_get_strokes(self
, context
)
4064 # straightening function (no GP) -> loops ignore modifiers
4065 straightening
= True
4068 bm_mod
.verts
.ensure_lookup_table()
4069 bm_mod
.edges
.ensure_lookup_table()
4070 bm_mod
.faces
.ensure_lookup_table()
4071 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
4072 if not straightening
:
4073 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
4075 # get loops and strokes
4076 if get_strokes(self
, context
):
4078 derived
, bm_mod
, loops
= get_connected_input(object, bm
, False, input='selected')
4079 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4080 loops
= check_loops(loops
, mapping
, bm_mod
)
4082 strokes
= gstretch_get_strokes(self
, context
)
4084 # straightening function (no GP) -> loops ignore modifiers
4088 bm_mod
.verts
.ensure_lookup_table()
4089 bm_mod
.edges
.ensure_lookup_table()
4090 bm_mod
.faces
.ensure_lookup_table()
4092 edgekey(edge
) for edge
in bm_mod
.edges
if
4093 edge
.select
and not edge
.hide
4095 loops
= get_connected_selections(edge_keys
)
4096 loops
= check_loops(loops
, mapping
, bm_mod
)
4097 # create fake strokes
4098 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
4100 # saving cache for faster execution next time
4103 safe_strokes
= gstretch_true_to_safe_strokes(strokes
)
4106 cache_write("Gstretch", object, bm
, False, False,
4107 safe_strokes
, loops
, derived
, mapping
)
4109 # pair loops and strokes
4110 ls_pairs
= gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
)
4111 ls_pairs
= gstretch_align_pairs(ls_pairs
, object, bm_mod
, self
.method
)
4115 # no selected geometry, convert GP to verts
4117 move
.append(gstretch_create_verts(object, bm
, strokes
,
4118 self
.method
, self
.conversion
, self
.conversion_distance
,
4119 self
.conversion_max
, self
.conversion_min
,
4120 self
.conversion_vertices
))
4121 for stroke
in strokes
:
4122 gstretch_erase_stroke(stroke
, context
)
4124 for (loop
, stroke
) in ls_pairs
:
4125 move
.append(gstretch_calculate_verts(loop
, stroke
, object,
4126 bm_mod
, self
.method
))
4127 if self
.delete_strokes
:
4128 if type(stroke
) != bpy
.types
.GPencilStroke
:
4129 # in case of cached fake stroke, get the real one
4130 if get_strokes(self
, context
):
4131 strokes
= gstretch_get_strokes(self
, context
)
4132 if loops
and strokes
:
4133 ls_pairs
= gstretch_match_loops_strokes(loops
,
4134 strokes
, object, bm_mod
)
4135 ls_pairs
= gstretch_align_pairs(ls_pairs
,
4136 object, bm_mod
, self
.method
)
4137 for (l
, s
) in ls_pairs
:
4141 gstretch_erase_stroke(stroke
, context
)
4143 # move vertices to new locations
4144 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4145 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4148 bmesh
.update_edit_mesh(object.data
, loop_triangles
=True, destructive
=True)
4149 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4160 class Relax(Operator
):
4161 bl_idname
= "mesh.looptools_relax"
4163 bl_description
= "Relax the loop, so it is smoother"
4164 bl_options
= {'REGISTER', 'UNDO'}
4166 input: EnumProperty(
4168 items
=(("all", "Parallel (all)", "Also use non-selected "
4169 "parallel loops as input"),
4170 ("selected", "Selection", "Only use selected vertices as input")),
4171 description
="Loops that are relaxed",
4174 interpolation
: EnumProperty(
4175 name
="Interpolation",
4176 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4177 ("linear", "Linear", "Simple and fast linear algorithm")),
4178 description
="Algorithm used for interpolation",
4181 iterations
: EnumProperty(
4183 items
=(("1", "1", "One"),
4184 ("3", "3", "Three"),
4186 ("10", "10", "Ten"),
4187 ("25", "25", "Twenty-five")),
4188 description
="Number of times the loop is relaxed",
4191 regular
: BoolProperty(
4193 description
="Distribute vertices at constant distances along the loop",
4198 def poll(cls
, context
):
4199 ob
= context
.active_object
4200 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4202 def draw(self
, context
):
4203 layout
= self
.layout
4204 col
= layout
.column()
4206 col
.prop(self
, "interpolation")
4207 col
.prop(self
, "input")
4208 col
.prop(self
, "iterations")
4209 col
.prop(self
, "regular")
4211 def invoke(self
, context
, event
):
4212 # load custom settings
4214 return self
.execute(context
)
4216 def execute(self
, context
):
4218 object, bm
= initialise()
4219 settings_write(self
)
4220 # check cache to see if we can save time
4221 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Relax",
4222 object, bm
, self
.input, False)
4224 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
4227 derived
, bm_mod
, loops
= get_connected_input(object, bm
, False, self
.input)
4228 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4229 loops
= check_loops(loops
, mapping
, bm_mod
)
4230 knots
, points
= relax_calculate_knots(loops
)
4232 # saving cache for faster execution next time
4234 cache_write("Relax", object, bm
, self
.input, False, False, loops
,
4237 for iteration
in range(int(self
.iterations
)):
4238 # calculate splines and new positions
4239 tknots
, tpoints
= relax_calculate_t(bm_mod
, knots
, points
,
4242 for i
in range(len(knots
)):
4243 splines
.append(calculate_splines(self
.interpolation
, bm_mod
,
4244 tknots
[i
], knots
[i
]))
4245 move
= [relax_calculate_verts(bm_mod
, self
.interpolation
,
4246 tknots
, knots
, tpoints
, points
, splines
)]
4247 move_verts(object, bm
, mapping
, move
, False, -1)
4258 class Space(Operator
):
4259 bl_idname
= "mesh.looptools_space"
4261 bl_description
= "Space the vertices in a regular distribution on the loop"
4262 bl_options
= {'REGISTER', 'UNDO'}
4264 influence
: FloatProperty(
4266 description
="Force of the tool",
4271 subtype
='PERCENTAGE'
4273 input: EnumProperty(
4275 items
=(("all", "Parallel (all)", "Also use non-selected "
4276 "parallel loops as input"),
4277 ("selected", "Selection", "Only use selected vertices as input")),
4278 description
="Loops that are spaced",
4281 interpolation
: EnumProperty(
4282 name
="Interpolation",
4283 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4284 ("linear", "Linear", "Vertices are projected on existing edges")),
4285 description
="Algorithm used for interpolation",
4288 lock_x
: BoolProperty(
4290 description
="Lock editing of the x-coordinate",
4293 lock_y
: BoolProperty(
4295 description
="Lock editing of the y-coordinate",
4298 lock_z
: BoolProperty(
4300 description
="Lock editing of the z-coordinate",
4305 def poll(cls
, context
):
4306 ob
= context
.active_object
4307 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4309 def draw(self
, context
):
4310 layout
= self
.layout
4311 col
= layout
.column()
4313 col
.prop(self
, "interpolation")
4314 col
.prop(self
, "input")
4317 col_move
= col
.column(align
=True)
4318 row
= col_move
.row(align
=True)
4320 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
4322 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
4324 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
4326 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
4328 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
4330 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
4331 col_move
.prop(self
, "influence")
4333 def invoke(self
, context
, event
):
4334 # load custom settings
4336 return self
.execute(context
)
4338 def execute(self
, context
):
4340 object, bm
= initialise()
4341 settings_write(self
)
4342 # check cache to see if we can save time
4343 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Space",
4344 object, bm
, self
.input, False)
4346 derived
, bm_mod
= get_derived_bmesh(object, bm
, True)
4349 derived
, bm_mod
, loops
= get_connected_input(object, bm
, True, self
.input)
4350 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4351 loops
= check_loops(loops
, mapping
, bm_mod
)
4353 # saving cache for faster execution next time
4355 cache_write("Space", object, bm
, self
.input, False, False, loops
,
4360 # calculate splines and new positions
4361 if loop
[1]: # circular
4362 loop
[0].append(loop
[0][0])
4363 tknots
, tpoints
= space_calculate_t(bm_mod
, loop
[0][:])
4364 splines
= calculate_splines(self
.interpolation
, bm_mod
,
4366 move
.append(space_calculate_verts(bm_mod
, self
.interpolation
,
4367 tknots
, tpoints
, loop
[0][:-1], splines
))
4368 # move vertices to new locations
4369 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4370 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4373 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4380 cache_delete("Space")
4385 # ########################################
4386 # ##### GUI and registration #############
4387 # ########################################
4389 # menu containing all tools
4390 class VIEW3D_MT_edit_mesh_looptools(Menu
):
4391 bl_label
= "LoopTools"
4393 def draw(self
, context
):
4394 layout
= self
.layout
4396 layout
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4397 layout
.operator("mesh.looptools_circle")
4398 layout
.operator("mesh.looptools_curve")
4399 layout
.operator("mesh.looptools_flatten")
4400 layout
.operator("mesh.looptools_gstretch")
4401 layout
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4402 layout
.operator("mesh.looptools_relax")
4403 layout
.operator("mesh.looptools_space")
4406 # panel containing all tools
4407 class VIEW3D_PT_tools_looptools(Panel
):
4408 bl_space_type
= 'VIEW_3D'
4409 bl_region_type
= 'UI'
4410 bl_category
= 'Edit'
4411 bl_context
= "mesh_edit"
4412 bl_label
= "LoopTools"
4413 bl_options
= {'DEFAULT_CLOSED'}
4415 def draw(self
, context
):
4416 layout
= self
.layout
4417 col
= layout
.column(align
=True)
4418 lt
= context
.window_manager
.looptools
4420 # bridge - first line
4421 split
= col
.split(factor
=0.15, align
=True)
4422 if lt
.display_bridge
:
4423 split
.prop(lt
, "display_bridge", text
="", icon
='DOWNARROW_HLT')
4425 split
.prop(lt
, "display_bridge", text
="", icon
='RIGHTARROW')
4426 split
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4428 if lt
.display_bridge
:
4429 box
= col
.column(align
=True).box().column()
4430 # box.prop(self, "mode")
4433 col_top
= box
.column(align
=True)
4434 row
= col_top
.row(align
=True)
4435 col_left
= row
.column(align
=True)
4436 col_right
= row
.column(align
=True)
4437 col_right
.active
= lt
.bridge_segments
!= 1
4438 col_left
.prop(lt
, "bridge_segments")
4439 col_right
.prop(lt
, "bridge_min_width", text
="")
4441 bottom_left
= col_left
.row()
4442 bottom_left
.active
= lt
.bridge_segments
!= 1
4443 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4444 bottom_right
= col_right
.row()
4445 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4446 bottom_right
.prop(lt
, "bridge_cubic_strength")
4447 # boolean properties
4448 col_top
.prop(lt
, "bridge_remove_faces")
4450 # override properties
4452 row
= box
.row(align
=True)
4453 row
.prop(lt
, "bridge_twist")
4454 row
.prop(lt
, "bridge_reverse")
4456 # circle - first line
4457 split
= col
.split(factor
=0.15, align
=True)
4458 if lt
.display_circle
:
4459 split
.prop(lt
, "display_circle", text
="", icon
='DOWNARROW_HLT')
4461 split
.prop(lt
, "display_circle", text
="", icon
='RIGHTARROW')
4462 split
.operator("mesh.looptools_circle")
4464 if lt
.display_circle
:
4465 box
= col
.column(align
=True).box().column()
4466 box
.prop(lt
, "circle_fit")
4469 box
.prop(lt
, "circle_flatten")
4470 row
= box
.row(align
=True)
4471 row
.prop(lt
, "circle_custom_radius")
4472 row_right
= row
.row(align
=True)
4473 row_right
.active
= lt
.circle_custom_radius
4474 row_right
.prop(lt
, "circle_radius", text
="")
4475 box
.prop(lt
, "circle_regular")
4478 col_move
= box
.column(align
=True)
4479 row
= col_move
.row(align
=True)
4480 if lt
.circle_lock_x
:
4481 row
.prop(lt
, "circle_lock_x", text
="X", icon
='LOCKED')
4483 row
.prop(lt
, "circle_lock_x", text
="X", icon
='UNLOCKED')
4484 if lt
.circle_lock_y
:
4485 row
.prop(lt
, "circle_lock_y", text
="Y", icon
='LOCKED')
4487 row
.prop(lt
, "circle_lock_y", text
="Y", icon
='UNLOCKED')
4488 if lt
.circle_lock_z
:
4489 row
.prop(lt
, "circle_lock_z", text
="Z", icon
='LOCKED')
4491 row
.prop(lt
, "circle_lock_z", text
="Z", icon
='UNLOCKED')
4492 col_move
.prop(lt
, "circle_influence")
4494 # curve - first line
4495 split
= col
.split(factor
=0.15, align
=True)
4496 if lt
.display_curve
:
4497 split
.prop(lt
, "display_curve", text
="", icon
='DOWNARROW_HLT')
4499 split
.prop(lt
, "display_curve", text
="", icon
='RIGHTARROW')
4500 split
.operator("mesh.looptools_curve")
4502 if lt
.display_curve
:
4503 box
= col
.column(align
=True).box().column()
4504 box
.prop(lt
, "curve_interpolation")
4505 box
.prop(lt
, "curve_restriction")
4506 box
.prop(lt
, "curve_boundaries")
4507 box
.prop(lt
, "curve_regular")
4510 col_move
= box
.column(align
=True)
4511 row
= col_move
.row(align
=True)
4513 row
.prop(lt
, "curve_lock_x", text
="X", icon
='LOCKED')
4515 row
.prop(lt
, "curve_lock_x", text
="X", icon
='UNLOCKED')
4517 row
.prop(lt
, "curve_lock_y", text
="Y", icon
='LOCKED')
4519 row
.prop(lt
, "curve_lock_y", text
="Y", icon
='UNLOCKED')
4521 row
.prop(lt
, "curve_lock_z", text
="Z", icon
='LOCKED')
4523 row
.prop(lt
, "curve_lock_z", text
="Z", icon
='UNLOCKED')
4524 col_move
.prop(lt
, "curve_influence")
4526 # flatten - first line
4527 split
= col
.split(factor
=0.15, align
=True)
4528 if lt
.display_flatten
:
4529 split
.prop(lt
, "display_flatten", text
="", icon
='DOWNARROW_HLT')
4531 split
.prop(lt
, "display_flatten", text
="", icon
='RIGHTARROW')
4532 split
.operator("mesh.looptools_flatten")
4533 # flatten - settings
4534 if lt
.display_flatten
:
4535 box
= col
.column(align
=True).box().column()
4536 box
.prop(lt
, "flatten_plane")
4537 # box.prop(lt, "flatten_restriction")
4540 col_move
= box
.column(align
=True)
4541 row
= col_move
.row(align
=True)
4542 if lt
.flatten_lock_x
:
4543 row
.prop(lt
, "flatten_lock_x", text
="X", icon
='LOCKED')
4545 row
.prop(lt
, "flatten_lock_x", text
="X", icon
='UNLOCKED')
4546 if lt
.flatten_lock_y
:
4547 row
.prop(lt
, "flatten_lock_y", text
="Y", icon
='LOCKED')
4549 row
.prop(lt
, "flatten_lock_y", text
="Y", icon
='UNLOCKED')
4550 if lt
.flatten_lock_z
:
4551 row
.prop(lt
, "flatten_lock_z", text
="Z", icon
='LOCKED')
4553 row
.prop(lt
, "flatten_lock_z", text
="Z", icon
='UNLOCKED')
4554 col_move
.prop(lt
, "flatten_influence")
4556 # gstretch - first line
4557 split
= col
.split(factor
=0.15, align
=True)
4558 if lt
.display_gstretch
:
4559 split
.prop(lt
, "display_gstretch", text
="", icon
='DOWNARROW_HLT')
4561 split
.prop(lt
, "display_gstretch", text
="", icon
='RIGHTARROW')
4562 split
.operator("mesh.looptools_gstretch")
4564 if lt
.display_gstretch
:
4565 box
= col
.column(align
=True).box().column()
4566 box
.prop(lt
, "gstretch_use_guide")
4567 if lt
.gstretch_use_guide
== "GPencil":
4568 box
.prop(lt
, "gstretch_guide")
4569 box
.prop(lt
, "gstretch_method")
4571 col_conv
= box
.column(align
=True)
4572 col_conv
.prop(lt
, "gstretch_conversion", text
="")
4573 if lt
.gstretch_conversion
== 'distance':
4574 col_conv
.prop(lt
, "gstretch_conversion_distance")
4575 elif lt
.gstretch_conversion
== 'limit_vertices':
4576 row
= col_conv
.row(align
=True)
4577 row
.prop(lt
, "gstretch_conversion_min", text
="Min")
4578 row
.prop(lt
, "gstretch_conversion_max", text
="Max")
4579 elif lt
.gstretch_conversion
== 'vertices':
4580 col_conv
.prop(lt
, "gstretch_conversion_vertices")
4583 col_move
= box
.column(align
=True)
4584 row
= col_move
.row(align
=True)
4585 if lt
.gstretch_lock_x
:
4586 row
.prop(lt
, "gstretch_lock_x", text
="X", icon
='LOCKED')
4588 row
.prop(lt
, "gstretch_lock_x", text
="X", icon
='UNLOCKED')
4589 if lt
.gstretch_lock_y
:
4590 row
.prop(lt
, "gstretch_lock_y", text
="Y", icon
='LOCKED')
4592 row
.prop(lt
, "gstretch_lock_y", text
="Y", icon
='UNLOCKED')
4593 if lt
.gstretch_lock_z
:
4594 row
.prop(lt
, "gstretch_lock_z", text
="Z", icon
='LOCKED')
4596 row
.prop(lt
, "gstretch_lock_z", text
="Z", icon
='UNLOCKED')
4597 col_move
.prop(lt
, "gstretch_influence")
4598 if lt
.gstretch_use_guide
== "Annotation":
4599 box
.operator("remove.annotation", text
="Delete Annotation Strokes")
4600 if lt
.gstretch_use_guide
== "GPencil":
4601 box
.operator("remove.gp", text
="Delete GPencil Strokes")
4604 split
= col
.split(factor
=0.15, align
=True)
4606 split
.prop(lt
, "display_loft", text
="", icon
='DOWNARROW_HLT')
4608 split
.prop(lt
, "display_loft", text
="", icon
='RIGHTARROW')
4609 split
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4612 box
= col
.column(align
=True).box().column()
4613 # box.prop(self, "mode")
4616 col_top
= box
.column(align
=True)
4617 row
= col_top
.row(align
=True)
4618 col_left
= row
.column(align
=True)
4619 col_right
= row
.column(align
=True)
4620 col_right
.active
= lt
.bridge_segments
!= 1
4621 col_left
.prop(lt
, "bridge_segments")
4622 col_right
.prop(lt
, "bridge_min_width", text
="")
4624 bottom_left
= col_left
.row()
4625 bottom_left
.active
= lt
.bridge_segments
!= 1
4626 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4627 bottom_right
= col_right
.row()
4628 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4629 bottom_right
.prop(lt
, "bridge_cubic_strength")
4630 # boolean properties
4631 col_top
.prop(lt
, "bridge_remove_faces")
4632 col_top
.prop(lt
, "bridge_loft_loop")
4634 # override properties
4636 row
= box
.row(align
=True)
4637 row
.prop(lt
, "bridge_twist")
4638 row
.prop(lt
, "bridge_reverse")
4640 # relax - first line
4641 split
= col
.split(factor
=0.15, align
=True)
4642 if lt
.display_relax
:
4643 split
.prop(lt
, "display_relax", text
="", icon
='DOWNARROW_HLT')
4645 split
.prop(lt
, "display_relax", text
="", icon
='RIGHTARROW')
4646 split
.operator("mesh.looptools_relax")
4648 if lt
.display_relax
:
4649 box
= col
.column(align
=True).box().column()
4650 box
.prop(lt
, "relax_interpolation")
4651 box
.prop(lt
, "relax_input")
4652 box
.prop(lt
, "relax_iterations")
4653 box
.prop(lt
, "relax_regular")
4655 # space - first line
4656 split
= col
.split(factor
=0.15, align
=True)
4657 if lt
.display_space
:
4658 split
.prop(lt
, "display_space", text
="", icon
='DOWNARROW_HLT')
4660 split
.prop(lt
, "display_space", text
="", icon
='RIGHTARROW')
4661 split
.operator("mesh.looptools_space")
4663 if lt
.display_space
:
4664 box
= col
.column(align
=True).box().column()
4665 box
.prop(lt
, "space_interpolation")
4666 box
.prop(lt
, "space_input")
4669 col_move
= box
.column(align
=True)
4670 row
= col_move
.row(align
=True)
4672 row
.prop(lt
, "space_lock_x", text
="X", icon
='LOCKED')
4674 row
.prop(lt
, "space_lock_x", text
="X", icon
='UNLOCKED')
4676 row
.prop(lt
, "space_lock_y", text
="Y", icon
='LOCKED')
4678 row
.prop(lt
, "space_lock_y", text
="Y", icon
='UNLOCKED')
4680 row
.prop(lt
, "space_lock_z", text
="Z", icon
='LOCKED')
4682 row
.prop(lt
, "space_lock_z", text
="Z", icon
='UNLOCKED')
4683 col_move
.prop(lt
, "space_influence")
4686 # property group containing all properties for the gui in the panel
4687 class LoopToolsProps(PropertyGroup
):
4689 Fake module like class
4690 bpy.context.window_manager.looptools
4692 # general display properties
4693 display_bridge
: BoolProperty(
4694 name
="Bridge settings",
4695 description
="Display settings of the Bridge tool",
4698 display_circle
: BoolProperty(
4699 name
="Circle settings",
4700 description
="Display settings of the Circle tool",
4703 display_curve
: BoolProperty(
4704 name
="Curve settings",
4705 description
="Display settings of the Curve tool",
4708 display_flatten
: BoolProperty(
4709 name
="Flatten settings",
4710 description
="Display settings of the Flatten tool",
4713 display_gstretch
: BoolProperty(
4714 name
="Gstretch settings",
4715 description
="Display settings of the Gstretch tool",
4718 display_loft
: BoolProperty(
4719 name
="Loft settings",
4720 description
="Display settings of the Loft tool",
4723 display_relax
: BoolProperty(
4724 name
="Relax settings",
4725 description
="Display settings of the Relax tool",
4728 display_space
: BoolProperty(
4729 name
="Space settings",
4730 description
="Display settings of the Space tool",
4735 bridge_cubic_strength
: FloatProperty(
4737 description
="Higher strength results in more fluid curves",
4742 bridge_interpolation
: EnumProperty(
4743 name
="Interpolation mode",
4744 items
=(('cubic', "Cubic", "Gives curved results"),
4745 ('linear', "Linear", "Basic, fast, straight interpolation")),
4746 description
="Interpolation mode: algorithm used when creating segments",
4749 bridge_loft
: BoolProperty(
4751 description
="Loft multiple loops, instead of considering them as "
4752 "a multi-input for bridging",
4755 bridge_loft_loop
: BoolProperty(
4757 description
="Connect the first and the last loop with each other",
4760 bridge_min_width
: IntProperty(
4761 name
="Minimum width",
4762 description
="Segments with an edge smaller than this are merged "
4763 "(compared to base edge)",
4767 subtype
='PERCENTAGE'
4769 bridge_mode
: EnumProperty(
4771 items
=(('basic', "Basic", "Fast algorithm"),
4772 ('shortest', "Shortest edge", "Slower algorithm with "
4773 "better vertex matching")),
4774 description
="Algorithm used for bridging",
4777 bridge_remove_faces
: BoolProperty(
4778 name
="Remove faces",
4779 description
="Remove faces that are internal after bridging",
4782 bridge_reverse
: BoolProperty(
4784 description
="Manually override the direction in which the loops "
4785 "are bridged. Only use if the tool gives the wrong result",
4788 bridge_segments
: IntProperty(
4790 description
="Number of segments used to bridge the gap (0=automatic)",
4795 bridge_twist
: IntProperty(
4797 description
="Twist what vertices are connected to each other",
4802 circle_custom_radius
: BoolProperty(
4804 description
="Force a custom radius",
4807 circle_fit
: EnumProperty(
4809 items
=(("best", "Best fit", "Non-linear least squares"),
4810 ("inside", "Fit inside", "Only move vertices towards the center")),
4811 description
="Method used for fitting a circle to the vertices",
4814 circle_flatten
: BoolProperty(
4816 description
="Flatten the circle, instead of projecting it on the mesh",
4819 circle_influence
: FloatProperty(
4821 description
="Force of the tool",
4826 subtype
='PERCENTAGE'
4828 circle_lock_x
: BoolProperty(
4830 description
="Lock editing of the x-coordinate",
4833 circle_lock_y
: BoolProperty(
4835 description
="Lock editing of the y-coordinate",
4838 circle_lock_z
: BoolProperty(
4840 description
="Lock editing of the z-coordinate",
4843 circle_radius
: FloatProperty(
4845 description
="Custom radius for circle",
4850 circle_regular
: BoolProperty(
4852 description
="Distribute vertices at constant distances along the circle",
4856 curve_boundaries
: BoolProperty(
4858 description
="Limit the tool to work within the boundaries of the "
4859 "selected vertices",
4862 curve_influence
: FloatProperty(
4864 description
="Force of the tool",
4869 subtype
='PERCENTAGE'
4871 curve_interpolation
: EnumProperty(
4872 name
="Interpolation",
4873 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4874 ("linear", "Linear", "Simple and fast linear algorithm")),
4875 description
="Algorithm used for interpolation",
4878 curve_lock_x
: BoolProperty(
4880 description
="Lock editing of the x-coordinate",
4883 curve_lock_y
: BoolProperty(
4885 description
="Lock editing of the y-coordinate",
4888 curve_lock_z
: BoolProperty(
4890 description
="Lock editing of the z-coordinate",
4893 curve_regular
: BoolProperty(
4895 description
="Distribute vertices at constant distances along the curve",
4898 curve_restriction
: EnumProperty(
4900 items
=(("none", "None", "No restrictions on vertex movement"),
4901 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
4902 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
4903 description
="Restrictions on how the vertices can be moved",
4907 # flatten properties
4908 flatten_influence
: FloatProperty(
4910 description
="Force of the tool",
4915 subtype
='PERCENTAGE'
4917 flatten_lock_x
: BoolProperty(
4919 description
="Lock editing of the x-coordinate",
4921 flatten_lock_y
: BoolProperty(name
="Lock Y",
4922 description
="Lock editing of the y-coordinate",
4925 flatten_lock_z
: BoolProperty(
4927 description
="Lock editing of the z-coordinate",
4930 flatten_plane
: EnumProperty(
4932 items
=(("best_fit", "Best fit", "Calculate a best fitting plane"),
4933 ("normal", "Normal", "Derive plane from averaging vertex "
4935 ("view", "View", "Flatten on a plane perpendicular to the "
4937 description
="Plane on which vertices are flattened",
4940 flatten_restriction
: EnumProperty(
4942 items
=(("none", "None", "No restrictions on vertex movement"),
4943 ("bounding_box", "Bounding box", "Vertices are restricted to "
4944 "movement inside the bounding box of the selection")),
4945 description
="Restrictions on how the vertices can be moved",
4949 # gstretch properties
4950 gstretch_conversion
: EnumProperty(
4952 items
=(("distance", "Distance", "Set the distance between vertices "
4953 "of the converted stroke"),
4954 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
4955 "number of vertices that converted GP strokes will have"),
4956 ("vertices", "Exact vertices", "Set the exact number of vertices "
4957 "that converted strokes will have. Short strokes "
4958 "with few points may contain less vertices than this number."),
4959 ("none", "No simplification", "Convert each point "
4961 description
="If strokes are converted to geometry, "
4962 "use this simplification method",
4963 default
='limit_vertices'
4965 gstretch_conversion_distance
: FloatProperty(
4967 description
="Absolute distance between vertices along the converted "
4974 gstretch_conversion_max
: IntProperty(
4975 name
="Max Vertices",
4976 description
="Maximum number of vertices strokes will "
4977 "have, when they are converted to geomtery",
4981 update
=gstretch_update_min
4983 gstretch_conversion_min
: IntProperty(
4984 name
="Min Vertices",
4985 description
="Minimum number of vertices strokes will "
4986 "have, when they are converted to geomtery",
4990 update
=gstretch_update_max
4992 gstretch_conversion_vertices
: IntProperty(
4994 description
="Number of vertices strokes will "
4995 "have, when they are converted to geometry. If strokes have less "
4996 "points than required, the 'Spread evenly' method is used",
5001 gstretch_delete_strokes
: BoolProperty(
5002 name
="Delete strokes",
5003 description
="Remove Grease Pencil strokes if they have been used "
5004 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
5007 gstretch_influence
: FloatProperty(
5009 description
="Force of the tool",
5014 subtype
='PERCENTAGE'
5016 gstretch_lock_x
: BoolProperty(
5018 description
="Lock editing of the x-coordinate",
5021 gstretch_lock_y
: BoolProperty(
5023 description
="Lock editing of the y-coordinate",
5026 gstretch_lock_z
: BoolProperty(
5028 description
="Lock editing of the z-coordinate",
5031 gstretch_method
: EnumProperty(
5033 items
=(("project", "Project", "Project vertices onto the stroke, "
5034 "using vertex normals and connected edges"),
5035 ("irregular", "Spread", "Distribute vertices along the full "
5036 "stroke, retaining relative distances between the vertices"),
5037 ("regular", "Spread evenly", "Distribute vertices at regular "
5038 "distances along the full stroke")),
5039 description
="Method of distributing the vertices over the Grease "
5043 gstretch_use_guide
: EnumProperty(
5045 items
=(("None", "None", "None"),
5046 ("Annotation", "Annotation", "Annotation"),
5047 ("GPencil", "GPencil", "GPencil")),
5050 gstretch_guide
: PointerProperty(
5051 name
="GPencil object",
5052 description
="Set GPencil object",
5053 type=bpy
.types
.Object
5057 relax_input
: EnumProperty(name
="Input",
5058 items
=(("all", "Parallel (all)", "Also use non-selected "
5059 "parallel loops as input"),
5060 ("selected", "Selection", "Only use selected vertices as input")),
5061 description
="Loops that are relaxed",
5064 relax_interpolation
: EnumProperty(
5065 name
="Interpolation",
5066 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5067 ("linear", "Linear", "Simple and fast linear algorithm")),
5068 description
="Algorithm used for interpolation",
5071 relax_iterations
: EnumProperty(name
="Iterations",
5072 items
=(("1", "1", "One"),
5073 ("3", "3", "Three"),
5075 ("10", "10", "Ten"),
5076 ("25", "25", "Twenty-five")),
5077 description
="Number of times the loop is relaxed",
5080 relax_regular
: BoolProperty(
5082 description
="Distribute vertices at constant distances along the loop",
5087 space_influence
: FloatProperty(
5089 description
="Force of the tool",
5094 subtype
='PERCENTAGE'
5096 space_input
: EnumProperty(
5098 items
=(("all", "Parallel (all)", "Also use non-selected "
5099 "parallel loops as input"),
5100 ("selected", "Selection", "Only use selected vertices as input")),
5101 description
="Loops that are spaced",
5104 space_interpolation
: EnumProperty(
5105 name
="Interpolation",
5106 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5107 ("linear", "Linear", "Vertices are projected on existing edges")),
5108 description
="Algorithm used for interpolation",
5111 space_lock_x
: BoolProperty(
5113 description
="Lock editing of the x-coordinate",
5116 space_lock_y
: BoolProperty(
5118 description
="Lock editing of the y-coordinate",
5121 space_lock_z
: BoolProperty(
5123 description
="Lock editing of the z-coordinate",
5127 # draw function for integration in menus
5128 def menu_func(self
, context
):
5129 self
.layout
.menu("VIEW3D_MT_edit_mesh_looptools")
5130 self
.layout
.separator()
5133 # Add-ons Preferences Update Panel
5135 # Define Panel classes for updating
5137 VIEW3D_PT_tools_looptools
,
5141 def update_panel(self
, context
):
5142 message
= "LoopTools: Updating Panel locations has failed"
5144 for panel
in panels
:
5145 if "bl_rna" in panel
.__dict
__:
5146 bpy
.utils
.unregister_class(panel
)
5148 for panel
in panels
:
5149 panel
.bl_category
= context
.preferences
.addons
[__name__
].preferences
.category
5150 bpy
.utils
.register_class(panel
)
5152 except Exception as e
:
5153 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
5157 class LoopPreferences(AddonPreferences
):
5158 # this must match the addon name, use '__package__'
5159 # when defining this in a submodule of a python package.
5160 bl_idname
= __name__
5162 category
: StringProperty(
5163 name
="Tab Category",
5164 description
="Choose a name for the category of the panel",
5169 def draw(self
, context
):
5170 layout
= self
.layout
5174 col
.label(text
="Tab Category:")
5175 col
.prop(self
, "category", text
="")
5178 # define classes for registration
5180 VIEW3D_MT_edit_mesh_looptools
,
5181 VIEW3D_PT_tools_looptools
,
5196 # registering and menu integration
5199 bpy
.utils
.register_class(cls
)
5200 bpy
.types
.VIEW3D_MT_edit_mesh_context_menu
.prepend(menu_func
)
5201 bpy
.types
.WindowManager
.looptools
= PointerProperty(type=LoopToolsProps
)
5202 update_panel(None, bpy
.context
)
5205 # unregistering and removing menus
5207 for cls
in reversed(classes
):
5208 bpy
.utils
.unregister_class(cls
)
5209 bpy
.types
.VIEW3D_MT_edit_mesh_context_menu
.remove(menu_func
)
5211 del bpy
.types
.WindowManager
.looptools
5212 except Exception as e
:
5213 print('unregister fail:\n', e
)
5217 if __name__
== "__main__":