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()
808 # get all mirror vectors
810 if object.data
.use_mirror_x
:
811 mirror_Vectors
.append(mathutils
.Vector((-1, 1, 1)))
812 if object.data
.use_mirror_y
:
813 mirror_Vectors
.append(mathutils
.Vector((1, -1, 1)))
814 if object.data
.use_mirror_x
and object.data
.use_mirror_y
:
815 mirror_Vectors
.append(mathutils
.Vector((-1, -1, 1)))
816 z_mirror_Vectors
= []
817 if object.data
.use_mirror_z
:
818 for v
in mirror_Vectors
:
819 z_mirror_Vectors
.append(mathutils
.Vector((1, 1, -1)) * v
)
820 mirror_Vectors
.extend(z_mirror_Vectors
)
821 mirror_Vectors
.append(mathutils
.Vector((1, 1, -1)))
824 for index
, loc
in loop
:
826 if mapping
[index
] == -1:
829 index
= mapping
[index
]
831 delta
= (loc
- bm
.verts
[index
].co
) @ mat_inv
839 loc
= bm
.verts
[index
].co
+ delta
843 new_loc
= loc
* (influence
/ 100) + \
844 bm
.verts
[index
].co
* ((100 - influence
) / 100)
846 for vert
in bm
.verts
:
847 for mirror_Vector
in mirror_Vectors
:
848 if vert
.co
== mirror_Vector
* bm
.verts
[index
].co
:
849 vert
.co
= mirror_Vector
* new_loc
851 bm
.verts
[index
].co
= new_loc
856 bm
.verts
.ensure_lookup_table()
857 bm
.edges
.ensure_lookup_table()
858 bm
.faces
.ensure_lookup_table()
861 # load custom tool settings
862 def settings_load(self
):
863 lt
= bpy
.context
.window_manager
.looptools
864 tool
= self
.name
.split()[0].lower()
865 keys
= self
.as_keywords().keys()
867 setattr(self
, key
, getattr(lt
, tool
+ "_" + key
))
870 # store custom tool settings
871 def settings_write(self
):
872 lt
= bpy
.context
.window_manager
.looptools
873 tool
= self
.name
.split()[0].lower()
874 keys
= self
.as_keywords().keys()
876 setattr(lt
, tool
+ "_" + key
, getattr(self
, key
))
879 # clean up and set settings back to original state
881 # update editmesh cached data
882 obj
= bpy
.context
.active_object
883 if obj
.mode
== 'EDIT':
884 bmesh
.update_edit_mesh(obj
.data
, loop_triangles
=True, destructive
=True)
887 # ########################################
888 # ##### Bridge functions #################
889 # ########################################
891 # calculate a cubic spline through the middle section of 4 given coordinates
892 def bridge_calculate_cubic_spline(bm
, coordinates
):
898 for i
in coordinates
:
899 a
.append(float(i
[j
]))
902 h
.append(x
[i
+ 1] - x
[i
])
904 for i
in range(1, 3):
905 q
.append(3.0 / h
[i
] * (a
[i
+ 1] - a
[i
]) - 3.0 / h
[i
- 1] * (a
[i
] - a
[i
- 1]))
909 for i
in range(1, 3):
910 l
.append(2.0 * (x
[i
+ 1] - x
[i
- 1]) - h
[i
- 1] * u
[i
- 1])
911 u
.append(h
[i
] / l
[i
])
912 z
.append((q
[i
] - h
[i
- 1] * z
[i
- 1]) / l
[i
])
915 b
= [False for i
in range(3)]
916 c
= [False for i
in range(4)]
917 d
= [False for i
in range(3)]
919 for i
in range(2, -1, -1):
920 c
[i
] = z
[i
] - u
[i
] * c
[i
+ 1]
921 b
[i
] = (a
[i
+ 1] - a
[i
]) / h
[i
] - h
[i
] * (c
[i
+ 1] + 2.0 * c
[i
]) / 3.0
922 d
[i
] = (c
[i
+ 1] - c
[i
]) / (3.0 * h
[i
])
924 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
925 spline
= [result
[1], result
[4], result
[7]]
930 # return a list with new vertex location vectors, a list with face vertex
931 # integers, and the highest vertex integer in the virtual mesh
932 def bridge_calculate_geometry(bm
, lines
, vertex_normals
, segments
,
933 interpolation
, cubic_strength
, min_width
, max_vert_index
):
937 # calculate location based on interpolation method
938 def get_location(line
, segment
, splines
):
939 v1
= bm
.verts
[lines
[line
][0]].co
940 v2
= bm
.verts
[lines
[line
][1]].co
941 if interpolation
== 'linear':
942 return v1
+ (segment
/ segments
) * (v2
- v1
)
943 else: # interpolation == 'cubic'
944 m
= (segment
/ segments
)
945 ax
, bx
, cx
, dx
, tx
= splines
[line
][0]
946 x
= ax
+ bx
* m
+ cx
* m
** 2 + dx
* m
** 3
947 ay
, by
, cy
, dy
, ty
= splines
[line
][1]
948 y
= ay
+ by
* m
+ cy
* m
** 2 + dy
* m
** 3
949 az
, bz
, cz
, dz
, tz
= splines
[line
][2]
950 z
= az
+ bz
* m
+ cz
* m
** 2 + dz
* m
** 3
951 return mathutils
.Vector((x
, y
, z
))
953 # no interpolation needed
955 for i
, line
in enumerate(lines
):
956 if i
< len(lines
) - 1:
957 faces
.append([line
[0], lines
[i
+ 1][0], lines
[i
+ 1][1], line
[1]])
958 # more than 1 segment, interpolate
960 # calculate splines (if necessary) once, so no recalculations needed
961 if interpolation
== 'cubic':
964 v1
= bm
.verts
[line
[0]].co
965 v2
= bm
.verts
[line
[1]].co
966 size
= (v2
- v1
).length
* cubic_strength
967 splines
.append(bridge_calculate_cubic_spline(bm
,
968 [v1
+ size
* vertex_normals
[line
[0]], v1
, v2
,
969 v2
+ size
* vertex_normals
[line
[1]]]))
973 # create starting situation
974 virtual_width
= [(bm
.verts
[lines
[i
][0]].co
-
975 bm
.verts
[lines
[i
+ 1][0]].co
).length
for i
976 in range(len(lines
) - 1)]
977 new_verts
= [get_location(0, seg
, splines
) for seg
in range(1,
979 first_line_indices
= [i
for i
in range(max_vert_index
+ 1,
980 max_vert_index
+ segments
)]
982 prev_verts
= new_verts
[:] # vertex locations of verts on previous line
983 prev_vert_indices
= first_line_indices
[:]
984 max_vert_index
+= segments
- 1 # highest vertex index in virtual mesh
985 next_verts
= [] # vertex locations of verts on current line
986 next_vert_indices
= []
988 for i
, line
in enumerate(lines
):
989 if i
< len(lines
) - 1:
993 for seg
in range(1, segments
):
994 loc1
= prev_verts
[seg
- 1]
995 loc2
= get_location(i
+ 1, seg
, splines
)
996 if (loc1
- loc2
).length
< (min_width
/ 100) * virtual_width
[i
] \
997 and line
[1] == lines
[i
+ 1][1]:
998 # triangle, no new vertex
999 faces
.append([v1
, v2
, prev_vert_indices
[seg
- 1],
1000 prev_vert_indices
[seg
- 1]])
1001 next_verts
+= prev_verts
[seg
- 1:]
1002 next_vert_indices
+= prev_vert_indices
[seg
- 1:]
1006 if i
== len(lines
) - 2 and lines
[0] == lines
[-1]:
1007 # quad with first line, no new vertex
1008 faces
.append([v1
, v2
, first_line_indices
[seg
- 1],
1009 prev_vert_indices
[seg
- 1]])
1010 v2
= first_line_indices
[seg
- 1]
1011 v1
= prev_vert_indices
[seg
- 1]
1013 # quad, add new vertex
1015 faces
.append([v1
, v2
, max_vert_index
,
1016 prev_vert_indices
[seg
- 1]])
1018 v1
= prev_vert_indices
[seg
- 1]
1019 new_verts
.append(loc2
)
1020 next_verts
.append(loc2
)
1021 next_vert_indices
.append(max_vert_index
)
1023 faces
.append([v1
, v2
, lines
[i
+ 1][1], line
[1]])
1025 prev_verts
= next_verts
[:]
1026 prev_vert_indices
= next_vert_indices
[:]
1028 next_vert_indices
= []
1030 return(new_verts
, faces
, max_vert_index
)
1033 # calculate lines (list of lists, vertex indices) that are used for bridging
1034 def bridge_calculate_lines(bm
, loops
, mode
, twist
, reverse
):
1036 loop1
, loop2
= [i
[0] for i
in loops
]
1037 loop1_circular
, loop2_circular
= [i
[1] for i
in loops
]
1038 circular
= loop1_circular
or loop2_circular
1041 # calculate loop centers
1043 for loop
in [loop1
, loop2
]:
1044 center
= mathutils
.Vector()
1046 center
+= bm
.verts
[vertex
].co
1048 centers
.append(center
)
1049 for i
, loop
in enumerate([loop1
, loop2
]):
1051 if bm
.verts
[vertex
].co
== centers
[i
]:
1052 # prevent zero-length vectors in angle comparisons
1053 centers
[i
] += mathutils
.Vector((0.01, 0, 0))
1055 center1
, center2
= centers
1057 # calculate the normals of the virtual planes that the loops are on
1059 normal_plurity
= False
1060 for i
, loop
in enumerate([loop1
, loop2
]):
1062 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1065 x
, y
, z
= centers
[i
]
1066 for loc
in [bm
.verts
[vertex
].co
for vertex
in loop
]:
1067 mat
[0][0] += (loc
[0] - x
) ** 2
1068 mat
[1][0] += (loc
[0] - x
) * (loc
[1] - y
)
1069 mat
[2][0] += (loc
[0] - x
) * (loc
[2] - z
)
1070 mat
[0][1] += (loc
[1] - y
) * (loc
[0] - x
)
1071 mat
[1][1] += (loc
[1] - y
) ** 2
1072 mat
[2][1] += (loc
[1] - y
) * (loc
[2] - z
)
1073 mat
[0][2] += (loc
[2] - z
) * (loc
[0] - x
)
1074 mat
[1][2] += (loc
[2] - z
) * (loc
[1] - y
)
1075 mat
[2][2] += (loc
[2] - z
) ** 2
1078 if sum(mat
[0]) < 1e-6 or sum(mat
[1]) < 1e-6 or sum(mat
[2]) < 1e-6:
1079 normal_plurity
= True
1083 if sum(mat
[0]) == 0:
1084 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
1085 elif sum(mat
[1]) == 0:
1086 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
1087 elif sum(mat
[2]) == 0:
1088 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
1090 # warning! this is different from .normalize()
1093 vec
= mathutils
.Vector((1.0, 1.0, 1.0))
1094 vec2
= (mat
@ vec
) / (mat
@ vec
).length
1095 while vec
!= vec2
and iter < itermax
:
1099 if vec2
.length
!= 0:
1101 if vec2
.length
== 0:
1102 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
1104 normals
.append(normal
)
1105 # have plane normals face in the same direction (maximum angle: 90 degrees)
1106 if ((center1
+ normals
[0]) - center2
).length
< \
1107 ((center1
- normals
[0]) - center2
).length
:
1109 if ((center2
+ normals
[1]) - center1
).length
> \
1110 ((center2
- normals
[1]) - center1
).length
:
1113 # rotation matrix, representing the difference between the plane normals
1114 axis
= normals
[0].cross(normals
[1])
1115 axis
= mathutils
.Vector([loc
if abs(loc
) > 1e-8 else 0 for loc
in axis
])
1116 if axis
.angle(mathutils
.Vector((0, 0, 1)), 0) > 1.5707964:
1118 angle
= normals
[0].dot(normals
[1])
1119 rotation_matrix
= mathutils
.Matrix
.Rotation(angle
, 4, axis
)
1121 # if circular, rotate loops so they are aligned
1123 # make sure loop1 is the circular one (or both are circular)
1124 if loop2_circular
and not loop1_circular
:
1125 loop1_circular
, loop2_circular
= True, False
1126 loop1
, loop2
= loop2
, loop1
1128 # match start vertex of loop1 with loop2
1129 target_vector
= bm
.verts
[loop2
[0]].co
- center2
1130 dif_angles
= [[(rotation_matrix
@ (bm
.verts
[vertex
].co
- center1
)
1131 ).angle(target_vector
, 0), False, i
] for
1132 i
, vertex
in enumerate(loop1
)]
1134 if len(loop1
) != len(loop2
):
1135 angle_limit
= dif_angles
[0][0] * 1.2 # 20% margin
1137 [(bm
.verts
[loop2
[0]].co
-
1138 bm
.verts
[loop1
[index
]].co
).length
, angle
, index
] for
1139 angle
, distance
, index
in dif_angles
if angle
<= angle_limit
1142 loop1
= loop1
[dif_angles
[0][2]:] + loop1
[:dif_angles
[0][2]]
1144 # have both loops face the same way
1145 if normal_plurity
and not circular
:
1146 second_to_first
, second_to_second
, second_to_last
= [
1147 (bm
.verts
[loop1
[1]].co
- center1
).angle(
1148 bm
.verts
[loop2
[i
]].co
- center2
) for i
in [0, 1, -1]
1150 last_to_first
, last_to_second
= [
1151 (bm
.verts
[loop1
[-1]].co
-
1152 center1
).angle(bm
.verts
[loop2
[i
]].co
- center2
) for
1155 if (min(last_to_first
, last_to_second
) * 1.1 < min(second_to_first
,
1156 second_to_second
)) or (loop2_circular
and second_to_last
* 1.1 <
1157 min(second_to_first
, second_to_second
)):
1160 loop1
= [loop1
[-1]] + loop1
[:-1]
1162 angle
= (bm
.verts
[loop1
[0]].co
- center1
).\
1163 cross(bm
.verts
[loop1
[1]].co
- center1
).angle(normals
[0], 0)
1164 target_angle
= (bm
.verts
[loop2
[0]].co
- center2
).\
1165 cross(bm
.verts
[loop2
[1]].co
- center2
).angle(normals
[1], 0)
1166 limit
= 1.5707964 # 0.5*pi, 90 degrees
1167 if not ((angle
> limit
and target_angle
> limit
) or
1168 (angle
< limit
and target_angle
< limit
)):
1171 loop1
= [loop1
[-1]] + loop1
[:-1]
1172 elif normals
[0].angle(normals
[1]) > limit
:
1175 loop1
= [loop1
[-1]] + loop1
[:-1]
1177 # both loops have the same length
1178 if len(loop1
) == len(loop2
):
1181 if abs(twist
) < len(loop1
):
1182 loop1
= loop1
[twist
:] + loop1
[:twist
]
1186 lines
.append([loop1
[0], loop2
[0]])
1187 for i
in range(1, len(loop1
)):
1188 lines
.append([loop1
[i
], loop2
[i
]])
1190 # loops of different lengths
1192 # make loop1 longest loop
1193 if len(loop2
) > len(loop1
):
1194 loop1
, loop2
= loop2
, loop1
1195 loop1_circular
, loop2_circular
= loop2_circular
, loop1_circular
1199 if abs(twist
) < len(loop1
):
1200 loop1
= loop1
[twist
:] + loop1
[:twist
]
1204 # shortest angle difference doesn't always give correct start vertex
1205 if loop1_circular
and not loop2_circular
:
1208 if len(loop1
) - shifting
< len(loop2
):
1211 to_last
, to_first
= [
1212 (rotation_matrix
@ (bm
.verts
[loop1
[-1]].co
- center1
)).angle(
1213 (bm
.verts
[loop2
[i
]].co
- center2
), 0) for i
in [-1, 0]
1215 if to_first
< to_last
:
1216 loop1
= [loop1
[-1]] + loop1
[:-1]
1222 # basic shortest side first
1224 lines
.append([loop1
[0], loop2
[0]])
1225 for i
in range(1, len(loop1
)):
1226 if i
>= len(loop2
) - 1:
1228 lines
.append([loop1
[i
], loop2
[-1]])
1231 lines
.append([loop1
[i
], loop2
[i
]])
1233 # shortest edge algorithm
1234 else: # mode == 'shortest'
1235 lines
.append([loop1
[0], loop2
[0]])
1237 for i
in range(len(loop1
) - 1):
1238 if prev_vert2
== len(loop2
) - 1 and not loop2_circular
:
1239 # force triangles, reached end of loop2
1241 elif prev_vert2
== len(loop2
) - 1 and loop2_circular
:
1242 # at end of loop2, but circular, so check with first vert
1243 tri
, quad
= [(bm
.verts
[loop1
[i
+ 1]].co
-
1244 bm
.verts
[loop2
[j
]].co
).length
1245 for j
in [prev_vert2
, 0]]
1247 elif len(loop1
) - 1 - i
== len(loop2
) - 1 - prev_vert2
and \
1249 # force quads, otherwise won't make it to end of loop2
1252 # calculate if tri or quad gives shortest edge
1253 tri
, quad
= [(bm
.verts
[loop1
[i
+ 1]].co
-
1254 bm
.verts
[loop2
[j
]].co
).length
1255 for j
in range(prev_vert2
, prev_vert2
+ 2)]
1259 lines
.append([loop1
[i
+ 1], loop2
[prev_vert2
]])
1260 if circle_full
== 2:
1263 elif not circle_full
:
1264 lines
.append([loop1
[i
+ 1], loop2
[prev_vert2
+ 1]])
1266 # quad to first vertex of loop2
1268 lines
.append([loop1
[i
+ 1], loop2
[0]])
1272 # final face for circular loops
1273 if loop1_circular
and loop2_circular
:
1274 lines
.append([loop1
[0], loop2
[0]])
1279 # calculate number of segments needed
1280 def bridge_calculate_segments(bm
, lines
, loops
, segments
):
1281 # return if amount of segments is set by user
1286 average_edge_length
= [
1287 (bm
.verts
[vertex
].co
-
1288 bm
.verts
[loop
[0][i
+ 1]].co
).length
for loop
in loops
for
1289 i
, vertex
in enumerate(loop
[0][:-1])
1291 # closing edges of circular loops
1292 average_edge_length
+= [
1293 (bm
.verts
[loop
[0][-1]].co
-
1294 bm
.verts
[loop
[0][0]].co
).length
for loop
in loops
if loop
[1]
1298 average_edge_length
= sum(average_edge_length
) / len(average_edge_length
)
1299 average_bridge_length
= sum(
1301 bm
.verts
[v2
].co
).length
for v1
, v2
in lines
]
1304 segments
= max(1, round(average_bridge_length
/ average_edge_length
))
1309 # return dictionary with vertex index as key, and the normal vector as value
1310 def bridge_calculate_virtual_vertex_normals(bm
, lines
, loops
, edge_faces
,
1312 if not edge_faces
: # interpolation isn't set to cubic
1315 # pity reduce() isn't one of the basic functions in python anymore
1316 def average_vector_dictionary(dic
):
1317 for key
, vectors
in dic
.items():
1318 # if type(vectors) == type([]) and len(vectors) > 1:
1319 if len(vectors
) > 1:
1320 average
= mathutils
.Vector()
1321 for vector
in vectors
:
1323 average
/= len(vectors
)
1324 dic
[key
] = [average
]
1327 # get all edges of the loop
1329 [edgekey_to_edge
[tuple(sorted([loops
[j
][0][i
],
1330 loops
[j
][0][i
+ 1]]))] for i
in range(len(loops
[j
][0]) - 1)] for
1333 edges
= edges
[0] + edges
[1]
1335 if loops
[j
][1]: # circular
1336 edges
.append(edgekey_to_edge
[tuple(sorted([loops
[j
][0][0],
1337 loops
[j
][0][-1]]))])
1340 calculation based on face topology (assign edge-normals to vertices)
1342 edge_normal = face_normal x edge_vector
1343 vertex_normal = average(edge_normals)
1345 vertex_normals
= dict([(vertex
, []) for vertex
in loops
[0][0] + loops
[1][0]])
1347 faces
= edge_faces
[edgekey(edge
)] # valid faces connected to edge
1350 # get edge coordinates
1351 v1
, v2
= [bm
.verts
[edgekey(edge
)[i
]].co
for i
in [0, 1]]
1352 edge_vector
= v1
- v2
1353 if edge_vector
.length
< 1e-4:
1354 # zero-length edge, vertices at same location
1356 edge_center
= (v1
+ v2
) / 2
1358 # average face coordinates, if connected to more than 1 valid face
1360 face_normal
= mathutils
.Vector()
1361 face_center
= mathutils
.Vector()
1363 face_normal
+= face
.normal
1364 face_center
+= face
.calc_center_median()
1365 face_normal
/= len(faces
)
1366 face_center
/= len(faces
)
1368 face_normal
= faces
[0].normal
1369 face_center
= faces
[0].calc_center_median()
1370 if face_normal
.length
< 1e-4:
1371 # faces with a surface of 0 have no face normal
1374 # calculate virtual edge normal
1375 edge_normal
= edge_vector
.cross(face_normal
)
1376 edge_normal
.length
= 0.01
1377 if (face_center
- (edge_center
+ edge_normal
)).length
> \
1378 (face_center
- (edge_center
- edge_normal
)).length
:
1379 # make normal face the correct way
1380 edge_normal
.negate()
1381 edge_normal
.normalize()
1382 # add virtual edge normal as entry for both vertices it connects
1383 for vertex
in edgekey(edge
):
1384 vertex_normals
[vertex
].append(edge_normal
)
1387 calculation based on connection with other loop (vertex focused method)
1388 - used for vertices that aren't connected to any valid faces
1390 plane_normal = edge_vector x connection_vector
1391 vertex_normal = plane_normal x edge_vector
1394 vertex
for vertex
, normal
in vertex_normals
.items() if not normal
1398 # edge vectors connected to vertices
1399 edge_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1401 for v
in edgekey(edge
):
1402 if v
in edge_vectors
:
1403 edge_vector
= bm
.verts
[edgekey(edge
)[0]].co
- \
1404 bm
.verts
[edgekey(edge
)[1]].co
1405 if edge_vector
.length
< 1e-4:
1406 # zero-length edge, vertices at same location
1408 edge_vectors
[v
].append(edge_vector
)
1410 # connection vectors between vertices of both loops
1411 connection_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1412 connections
= dict([[vertex
, []] for vertex
in vertices
])
1413 for v1
, v2
in lines
:
1414 if v1
in connection_vectors
or v2
in connection_vectors
:
1415 new_vector
= bm
.verts
[v1
].co
- bm
.verts
[v2
].co
1416 if new_vector
.length
< 1e-4:
1417 # zero-length connection vector,
1418 # vertices in different loops at same location
1420 if v1
in connection_vectors
:
1421 connection_vectors
[v1
].append(new_vector
)
1422 connections
[v1
].append(v2
)
1423 if v2
in connection_vectors
:
1424 connection_vectors
[v2
].append(new_vector
)
1425 connections
[v2
].append(v1
)
1426 connection_vectors
= average_vector_dictionary(connection_vectors
)
1427 connection_vectors
= dict(
1428 [[vertex
, vector
[0]] if vector
else
1429 [vertex
, []] for vertex
, vector
in connection_vectors
.items()]
1432 for vertex
, values
in edge_vectors
.items():
1433 # vertex normal doesn't matter, just assign a random vector to it
1434 if not connection_vectors
[vertex
]:
1435 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1438 # calculate to what location the vertex is connected,
1439 # used to determine what way to flip the normal
1440 connected_center
= mathutils
.Vector()
1441 for v
in connections
[vertex
]:
1442 connected_center
+= bm
.verts
[v
].co
1443 if len(connections
[vertex
]) > 1:
1444 connected_center
/= len(connections
[vertex
])
1445 if len(connections
[vertex
]) == 0:
1446 # shouldn't be possible, but better safe than sorry
1447 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1450 # can't do proper calculations, because of zero-length vector
1452 if (connected_center
- (bm
.verts
[vertex
].co
+
1453 connection_vectors
[vertex
])).length
< (connected_center
-
1454 (bm
.verts
[vertex
].co
- connection_vectors
[vertex
])).length
:
1455 connection_vectors
[vertex
].negate()
1456 vertex_normals
[vertex
] = [connection_vectors
[vertex
].normalized()]
1459 # calculate vertex normals using edge-vectors,
1460 # connection-vectors and the derived plane normal
1461 for edge_vector
in values
:
1462 plane_normal
= edge_vector
.cross(connection_vectors
[vertex
])
1463 vertex_normal
= edge_vector
.cross(plane_normal
)
1464 vertex_normal
.length
= 0.1
1465 if (connected_center
- (bm
.verts
[vertex
].co
+
1466 vertex_normal
)).length
< (connected_center
-
1467 (bm
.verts
[vertex
].co
- vertex_normal
)).length
:
1468 # make normal face the correct way
1469 vertex_normal
.negate()
1470 vertex_normal
.normalize()
1471 vertex_normals
[vertex
].append(vertex_normal
)
1473 # average virtual vertex normals, based on all edges it's connected to
1474 vertex_normals
= average_vector_dictionary(vertex_normals
)
1475 vertex_normals
= dict([[vertex
, vector
[0]] for vertex
, vector
in vertex_normals
.items()])
1477 return(vertex_normals
)
1480 # add vertices to mesh
1481 def bridge_create_vertices(bm
, vertices
):
1482 for i
in range(len(vertices
)):
1483 bm
.verts
.new(vertices
[i
])
1484 bm
.verts
.ensure_lookup_table()
1488 def bridge_create_faces(object, bm
, faces
, twist
):
1489 # have the normal point the correct way
1491 [face
.reverse() for face
in faces
]
1492 faces
= [face
[2:] + face
[:2] if face
[0] == face
[1] else face
for face
in faces
]
1494 # eekadoodle prevention
1495 for i
in range(len(faces
)):
1496 if not faces
[i
][-1]:
1497 if faces
[i
][0] == faces
[i
][-1]:
1498 faces
[i
] = [faces
[i
][1], faces
[i
][2], faces
[i
][3], faces
[i
][1]]
1500 faces
[i
] = [faces
[i
][-1]] + faces
[i
][:-1]
1501 # result of converting from pre-bmesh period
1502 if faces
[i
][-1] == faces
[i
][-2]:
1503 faces
[i
] = faces
[i
][:-1]
1506 for i
in range(len(faces
)):
1508 new_faces
.append(bm
.faces
.new([bm
.verts
[v
] for v
in faces
[i
]]))
1510 # face already exists
1513 object.data
.update(calc_edges
=True) # calc_edges prevents memory-corruption
1515 bm
.verts
.ensure_lookup_table()
1516 bm
.edges
.ensure_lookup_table()
1517 bm
.faces
.ensure_lookup_table()
1522 # calculate input loops
1523 def bridge_get_input(bm
):
1524 # create list of internal edges, which should be skipped
1525 eks_of_selected_faces
= [
1526 item
for sublist
in [face_edgekeys(face
) for
1527 face
in bm
.faces
if face
.select
and not face
.hide
] for item
in sublist
1530 for ek
in eks_of_selected_faces
:
1531 if ek
in edge_count
:
1535 internal_edges
= [ek
for ek
in edge_count
if edge_count
[ek
] > 1]
1537 # sort correct edges into loops
1539 edgekey(edge
) for edge
in bm
.edges
if edge
.select
and
1540 not edge
.hide
and edgekey(edge
) not in internal_edges
1542 loops
= get_connected_selections(selected_edges
)
1547 # return values needed by the bridge operator
1548 def bridge_initialise(bm
, interpolation
):
1549 if interpolation
== 'cubic':
1550 # dict with edge-key as key and list of connected valid faces as value
1552 face
.index
for face
in bm
.faces
if face
.select
or
1556 [[edgekey(edge
), []] for edge
in bm
.edges
if not edge
.hide
]
1558 for face
in bm
.faces
:
1559 if face
.index
in face_blacklist
:
1561 for key
in face_edgekeys(face
):
1562 edge_faces
[key
].append(face
)
1563 # dictionary with the edge-key as key and edge as value
1564 edgekey_to_edge
= dict(
1565 [[edgekey(edge
), edge
] for edge
in bm
.edges
if edge
.select
and not edge
.hide
]
1569 edgekey_to_edge
= False
1571 # selected faces input
1572 old_selected_faces
= [
1573 face
.index
for face
in bm
.faces
if face
.select
and not face
.hide
1576 # find out if faces created by bridging should be smoothed
1579 if sum([face
.smooth
for face
in bm
.faces
]) / len(bm
.faces
) >= 0.5:
1582 return(edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
)
1585 # return a string with the input method
1586 def bridge_input_method(loft
, loft_loop
):
1590 method
= "Loft loop"
1592 method
= "Loft no-loop"
1599 # match up loops in pairs, used for multi-input bridging
1600 def bridge_match_loops(bm
, loops
):
1601 # calculate average loop normals and centers
1604 for vertices
, circular
in loops
:
1605 normal
= mathutils
.Vector()
1606 center
= mathutils
.Vector()
1607 for vertex
in vertices
:
1608 normal
+= bm
.verts
[vertex
].normal
1609 center
+= bm
.verts
[vertex
].co
1610 normals
.append(normal
/ len(vertices
) / 10)
1611 centers
.append(center
/ len(vertices
))
1613 # possible matches if loop normals are faced towards the center
1615 matches
= dict([[i
, []] for i
in range(len(loops
))])
1617 for i
in range(len(loops
) + 1):
1618 for j
in range(i
+ 1, len(loops
)):
1619 if (centers
[i
] - centers
[j
]).length
> \
1620 (centers
[i
] - (centers
[j
] + normals
[j
])).length
and \
1621 (centers
[j
] - centers
[i
]).length
> \
1622 (centers
[j
] - (centers
[i
] + normals
[i
])).length
:
1624 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1625 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1626 # if no loops face each other, just make matches between all the loops
1627 if matches_amount
== 0:
1628 for i
in range(len(loops
) + 1):
1629 for j
in range(i
+ 1, len(loops
)):
1630 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1631 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1632 for key
, value
in matches
.items():
1635 # matches based on distance between centers and number of vertices in loops
1637 for loop_index
in range(len(loops
)):
1638 if loop_index
in new_order
:
1640 loop_matches
= matches
[loop_index
]
1641 if not loop_matches
:
1643 shortest_distance
= loop_matches
[0][0]
1644 shortest_distance
*= 1.1
1646 [abs(len(loops
[loop_index
][0]) -
1647 len(loops
[loop
[2]][0])), loop
[0], loop
[1], loop
[2]] for loop
in
1648 loop_matches
if loop
[0] < shortest_distance
1651 for match
in loop_matches
:
1652 if match
[3] not in new_order
:
1653 new_order
+= [loop_index
, match
[3]]
1656 # reorder loops based on matches
1657 if len(new_order
) >= 2:
1658 loops
= [loops
[i
] for i
in new_order
]
1663 # remove old_selected_faces
1664 def bridge_remove_internal_faces(bm
, old_selected_faces
):
1665 # collect bmesh faces and internal bmesh edges
1666 remove_faces
= [bm
.faces
[face
] for face
in old_selected_faces
]
1667 edges
= collections
.Counter(
1668 [edge
.index
for face
in remove_faces
for edge
in face
.edges
]
1670 remove_edges
= [bm
.edges
[edge
] for edge
in edges
if edges
[edge
] > 1]
1672 # remove internal faces and edges
1673 for face
in remove_faces
:
1674 bm
.faces
.remove(face
)
1675 for edge
in remove_edges
:
1676 bm
.edges
.remove(edge
)
1678 bm
.faces
.ensure_lookup_table()
1679 bm
.edges
.ensure_lookup_table()
1680 bm
.verts
.ensure_lookup_table()
1683 # update list of internal faces that are flagged for removal
1684 def bridge_save_unused_faces(bm
, old_selected_faces
, loops
):
1685 # key: vertex index, value: lists of selected faces using it
1687 vertex_to_face
= dict([[i
, []] for i
in range(len(bm
.verts
))])
1688 [[vertex_to_face
[vertex
.index
].append(face
) for vertex
in
1689 bm
.faces
[face
].verts
] for face
in old_selected_faces
]
1691 # group selected faces that are connected
1694 for face
in old_selected_faces
:
1695 if face
in grouped_faces
:
1697 grouped_faces
.append(face
)
1701 grow_face
= new_faces
[0]
1702 for vertex
in bm
.faces
[grow_face
].verts
:
1703 vertex_face_group
= [
1704 face
for face
in vertex_to_face
[vertex
.index
] if
1705 face
not in grouped_faces
1707 new_faces
+= vertex_face_group
1708 grouped_faces
+= vertex_face_group
1709 group
+= vertex_face_group
1711 groups
.append(group
)
1713 # key: vertex index, value: True/False (is it in a loop that is used)
1714 used_vertices
= dict([[i
, 0] for i
in range(len(bm
.verts
))])
1716 for vertex
in loop
[0]:
1717 used_vertices
[vertex
] = True
1719 # check if group is bridged, if not remove faces from internal faces list
1720 for group
in groups
:
1725 for vertex
in bm
.faces
[face
].verts
:
1726 if used_vertices
[vertex
.index
]:
1731 old_selected_faces
.remove(face
)
1734 # add the newly created faces to the selection
1735 def bridge_select_new_faces(new_faces
, smooth
):
1736 for face
in new_faces
:
1737 face
.select_set(True)
1738 face
.smooth
= smooth
1741 # sort loops, so they are connected in the correct order when lofting
1742 def bridge_sort_loops(bm
, loops
, loft_loop
):
1743 # simplify loops to single points, and prepare for pathfinding
1745 [sum([bm
.verts
[i
].co
[j
] for i
in loop
[0]]) /
1746 len(loop
[0]) for loop
in loops
] for j
in range(3)
1748 nodes
= [mathutils
.Vector((x
[i
], y
[i
], z
[i
])) for i
in range(len(loops
))]
1751 open = [i
for i
in range(1, len(loops
))]
1753 # connect node to path, that is shortest to active_node
1754 while len(open) > 0:
1755 distances
= [(nodes
[active_node
] - nodes
[i
]).length
for i
in open]
1756 active_node
= open[distances
.index(min(distances
))]
1757 open.remove(active_node
)
1758 path
.append([active_node
, min(distances
)])
1759 # check if we didn't start in the middle of the path
1760 for i
in range(2, len(path
)):
1761 if (nodes
[path
[i
][0]] - nodes
[0]).length
< path
[i
][1]:
1764 path
= path
[:-i
] + temp
1768 loops
= [loops
[i
[0]] for i
in path
]
1769 # if requested, duplicate first loop at last position, so loft can loop
1771 loops
= loops
+ [loops
[0]]
1776 # remapping old indices to new position in list
1777 def bridge_update_old_selection(bm
, old_selected_faces
):
1779 old_indices = old_selected_faces[:]
1780 old_selected_faces = []
1781 for i, face in enumerate(bm.faces):
1782 if face.index in old_indices:
1783 old_selected_faces.append(i)
1785 old_selected_faces
= [
1786 i
for i
, face
in enumerate(bm
.faces
) if face
.index
in old_selected_faces
1789 return(old_selected_faces
)
1792 # ########################################
1793 # ##### Circle functions #################
1794 # ########################################
1796 # convert 3d coordinates to 2d coordinates on plane
1797 def circle_3d_to_2d(bm_mod
, loop
, com
, normal
):
1798 # project vertices onto the plane
1799 verts
= [bm_mod
.verts
[v
] for v
in loop
[0]]
1800 verts_projected
= [[v
.co
- (v
.co
- com
).dot(normal
) * normal
, v
.index
]
1803 # calculate two vectors (p and q) along the plane
1804 m
= mathutils
.Vector((normal
[0] + 1.0, normal
[1], normal
[2]))
1805 p
= m
- (m
.dot(normal
) * normal
)
1807 m
= mathutils
.Vector((normal
[0], normal
[1] + 1.0, normal
[2]))
1808 p
= m
- (m
.dot(normal
) * normal
)
1811 # change to 2d coordinates using perpendicular projection
1813 for loc
, vert
in verts_projected
:
1815 x
= p
.dot(vloc
) / p
.dot(p
)
1816 y
= q
.dot(vloc
) / q
.dot(q
)
1817 locs_2d
.append([x
, y
, vert
])
1819 return(locs_2d
, p
, q
)
1822 # calculate a best-fit circle to the 2d locations on the plane
1823 def circle_calculate_best_fit(locs_2d
):
1829 # calculate center and radius (non-linear least squares solution)
1830 for iter in range(500):
1834 d
= (v
[0] ** 2 - 2.0 * x0
* v
[0] + v
[1] ** 2 - 2.0 * y0
* v
[1] + x0
** 2 + y0
** 2) ** 0.5
1835 jmat
.append([(x0
- v
[0]) / d
, (y0
- v
[1]) / d
, -1.0])
1836 k
.append(-(((v
[0] - x0
) ** 2 + (v
[1] - y0
) ** 2) ** 0.5 - r
))
1837 jmat2
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1841 k2
= mathutils
.Vector((0.0, 0.0, 0.0))
1842 for i
in range(len(jmat
)):
1843 k2
+= mathutils
.Vector(jmat
[i
]) * k
[i
]
1844 jmat2
[0][0] += jmat
[i
][0] ** 2
1845 jmat2
[1][0] += jmat
[i
][0] * jmat
[i
][1]
1846 jmat2
[2][0] += jmat
[i
][0] * jmat
[i
][2]
1847 jmat2
[1][1] += jmat
[i
][1] ** 2
1848 jmat2
[2][1] += jmat
[i
][1] * jmat
[i
][2]
1849 jmat2
[2][2] += jmat
[i
][2] ** 2
1850 jmat2
[0][1] = jmat2
[1][0]
1851 jmat2
[0][2] = jmat2
[2][0]
1852 jmat2
[1][2] = jmat2
[2][1]
1857 dx0
, dy0
, dr
= jmat2
@ k2
1861 # stop iterating if we're close enough to optimal solution
1862 if abs(dx0
) < 1e-6 and abs(dy0
) < 1e-6 and abs(dr
) < 1e-6:
1865 # return center of circle and radius
1869 # calculate circle so no vertices have to be moved away from the center
1870 def circle_calculate_min_fit(locs_2d
):
1872 x0
= (min([i
[0] for i
in locs_2d
]) + max([i
[0] for i
in locs_2d
])) / 2.0
1873 y0
= (min([i
[1] for i
in locs_2d
]) + max([i
[1] for i
in locs_2d
])) / 2.0
1874 center
= mathutils
.Vector([x0
, y0
])
1876 r
= min([(mathutils
.Vector([i
[0], i
[1]]) - center
).length
for i
in locs_2d
])
1878 # return center of circle and radius
1882 # calculate the new locations of the vertices that need to be moved
1883 def circle_calculate_verts(flatten
, bm_mod
, locs_2d
, com
, p
, q
, normal
):
1884 # changing 2d coordinates back to 3d coordinates
1887 locs_3d
.append([loc
[2], loc
[0] * p
+ loc
[1] * q
+ com
])
1889 if flatten
: # flat circle
1892 else: # project the locations on the existing mesh
1893 vert_edges
= dict_vert_edges(bm_mod
)
1894 vert_faces
= dict_vert_faces(bm_mod
)
1895 faces
= [f
for f
in bm_mod
.faces
if not f
.hide
]
1896 rays
= [normal
, -normal
]
1900 if bm_mod
.verts
[loc
[0]].co
== loc
[1]: # vertex hasn't moved
1903 dif
= normal
.angle(loc
[1] - bm_mod
.verts
[loc
[0]].co
)
1904 if -1e-6 < dif
< 1e-6 or math
.pi
- 1e-6 < dif
< math
.pi
+ 1e-6:
1905 # original location is already along projection normal
1906 projection
= bm_mod
.verts
[loc
[0]].co
1908 # quick search through adjacent faces
1909 for face
in vert_faces
[loc
[0]]:
1910 verts
= [v
.co
for v
in bm_mod
.faces
[face
].verts
]
1911 if len(verts
) == 3: # triangle
1915 v1
, v2
, v3
, v4
= verts
[:4]
1917 intersect
= mathutils
.geometry
.\
1918 intersect_ray_tri(v1
, v2
, v3
, ray
, loc
[1])
1920 projection
= intersect
1923 intersect
= mathutils
.geometry
.\
1924 intersect_ray_tri(v1
, v3
, v4
, ray
, loc
[1])
1926 projection
= intersect
1931 # check if projection is on adjacent edges
1932 for edgekey
in vert_edges
[loc
[0]]:
1933 line1
= bm_mod
.verts
[edgekey
[0]].co
1934 line2
= bm_mod
.verts
[edgekey
[1]].co
1935 intersect
, dist
= mathutils
.geometry
.intersect_point_line(
1936 loc
[1], line1
, line2
1938 if 1e-6 < dist
< 1 - 1e-6:
1939 projection
= intersect
1942 # full search through the entire mesh
1945 verts
= [v
.co
for v
in face
.verts
]
1946 if len(verts
) == 3: # triangle
1950 v1
, v2
, v3
, v4
= verts
[:4]
1952 intersect
= mathutils
.geometry
.intersect_ray_tri(
1953 v1
, v2
, v3
, ray
, loc
[1]
1956 hits
.append([(loc
[1] - intersect
).length
,
1960 intersect
= mathutils
.geometry
.intersect_ray_tri(
1961 v1
, v3
, v4
, ray
, loc
[1]
1964 hits
.append([(loc
[1] - intersect
).length
,
1968 # if more than 1 hit with mesh, closest hit is new loc
1970 projection
= hits
[0][1]
1972 # nothing to project on, remain at flat location
1974 new_locs
.append([loc
[0], projection
])
1976 # return new positions of projected circle
1980 # check loops and only return valid ones
1981 def circle_check_loops(single_loops
, loops
, mapping
, bm_mod
):
1982 valid_single_loops
= {}
1984 for i
, [loop
, circular
] in enumerate(loops
):
1985 # loop needs to have at least 3 vertices
1988 # loop needs at least 1 vertex in the original, non-mirrored mesh
1992 if mapping
[vert
] > -1:
1997 # loop has to be non-collinear
1999 loc0
= mathutils
.Vector(bm_mod
.verts
[loop
[0]].co
[:])
2000 loc1
= mathutils
.Vector(bm_mod
.verts
[loop
[1]].co
[:])
2002 locn
= mathutils
.Vector(bm_mod
.verts
[v
].co
[:])
2003 if loc0
== loc1
or loc1
== locn
:
2009 if -1e-6 < d1
.angle(d2
, 0) < 1e-6:
2017 # passed all tests, loop is valid
2018 valid_loops
.append([loop
, circular
])
2019 valid_single_loops
[len(valid_loops
) - 1] = single_loops
[i
]
2021 return(valid_single_loops
, valid_loops
)
2024 # calculate the location of single input vertices that need to be flattened
2025 def circle_flatten_singles(bm_mod
, com
, p
, q
, normal
, single_loop
):
2027 for vert
in single_loop
:
2028 loc
= mathutils
.Vector(bm_mod
.verts
[vert
].co
[:])
2029 new_locs
.append([vert
, loc
- (loc
- com
).dot(normal
) * normal
])
2034 # calculate input loops
2035 def circle_get_input(object, bm
):
2036 # get mesh with modifiers applied
2037 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
2039 # create list of edge-keys based on selection state
2041 for face
in bm
.faces
:
2042 if face
.select
and not face
.hide
:
2046 # get selected, non-hidden , non-internal edge-keys
2048 key
for keys
in [face_edgekeys(face
) for face
in
2049 bm_mod
.faces
if face
.select
and not face
.hide
] for key
in keys
2052 for ek
in eks_selected
:
2053 if ek
in edge_count
:
2058 edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and
2059 not edge
.hide
and edge_count
.get(edgekey(edge
), 1) == 1
2062 # no faces, so no internal edges either
2064 edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and not edge
.hide
2067 # add edge-keys around single vertices
2068 verts_connected
= dict(
2069 [[vert
, 1] for edge
in [edge
for edge
in
2070 bm_mod
.edges
if edge
.select
and not edge
.hide
] for vert
in
2074 vert
.index
for vert
in bm_mod
.verts
if
2075 vert
.select
and not vert
.hide
and
2076 not verts_connected
.get(vert
.index
, False)
2079 if single_vertices
and len(bm
.faces
) > 0:
2080 vert_to_single
= dict(
2081 [[v
.index
, []] for v
in bm_mod
.verts
if not v
.hide
]
2083 for face
in [face
for face
in bm_mod
.faces
if not face
.select
and not face
.hide
]:
2084 for vert
in face
.verts
:
2086 if vert
in single_vertices
:
2087 for ek
in face_edgekeys(face
):
2089 edge_keys
.append(ek
)
2090 if vert
not in vert_to_single
[ek
[0]]:
2091 vert_to_single
[ek
[0]].append(vert
)
2092 if vert
not in vert_to_single
[ek
[1]]:
2093 vert_to_single
[ek
[1]].append(vert
)
2096 # sort edge-keys into loops
2097 loops
= get_connected_selections(edge_keys
)
2099 # find out to which loops the single vertices belong
2100 single_loops
= dict([[i
, []] for i
in range(len(loops
))])
2101 if single_vertices
and len(bm
.faces
) > 0:
2102 for i
, [loop
, circular
] in enumerate(loops
):
2104 if vert_to_single
[vert
]:
2105 for single
in vert_to_single
[vert
]:
2106 if single
not in single_loops
[i
]:
2107 single_loops
[i
].append(single
)
2109 return(derived
, bm_mod
, single_vertices
, single_loops
, loops
)
2112 # recalculate positions based on the influence of the circle shape
2113 def circle_influence_locs(locs_2d
, new_locs_2d
, influence
):
2114 for i
in range(len(locs_2d
)):
2115 oldx
, oldy
, j
= locs_2d
[i
]
2116 newx
, newy
, k
= new_locs_2d
[i
]
2117 altx
= newx
* (influence
/ 100) + oldx
* ((100 - influence
) / 100)
2118 alty
= newy
* (influence
/ 100) + oldy
* ((100 - influence
) / 100)
2119 locs_2d
[i
] = [altx
, alty
, j
]
2124 # project 2d locations on circle, respecting distance relations between verts
2125 def circle_project_non_regular(locs_2d
, x0
, y0
, r
, angle
):
2126 for i
in range(len(locs_2d
)):
2127 x
, y
, j
= locs_2d
[i
]
2128 loc
= mathutils
.Vector([x
- x0
, y
- y0
])
2129 mat_rot
= mathutils
.Matrix
.Rotation(angle
, 2, 'X')
2132 locs_2d
[i
] = [loc
[0], loc
[1], j
]
2137 # project 2d locations on circle, with equal distance between all vertices
2138 def circle_project_regular(locs_2d
, x0
, y0
, r
, angle
):
2139 # find offset angle and circling direction
2140 x
, y
, i
= locs_2d
[0]
2141 loc
= mathutils
.Vector([x
- x0
, y
- y0
])
2143 offset_angle
= loc
.angle(mathutils
.Vector([1.0, 0.0]), 0.0)
2144 loca
= mathutils
.Vector([x
- x0
, y
- y0
, 0.0])
2147 x
, y
, j
= locs_2d
[1]
2148 locb
= mathutils
.Vector([x
- x0
, y
- y0
, 0.0])
2149 if loca
.cross(locb
)[2] >= 0:
2153 # distribute vertices along the circle
2154 for i
in range(len(locs_2d
)):
2155 t
= offset_angle
+ ccw
* (i
/ len(locs_2d
) * 2 * math
.pi
)
2156 x
= math
.cos(t
+ angle
) * r
2157 y
= math
.sin(t
+ angle
) * r
2158 locs_2d
[i
] = [x
, y
, locs_2d
[i
][2]]
2163 # shift loop, so the first vertex is closest to the center
2164 def circle_shift_loop(bm_mod
, loop
, com
):
2165 verts
, circular
= loop
2167 [(bm_mod
.verts
[vert
].co
- com
).length
, i
] for i
, vert
in enumerate(verts
)
2170 shift
= distances
[0][1]
2171 loop
= [verts
[shift
:] + verts
[:shift
], circular
]
2176 # ########################################
2177 # ##### Curve functions ##################
2178 # ########################################
2180 # create lists with knots and points, all correctly sorted
2181 def curve_calculate_knots(loop
, verts_selected
):
2182 knots
= [v
for v
in loop
[0] if v
in verts_selected
]
2184 # circular loop, potential for weird splines
2186 offset
= int(len(loop
[0]) / 4)
2189 kpos
.append(loop
[0].index(k
))
2191 for i
in range(len(kpos
) - 1):
2192 kdif
.append(kpos
[i
+ 1] - kpos
[i
])
2193 kdif
.append(len(loop
[0]) - kpos
[-1] + kpos
[0])
2197 kadd
.append([kdif
.index(k
), True])
2198 # next 2 lines are optional, they insert
2199 # an extra control point in small gaps
2201 # kadd.append([kdif.index(k), False])
2204 for k
in kadd
: # extra knots to be added
2205 if k
[1]: # big gap (break circular spline)
2206 kpos
= loop
[0].index(knots
[k
[0]]) + offset
2207 if kpos
> len(loop
[0]) - 1:
2208 kpos
-= len(loop
[0])
2209 kins
.append([knots
[k
[0]], loop
[0][kpos
]])
2211 if kpos2
> len(knots
) - 1:
2213 kpos2
= loop
[0].index(knots
[kpos2
]) - offset
2215 kpos2
+= len(loop
[0])
2216 kins
.append([loop
[0][kpos
], loop
[0][kpos2
]])
2217 krot
= loop
[0][kpos2
]
2218 else: # small gap (keep circular spline)
2219 k1
= loop
[0].index(knots
[k
[0]])
2221 if k2
> len(knots
) - 1:
2223 k2
= loop
[0].index(knots
[k2
])
2225 dif
= len(loop
[0]) - 1 - k1
+ k2
2228 kn
= k1
+ int(dif
/ 2)
2229 if kn
> len(loop
[0]) - 1:
2231 kins
.append([loop
[0][k1
], loop
[0][kn
]])
2232 for j
in kins
: # insert new knots
2233 knots
.insert(knots
.index(j
[0]) + 1, j
[1])
2234 if not krot
: # circular loop
2235 knots
.append(knots
[0])
2236 points
= loop
[0][loop
[0].index(knots
[0]):]
2237 points
+= loop
[0][0:loop
[0].index(knots
[0]) + 1]
2238 else: # non-circular loop (broken by script)
2239 krot
= knots
.index(krot
)
2240 knots
= knots
[krot
:] + knots
[0:krot
]
2241 if loop
[0].index(knots
[0]) > loop
[0].index(knots
[-1]):
2242 points
= loop
[0][loop
[0].index(knots
[0]):]
2243 points
+= loop
[0][0:loop
[0].index(knots
[-1]) + 1]
2245 points
= loop
[0][loop
[0].index(knots
[0]):loop
[0].index(knots
[-1]) + 1]
2246 # non-circular loop, add first and last point as knots
2248 if loop
[0][0] not in knots
:
2249 knots
.insert(0, loop
[0][0])
2250 if loop
[0][-1] not in knots
:
2251 knots
.append(loop
[0][-1])
2253 return(knots
, points
)
2256 # calculate relative positions compared to first knot
2257 def curve_calculate_t(bm_mod
, knots
, points
, pknots
, regular
, circular
):
2264 loc
= pknots
[knots
.index(p
)] # use projected knot location
2266 loc
= mathutils
.Vector(bm_mod
.verts
[p
].co
[:])
2269 len_total
+= (loc
- loc_prev
).length
2270 tpoints
.append(len_total
)
2275 tknots
.append(tpoints
[points
.index(p
)])
2277 tknots
[-1] = tpoints
[-1]
2281 tpoints_average
= tpoints
[-1] / (len(tpoints
) - 1)
2282 for i
in range(1, len(tpoints
) - 1):
2283 tpoints
[i
] = i
* tpoints_average
2284 for i
in range(len(knots
)):
2285 tknots
[i
] = tpoints
[points
.index(knots
[i
])]
2287 tknots
[-1] = tpoints
[-1]
2289 return(tknots
, tpoints
)
2292 # change the location of non-selected points to their place on the spline
2293 def curve_calculate_vertices(bm_mod
, knots
, tknots
, points
, tpoints
, splines
,
2294 interpolation
, restriction
):
2301 m
= tpoints
[points
.index(p
)]
2309 if n
> len(splines
) - 1:
2310 n
= len(splines
) - 1
2314 if interpolation
== 'cubic':
2315 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
2316 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
2317 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
2318 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
2319 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
2320 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
2321 newloc
= mathutils
.Vector([x
, y
, z
])
2322 else: # interpolation == 'linear'
2323 a
, d
, t
, u
= splines
[n
]
2324 newloc
= ((m
- t
) / u
) * d
+ a
2326 if restriction
!= 'none': # vertex movement is restricted
2328 else: # set the vertex to its new location
2329 move
.append([p
, newloc
])
2331 if restriction
!= 'none': # vertex movement is restricted
2336 move
.append([p
, bm_mod
.verts
[p
].co
])
2338 oldloc
= bm_mod
.verts
[p
].co
2339 normal
= bm_mod
.verts
[p
].normal
2340 dloc
= newloc
- oldloc
2341 if dloc
.length
< 1e-6:
2342 move
.append([p
, newloc
])
2343 elif restriction
== 'extrude': # only extrusions
2344 if dloc
.angle(normal
, 0) < 0.5 * math
.pi
+ 1e-6:
2345 move
.append([p
, newloc
])
2346 else: # restriction == 'indent' only indentations
2347 if dloc
.angle(normal
) > 0.5 * math
.pi
- 1e-6:
2348 move
.append([p
, newloc
])
2353 # trim loops to part between first and last selected vertices (including)
2354 def curve_cut_boundaries(bm_mod
, loops
):
2356 for loop
, circular
in loops
:
2358 selected
= [bm_mod
.verts
[v
].select
for v
in loop
]
2359 first
= selected
.index(True)
2361 last
= -selected
.index(True)
2363 if len(loop
[first
:]) < len(loop
)/2:
2364 cut_loops
.append([loop
[first
:], False])
2366 if len(loop
[first
:last
]) < len(loop
)/2:
2367 cut_loops
.append([loop
[first
:last
], False])
2369 selected
= [bm_mod
.verts
[v
].select
for v
in loop
]
2370 first
= selected
.index(True)
2372 last
= -selected
.index(True)
2374 cut_loops
.append([loop
[first
:], circular
])
2376 cut_loops
.append([loop
[first
:last
], circular
])
2381 # calculate input loops
2382 def curve_get_input(object, bm
, boundaries
):
2383 # get mesh with modifiers applied
2384 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
2386 # vertices that still need a loop to run through it
2388 v
.index
for v
in bm_mod
.verts
if v
.select
and not v
.hide
2390 # necessary dictionaries
2391 vert_edges
= dict_vert_edges(bm_mod
)
2392 edge_faces
= dict_edge_faces(bm_mod
)
2394 # find loops through each selected vertex
2395 while len(verts_unsorted
) > 0:
2396 loops
= curve_vertex_loops(bm_mod
, verts_unsorted
[0], vert_edges
,
2398 verts_unsorted
.pop(0)
2400 # check if loop is fully selected
2401 search_perpendicular
= False
2403 for loop
, circular
in loops
:
2405 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2406 if len(selected
) < 2:
2407 # only one selected vertex on loop, don't use
2410 elif len(selected
) == len(loop
):
2411 search_perpendicular
= loop
2413 # entire loop is selected, find perpendicular loops
2414 if search_perpendicular
:
2416 if vert
in verts_unsorted
:
2417 verts_unsorted
.remove(vert
)
2418 perp_loops
= curve_perpendicular_loops(bm_mod
, loop
,
2419 vert_edges
, edge_faces
)
2420 for perp_loop
in perp_loops
:
2421 correct_loops
.append(perp_loop
)
2424 for loop
, circular
in loops
:
2425 correct_loops
.append([loop
, circular
])
2429 correct_loops
= curve_cut_boundaries(bm_mod
, correct_loops
)
2431 return(derived
, bm_mod
, correct_loops
)
2434 # return all loops that are perpendicular to the given one
2435 def curve_perpendicular_loops(bm_mod
, start_loop
, vert_edges
, edge_faces
):
2436 # find perpendicular loops
2438 for start_vert
in start_loop
:
2439 loops
= curve_vertex_loops(bm_mod
, start_vert
, vert_edges
,
2441 for loop
, circular
in loops
:
2442 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2443 if len(selected
) == len(loop
):
2446 perp_loops
.append([loop
, circular
, loop
.index(start_vert
)])
2448 # trim loops to same lengths
2450 [len(loop
[0]), i
] for i
, loop
in enumerate(perp_loops
) if not loop
[1]
2453 # all loops are circular, not trimming
2454 return([[loop
[0], loop
[1]] for loop
in perp_loops
])
2456 shortest
= min(shortest
)
2457 shortest_start
= perp_loops
[shortest
[1]][2]
2458 before_start
= shortest_start
2459 after_start
= shortest
[0] - shortest_start
- 1
2460 bigger_before
= before_start
> after_start
2462 for loop
in perp_loops
:
2463 # have the loop face the same direction as the shortest one
2465 if loop
[2] < len(loop
[0]) / 2:
2467 loop
[2] = len(loop
[0]) - loop
[2] - 1
2469 if loop
[2] > len(loop
[0]) / 2:
2471 loop
[2] = len(loop
[0]) - loop
[2] - 1
2472 # circular loops can shift, to prevent wrong trimming
2474 shift
= shortest_start
- loop
[2]
2475 if loop
[2] + shift
> 0 and loop
[2] + shift
< len(loop
[0]):
2476 loop
[0] = loop
[0][-shift
:] + loop
[0][:-shift
]
2479 loop
[2] += len(loop
[0])
2480 elif loop
[2] > len(loop
[0]) - 1:
2481 loop
[2] -= len(loop
[0])
2483 start
= max(0, loop
[2] - before_start
)
2484 end
= min(len(loop
[0]), loop
[2] + after_start
+ 1)
2485 trimmed_loops
.append([loop
[0][start
:end
], False])
2487 return(trimmed_loops
)
2490 # project knots on non-selected geometry
2491 def curve_project_knots(bm_mod
, verts_selected
, knots
, points
, circular
):
2492 # function to project vertex on edge
2493 def project(v1
, v2
, v3
):
2494 # v1 and v2 are part of a line
2495 # v3 is projected onto it
2501 if circular
: # project all knots
2505 else: # first and last knot shouldn't be projected
2508 pknots
= [mathutils
.Vector(bm_mod
.verts
[knots
[0]].co
[:])]
2509 for knot
in knots
[start
:end
]:
2510 if knot
in verts_selected
:
2511 knot_left
= knot_right
= False
2512 for i
in range(points
.index(knot
) - 1, -1 * len(points
), -1):
2513 if points
[i
] not in knots
:
2514 knot_left
= points
[i
]
2516 for i
in range(points
.index(knot
) + 1, 2 * len(points
)):
2517 if i
> len(points
) - 1:
2519 if points
[i
] not in knots
:
2520 knot_right
= points
[i
]
2522 if knot_left
and knot_right
and knot_left
!= knot_right
:
2523 knot_left
= mathutils
.Vector(bm_mod
.verts
[knot_left
].co
[:])
2524 knot_right
= mathutils
.Vector(bm_mod
.verts
[knot_right
].co
[:])
2525 knot
= mathutils
.Vector(bm_mod
.verts
[knot
].co
[:])
2526 pknots
.append(project(knot_left
, knot_right
, knot
))
2528 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2529 else: # knot isn't selected, so shouldn't be changed
2530 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2532 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knots
[-1]].co
[:]))
2537 # find all loops through a given vertex
2538 def curve_vertex_loops(bm_mod
, start_vert
, vert_edges
, edge_faces
):
2542 for edge
in vert_edges
[start_vert
]:
2543 if edge
in edges_used
:
2548 active_faces
= edge_faces
[edge
]
2553 new_edges
= vert_edges
[new_vert
]
2554 loop
.append(new_vert
)
2556 edges_used
.append(tuple(sorted([loop
[-1], loop
[-2]])))
2557 if len(new_edges
) < 3 or len(new_edges
) > 4:
2562 for new_edge
in new_edges
:
2563 if new_edge
in edges_used
:
2566 for new_face
in edge_faces
[new_edge
]:
2567 if new_face
in active_faces
:
2572 # found correct new edge
2573 active_faces
= edge_faces
[new_edge
]
2579 if new_vert
== loop
[0]:
2587 loops
.append([loop
, circular
])
2592 # ########################################
2593 # ##### Flatten functions ################
2594 # ########################################
2596 # sort input into loops
2597 def flatten_get_input(bm
):
2598 vert_verts
= dict_vert_verts(
2599 [edgekey(edge
) for edge
in bm
.edges
if edge
.select
and not edge
.hide
]
2601 verts
= [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
2603 # no connected verts, consider all selected verts as a single input
2605 return([[verts
, False]])
2608 while len(verts
) > 0:
2612 if loop
[-1] in vert_verts
:
2613 to_grow
= vert_verts
[loop
[-1]]
2617 while len(to_grow
) > 0:
2618 new_vert
= to_grow
[0]
2620 if new_vert
in loop
:
2622 loop
.append(new_vert
)
2623 verts
.remove(new_vert
)
2624 to_grow
+= vert_verts
[new_vert
]
2626 loops
.append([loop
, False])
2631 # calculate position of vertex projections on plane
2632 def flatten_project(bm
, loop
, com
, normal
):
2633 verts
= [bm
.verts
[v
] for v
in loop
[0]]
2635 [v
.index
, mathutils
.Vector(v
.co
[:]) -
2636 (mathutils
.Vector(v
.co
[:]) - com
).dot(normal
) * normal
] for v
in verts
2639 return(verts_projected
)
2642 # ########################################
2643 # ##### Gstretch functions ###############
2644 # ########################################
2646 # fake stroke class, used to create custom strokes if no GP data is found
2647 class gstretch_fake_stroke():
2648 def __init__(self
, points
):
2649 self
.points
= [gstretch_fake_stroke_point(p
) for p
in points
]
2652 # fake stroke point class, used in fake strokes
2653 class gstretch_fake_stroke_point():
2654 def __init__(self
, loc
):
2658 # flips loops, if necessary, to obtain maximum alignment to stroke
2659 def gstretch_align_pairs(ls_pairs
, object, bm_mod
, method
):
2660 # returns total distance between all verts in loop and corresponding stroke
2661 def distance_loop_stroke(loop
, stroke
, object, bm_mod
, method
):
2662 stroke_lengths_cache
= False
2663 loop_length
= len(loop
[0])
2666 if method
!= 'regular':
2667 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2669 for i
, v_index
in enumerate(loop
[0]):
2670 if method
== 'regular':
2671 relative_distance
= i
/ (loop_length
- 1)
2673 relative_distance
= relative_lengths
[i
]
2675 loc1
= object.matrix_world
@ bm_mod
.verts
[v_index
].co
2676 loc2
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2677 relative_distance
, stroke_lengths_cache
)
2678 total_distance
+= (loc2
- loc1
).length
2680 return(total_distance
)
2683 for (loop
, stroke
) in ls_pairs
:
2684 total_dist
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2687 total_dist_rev
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2689 if total_dist_rev
> total_dist
:
2695 # calculate vertex positions on stroke
2696 def gstretch_calculate_verts(loop
, stroke
, object, bm_mod
, method
):
2698 stroke_lengths_cache
= False
2699 loop_length
= len(loop
[0])
2700 matrix_inverse
= object.matrix_world
.inverted()
2702 # return intersection of line with stroke, or None
2703 def intersect_line_stroke(vec1
, vec2
, stroke
):
2704 for i
, p
in enumerate(stroke
.points
[1:]):
2705 intersections
= mathutils
.geometry
.intersect_line_line(vec1
, vec2
,
2706 p
.co
, stroke
.points
[i
].co
)
2707 if intersections
and \
2708 (intersections
[0] - intersections
[1]).length
< 1e-2:
2709 x
, dist
= mathutils
.geometry
.intersect_point_line(
2710 intersections
[0], p
.co
, stroke
.points
[i
].co
)
2712 return(intersections
[0])
2715 if method
== 'project':
2716 vert_edges
= dict_vert_edges(bm_mod
)
2718 for v_index
in loop
[0]:
2720 for ek
in vert_edges
[v_index
]:
2722 v1
= bm_mod
.verts
[v1
]
2723 v2
= bm_mod
.verts
[v2
]
2724 if v1
.select
+ v2
.select
== 1 and not v1
.hide
and not v2
.hide
:
2725 vec1
= object.matrix_world
@ v1
.co
2726 vec2
= object.matrix_world
@ v2
.co
2727 intersection
= intersect_line_stroke(vec1
, vec2
, stroke
)
2730 if not intersection
:
2731 v
= bm_mod
.verts
[v_index
]
2732 intersection
= intersect_line_stroke(v
.co
, v
.co
+ v
.normal
,
2735 move
.append([v_index
, matrix_inverse
@ intersection
])
2738 if method
== 'irregular':
2739 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2741 for i
, v_index
in enumerate(loop
[0]):
2742 if method
== 'regular':
2743 relative_distance
= i
/ (loop_length
- 1)
2744 else: # method == 'irregular'
2745 relative_distance
= relative_lengths
[i
]
2746 loc
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2747 relative_distance
, stroke_lengths_cache
)
2748 loc
= matrix_inverse
@ loc
2749 move
.append([v_index
, loc
])
2754 # create new vertices, based on GP strokes
2755 def gstretch_create_verts(object, bm_mod
, strokes
, method
, conversion
,
2756 conversion_distance
, conversion_max
, conversion_min
, conversion_vertices
):
2759 mat_world
= object.matrix_world
.inverted()
2760 singles
= gstretch_match_single_verts(bm_mod
, strokes
, mat_world
)
2762 for stroke
in strokes
:
2763 stroke_verts
.append([stroke
, []])
2765 if conversion
== 'vertices':
2766 min_end_point
= conversion_vertices
2767 end_point
= conversion_vertices
2768 elif conversion
== 'limit_vertices':
2769 min_end_point
= conversion_min
2770 end_point
= conversion_max
2772 end_point
= len(stroke
.points
)
2773 # creation of new vertices at fixed user-defined distances
2774 if conversion
== 'distance':
2776 prev_point
= stroke
.points
[0]
2777 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
@ prev_point
.co
))
2779 limit
= conversion_distance
2780 for point
in stroke
.points
:
2781 new_distance
= distance
+ (point
.co
- prev_point
.co
).length
2783 while new_distance
> limit
:
2784 to_cover
= limit
- distance
+ (limit
* iteration
)
2785 new_loc
= prev_point
.co
+ to_cover
* \
2786 (point
.co
- prev_point
.co
).normalized()
2787 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* new_loc
))
2788 new_distance
-= limit
2790 distance
= new_distance
2792 # creation of new vertices for other methods
2794 # add vertices at stroke points
2795 for point
in stroke
.points
[:end_point
]:
2796 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
@ point
.co
))
2797 # add more vertices, beyond the points that are available
2798 if min_end_point
> min(len(stroke
.points
), end_point
):
2799 for i
in range(min_end_point
-
2800 (min(len(stroke
.points
), end_point
))):
2801 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
@ point
.co
))
2802 # force even spreading of points, so they are placed on stroke
2804 bm_mod
.verts
.ensure_lookup_table()
2805 bm_mod
.verts
.index_update()
2806 for stroke
, verts_seq
in stroke_verts
:
2807 if len(verts_seq
) < 2:
2809 # spread vertices evenly over the stroke
2810 if method
== 'regular':
2811 loop
= [[vert
.index
for vert
in verts_seq
], False]
2812 move
+= gstretch_calculate_verts(loop
, stroke
, object, bm_mod
,
2815 for i
, vert
in enumerate(verts_seq
):
2817 bm_mod
.edges
.new((verts_seq
[i
- 1], verts_seq
[i
]))
2819 # connect single vertices to the closest stroke
2821 for vert
, m_stroke
, point
in singles
:
2822 if m_stroke
!= stroke
:
2824 bm_mod
.edges
.new((vert
, verts_seq
[point
]))
2825 bm_mod
.edges
.ensure_lookup_table()
2826 bmesh
.update_edit_mesh(object.data
)
2831 # erases the grease pencil stroke
2832 def gstretch_erase_stroke(stroke
, context
):
2833 # change 3d coordinate into a stroke-point
2834 def sp(loc
, context
):
2838 'location': (0, 0, 0),
2840 view3d_utils
.location_3d_to_region_2d(
2841 context
.region
, context
.space_data
.region_3d
, loc
)
2848 if type(stroke
) != bpy
.types
.GPencilStroke
:
2849 # fake stroke, there is nothing to delete
2852 erase_stroke
= [sp(p
.co
, context
) for p
in stroke
.points
]
2854 erase_stroke
[0]['is_start'] = True
2855 #bpy.ops.gpencil.draw(mode='ERASER', stroke=erase_stroke)
2856 bpy
.ops
.gpencil
.data_unlink()
2860 # get point on stroke, given by relative distance (0.0 - 1.0)
2861 def gstretch_eval_stroke(stroke
, distance
, stroke_lengths_cache
=False):
2862 # use cache if available
2863 if not stroke_lengths_cache
:
2865 for i
, p
in enumerate(stroke
.points
[1:]):
2866 lengths
.append((p
.co
- stroke
.points
[i
].co
).length
+ lengths
[-1])
2867 total_length
= max(lengths
[-1], 1e-7)
2868 stroke_lengths_cache
= [length
/ total_length
for length
in
2870 stroke_lengths
= stroke_lengths_cache
[:]
2872 if distance
in stroke_lengths
:
2873 loc
= stroke
.points
[stroke_lengths
.index(distance
)].co
2874 elif distance
> stroke_lengths
[-1]:
2875 # should be impossible, but better safe than sorry
2876 loc
= stroke
.points
[-1].co
2878 stroke_lengths
.append(distance
)
2879 stroke_lengths
.sort()
2880 stroke_index
= stroke_lengths
.index(distance
)
2881 interval_length
= stroke_lengths
[
2882 stroke_index
+ 1] - stroke_lengths
[stroke_index
- 1
2884 distance_relative
= (distance
- stroke_lengths
[stroke_index
- 1]) / interval_length
2885 interval_vector
= stroke
.points
[stroke_index
].co
- stroke
.points
[stroke_index
- 1].co
2886 loc
= stroke
.points
[stroke_index
- 1].co
+ distance_relative
* interval_vector
2888 return(loc
, stroke_lengths_cache
)
2891 # create fake grease pencil strokes for the active object
2892 def gstretch_get_fake_strokes(object, bm_mod
, loops
):
2895 p1
= object.matrix_world
@ bm_mod
.verts
[loop
[0][0]].co
2896 p2
= object.matrix_world
@ bm_mod
.verts
[loop
[0][-1]].co
2897 strokes
.append(gstretch_fake_stroke([p1
, p2
]))
2902 def gstretch_get_strokes(self
, context
):
2903 looptools
= context
.window_manager
.looptools
2904 gp
= get_strokes(self
, context
)
2907 if looptools
.gstretch_use_guide
== "Annotation":
2908 layer
= bpy
.data
.grease_pencils
[0].layers
.active
2909 if looptools
.gstretch_use_guide
== "GPencil" and not looptools
.gstretch_guide
== None:
2910 layer
= looptools
.gstretch_guide
.data
.layers
.active
2913 frame
= layer
.active_frame
2916 strokes
= frame
.strokes
2917 if len(strokes
) < 1:
2922 # returns a list with loop-stroke pairs
2923 def gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
):
2924 if not loops
or not strokes
:
2927 # calculate loop centers
2929 bm_mod
.verts
.ensure_lookup_table()
2931 center
= mathutils
.Vector()
2932 for v_index
in loop
[0]:
2933 center
+= bm_mod
.verts
[v_index
].co
2934 center
/= len(loop
[0])
2935 center
= object.matrix_world
@ center
2936 loop_centers
.append([center
, loop
])
2938 # calculate stroke centers
2940 for stroke
in strokes
:
2941 center
= mathutils
.Vector()
2942 for p
in stroke
.points
:
2944 center
/= len(stroke
.points
)
2945 stroke_centers
.append([center
, stroke
, 0])
2947 # match, first by stroke use count, then by distance
2949 for lc
in loop_centers
:
2951 for i
, sc
in enumerate(stroke_centers
):
2952 distances
.append([sc
[2], (lc
[0] - sc
[0]).length
, i
])
2954 best_stroke
= distances
[0][2]
2955 ls_pairs
.append([lc
[1], stroke_centers
[best_stroke
][1]])
2956 stroke_centers
[best_stroke
][2] += 1 # increase stroke use count
2961 # match single selected vertices to the closest stroke endpoint
2962 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2963 def gstretch_match_single_verts(bm_mod
, strokes
, mat_world
):
2964 # calculate stroke endpoints in object space
2966 for stroke
in strokes
:
2967 endpoints
.append((mat_world
@ stroke
.points
[0].co
, stroke
, 0))
2968 endpoints
.append((mat_world
@ stroke
.points
[-1].co
, stroke
, -1))
2971 # find single vertices (not connected to other selected verts)
2972 for vert
in bm_mod
.verts
:
2976 for edge
in vert
.link_edges
:
2977 if edge
.other_vert(vert
).select
:
2982 # calculate distances from vertex to endpoints
2983 distance
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2984 endpoint_index
) for endpoint_index
, (loc
, stroke
, stroke_point
) in
2985 enumerate(endpoints
)]
2987 distances
.append(distance
[0])
2989 # create matches, based on shortest distance first
2993 singles
.append((distances
[0][1], distances
[0][2], distances
[0][3]))
2994 endpoints
.pop(distances
[0][4])
2997 for (i
, vert
, j
, k
, l
) in distances
:
2998 distance_new
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2999 endpoint_index
) for endpoint_index
, (loc
, stroke
,
3000 stroke_point
) in enumerate(endpoints
)]
3002 distances_new
.append(distance_new
[0])
3003 distances
= distances_new
3008 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
3009 def gstretch_relative_lengths(loop
, bm_mod
):
3011 for i
, v_index
in enumerate(loop
[0][1:]):
3013 (bm_mod
.verts
[v_index
].co
-
3014 bm_mod
.verts
[loop
[0][i
]].co
).length
+ lengths
[-1]
3016 total_length
= max(lengths
[-1], 1e-7)
3017 relative_lengths
= [length
/ total_length
for length
in
3020 return(relative_lengths
)
3023 # convert cache-stored strokes into usable (fake) GP strokes
3024 def gstretch_safe_to_true_strokes(safe_strokes
):
3026 for safe_stroke
in safe_strokes
:
3027 strokes
.append(gstretch_fake_stroke(safe_stroke
))
3032 # convert a GP stroke into a list of points which can be stored in cache
3033 def gstretch_true_to_safe_strokes(strokes
):
3035 for stroke
in strokes
:
3036 safe_strokes
.append([p
.co
.copy() for p
in stroke
.points
])
3038 return(safe_strokes
)
3041 # force consistency in GUI, max value can never be lower than min value
3042 def gstretch_update_max(self
, context
):
3043 # called from operator settings (after execution)
3044 if 'conversion_min' in self
.keys():
3045 if self
.conversion_min
> self
.conversion_max
:
3046 self
.conversion_max
= self
.conversion_min
3047 # called from toolbar
3049 lt
= context
.window_manager
.looptools
3050 if lt
.gstretch_conversion_min
> lt
.gstretch_conversion_max
:
3051 lt
.gstretch_conversion_max
= lt
.gstretch_conversion_min
3054 # force consistency in GUI, min value can never be higher than max value
3055 def gstretch_update_min(self
, context
):
3056 # called from operator settings (after execution)
3057 if 'conversion_max' in self
.keys():
3058 if self
.conversion_max
< self
.conversion_min
:
3059 self
.conversion_min
= self
.conversion_max
3060 # called from toolbar
3062 lt
= context
.window_manager
.looptools
3063 if lt
.gstretch_conversion_max
< lt
.gstretch_conversion_min
:
3064 lt
.gstretch_conversion_min
= lt
.gstretch_conversion_max
3067 # ########################################
3068 # ##### Relax functions ##################
3069 # ########################################
3071 # create lists with knots and points, all correctly sorted
3072 def relax_calculate_knots(loops
):
3075 for loop
, circular
in loops
:
3079 if len(loop
) % 2 == 1: # odd
3080 extend
= [False, True, 0, 1, 0, 1]
3082 extend
= [True, False, 0, 1, 1, 2]
3084 if len(loop
) % 2 == 1: # odd
3085 extend
= [False, False, 0, 1, 1, 2]
3087 extend
= [False, False, 0, 1, 1, 2]
3090 loop
= [loop
[-1]] + loop
+ [loop
[0]]
3091 for i
in range(extend
[2 + 2 * j
], len(loop
), 2):
3092 knots
[j
].append(loop
[i
])
3093 for i
in range(extend
[3 + 2 * j
], len(loop
), 2):
3094 if loop
[i
] == loop
[-1] and not circular
:
3096 if len(points
[j
]) == 0:
3097 points
[j
].append(loop
[i
])
3098 elif loop
[i
] != points
[j
][0]:
3099 points
[j
].append(loop
[i
])
3101 if knots
[j
][0] != knots
[j
][-1]:
3102 knots
[j
].append(knots
[j
][0])
3103 if len(points
[1]) == 0:
3109 all_points
.append(p
)
3111 return(all_knots
, all_points
)
3114 # calculate relative positions compared to first knot
3115 def relax_calculate_t(bm_mod
, knots
, points
, regular
):
3118 for i
in range(len(knots
)):
3119 amount
= len(knots
[i
]) + len(points
[i
])
3121 for j
in range(amount
):
3123 mix
.append([True, knots
[i
][round(j
/ 2)]])
3124 elif j
== amount
- 1:
3125 mix
.append([True, knots
[i
][-1]])
3127 mix
.append([False, points
[i
][int(j
/ 2)]])
3133 loc
= mathutils
.Vector(bm_mod
.verts
[m
[1]].co
[:])
3136 len_total
+= (loc
- loc_prev
).length
3138 tknots
.append(len_total
)
3140 tpoints
.append(len_total
)
3144 for p
in range(len(points
[i
])):
3145 tpoints
.append((tknots
[p
] + tknots
[p
+ 1]) / 2)
3146 all_tknots
.append(tknots
)
3147 all_tpoints
.append(tpoints
)
3149 return(all_tknots
, all_tpoints
)
3152 # change the location of the points to their place on the spline
3153 def relax_calculate_verts(bm_mod
, interpolation
, tknots
, knots
, tpoints
,
3157 for i
in range(len(knots
)):
3159 m
= tpoints
[i
][points
[i
].index(p
)]
3161 n
= tknots
[i
].index(m
)
3167 if n
> len(splines
[i
]) - 1:
3168 n
= len(splines
[i
]) - 1
3172 if interpolation
== 'cubic':
3173 ax
, bx
, cx
, dx
, tx
= splines
[i
][n
][0]
3174 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
3175 ay
, by
, cy
, dy
, ty
= splines
[i
][n
][1]
3176 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
3177 az
, bz
, cz
, dz
, tz
= splines
[i
][n
][2]
3178 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
3179 change
.append([p
, mathutils
.Vector([x
, y
, z
])])
3180 else: # interpolation == 'linear'
3181 a
, d
, t
, u
= splines
[i
][n
]
3184 change
.append([p
, ((m
- t
) / u
) * d
+ a
])
3186 move
.append([c
[0], (bm_mod
.verts
[c
[0]].co
+ c
[1]) / 2])
3191 # ########################################
3192 # ##### Space functions ##################
3193 # ########################################
3195 # calculate relative positions compared to first knot
3196 def space_calculate_t(bm_mod
, knots
):
3201 loc
= mathutils
.Vector(bm_mod
.verts
[k
].co
[:])
3204 len_total
+= (loc
- loc_prev
).length
3205 tknots
.append(len_total
)
3208 t_per_segment
= len_total
/ (amount
- 1)
3209 tpoints
= [i
* t_per_segment
for i
in range(amount
)]
3211 return(tknots
, tpoints
)
3214 # change the location of the points to their place on the spline
3215 def space_calculate_verts(bm_mod
, interpolation
, tknots
, tpoints
, points
,
3219 m
= tpoints
[points
.index(p
)]
3227 if n
> len(splines
) - 1:
3228 n
= len(splines
) - 1
3232 if interpolation
== 'cubic':
3233 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
3234 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
3235 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
3236 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
3237 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
3238 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
3239 move
.append([p
, mathutils
.Vector([x
, y
, z
])])
3240 else: # interpolation == 'linear'
3241 a
, d
, t
, u
= splines
[n
]
3242 move
.append([p
, ((m
- t
) / u
) * d
+ a
])
3247 # ########################################
3248 # ##### Operators ########################
3249 # ########################################
3252 class Bridge(Operator
):
3253 bl_idname
= 'mesh.looptools_bridge'
3254 bl_label
= "Bridge / Loft"
3255 bl_description
= "Bridge two, or loft several, loops of vertices"
3256 bl_options
= {'REGISTER', 'UNDO'}
3258 cubic_strength
: FloatProperty(
3260 description
="Higher strength results in more fluid curves",
3265 interpolation
: EnumProperty(
3266 name
="Interpolation mode",
3267 items
=(('cubic', "Cubic", "Gives curved results"),
3268 ('linear', "Linear", "Basic, fast, straight interpolation")),
3269 description
="Interpolation mode: algorithm used when creating "
3275 description
="Loft multiple loops, instead of considering them as "
3276 "a multi-input for bridging",
3279 loft_loop
: BoolProperty(
3281 description
="Connect the first and the last loop with each other",
3284 min_width
: IntProperty(
3285 name
="Minimum width",
3286 description
="Segments with an edge smaller than this are merged "
3287 "(compared to base edge)",
3291 subtype
='PERCENTAGE'
3295 items
=(('basic', "Basic", "Fast algorithm"),
3296 ('shortest', "Shortest edge", "Slower algorithm with better vertex matching")),
3297 description
="Algorithm used for bridging",
3300 remove_faces
: BoolProperty(
3301 name
="Remove faces",
3302 description
="Remove faces that are internal after bridging",
3305 reverse
: BoolProperty(
3307 description
="Manually override the direction in which the loops "
3308 "are bridged. Only use if the tool gives the wrong result",
3311 segments
: IntProperty(
3313 description
="Number of segments used to bridge the gap (0=automatic)",
3320 description
="Twist what vertices are connected to each other",
3325 def poll(cls
, context
):
3326 ob
= context
.active_object
3327 return (ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3329 def draw(self
, context
):
3330 layout
= self
.layout
3331 # layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3334 col_top
= layout
.column(align
=True)
3335 row
= col_top
.row(align
=True)
3336 col_left
= row
.column(align
=True)
3337 col_right
= row
.column(align
=True)
3338 col_right
.active
= self
.segments
!= 1
3339 col_left
.prop(self
, "segments")
3340 col_right
.prop(self
, "min_width", text
="")
3342 bottom_left
= col_left
.row()
3343 bottom_left
.active
= self
.segments
!= 1
3344 bottom_left
.prop(self
, "interpolation", text
="")
3345 bottom_right
= col_right
.row()
3346 bottom_right
.active
= self
.interpolation
== 'cubic'
3347 bottom_right
.prop(self
, "cubic_strength")
3348 # boolean properties
3349 col_top
.prop(self
, "remove_faces")
3351 col_top
.prop(self
, "loft_loop")
3353 # override properties
3355 row
= layout
.row(align
=True)
3356 row
.prop(self
, "twist")
3357 row
.prop(self
, "reverse")
3359 def invoke(self
, context
, event
):
3360 # load custom settings
3361 context
.window_manager
.looptools
.bridge_loft
= self
.loft
3363 return self
.execute(context
)
3365 def execute(self
, context
):
3367 object, bm
= initialise()
3368 edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
= \
3369 bridge_initialise(bm
, self
.interpolation
)
3370 settings_write(self
)
3372 # check cache to see if we can save time
3373 input_method
= bridge_input_method(self
.loft
, self
.loft_loop
)
3374 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Bridge",
3375 object, bm
, input_method
, False)
3378 loops
= bridge_get_input(bm
)
3380 # reorder loops if there are more than 2
3383 loops
= bridge_sort_loops(bm
, loops
, self
.loft_loop
)
3385 loops
= bridge_match_loops(bm
, loops
)
3387 # saving cache for faster execution next time
3389 cache_write("Bridge", object, bm
, input_method
, False, False,
3390 loops
, False, False)
3393 # calculate new geometry
3396 max_vert_index
= len(bm
.verts
) - 1
3397 for i
in range(1, len(loops
)):
3398 if not self
.loft
and i
% 2 == 0:
3400 lines
= bridge_calculate_lines(bm
, loops
[i
- 1:i
+ 1],
3401 self
.mode
, self
.twist
, self
.reverse
)
3402 vertex_normals
= bridge_calculate_virtual_vertex_normals(bm
,
3403 lines
, loops
[i
- 1:i
+ 1], edge_faces
, edgekey_to_edge
)
3404 segments
= bridge_calculate_segments(bm
, lines
,
3405 loops
[i
- 1:i
+ 1], self
.segments
)
3406 new_verts
, new_faces
, max_vert_index
= \
3407 bridge_calculate_geometry(
3408 bm
, lines
, vertex_normals
,
3409 segments
, self
.interpolation
, self
.cubic_strength
,
3410 self
.min_width
, max_vert_index
3413 vertices
+= new_verts
3416 # make sure faces in loops that aren't used, aren't removed
3417 if self
.remove_faces
and old_selected_faces
:
3418 bridge_save_unused_faces(bm
, old_selected_faces
, loops
)
3421 bridge_create_vertices(bm
, vertices
)
3422 # delete internal faces
3423 if self
.remove_faces
and old_selected_faces
:
3424 bridge_remove_internal_faces(bm
, old_selected_faces
)
3427 new_faces
= bridge_create_faces(object, bm
, faces
, self
.twist
)
3428 bridge_select_new_faces(new_faces
, smooth
)
3429 # edge-data could have changed, can't use cache next run
3430 if faces
and not vertices
:
3431 cache_delete("Bridge")
3432 # make sure normals are facing outside
3433 bmesh
.update_edit_mesh(object.data
, loop_triangles
=False, destructive
=True)
3434 bpy
.ops
.mesh
.normals_make_consistent()
3443 class Circle(Operator
):
3444 bl_idname
= "mesh.looptools_circle"
3446 bl_description
= "Move selected vertices into a circle shape"
3447 bl_options
= {'REGISTER', 'UNDO'}
3449 custom_radius
: BoolProperty(
3451 description
="Force a custom radius",
3456 items
=(("best", "Best fit", "Non-linear least squares"),
3457 ("inside", "Fit inside", "Only move vertices towards the center")),
3458 description
="Method used for fitting a circle to the vertices",
3461 flatten
: BoolProperty(
3463 description
="Flatten the circle, instead of projecting it on the mesh",
3466 influence
: FloatProperty(
3468 description
="Force of the tool",
3473 subtype
='PERCENTAGE'
3475 lock_x
: BoolProperty(
3477 description
="Lock editing of the x-coordinate",
3480 lock_y
: BoolProperty(
3482 description
="Lock editing of the y-coordinate",
3485 lock_z
: BoolProperty(name
="Lock Z",
3486 description
="Lock editing of the z-coordinate",
3489 radius
: FloatProperty(
3491 description
="Custom radius for circle",
3496 angle
: FloatProperty(
3498 description
="Rotate a circle by an angle",
3500 default
=math
.radians(0.0),
3501 soft_min
=math
.radians(-360.0),
3502 soft_max
=math
.radians(360.0)
3504 regular
: BoolProperty(
3506 description
="Distribute vertices at constant distances along the circle",
3511 def poll(cls
, context
):
3512 ob
= context
.active_object
3513 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3515 def draw(self
, context
):
3516 layout
= self
.layout
3517 col
= layout
.column()
3519 col
.prop(self
, "fit")
3522 col
.prop(self
, "flatten")
3523 row
= col
.row(align
=True)
3524 row
.prop(self
, "custom_radius")
3525 row_right
= row
.row(align
=True)
3526 row_right
.active
= self
.custom_radius
3527 row_right
.prop(self
, "radius", text
="")
3528 col
.prop(self
, "regular")
3529 col
.prop(self
, "angle")
3532 col_move
= col
.column(align
=True)
3533 row
= col_move
.row(align
=True)
3535 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3537 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3539 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3541 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3543 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3545 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3546 col_move
.prop(self
, "influence")
3548 def invoke(self
, context
, event
):
3549 # load custom settings
3551 return self
.execute(context
)
3553 def execute(self
, context
):
3555 object, bm
= initialise()
3556 settings_write(self
)
3557 # check cache to see if we can save time
3558 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Circle",
3559 object, bm
, False, False)
3561 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
3564 derived
, bm_mod
, single_vertices
, single_loops
, loops
= \
3565 circle_get_input(object, bm
)
3566 mapping
= get_mapping(derived
, bm
, bm_mod
, single_vertices
,
3568 single_loops
, loops
= circle_check_loops(single_loops
, loops
,
3571 # saving cache for faster execution next time
3573 cache_write("Circle", object, bm
, False, False, single_loops
,
3574 loops
, derived
, mapping
)
3577 for i
, loop
in enumerate(loops
):
3578 # best fitting flat plane
3579 com
, normal
= calculate_plane(bm_mod
, loop
)
3580 # if circular, shift loop so we get a good starting vertex
3582 loop
= circle_shift_loop(bm_mod
, loop
, com
)
3583 # flatten vertices on plane
3584 locs_2d
, p
, q
= circle_3d_to_2d(bm_mod
, loop
, com
, normal
)
3586 if self
.fit
== 'best':
3587 x0
, y0
, r
= circle_calculate_best_fit(locs_2d
)
3588 else: # self.fit == 'inside'
3589 x0
, y0
, r
= circle_calculate_min_fit(locs_2d
)
3591 if self
.custom_radius
:
3592 r
= self
.radius
/ p
.length
3593 # calculate positions on circle
3595 new_locs_2d
= circle_project_regular(locs_2d
[:], x0
, y0
, r
, self
.angle
)
3597 new_locs_2d
= circle_project_non_regular(locs_2d
[:], x0
, y0
, r
, self
.angle
)
3598 # take influence into account
3599 locs_2d
= circle_influence_locs(locs_2d
, new_locs_2d
,
3601 # calculate 3d positions of the created 2d input
3602 move
.append(circle_calculate_verts(self
.flatten
, bm_mod
,
3603 locs_2d
, com
, p
, q
, normal
))
3604 # flatten single input vertices on plane defined by loop
3605 if self
.flatten
and single_loops
:
3606 move
.append(circle_flatten_singles(bm_mod
, com
, p
, q
,
3607 normal
, single_loops
[i
]))
3609 # move vertices to new locations
3610 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3611 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3614 move_verts(object, bm
, mapping
, move
, lock
, -1)
3625 class Curve(Operator
):
3626 bl_idname
= "mesh.looptools_curve"
3628 bl_description
= "Turn a loop into a smooth curve"
3629 bl_options
= {'REGISTER', 'UNDO'}
3631 boundaries
: BoolProperty(
3633 description
="Limit the tool to work within the boundaries of the selected vertices",
3636 influence
: FloatProperty(
3638 description
="Force of the tool",
3643 subtype
='PERCENTAGE'
3645 interpolation
: EnumProperty(
3646 name
="Interpolation",
3647 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
3648 ("linear", "Linear", "Simple and fast linear algorithm")),
3649 description
="Algorithm used for interpolation",
3652 lock_x
: BoolProperty(
3654 description
="Lock editing of the x-coordinate",
3657 lock_y
: BoolProperty(
3659 description
="Lock editing of the y-coordinate",
3662 lock_z
: BoolProperty(
3664 description
="Lock editing of the z-coordinate",
3667 regular
: BoolProperty(
3669 description
="Distribute vertices at constant distances along the curve",
3672 restriction
: EnumProperty(
3674 items
=(("none", "None", "No restrictions on vertex movement"),
3675 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
3676 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
3677 description
="Restrictions on how the vertices can be moved",
3682 def poll(cls
, context
):
3683 ob
= context
.active_object
3684 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3686 def draw(self
, context
):
3687 layout
= self
.layout
3688 col
= layout
.column()
3690 col
.prop(self
, "interpolation")
3691 col
.prop(self
, "restriction")
3692 col
.prop(self
, "boundaries")
3693 col
.prop(self
, "regular")
3696 col_move
= col
.column(align
=True)
3697 row
= col_move
.row(align
=True)
3699 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3701 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3703 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3705 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3707 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3709 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3710 col_move
.prop(self
, "influence")
3712 def invoke(self
, context
, event
):
3713 # load custom settings
3715 return self
.execute(context
)
3717 def execute(self
, context
):
3719 object, bm
= initialise()
3720 settings_write(self
)
3721 # check cache to see if we can save time
3722 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Curve",
3723 object, bm
, False, self
.boundaries
)
3725 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
3728 derived
, bm_mod
, loops
= curve_get_input(object, bm
, self
.boundaries
)
3729 mapping
= get_mapping(derived
, bm
, bm_mod
, False, True, loops
)
3730 loops
= check_loops(loops
, mapping
, bm_mod
)
3732 v
.index
for v
in bm_mod
.verts
if v
.select
and not v
.hide
3735 # saving cache for faster execution next time
3737 cache_write("Curve", object, bm
, False, self
.boundaries
, False,
3738 loops
, derived
, mapping
)
3742 knots
, points
= curve_calculate_knots(loop
, verts_selected
)
3743 pknots
= curve_project_knots(bm_mod
, verts_selected
, knots
,
3745 tknots
, tpoints
= curve_calculate_t(bm_mod
, knots
, points
,
3746 pknots
, self
.regular
, loop
[1])
3747 splines
= calculate_splines(self
.interpolation
, bm_mod
,
3749 move
.append(curve_calculate_vertices(bm_mod
, knots
, tknots
,
3750 points
, tpoints
, splines
, self
.interpolation
,
3753 # move vertices to new locations
3754 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3755 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3758 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
3769 class Flatten(Operator
):
3770 bl_idname
= "mesh.looptools_flatten"
3771 bl_label
= "Flatten"
3772 bl_description
= "Flatten vertices on a best-fitting plane"
3773 bl_options
= {'REGISTER', 'UNDO'}
3775 influence
: FloatProperty(
3777 description
="Force of the tool",
3782 subtype
='PERCENTAGE'
3784 lock_x
: BoolProperty(
3786 description
="Lock editing of the x-coordinate",
3789 lock_y
: BoolProperty(
3791 description
="Lock editing of the y-coordinate",
3794 lock_z
: BoolProperty(name
="Lock Z",
3795 description
="Lock editing of the z-coordinate",
3798 plane
: EnumProperty(
3800 items
=(("best_fit", "Best fit", "Calculate a best fitting plane"),
3801 ("normal", "Normal", "Derive plane from averaging vertex normals"),
3802 ("view", "View", "Flatten on a plane perpendicular to the viewing angle")),
3803 description
="Plane on which vertices are flattened",
3806 restriction
: EnumProperty(
3808 items
=(("none", "None", "No restrictions on vertex movement"),
3809 ("bounding_box", "Bounding box", "Vertices are restricted to "
3810 "movement inside the bounding box of the selection")),
3811 description
="Restrictions on how the vertices can be moved",
3816 def poll(cls
, context
):
3817 ob
= context
.active_object
3818 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3820 def draw(self
, context
):
3821 layout
= self
.layout
3822 col
= layout
.column()
3824 col
.prop(self
, "plane")
3825 # col.prop(self, "restriction")
3828 col_move
= col
.column(align
=True)
3829 row
= col_move
.row(align
=True)
3831 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3833 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3835 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3837 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3839 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3841 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3842 col_move
.prop(self
, "influence")
3844 def invoke(self
, context
, event
):
3845 # load custom settings
3847 return self
.execute(context
)
3849 def execute(self
, context
):
3851 object, bm
= initialise()
3852 settings_write(self
)
3853 # check cache to see if we can save time
3854 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Flatten",
3855 object, bm
, False, False)
3857 # order input into virtual loops
3858 loops
= flatten_get_input(bm
)
3859 loops
= check_loops(loops
, mapping
, bm
)
3861 # saving cache for faster execution next time
3863 cache_write("Flatten", object, bm
, False, False, False, loops
,
3868 # calculate plane and position of vertices on them
3869 com
, normal
= calculate_plane(bm
, loop
, method
=self
.plane
,
3871 to_move
= flatten_project(bm
, loop
, com
, normal
)
3872 if self
.restriction
== 'none':
3873 move
.append(to_move
)
3875 move
.append(to_move
)
3877 # move vertices to new locations
3878 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3879 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3882 move_verts(object, bm
, False, move
, lock
, self
.influence
)
3890 # Annotation operator
3891 class RemoveAnnotation(Operator
):
3892 bl_idname
= "remove.annotation"
3893 bl_label
= "Remove Annotation"
3894 bl_description
= "Remove all Annotation Strokes"
3895 bl_options
= {'REGISTER', 'UNDO'}
3897 def execute(self
, context
):
3900 bpy
.data
.grease_pencils
[0].layers
.active
.clear()
3902 self
.report({'INFO'}, "No Annotation data to Unlink")
3903 return {'CANCELLED'}
3908 class RemoveGPencil(Operator
):
3909 bl_idname
= "remove.gp"
3910 bl_label
= "Remove GPencil"
3911 bl_description
= "Remove all GPencil Strokes"
3912 bl_options
= {'REGISTER', 'UNDO'}
3914 def execute(self
, context
):
3917 looptools
= context
.window_manager
.looptools
3918 looptools
.gstretch_guide
.data
.layers
.data
.clear()
3919 looptools
.gstretch_guide
.data
.update_tag()
3921 self
.report({'INFO'}, "No GPencil data to Unlink")
3922 return {'CANCELLED'}
3927 class GStretch(Operator
):
3928 bl_idname
= "mesh.looptools_gstretch"
3929 bl_label
= "Gstretch"
3930 bl_description
= "Stretch selected vertices to active stroke"
3931 bl_options
= {'REGISTER', 'UNDO'}
3933 conversion
: EnumProperty(
3935 items
=(("distance", "Distance", "Set the distance between vertices "
3936 "of the converted stroke"),
3937 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
3938 "number of vertices that converted strokes will have"),
3939 ("vertices", "Exact vertices", "Set the exact number of vertices "
3940 "that converted strokes will have. Short strokes "
3941 "with few points may contain less vertices than this number."),
3942 ("none", "No simplification", "Convert each point "
3944 description
="If strokes are converted to geometry, "
3945 "use this simplification method",
3946 default
='limit_vertices'
3948 conversion_distance
: FloatProperty(
3950 description
="Absolute distance between vertices along the converted "
3957 conversion_max
: IntProperty(
3958 name
="Max Vertices",
3959 description
="Maximum number of vertices strokes will "
3960 "have, when they are converted to geomtery",
3964 update
=gstretch_update_min
3966 conversion_min
: IntProperty(
3967 name
="Min Vertices",
3968 description
="Minimum number of vertices strokes will "
3969 "have, when they are converted to geomtery",
3973 update
=gstretch_update_max
3975 conversion_vertices
: IntProperty(
3977 description
="Number of vertices strokes will "
3978 "have, when they are converted to geometry. If strokes have less "
3979 "points than required, the 'Spread evenly' method is used",
3984 delete_strokes
: BoolProperty(
3985 name
="Delete strokes",
3986 description
="Remove strokes if they have been used."
3987 "WARNING: DOES NOT SUPPORT UNDO",
3990 influence
: FloatProperty(
3992 description
="Force of the tool",
3997 subtype
='PERCENTAGE'
3999 lock_x
: BoolProperty(
4001 description
="Lock editing of the x-coordinate",
4004 lock_y
: BoolProperty(
4006 description
="Lock editing of the y-coordinate",
4009 lock_z
: BoolProperty(
4011 description
="Lock editing of the z-coordinate",
4014 method
: EnumProperty(
4016 items
=(("project", "Project", "Project vertices onto the stroke, "
4017 "using vertex normals and connected edges"),
4018 ("irregular", "Spread", "Distribute vertices along the full "
4019 "stroke, retaining relative distances between the vertices"),
4020 ("regular", "Spread evenly", "Distribute vertices at regular "
4021 "distances along the full stroke")),
4022 description
="Method of distributing the vertices over the "
4028 def poll(cls
, context
):
4029 ob
= context
.active_object
4030 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4032 def draw(self
, context
):
4033 looptools
= context
.window_manager
.looptools
4034 layout
= self
.layout
4035 col
= layout
.column()
4037 col
.prop(self
, "method")
4040 col_conv
= col
.column(align
=True)
4041 col_conv
.prop(self
, "conversion", text
="")
4042 if self
.conversion
== 'distance':
4043 col_conv
.prop(self
, "conversion_distance")
4044 elif self
.conversion
== 'limit_vertices':
4045 row
= col_conv
.row(align
=True)
4046 row
.prop(self
, "conversion_min", text
="Min")
4047 row
.prop(self
, "conversion_max", text
="Max")
4048 elif self
.conversion
== 'vertices':
4049 col_conv
.prop(self
, "conversion_vertices")
4052 col_move
= col
.column(align
=True)
4053 row
= col_move
.row(align
=True)
4055 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
4057 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
4059 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
4061 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
4063 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
4065 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
4066 col_move
.prop(self
, "influence")
4068 if looptools
.gstretch_use_guide
== "Annotation":
4069 col
.operator("remove.annotation", text
="Delete annotation strokes")
4070 if looptools
.gstretch_use_guide
== "GPencil":
4071 col
.operator("remove.gp", text
="Delete GPencil strokes")
4073 def invoke(self
, context
, event
):
4074 # flush cached strokes
4075 if 'Gstretch' in looptools_cache
:
4076 looptools_cache
['Gstretch']['single_loops'] = []
4077 # load custom settings
4079 return self
.execute(context
)
4081 def execute(self
, context
):
4083 object, bm
= initialise()
4084 settings_write(self
)
4086 # check cache to see if we can save time
4087 cached
, safe_strokes
, loops
, derived
, mapping
= cache_read("Gstretch",
4088 object, bm
, False, False)
4090 straightening
= False
4092 strokes
= gstretch_safe_to_true_strokes(safe_strokes
)
4093 # cached strokes were flushed (see operator's invoke function)
4094 elif get_strokes(self
, context
):
4095 strokes
= gstretch_get_strokes(self
, context
)
4097 # straightening function (no GP) -> loops ignore modifiers
4098 straightening
= True
4101 bm_mod
.verts
.ensure_lookup_table()
4102 bm_mod
.edges
.ensure_lookup_table()
4103 bm_mod
.faces
.ensure_lookup_table()
4104 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
4105 if not straightening
:
4106 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
4108 # get loops and strokes
4109 if get_strokes(self
, context
):
4111 derived
, bm_mod
, loops
= get_connected_input(object, bm
, False, input='selected')
4112 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4113 loops
= check_loops(loops
, mapping
, bm_mod
)
4115 strokes
= gstretch_get_strokes(self
, context
)
4117 # straightening function (no GP) -> loops ignore modifiers
4121 bm_mod
.verts
.ensure_lookup_table()
4122 bm_mod
.edges
.ensure_lookup_table()
4123 bm_mod
.faces
.ensure_lookup_table()
4125 edgekey(edge
) for edge
in bm_mod
.edges
if
4126 edge
.select
and not edge
.hide
4128 loops
= get_connected_selections(edge_keys
)
4129 loops
= check_loops(loops
, mapping
, bm_mod
)
4130 # create fake strokes
4131 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
4133 # saving cache for faster execution next time
4136 safe_strokes
= gstretch_true_to_safe_strokes(strokes
)
4139 cache_write("Gstretch", object, bm
, False, False,
4140 safe_strokes
, loops
, derived
, mapping
)
4142 # pair loops and strokes
4143 ls_pairs
= gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
)
4144 ls_pairs
= gstretch_align_pairs(ls_pairs
, object, bm_mod
, self
.method
)
4148 # no selected geometry, convert GP to verts
4150 move
.append(gstretch_create_verts(object, bm
, strokes
,
4151 self
.method
, self
.conversion
, self
.conversion_distance
,
4152 self
.conversion_max
, self
.conversion_min
,
4153 self
.conversion_vertices
))
4154 for stroke
in strokes
:
4155 gstretch_erase_stroke(stroke
, context
)
4157 for (loop
, stroke
) in ls_pairs
:
4158 move
.append(gstretch_calculate_verts(loop
, stroke
, object,
4159 bm_mod
, self
.method
))
4160 if self
.delete_strokes
:
4161 if type(stroke
) != bpy
.types
.GPencilStroke
:
4162 # in case of cached fake stroke, get the real one
4163 if get_strokes(self
, context
):
4164 strokes
= gstretch_get_strokes(self
, context
)
4165 if loops
and strokes
:
4166 ls_pairs
= gstretch_match_loops_strokes(loops
,
4167 strokes
, object, bm_mod
)
4168 ls_pairs
= gstretch_align_pairs(ls_pairs
,
4169 object, bm_mod
, self
.method
)
4170 for (l
, s
) in ls_pairs
:
4174 gstretch_erase_stroke(stroke
, context
)
4176 # move vertices to new locations
4177 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4178 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4181 bmesh
.update_edit_mesh(object.data
, loop_triangles
=True, destructive
=True)
4182 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4193 class Relax(Operator
):
4194 bl_idname
= "mesh.looptools_relax"
4196 bl_description
= "Relax the loop, so it is smoother"
4197 bl_options
= {'REGISTER', 'UNDO'}
4199 input: EnumProperty(
4201 items
=(("all", "Parallel (all)", "Also use non-selected "
4202 "parallel loops as input"),
4203 ("selected", "Selection", "Only use selected vertices as input")),
4204 description
="Loops that are relaxed",
4207 interpolation
: EnumProperty(
4208 name
="Interpolation",
4209 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4210 ("linear", "Linear", "Simple and fast linear algorithm")),
4211 description
="Algorithm used for interpolation",
4214 iterations
: EnumProperty(
4216 items
=(("1", "1", "One"),
4217 ("3", "3", "Three"),
4219 ("10", "10", "Ten"),
4220 ("25", "25", "Twenty-five")),
4221 description
="Number of times the loop is relaxed",
4224 regular
: BoolProperty(
4226 description
="Distribute vertices at constant distances along the loop",
4231 def poll(cls
, context
):
4232 ob
= context
.active_object
4233 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4235 def draw(self
, context
):
4236 layout
= self
.layout
4237 col
= layout
.column()
4239 col
.prop(self
, "interpolation")
4240 col
.prop(self
, "input")
4241 col
.prop(self
, "iterations")
4242 col
.prop(self
, "regular")
4244 def invoke(self
, context
, event
):
4245 # load custom settings
4247 return self
.execute(context
)
4249 def execute(self
, context
):
4251 object, bm
= initialise()
4252 settings_write(self
)
4253 # check cache to see if we can save time
4254 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Relax",
4255 object, bm
, self
.input, False)
4257 derived
, bm_mod
= get_derived_bmesh(object, bm
, False)
4260 derived
, bm_mod
, loops
= get_connected_input(object, bm
, False, self
.input)
4261 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4262 loops
= check_loops(loops
, mapping
, bm_mod
)
4263 knots
, points
= relax_calculate_knots(loops
)
4265 # saving cache for faster execution next time
4267 cache_write("Relax", object, bm
, self
.input, False, False, loops
,
4270 for iteration
in range(int(self
.iterations
)):
4271 # calculate splines and new positions
4272 tknots
, tpoints
= relax_calculate_t(bm_mod
, knots
, points
,
4275 for i
in range(len(knots
)):
4276 splines
.append(calculate_splines(self
.interpolation
, bm_mod
,
4277 tknots
[i
], knots
[i
]))
4278 move
= [relax_calculate_verts(bm_mod
, self
.interpolation
,
4279 tknots
, knots
, tpoints
, points
, splines
)]
4280 move_verts(object, bm
, mapping
, move
, False, -1)
4291 class Space(Operator
):
4292 bl_idname
= "mesh.looptools_space"
4294 bl_description
= "Space the vertices in a regular distribution on the loop"
4295 bl_options
= {'REGISTER', 'UNDO'}
4297 influence
: FloatProperty(
4299 description
="Force of the tool",
4304 subtype
='PERCENTAGE'
4306 input: EnumProperty(
4308 items
=(("all", "Parallel (all)", "Also use non-selected "
4309 "parallel loops as input"),
4310 ("selected", "Selection", "Only use selected vertices as input")),
4311 description
="Loops that are spaced",
4314 interpolation
: EnumProperty(
4315 name
="Interpolation",
4316 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4317 ("linear", "Linear", "Vertices are projected on existing edges")),
4318 description
="Algorithm used for interpolation",
4321 lock_x
: BoolProperty(
4323 description
="Lock editing of the x-coordinate",
4326 lock_y
: BoolProperty(
4328 description
="Lock editing of the y-coordinate",
4331 lock_z
: BoolProperty(
4333 description
="Lock editing of the z-coordinate",
4338 def poll(cls
, context
):
4339 ob
= context
.active_object
4340 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4342 def draw(self
, context
):
4343 layout
= self
.layout
4344 col
= layout
.column()
4346 col
.prop(self
, "interpolation")
4347 col
.prop(self
, "input")
4350 col_move
= col
.column(align
=True)
4351 row
= col_move
.row(align
=True)
4353 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
4355 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
4357 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
4359 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
4361 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
4363 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
4364 col_move
.prop(self
, "influence")
4366 def invoke(self
, context
, event
):
4367 # load custom settings
4369 return self
.execute(context
)
4371 def execute(self
, context
):
4373 object, bm
= initialise()
4374 settings_write(self
)
4375 # check cache to see if we can save time
4376 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Space",
4377 object, bm
, self
.input, False)
4379 derived
, bm_mod
= get_derived_bmesh(object, bm
, True)
4382 derived
, bm_mod
, loops
= get_connected_input(object, bm
, True, self
.input)
4383 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4384 loops
= check_loops(loops
, mapping
, bm_mod
)
4386 # saving cache for faster execution next time
4388 cache_write("Space", object, bm
, self
.input, False, False, loops
,
4393 # calculate splines and new positions
4394 if loop
[1]: # circular
4395 loop
[0].append(loop
[0][0])
4396 tknots
, tpoints
= space_calculate_t(bm_mod
, loop
[0][:])
4397 splines
= calculate_splines(self
.interpolation
, bm_mod
,
4399 move
.append(space_calculate_verts(bm_mod
, self
.interpolation
,
4400 tknots
, tpoints
, loop
[0][:-1], splines
))
4401 # move vertices to new locations
4402 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4403 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4406 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4413 cache_delete("Space")
4418 # ########################################
4419 # ##### GUI and registration #############
4420 # ########################################
4422 # menu containing all tools
4423 class VIEW3D_MT_edit_mesh_looptools(Menu
):
4424 bl_label
= "LoopTools"
4426 def draw(self
, context
):
4427 layout
= self
.layout
4429 layout
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4430 layout
.operator("mesh.looptools_circle")
4431 layout
.operator("mesh.looptools_curve")
4432 layout
.operator("mesh.looptools_flatten")
4433 layout
.operator("mesh.looptools_gstretch")
4434 layout
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4435 layout
.operator("mesh.looptools_relax")
4436 layout
.operator("mesh.looptools_space")
4439 # panel containing all tools
4440 class VIEW3D_PT_tools_looptools(Panel
):
4441 bl_space_type
= 'VIEW_3D'
4442 bl_region_type
= 'UI'
4443 bl_category
= 'Edit'
4444 bl_context
= "mesh_edit"
4445 bl_label
= "LoopTools"
4446 bl_options
= {'DEFAULT_CLOSED'}
4448 def draw(self
, context
):
4449 layout
= self
.layout
4450 col
= layout
.column(align
=True)
4451 lt
= context
.window_manager
.looptools
4453 # bridge - first line
4454 split
= col
.split(factor
=0.15, align
=True)
4455 if lt
.display_bridge
:
4456 split
.prop(lt
, "display_bridge", text
="", icon
='DOWNARROW_HLT')
4458 split
.prop(lt
, "display_bridge", text
="", icon
='RIGHTARROW')
4459 split
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4461 if lt
.display_bridge
:
4462 box
= col
.column(align
=True).box().column()
4463 # box.prop(self, "mode")
4466 col_top
= box
.column(align
=True)
4467 row
= col_top
.row(align
=True)
4468 col_left
= row
.column(align
=True)
4469 col_right
= row
.column(align
=True)
4470 col_right
.active
= lt
.bridge_segments
!= 1
4471 col_left
.prop(lt
, "bridge_segments")
4472 col_right
.prop(lt
, "bridge_min_width", text
="")
4474 bottom_left
= col_left
.row()
4475 bottom_left
.active
= lt
.bridge_segments
!= 1
4476 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4477 bottom_right
= col_right
.row()
4478 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4479 bottom_right
.prop(lt
, "bridge_cubic_strength")
4480 # boolean properties
4481 col_top
.prop(lt
, "bridge_remove_faces")
4483 # override properties
4485 row
= box
.row(align
=True)
4486 row
.prop(lt
, "bridge_twist")
4487 row
.prop(lt
, "bridge_reverse")
4489 # circle - first line
4490 split
= col
.split(factor
=0.15, align
=True)
4491 if lt
.display_circle
:
4492 split
.prop(lt
, "display_circle", text
="", icon
='DOWNARROW_HLT')
4494 split
.prop(lt
, "display_circle", text
="", icon
='RIGHTARROW')
4495 split
.operator("mesh.looptools_circle")
4497 if lt
.display_circle
:
4498 box
= col
.column(align
=True).box().column()
4499 box
.prop(lt
, "circle_fit")
4502 box
.prop(lt
, "circle_flatten")
4503 row
= box
.row(align
=True)
4504 row
.prop(lt
, "circle_custom_radius")
4505 row_right
= row
.row(align
=True)
4506 row_right
.active
= lt
.circle_custom_radius
4507 row_right
.prop(lt
, "circle_radius", text
="")
4508 box
.prop(lt
, "circle_regular")
4511 col_move
= box
.column(align
=True)
4512 row
= col_move
.row(align
=True)
4513 if lt
.circle_lock_x
:
4514 row
.prop(lt
, "circle_lock_x", text
="X", icon
='LOCKED')
4516 row
.prop(lt
, "circle_lock_x", text
="X", icon
='UNLOCKED')
4517 if lt
.circle_lock_y
:
4518 row
.prop(lt
, "circle_lock_y", text
="Y", icon
='LOCKED')
4520 row
.prop(lt
, "circle_lock_y", text
="Y", icon
='UNLOCKED')
4521 if lt
.circle_lock_z
:
4522 row
.prop(lt
, "circle_lock_z", text
="Z", icon
='LOCKED')
4524 row
.prop(lt
, "circle_lock_z", text
="Z", icon
='UNLOCKED')
4525 col_move
.prop(lt
, "circle_influence")
4527 # curve - first line
4528 split
= col
.split(factor
=0.15, align
=True)
4529 if lt
.display_curve
:
4530 split
.prop(lt
, "display_curve", text
="", icon
='DOWNARROW_HLT')
4532 split
.prop(lt
, "display_curve", text
="", icon
='RIGHTARROW')
4533 split
.operator("mesh.looptools_curve")
4535 if lt
.display_curve
:
4536 box
= col
.column(align
=True).box().column()
4537 box
.prop(lt
, "curve_interpolation")
4538 box
.prop(lt
, "curve_restriction")
4539 box
.prop(lt
, "curve_boundaries")
4540 box
.prop(lt
, "curve_regular")
4543 col_move
= box
.column(align
=True)
4544 row
= col_move
.row(align
=True)
4546 row
.prop(lt
, "curve_lock_x", text
="X", icon
='LOCKED')
4548 row
.prop(lt
, "curve_lock_x", text
="X", icon
='UNLOCKED')
4550 row
.prop(lt
, "curve_lock_y", text
="Y", icon
='LOCKED')
4552 row
.prop(lt
, "curve_lock_y", text
="Y", icon
='UNLOCKED')
4554 row
.prop(lt
, "curve_lock_z", text
="Z", icon
='LOCKED')
4556 row
.prop(lt
, "curve_lock_z", text
="Z", icon
='UNLOCKED')
4557 col_move
.prop(lt
, "curve_influence")
4559 # flatten - first line
4560 split
= col
.split(factor
=0.15, align
=True)
4561 if lt
.display_flatten
:
4562 split
.prop(lt
, "display_flatten", text
="", icon
='DOWNARROW_HLT')
4564 split
.prop(lt
, "display_flatten", text
="", icon
='RIGHTARROW')
4565 split
.operator("mesh.looptools_flatten")
4566 # flatten - settings
4567 if lt
.display_flatten
:
4568 box
= col
.column(align
=True).box().column()
4569 box
.prop(lt
, "flatten_plane")
4570 # box.prop(lt, "flatten_restriction")
4573 col_move
= box
.column(align
=True)
4574 row
= col_move
.row(align
=True)
4575 if lt
.flatten_lock_x
:
4576 row
.prop(lt
, "flatten_lock_x", text
="X", icon
='LOCKED')
4578 row
.prop(lt
, "flatten_lock_x", text
="X", icon
='UNLOCKED')
4579 if lt
.flatten_lock_y
:
4580 row
.prop(lt
, "flatten_lock_y", text
="Y", icon
='LOCKED')
4582 row
.prop(lt
, "flatten_lock_y", text
="Y", icon
='UNLOCKED')
4583 if lt
.flatten_lock_z
:
4584 row
.prop(lt
, "flatten_lock_z", text
="Z", icon
='LOCKED')
4586 row
.prop(lt
, "flatten_lock_z", text
="Z", icon
='UNLOCKED')
4587 col_move
.prop(lt
, "flatten_influence")
4589 # gstretch - first line
4590 split
= col
.split(factor
=0.15, align
=True)
4591 if lt
.display_gstretch
:
4592 split
.prop(lt
, "display_gstretch", text
="", icon
='DOWNARROW_HLT')
4594 split
.prop(lt
, "display_gstretch", text
="", icon
='RIGHTARROW')
4595 split
.operator("mesh.looptools_gstretch")
4597 if lt
.display_gstretch
:
4598 box
= col
.column(align
=True).box().column()
4599 box
.prop(lt
, "gstretch_use_guide")
4600 if lt
.gstretch_use_guide
== "GPencil":
4601 box
.prop(lt
, "gstretch_guide")
4602 box
.prop(lt
, "gstretch_method")
4604 col_conv
= box
.column(align
=True)
4605 col_conv
.prop(lt
, "gstretch_conversion", text
="")
4606 if lt
.gstretch_conversion
== 'distance':
4607 col_conv
.prop(lt
, "gstretch_conversion_distance")
4608 elif lt
.gstretch_conversion
== 'limit_vertices':
4609 row
= col_conv
.row(align
=True)
4610 row
.prop(lt
, "gstretch_conversion_min", text
="Min")
4611 row
.prop(lt
, "gstretch_conversion_max", text
="Max")
4612 elif lt
.gstretch_conversion
== 'vertices':
4613 col_conv
.prop(lt
, "gstretch_conversion_vertices")
4616 col_move
= box
.column(align
=True)
4617 row
= col_move
.row(align
=True)
4618 if lt
.gstretch_lock_x
:
4619 row
.prop(lt
, "gstretch_lock_x", text
="X", icon
='LOCKED')
4621 row
.prop(lt
, "gstretch_lock_x", text
="X", icon
='UNLOCKED')
4622 if lt
.gstretch_lock_y
:
4623 row
.prop(lt
, "gstretch_lock_y", text
="Y", icon
='LOCKED')
4625 row
.prop(lt
, "gstretch_lock_y", text
="Y", icon
='UNLOCKED')
4626 if lt
.gstretch_lock_z
:
4627 row
.prop(lt
, "gstretch_lock_z", text
="Z", icon
='LOCKED')
4629 row
.prop(lt
, "gstretch_lock_z", text
="Z", icon
='UNLOCKED')
4630 col_move
.prop(lt
, "gstretch_influence")
4631 if lt
.gstretch_use_guide
== "Annotation":
4632 box
.operator("remove.annotation", text
="Delete Annotation Strokes")
4633 if lt
.gstretch_use_guide
== "GPencil":
4634 box
.operator("remove.gp", text
="Delete GPencil Strokes")
4637 split
= col
.split(factor
=0.15, align
=True)
4639 split
.prop(lt
, "display_loft", text
="", icon
='DOWNARROW_HLT')
4641 split
.prop(lt
, "display_loft", text
="", icon
='RIGHTARROW')
4642 split
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4645 box
= col
.column(align
=True).box().column()
4646 # box.prop(self, "mode")
4649 col_top
= box
.column(align
=True)
4650 row
= col_top
.row(align
=True)
4651 col_left
= row
.column(align
=True)
4652 col_right
= row
.column(align
=True)
4653 col_right
.active
= lt
.bridge_segments
!= 1
4654 col_left
.prop(lt
, "bridge_segments")
4655 col_right
.prop(lt
, "bridge_min_width", text
="")
4657 bottom_left
= col_left
.row()
4658 bottom_left
.active
= lt
.bridge_segments
!= 1
4659 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4660 bottom_right
= col_right
.row()
4661 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4662 bottom_right
.prop(lt
, "bridge_cubic_strength")
4663 # boolean properties
4664 col_top
.prop(lt
, "bridge_remove_faces")
4665 col_top
.prop(lt
, "bridge_loft_loop")
4667 # override properties
4669 row
= box
.row(align
=True)
4670 row
.prop(lt
, "bridge_twist")
4671 row
.prop(lt
, "bridge_reverse")
4673 # relax - first line
4674 split
= col
.split(factor
=0.15, align
=True)
4675 if lt
.display_relax
:
4676 split
.prop(lt
, "display_relax", text
="", icon
='DOWNARROW_HLT')
4678 split
.prop(lt
, "display_relax", text
="", icon
='RIGHTARROW')
4679 split
.operator("mesh.looptools_relax")
4681 if lt
.display_relax
:
4682 box
= col
.column(align
=True).box().column()
4683 box
.prop(lt
, "relax_interpolation")
4684 box
.prop(lt
, "relax_input")
4685 box
.prop(lt
, "relax_iterations")
4686 box
.prop(lt
, "relax_regular")
4688 # space - first line
4689 split
= col
.split(factor
=0.15, align
=True)
4690 if lt
.display_space
:
4691 split
.prop(lt
, "display_space", text
="", icon
='DOWNARROW_HLT')
4693 split
.prop(lt
, "display_space", text
="", icon
='RIGHTARROW')
4694 split
.operator("mesh.looptools_space")
4696 if lt
.display_space
:
4697 box
= col
.column(align
=True).box().column()
4698 box
.prop(lt
, "space_interpolation")
4699 box
.prop(lt
, "space_input")
4702 col_move
= box
.column(align
=True)
4703 row
= col_move
.row(align
=True)
4705 row
.prop(lt
, "space_lock_x", text
="X", icon
='LOCKED')
4707 row
.prop(lt
, "space_lock_x", text
="X", icon
='UNLOCKED')
4709 row
.prop(lt
, "space_lock_y", text
="Y", icon
='LOCKED')
4711 row
.prop(lt
, "space_lock_y", text
="Y", icon
='UNLOCKED')
4713 row
.prop(lt
, "space_lock_z", text
="Z", icon
='LOCKED')
4715 row
.prop(lt
, "space_lock_z", text
="Z", icon
='UNLOCKED')
4716 col_move
.prop(lt
, "space_influence")
4719 # property group containing all properties for the gui in the panel
4720 class LoopToolsProps(PropertyGroup
):
4722 Fake module like class
4723 bpy.context.window_manager.looptools
4725 # general display properties
4726 display_bridge
: BoolProperty(
4727 name
="Bridge settings",
4728 description
="Display settings of the Bridge tool",
4731 display_circle
: BoolProperty(
4732 name
="Circle settings",
4733 description
="Display settings of the Circle tool",
4736 display_curve
: BoolProperty(
4737 name
="Curve settings",
4738 description
="Display settings of the Curve tool",
4741 display_flatten
: BoolProperty(
4742 name
="Flatten settings",
4743 description
="Display settings of the Flatten tool",
4746 display_gstretch
: BoolProperty(
4747 name
="Gstretch settings",
4748 description
="Display settings of the Gstretch tool",
4751 display_loft
: BoolProperty(
4752 name
="Loft settings",
4753 description
="Display settings of the Loft tool",
4756 display_relax
: BoolProperty(
4757 name
="Relax settings",
4758 description
="Display settings of the Relax tool",
4761 display_space
: BoolProperty(
4762 name
="Space settings",
4763 description
="Display settings of the Space tool",
4768 bridge_cubic_strength
: FloatProperty(
4770 description
="Higher strength results in more fluid curves",
4775 bridge_interpolation
: EnumProperty(
4776 name
="Interpolation mode",
4777 items
=(('cubic', "Cubic", "Gives curved results"),
4778 ('linear', "Linear", "Basic, fast, straight interpolation")),
4779 description
="Interpolation mode: algorithm used when creating segments",
4782 bridge_loft
: BoolProperty(
4784 description
="Loft multiple loops, instead of considering them as "
4785 "a multi-input for bridging",
4788 bridge_loft_loop
: BoolProperty(
4790 description
="Connect the first and the last loop with each other",
4793 bridge_min_width
: IntProperty(
4794 name
="Minimum width",
4795 description
="Segments with an edge smaller than this are merged "
4796 "(compared to base edge)",
4800 subtype
='PERCENTAGE'
4802 bridge_mode
: EnumProperty(
4804 items
=(('basic', "Basic", "Fast algorithm"),
4805 ('shortest', "Shortest edge", "Slower algorithm with "
4806 "better vertex matching")),
4807 description
="Algorithm used for bridging",
4810 bridge_remove_faces
: BoolProperty(
4811 name
="Remove faces",
4812 description
="Remove faces that are internal after bridging",
4815 bridge_reverse
: BoolProperty(
4817 description
="Manually override the direction in which the loops "
4818 "are bridged. Only use if the tool gives the wrong result",
4821 bridge_segments
: IntProperty(
4823 description
="Number of segments used to bridge the gap (0=automatic)",
4828 bridge_twist
: IntProperty(
4830 description
="Twist what vertices are connected to each other",
4835 circle_custom_radius
: BoolProperty(
4837 description
="Force a custom radius",
4840 circle_fit
: EnumProperty(
4842 items
=(("best", "Best fit", "Non-linear least squares"),
4843 ("inside", "Fit inside", "Only move vertices towards the center")),
4844 description
="Method used for fitting a circle to the vertices",
4847 circle_flatten
: BoolProperty(
4849 description
="Flatten the circle, instead of projecting it on the mesh",
4852 circle_influence
: FloatProperty(
4854 description
="Force of the tool",
4859 subtype
='PERCENTAGE'
4861 circle_lock_x
: BoolProperty(
4863 description
="Lock editing of the x-coordinate",
4866 circle_lock_y
: BoolProperty(
4868 description
="Lock editing of the y-coordinate",
4871 circle_lock_z
: BoolProperty(
4873 description
="Lock editing of the z-coordinate",
4876 circle_radius
: FloatProperty(
4878 description
="Custom radius for circle",
4883 circle_regular
: BoolProperty(
4885 description
="Distribute vertices at constant distances along the circle",
4888 circle_angle
: FloatProperty(
4890 description
="Rotate a circle by an angle",
4892 default
=math
.radians(0.0),
4893 soft_min
=math
.radians(-360.0),
4894 soft_max
=math
.radians(360.0)
4897 curve_boundaries
: BoolProperty(
4899 description
="Limit the tool to work within the boundaries of the "
4900 "selected vertices",
4903 curve_influence
: FloatProperty(
4905 description
="Force of the tool",
4910 subtype
='PERCENTAGE'
4912 curve_interpolation
: EnumProperty(
4913 name
="Interpolation",
4914 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4915 ("linear", "Linear", "Simple and fast linear algorithm")),
4916 description
="Algorithm used for interpolation",
4919 curve_lock_x
: BoolProperty(
4921 description
="Lock editing of the x-coordinate",
4924 curve_lock_y
: BoolProperty(
4926 description
="Lock editing of the y-coordinate",
4929 curve_lock_z
: BoolProperty(
4931 description
="Lock editing of the z-coordinate",
4934 curve_regular
: BoolProperty(
4936 description
="Distribute vertices at constant distances along the curve",
4939 curve_restriction
: EnumProperty(
4941 items
=(("none", "None", "No restrictions on vertex movement"),
4942 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
4943 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
4944 description
="Restrictions on how the vertices can be moved",
4948 # flatten properties
4949 flatten_influence
: FloatProperty(
4951 description
="Force of the tool",
4956 subtype
='PERCENTAGE'
4958 flatten_lock_x
: BoolProperty(
4960 description
="Lock editing of the x-coordinate",
4962 flatten_lock_y
: BoolProperty(name
="Lock Y",
4963 description
="Lock editing of the y-coordinate",
4966 flatten_lock_z
: BoolProperty(
4968 description
="Lock editing of the z-coordinate",
4971 flatten_plane
: EnumProperty(
4973 items
=(("best_fit", "Best fit", "Calculate a best fitting plane"),
4974 ("normal", "Normal", "Derive plane from averaging vertex "
4976 ("view", "View", "Flatten on a plane perpendicular to the "
4978 description
="Plane on which vertices are flattened",
4981 flatten_restriction
: EnumProperty(
4983 items
=(("none", "None", "No restrictions on vertex movement"),
4984 ("bounding_box", "Bounding box", "Vertices are restricted to "
4985 "movement inside the bounding box of the selection")),
4986 description
="Restrictions on how the vertices can be moved",
4990 # gstretch properties
4991 gstretch_conversion
: EnumProperty(
4993 items
=(("distance", "Distance", "Set the distance between vertices "
4994 "of the converted stroke"),
4995 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
4996 "number of vertices that converted GP strokes will have"),
4997 ("vertices", "Exact vertices", "Set the exact number of vertices "
4998 "that converted strokes will have. Short strokes "
4999 "with few points may contain less vertices than this number."),
5000 ("none", "No simplification", "Convert each point "
5002 description
="If strokes are converted to geometry, "
5003 "use this simplification method",
5004 default
='limit_vertices'
5006 gstretch_conversion_distance
: FloatProperty(
5008 description
="Absolute distance between vertices along the converted "
5015 gstretch_conversion_max
: IntProperty(
5016 name
="Max Vertices",
5017 description
="Maximum number of vertices strokes will "
5018 "have, when they are converted to geomtery",
5022 update
=gstretch_update_min
5024 gstretch_conversion_min
: IntProperty(
5025 name
="Min Vertices",
5026 description
="Minimum number of vertices strokes will "
5027 "have, when they are converted to geomtery",
5031 update
=gstretch_update_max
5033 gstretch_conversion_vertices
: IntProperty(
5035 description
="Number of vertices strokes will "
5036 "have, when they are converted to geometry. If strokes have less "
5037 "points than required, the 'Spread evenly' method is used",
5042 gstretch_delete_strokes
: BoolProperty(
5043 name
="Delete strokes",
5044 description
="Remove Grease Pencil strokes if they have been used "
5045 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
5048 gstretch_influence
: FloatProperty(
5050 description
="Force of the tool",
5055 subtype
='PERCENTAGE'
5057 gstretch_lock_x
: BoolProperty(
5059 description
="Lock editing of the x-coordinate",
5062 gstretch_lock_y
: BoolProperty(
5064 description
="Lock editing of the y-coordinate",
5067 gstretch_lock_z
: BoolProperty(
5069 description
="Lock editing of the z-coordinate",
5072 gstretch_method
: EnumProperty(
5074 items
=(("project", "Project", "Project vertices onto the stroke, "
5075 "using vertex normals and connected edges"),
5076 ("irregular", "Spread", "Distribute vertices along the full "
5077 "stroke, retaining relative distances between the vertices"),
5078 ("regular", "Spread evenly", "Distribute vertices at regular "
5079 "distances along the full stroke")),
5080 description
="Method of distributing the vertices over the Grease "
5084 gstretch_use_guide
: EnumProperty(
5086 items
=(("None", "None", "None"),
5087 ("Annotation", "Annotation", "Annotation"),
5088 ("GPencil", "GPencil", "GPencil")),
5091 gstretch_guide
: PointerProperty(
5092 name
="GPencil object",
5093 description
="Set GPencil object",
5094 type=bpy
.types
.Object
5098 relax_input
: EnumProperty(name
="Input",
5099 items
=(("all", "Parallel (all)", "Also use non-selected "
5100 "parallel loops as input"),
5101 ("selected", "Selection", "Only use selected vertices as input")),
5102 description
="Loops that are relaxed",
5105 relax_interpolation
: EnumProperty(
5106 name
="Interpolation",
5107 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5108 ("linear", "Linear", "Simple and fast linear algorithm")),
5109 description
="Algorithm used for interpolation",
5112 relax_iterations
: EnumProperty(name
="Iterations",
5113 items
=(("1", "1", "One"),
5114 ("3", "3", "Three"),
5116 ("10", "10", "Ten"),
5117 ("25", "25", "Twenty-five")),
5118 description
="Number of times the loop is relaxed",
5121 relax_regular
: BoolProperty(
5123 description
="Distribute vertices at constant distances along the loop",
5128 space_influence
: FloatProperty(
5130 description
="Force of the tool",
5135 subtype
='PERCENTAGE'
5137 space_input
: EnumProperty(
5139 items
=(("all", "Parallel (all)", "Also use non-selected "
5140 "parallel loops as input"),
5141 ("selected", "Selection", "Only use selected vertices as input")),
5142 description
="Loops that are spaced",
5145 space_interpolation
: EnumProperty(
5146 name
="Interpolation",
5147 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5148 ("linear", "Linear", "Vertices are projected on existing edges")),
5149 description
="Algorithm used for interpolation",
5152 space_lock_x
: BoolProperty(
5154 description
="Lock editing of the x-coordinate",
5157 space_lock_y
: BoolProperty(
5159 description
="Lock editing of the y-coordinate",
5162 space_lock_z
: BoolProperty(
5164 description
="Lock editing of the z-coordinate",
5168 # draw function for integration in menus
5169 def menu_func(self
, context
):
5170 self
.layout
.menu("VIEW3D_MT_edit_mesh_looptools")
5171 self
.layout
.separator()
5174 # Add-ons Preferences Update Panel
5176 # Define Panel classes for updating
5178 VIEW3D_PT_tools_looptools
,
5182 def update_panel(self
, context
):
5183 message
= "LoopTools: Updating Panel locations has failed"
5185 for panel
in panels
:
5186 if "bl_rna" in panel
.__dict
__:
5187 bpy
.utils
.unregister_class(panel
)
5189 for panel
in panels
:
5190 panel
.bl_category
= context
.preferences
.addons
[__name__
].preferences
.category
5191 bpy
.utils
.register_class(panel
)
5193 except Exception as e
:
5194 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
5198 class LoopPreferences(AddonPreferences
):
5199 # this must match the addon name, use '__package__'
5200 # when defining this in a submodule of a python package.
5201 bl_idname
= __name__
5203 category
: StringProperty(
5204 name
="Tab Category",
5205 description
="Choose a name for the category of the panel",
5210 def draw(self
, context
):
5211 layout
= self
.layout
5215 col
.label(text
="Tab Category:")
5216 col
.prop(self
, "category", text
="")
5219 # define classes for registration
5221 VIEW3D_MT_edit_mesh_looptools
,
5222 VIEW3D_PT_tools_looptools
,
5237 # registering and menu integration
5240 bpy
.utils
.register_class(cls
)
5241 bpy
.types
.VIEW3D_MT_edit_mesh_context_menu
.prepend(menu_func
)
5242 bpy
.types
.WindowManager
.looptools
= PointerProperty(type=LoopToolsProps
)
5243 update_panel(None, bpy
.context
)
5246 # unregistering and removing menus
5248 for cls
in reversed(classes
):
5249 bpy
.utils
.unregister_class(cls
)
5250 bpy
.types
.VIEW3D_MT_edit_mesh_context_menu
.remove(menu_func
)
5252 del bpy
.types
.WindowManager
.looptools
5253 except Exception as e
:
5254 print('unregister fail:\n', e
)
5258 if __name__
== "__main__":