1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
21 "author": "Bart Crouch",
23 "blender": (2, 80, 0),
24 "location": "View3D > Sidebar > Edit Tab / Edit Mode Context Menu",
26 "description": "Mesh modelling toolkit. Several tools to aid modelling",
27 "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
28 "Scripts/Modeling/LoopTools",
38 from bpy_extras
import view3d_utils
39 from bpy
.types
import (
46 from bpy
.props
import (
55 # ########################################
56 # ##### General functions ################
57 # ########################################
59 # used by all tools to improve speed on reruns Unlink
63 def get_strokes(self
, context
):
64 looptools
= context
.window_manager
.looptools
65 if looptools
.gstretch_use_guide
== "Annotation":
67 strokes
= bpy
.data
.grease_pencils
[0].layers
.active
.active_frame
.strokes
70 self
.report({'WARNING'}, "active Annotation strokes not found")
72 if looptools
.gstretch_use_guide
== "GPencil" and not looptools
.gstretch_guide
== None:
74 strokes
= looptools
.gstretch_guide
.data
.layers
.active
.active_frame
.strokes
77 self
.report({'WARNING'}, "active GPencil strokes not found")
82 # force a full recalculation next time
83 def cache_delete(tool
):
84 if tool
in looptools_cache
:
85 del looptools_cache
[tool
]
88 # check cache for stored information
89 def cache_read(tool
, object, bm
, input_method
, boundaries
):
90 # current tool not cached yet
91 if tool
not in looptools_cache
:
92 return(False, False, False, False, False)
93 # check if selected object didn't change
94 if object.name
!= looptools_cache
[tool
]["object"]:
95 return(False, False, False, False, False)
96 # check if input didn't change
97 if input_method
!= looptools_cache
[tool
]["input_method"]:
98 return(False, False, False, False, False)
99 if boundaries
!= looptools_cache
[tool
]["boundaries"]:
100 return(False, False, False, False, False)
101 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
and
102 mod
.type == 'MIRROR']
103 if modifiers
!= looptools_cache
[tool
]["modifiers"]:
104 return(False, False, False, False, False)
105 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
106 if input != looptools_cache
[tool
]["input"]:
107 return(False, False, False, False, False)
109 single_loops
= looptools_cache
[tool
]["single_loops"]
110 loops
= looptools_cache
[tool
]["loops"]
111 derived
= looptools_cache
[tool
]["derived"]
112 mapping
= looptools_cache
[tool
]["mapping"]
114 return(True, single_loops
, loops
, derived
, mapping
)
117 # store information in the cache
118 def cache_write(tool
, object, bm
, input_method
, boundaries
, single_loops
,
119 loops
, derived
, mapping
):
120 # clear cache of current tool
121 if tool
in looptools_cache
:
122 del looptools_cache
[tool
]
123 # prepare values to be saved to cache
124 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
125 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
126 and mod
.type == 'MIRROR']
128 looptools_cache
[tool
] = {
129 "input": input, "object": object.name
,
130 "input_method": input_method
, "boundaries": boundaries
,
131 "single_loops": single_loops
, "loops": loops
,
132 "derived": derived
, "mapping": mapping
, "modifiers": modifiers
}
135 # calculates natural cubic splines through all given knots
136 def calculate_cubic_splines(bm_mod
, tknots
, knots
):
137 # hack for circular loops
138 if knots
[0] == knots
[-1] and len(knots
) > 1:
141 for k
in range(-1, -5, -1):
142 if k
- 1 < -len(knots
):
144 k_new1
.append(knots
[k
- 1])
147 if k
+ 1 > len(knots
) - 1:
149 k_new2
.append(knots
[k
+ 1])
156 for t
in range(-1, -5, -1):
157 if t
- 1 < -len(tknots
):
159 total1
+= tknots
[t
] - tknots
[t
- 1]
160 t_new1
.append(tknots
[0] - total1
)
164 if t
+ 1 > len(tknots
) - 1:
166 total2
+= tknots
[t
+ 1] - tknots
[t
]
167 t_new2
.append(tknots
[-1] + total2
)
180 locs
= [bm_mod
.verts
[k
].co
[:] for k
in knots
]
187 for i
in range(n
- 1):
188 if x
[i
+ 1] - x
[i
] == 0:
191 h
.append(x
[i
+ 1] - x
[i
])
193 for i
in range(1, n
- 1):
194 q
.append(3 / h
[i
] * (a
[i
+ 1] - a
[i
]) - 3 / h
[i
- 1] * (a
[i
] - a
[i
- 1]))
198 for i
in range(1, n
- 1):
199 l
.append(2 * (x
[i
+ 1] - x
[i
- 1]) - h
[i
- 1] * u
[i
- 1])
202 u
.append(h
[i
] / l
[i
])
203 z
.append((q
[i
] - h
[i
- 1] * z
[i
- 1]) / l
[i
])
206 b
= [False for i
in range(n
- 1)]
207 c
= [False for i
in range(n
)]
208 d
= [False for i
in range(n
- 1)]
210 for i
in range(n
- 2, -1, -1):
211 c
[i
] = z
[i
] - u
[i
] * c
[i
+ 1]
212 b
[i
] = (a
[i
+ 1] - a
[i
]) / h
[i
] - h
[i
] * (c
[i
+ 1] + 2 * c
[i
]) / 3
213 d
[i
] = (c
[i
+ 1] - c
[i
]) / (3 * h
[i
])
214 for i
in range(n
- 1):
215 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
217 for i
in range(len(knots
) - 1):
218 splines
.append([result
[i
], result
[i
+ n
- 1], result
[i
+ (n
- 1) * 2]])
219 if circular
: # cleaning up after hack
221 tknots
= tknots
[4:-4]
226 # calculates linear splines through all given knots
227 def calculate_linear_splines(bm_mod
, tknots
, knots
):
229 for i
in range(len(knots
) - 1):
230 a
= bm_mod
.verts
[knots
[i
]].co
231 b
= bm_mod
.verts
[knots
[i
+ 1]].co
234 u
= tknots
[i
+ 1] - t
235 splines
.append([a
, d
, t
, u
]) # [locStart, locDif, tStart, tDif]
240 # calculate a best-fit plane to the given vertices
241 def calculate_plane(bm_mod
, loop
, method
="best_fit", object=False):
242 # getting the vertex locations
243 locs
= [bm_mod
.verts
[v
].co
.copy() for v
in loop
[0]]
245 # calculating the center of masss
246 com
= mathutils
.Vector()
252 if method
== 'best_fit':
253 # creating the covariance matrix
254 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
259 mat
[0][0] += (loc
[0] - x
) ** 2
260 mat
[1][0] += (loc
[0] - x
) * (loc
[1] - y
)
261 mat
[2][0] += (loc
[0] - x
) * (loc
[2] - z
)
262 mat
[0][1] += (loc
[1] - y
) * (loc
[0] - x
)
263 mat
[1][1] += (loc
[1] - y
) ** 2
264 mat
[2][1] += (loc
[1] - y
) * (loc
[2] - z
)
265 mat
[0][2] += (loc
[2] - z
) * (loc
[0] - x
)
266 mat
[1][2] += (loc
[2] - z
) * (loc
[1] - y
)
267 mat
[2][2] += (loc
[2] - z
) ** 2
269 # calculating the normal to the plane
272 mat
= matrix_invert(mat
)
275 if math
.fabs(sum(mat
[0])) < math
.fabs(sum(mat
[1])):
276 if math
.fabs(sum(mat
[0])) < math
.fabs(sum(mat
[2])):
278 elif math
.fabs(sum(mat
[1])) < math
.fabs(sum(mat
[2])):
281 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
283 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
285 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
287 # warning! this is different from .normalize()
289 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
290 for i
in range(itermax
):
293 # Calculate length with double precision to avoid problems with `inf`
294 vec2_length
= math
.sqrt(vec2
[0] ** 2 + vec2
[1] ** 2 + vec2
[2] ** 2)
300 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
303 elif method
== 'normal':
304 # averaging the vertex normals
305 v_normals
= [bm_mod
.verts
[v
].normal
for v
in loop
[0]]
306 normal
= mathutils
.Vector()
307 for v_normal
in v_normals
:
309 normal
/= len(v_normals
)
312 elif method
== 'view':
313 # calculate view normal
314 rotation
= bpy
.context
.space_data
.region_3d
.view_matrix
.to_3x3().\
316 normal
= rotation
@ mathutils
.Vector((0.0, 0.0, 1.0))
318 normal
= object.matrix_world
.inverted().to_euler().to_matrix() @ \
324 # calculate splines based on given interpolation method (controller function)
325 def calculate_splines(interpolation
, bm_mod
, tknots
, knots
):
326 if interpolation
== 'cubic':
327 splines
= calculate_cubic_splines(bm_mod
, tknots
, knots
[:])
328 else: # interpolations == 'linear'
329 splines
= calculate_linear_splines(bm_mod
, tknots
, knots
[:])
334 # check loops and only return valid ones
335 def check_loops(loops
, mapping
, bm_mod
):
337 for loop
, circular
in loops
:
338 # loop needs to have at least 3 vertices
341 # loop needs at least 1 vertex in the original, non-mirrored mesh
345 if mapping
[vert
] > -1:
350 # vertices can not all be at the same location
352 for i
in range(len(loop
) - 1):
353 if (bm_mod
.verts
[loop
[i
]].co
- bm_mod
.verts
[loop
[i
+ 1]].co
).length
> 1e-6:
358 # passed all tests, loop is valid
359 valid_loops
.append([loop
, circular
])
364 # input: bmesh, output: dict with the edge-key as key and face-index as value
365 def dict_edge_faces(bm
):
366 edge_faces
= dict([[edgekey(edge
), []] for edge
in bm
.edges
if not edge
.hide
])
367 for face
in bm
.faces
:
370 for key
in face_edgekeys(face
):
371 edge_faces
[key
].append(face
.index
)
376 # input: bmesh (edge-faces optional), output: dict with face-face connections
377 def dict_face_faces(bm
, edge_faces
=False):
379 edge_faces
= dict_edge_faces(bm
)
381 connected_faces
= dict([[face
.index
, []] for face
in bm
.faces
if not face
.hide
])
382 for face
in bm
.faces
:
385 for edge_key
in face_edgekeys(face
):
386 for connected_face
in edge_faces
[edge_key
]:
387 if connected_face
== face
.index
:
389 connected_faces
[face
.index
].append(connected_face
)
391 return(connected_faces
)
394 # input: bmesh, output: dict with the vert index as key and edge-keys as value
395 def dict_vert_edges(bm
):
396 vert_edges
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
397 for edge
in bm
.edges
:
402 vert_edges
[vert
].append(ek
)
407 # input: bmesh, output: dict with the vert index as key and face index as value
408 def dict_vert_faces(bm
):
409 vert_faces
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
410 for face
in bm
.faces
:
412 for vert
in face
.verts
:
413 vert_faces
[vert
.index
].append(face
.index
)
418 # input: list of edge-keys, output: dictionary with vertex-vertex connections
419 def dict_vert_verts(edge_keys
):
420 # create connection data
424 if ek
[i
] in vert_verts
:
425 vert_verts
[ek
[i
]].append(ek
[1 - i
])
427 vert_verts
[ek
[i
]] = [ek
[1 - i
]]
432 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
434 return(tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])))
437 # returns the edgekeys of a bmesh face
438 def face_edgekeys(face
):
439 return([tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])) for edge
in face
.edges
])
442 # calculate input loops
443 def get_connected_input(object, bm
, input):
444 # get mesh with modifiers applied
445 derived
, bm_mod
= get_derived_bmesh(object, bm
)
447 # calculate selected loops
448 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and not edge
.hide
]
449 loops
= get_connected_selections(edge_keys
)
451 # if only selected loops are needed, we're done
452 if input == 'selected':
453 return(derived
, bm_mod
, loops
)
454 # elif input == 'all':
455 loops
= get_parallel_loops(bm_mod
, loops
)
457 return(derived
, bm_mod
, loops
)
460 # sorts all edge-keys into a list of loops
461 def get_connected_selections(edge_keys
):
462 # create connection data
463 vert_verts
= dict_vert_verts(edge_keys
)
465 # find loops consisting of connected selected edges
467 while len(vert_verts
) > 0:
468 loop
= [iter(vert_verts
.keys()).__next
__()]
474 # no more connection data for current vertex
475 if loop
[-1] not in vert_verts
:
483 for i
, next_vert
in enumerate(vert_verts
[loop
[-1]]):
484 if next_vert
not in loop
:
485 vert_verts
[loop
[-1]].pop(i
)
486 if len(vert_verts
[loop
[-1]]) == 0:
487 del vert_verts
[loop
[-1]]
488 # remove connection both ways
489 if next_vert
in vert_verts
:
490 if len(vert_verts
[next_vert
]) == 1:
491 del vert_verts
[next_vert
]
493 vert_verts
[next_vert
].remove(loop
[-1])
494 loop
.append(next_vert
)
498 # found one end of the loop, continue with next
502 # found both ends of the loop, stop growing
506 # check if loop is circular
507 if loop
[0] in vert_verts
:
508 if loop
[-1] in vert_verts
[loop
[0]]:
510 if len(vert_verts
[loop
[0]]) == 1:
511 del vert_verts
[loop
[0]]
513 vert_verts
[loop
[0]].remove(loop
[-1])
514 if len(vert_verts
[loop
[-1]]) == 1:
515 del vert_verts
[loop
[-1]]
517 vert_verts
[loop
[-1]].remove(loop
[0])
531 # get the derived mesh data, if there is a mirror modifier
532 def get_derived_bmesh(object, bm
):
533 # check for mirror modifiers
534 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
536 # disable other modifiers
537 show_viewport
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
]
538 for mod
in object.modifiers
:
539 if mod
.type != 'MIRROR':
540 mod
.show_viewport
= False
543 depsgraph
= bpy
.context
.evaluated_depsgraph_get()
544 object_eval
= object.evaluated_get(depsgraph
)
545 mesh_mod
= object_eval
.to_mesh()
546 bm_mod
.from_mesh(mesh_mod
)
547 object_eval
.to_mesh_clear()
548 # re-enable other modifiers
549 for mod_name
in show_viewport
:
550 object.modifiers
[mod_name
].show_viewport
= True
551 # no mirror modifiers, so no derived mesh necessary
556 bm_mod
.verts
.ensure_lookup_table()
557 bm_mod
.edges
.ensure_lookup_table()
558 bm_mod
.faces
.ensure_lookup_table()
560 return(derived
, bm_mod
)
563 # return a mapping of derived indices to indices
564 def get_mapping(derived
, bm
, bm_mod
, single_vertices
, full_search
, loops
):
569 verts
= [v
for v
in bm
.verts
if not v
.hide
]
571 verts
= [v
for v
in bm
.verts
if v
.select
and not v
.hide
]
573 # non-selected vertices around single vertices also need to be mapped
575 mapping
= dict([[vert
, -1] for vert
in single_vertices
])
576 verts_mod
= [bm_mod
.verts
[vert
] for vert
in single_vertices
]
578 for v_mod
in verts_mod
:
579 if (v
.co
- v_mod
.co
).length
< 1e-6:
580 mapping
[v_mod
.index
] = v
.index
582 real_singles
= [v_real
for v_real
in mapping
.values() if v_real
> -1]
584 verts_indices
= [vert
.index
for vert
in verts
]
585 for face
in [face
for face
in bm
.faces
if not face
.select
and not face
.hide
]:
586 for vert
in face
.verts
:
587 if vert
.index
in real_singles
:
589 if v
.index
not in verts_indices
:
594 # create mapping of derived indices to indices
595 mapping
= dict([[vert
, -1] for loop
in loops
for vert
in loop
[0]])
597 for single
in single_vertices
:
599 verts_mod
= [bm_mod
.verts
[i
] for i
in mapping
.keys()]
601 for v_mod
in verts_mod
:
602 if (v
.co
- v_mod
.co
).length
< 1e-6:
603 mapping
[v_mod
.index
] = v
.index
604 verts_mod
.remove(v_mod
)
610 # calculate the determinant of a matrix
611 def matrix_determinant(m
):
612 determinant
= m
[0][0] * m
[1][1] * m
[2][2] + m
[0][1] * m
[1][2] * m
[2][0] \
613 + m
[0][2] * m
[1][0] * m
[2][1] - m
[0][2] * m
[1][1] * m
[2][0] \
614 - m
[0][1] * m
[1][0] * m
[2][2] - m
[0][0] * m
[1][2] * m
[2][1]
619 # custom matrix inversion, to provide higher precision than the built-in one
620 def matrix_invert(m
):
621 r
= mathutils
.Matrix((
622 (m
[1][1] * m
[2][2] - m
[1][2] * m
[2][1], m
[0][2] * m
[2][1] - m
[0][1] * m
[2][2],
623 m
[0][1] * m
[1][2] - m
[0][2] * m
[1][1]),
624 (m
[1][2] * m
[2][0] - m
[1][0] * m
[2][2], m
[0][0] * m
[2][2] - m
[0][2] * m
[2][0],
625 m
[0][2] * m
[1][0] - m
[0][0] * m
[1][2]),
626 (m
[1][0] * m
[2][1] - m
[1][1] * m
[2][0], m
[0][1] * m
[2][0] - m
[0][0] * m
[2][1],
627 m
[0][0] * m
[1][1] - m
[0][1] * m
[1][0])))
629 return (r
* (1 / matrix_determinant(m
)))
632 # returns a list of all loops parallel to the input, input included
633 def get_parallel_loops(bm_mod
, loops
):
634 # get required dictionaries
635 edge_faces
= dict_edge_faces(bm_mod
)
636 connected_faces
= dict_face_faces(bm_mod
, edge_faces
)
637 # turn vertex loops into edge loops
640 edgeloop
= [[sorted([loop
[0][i
], loop
[0][i
+ 1]]) for i
in
641 range(len(loop
[0]) - 1)], loop
[1]]
642 if loop
[1]: # circular
643 edgeloop
[0].append(sorted([loop
[0][-1], loop
[0][0]]))
644 edgeloops
.append(edgeloop
[:])
645 # variables to keep track while iterating
649 for loop
in edgeloops
:
650 # initialise with original loop
651 all_edgeloops
.append(loop
[0])
655 if edge
[0] not in verts_used
:
656 verts_used
.append(edge
[0])
657 if edge
[1] not in verts_used
:
658 verts_used
.append(edge
[1])
660 # find parallel loops
661 while len(newloops
) > 0:
664 for i
in newloops
[-1]:
666 forbidden_side
= False
667 if i
not in edge_faces
:
668 # weird input with branches
671 for face
in edge_faces
[i
]:
672 if len(side_a
) == 0 and forbidden_side
!= "a":
678 elif side_a
[-1] in connected_faces
[face
] and \
679 forbidden_side
!= "a":
685 if len(side_b
) == 0 and forbidden_side
!= "b":
691 elif side_b
[-1] in connected_faces
[face
] and \
692 forbidden_side
!= "b":
700 # weird input with branches
713 for key
in face_edgekeys(bm_mod
.faces
[fi
]):
714 if key
[0] not in verts_used
and key
[1] not in \
716 extraloop
.append(key
)
719 for key
in extraloop
:
721 if new_vert
not in verts_used
:
722 verts_used
.append(new_vert
)
723 newloops
.append(extraloop
)
724 all_edgeloops
.append(extraloop
)
726 # input contains branches, only return selected loop
730 # change edgeloops into normal loops
732 for edgeloop
in all_edgeloops
:
734 # grow loop by comparing vertices between consecutive edge-keys
735 for i
in range(len(edgeloop
) - 1):
736 for vert
in range(2):
737 if edgeloop
[i
][vert
] in edgeloop
[i
+ 1]:
738 loop
.append(edgeloop
[i
][vert
])
741 # add starting vertex
742 for vert
in range(2):
743 if edgeloop
[0][vert
] != loop
[0]:
744 loop
= [edgeloop
[0][vert
]] + loop
747 for vert
in range(2):
748 if edgeloop
[-1][vert
] != loop
[-1]:
749 loop
.append(edgeloop
[-1][vert
])
751 # check if loop is circular
752 if loop
[0] == loop
[-1]:
757 loops
.append([loop
, circular
])
762 # gather initial data
764 object = bpy
.context
.active_object
765 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
766 # ensure that selection is synced for the derived mesh
767 bpy
.ops
.object.mode_set(mode
='OBJECT')
768 bpy
.ops
.object.mode_set(mode
='EDIT')
769 bm
= bmesh
.from_edit_mesh(object.data
)
771 bm
.verts
.ensure_lookup_table()
772 bm
.edges
.ensure_lookup_table()
773 bm
.faces
.ensure_lookup_table()
778 # move the vertices to their new locations
779 def move_verts(object, bm
, mapping
, move
, lock
, influence
):
781 lock_x
, lock_y
, lock_z
= lock
782 orient_slot
= bpy
.context
.scene
.transform_orientation_slots
[0]
783 custom
= orient_slot
.custom_orientation
785 mat
= custom
.matrix
.to_4x4().inverted() @ object.matrix_world
.copy()
786 elif orient_slot
.type == 'LOCAL':
787 mat
= mathutils
.Matrix
.Identity(4)
788 elif orient_slot
.type == 'VIEW':
789 mat
= bpy
.context
.region_data
.view_matrix
.copy() @ \
790 object.matrix_world
.copy()
791 else: # orientation == 'GLOBAL'
792 mat
= object.matrix_world
.copy()
793 mat_inv
= mat
.inverted()
796 for index
, loc
in loop
:
798 if mapping
[index
] == -1:
801 index
= mapping
[index
]
803 delta
= (loc
- bm
.verts
[index
].co
) @ mat_inv
811 loc
= bm
.verts
[index
].co
+ delta
815 new_loc
= loc
* (influence
/ 100) + \
816 bm
.verts
[index
].co
* ((100 - influence
) / 100)
817 bm
.verts
[index
].co
= new_loc
821 bm
.verts
.ensure_lookup_table()
822 bm
.edges
.ensure_lookup_table()
823 bm
.faces
.ensure_lookup_table()
826 # load custom tool settings
827 def settings_load(self
):
828 lt
= bpy
.context
.window_manager
.looptools
829 tool
= self
.name
.split()[0].lower()
830 keys
= self
.as_keywords().keys()
832 setattr(self
, key
, getattr(lt
, tool
+ "_" + key
))
835 # store custom tool settings
836 def settings_write(self
):
837 lt
= bpy
.context
.window_manager
.looptools
838 tool
= self
.name
.split()[0].lower()
839 keys
= self
.as_keywords().keys()
841 setattr(lt
, tool
+ "_" + key
, getattr(self
, key
))
844 # clean up and set settings back to original state
846 # update editmesh cached data
847 obj
= bpy
.context
.active_object
848 if obj
.mode
== 'EDIT':
849 bmesh
.update_edit_mesh(obj
.data
, loop_triangles
=True, destructive
=True)
852 # ########################################
853 # ##### Bridge functions #################
854 # ########################################
856 # calculate a cubic spline through the middle section of 4 given coordinates
857 def bridge_calculate_cubic_spline(bm
, coordinates
):
863 for i
in coordinates
:
864 a
.append(float(i
[j
]))
867 h
.append(x
[i
+ 1] - x
[i
])
869 for i
in range(1, 3):
870 q
.append(3.0 / h
[i
] * (a
[i
+ 1] - a
[i
]) - 3.0 / h
[i
- 1] * (a
[i
] - a
[i
- 1]))
874 for i
in range(1, 3):
875 l
.append(2.0 * (x
[i
+ 1] - x
[i
- 1]) - h
[i
- 1] * u
[i
- 1])
876 u
.append(h
[i
] / l
[i
])
877 z
.append((q
[i
] - h
[i
- 1] * z
[i
- 1]) / l
[i
])
880 b
= [False for i
in range(3)]
881 c
= [False for i
in range(4)]
882 d
= [False for i
in range(3)]
884 for i
in range(2, -1, -1):
885 c
[i
] = z
[i
] - u
[i
] * c
[i
+ 1]
886 b
[i
] = (a
[i
+ 1] - a
[i
]) / h
[i
] - h
[i
] * (c
[i
+ 1] + 2.0 * c
[i
]) / 3.0
887 d
[i
] = (c
[i
+ 1] - c
[i
]) / (3.0 * h
[i
])
889 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
890 spline
= [result
[1], result
[4], result
[7]]
895 # return a list with new vertex location vectors, a list with face vertex
896 # integers, and the highest vertex integer in the virtual mesh
897 def bridge_calculate_geometry(bm
, lines
, vertex_normals
, segments
,
898 interpolation
, cubic_strength
, min_width
, max_vert_index
):
902 # calculate location based on interpolation method
903 def get_location(line
, segment
, splines
):
904 v1
= bm
.verts
[lines
[line
][0]].co
905 v2
= bm
.verts
[lines
[line
][1]].co
906 if interpolation
== 'linear':
907 return v1
+ (segment
/ segments
) * (v2
- v1
)
908 else: # interpolation == 'cubic'
909 m
= (segment
/ segments
)
910 ax
, bx
, cx
, dx
, tx
= splines
[line
][0]
911 x
= ax
+ bx
* m
+ cx
* m
** 2 + dx
* m
** 3
912 ay
, by
, cy
, dy
, ty
= splines
[line
][1]
913 y
= ay
+ by
* m
+ cy
* m
** 2 + dy
* m
** 3
914 az
, bz
, cz
, dz
, tz
= splines
[line
][2]
915 z
= az
+ bz
* m
+ cz
* m
** 2 + dz
* m
** 3
916 return mathutils
.Vector((x
, y
, z
))
918 # no interpolation needed
920 for i
, line
in enumerate(lines
):
921 if i
< len(lines
) - 1:
922 faces
.append([line
[0], lines
[i
+ 1][0], lines
[i
+ 1][1], line
[1]])
923 # more than 1 segment, interpolate
925 # calculate splines (if necessary) once, so no recalculations needed
926 if interpolation
== 'cubic':
929 v1
= bm
.verts
[line
[0]].co
930 v2
= bm
.verts
[line
[1]].co
931 size
= (v2
- v1
).length
* cubic_strength
932 splines
.append(bridge_calculate_cubic_spline(bm
,
933 [v1
+ size
* vertex_normals
[line
[0]], v1
, v2
,
934 v2
+ size
* vertex_normals
[line
[1]]]))
938 # create starting situation
939 virtual_width
= [(bm
.verts
[lines
[i
][0]].co
-
940 bm
.verts
[lines
[i
+ 1][0]].co
).length
for i
941 in range(len(lines
) - 1)]
942 new_verts
= [get_location(0, seg
, splines
) for seg
in range(1,
944 first_line_indices
= [i
for i
in range(max_vert_index
+ 1,
945 max_vert_index
+ segments
)]
947 prev_verts
= new_verts
[:] # vertex locations of verts on previous line
948 prev_vert_indices
= first_line_indices
[:]
949 max_vert_index
+= segments
- 1 # highest vertex index in virtual mesh
950 next_verts
= [] # vertex locations of verts on current line
951 next_vert_indices
= []
953 for i
, line
in enumerate(lines
):
954 if i
< len(lines
) - 1:
958 for seg
in range(1, segments
):
959 loc1
= prev_verts
[seg
- 1]
960 loc2
= get_location(i
+ 1, seg
, splines
)
961 if (loc1
- loc2
).length
< (min_width
/ 100) * virtual_width
[i
] \
962 and line
[1] == lines
[i
+ 1][1]:
963 # triangle, no new vertex
964 faces
.append([v1
, v2
, prev_vert_indices
[seg
- 1],
965 prev_vert_indices
[seg
- 1]])
966 next_verts
+= prev_verts
[seg
- 1:]
967 next_vert_indices
+= prev_vert_indices
[seg
- 1:]
971 if i
== len(lines
) - 2 and lines
[0] == lines
[-1]:
972 # quad with first line, no new vertex
973 faces
.append([v1
, v2
, first_line_indices
[seg
- 1],
974 prev_vert_indices
[seg
- 1]])
975 v2
= first_line_indices
[seg
- 1]
976 v1
= prev_vert_indices
[seg
- 1]
978 # quad, add new vertex
980 faces
.append([v1
, v2
, max_vert_index
,
981 prev_vert_indices
[seg
- 1]])
983 v1
= prev_vert_indices
[seg
- 1]
984 new_verts
.append(loc2
)
985 next_verts
.append(loc2
)
986 next_vert_indices
.append(max_vert_index
)
988 faces
.append([v1
, v2
, lines
[i
+ 1][1], line
[1]])
990 prev_verts
= next_verts
[:]
991 prev_vert_indices
= next_vert_indices
[:]
993 next_vert_indices
= []
995 return(new_verts
, faces
, max_vert_index
)
998 # calculate lines (list of lists, vertex indices) that are used for bridging
999 def bridge_calculate_lines(bm
, loops
, mode
, twist
, reverse
):
1001 loop1
, loop2
= [i
[0] for i
in loops
]
1002 loop1_circular
, loop2_circular
= [i
[1] for i
in loops
]
1003 circular
= loop1_circular
or loop2_circular
1006 # calculate loop centers
1008 for loop
in [loop1
, loop2
]:
1009 center
= mathutils
.Vector()
1011 center
+= bm
.verts
[vertex
].co
1013 centers
.append(center
)
1014 for i
, loop
in enumerate([loop1
, loop2
]):
1016 if bm
.verts
[vertex
].co
== centers
[i
]:
1017 # prevent zero-length vectors in angle comparisons
1018 centers
[i
] += mathutils
.Vector((0.01, 0, 0))
1020 center1
, center2
= centers
1022 # calculate the normals of the virtual planes that the loops are on
1024 normal_plurity
= False
1025 for i
, loop
in enumerate([loop1
, loop2
]):
1027 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1030 x
, y
, z
= centers
[i
]
1031 for loc
in [bm
.verts
[vertex
].co
for vertex
in loop
]:
1032 mat
[0][0] += (loc
[0] - x
) ** 2
1033 mat
[1][0] += (loc
[0] - x
) * (loc
[1] - y
)
1034 mat
[2][0] += (loc
[0] - x
) * (loc
[2] - z
)
1035 mat
[0][1] += (loc
[1] - y
) * (loc
[0] - x
)
1036 mat
[1][1] += (loc
[1] - y
) ** 2
1037 mat
[2][1] += (loc
[1] - y
) * (loc
[2] - z
)
1038 mat
[0][2] += (loc
[2] - z
) * (loc
[0] - x
)
1039 mat
[1][2] += (loc
[2] - z
) * (loc
[1] - y
)
1040 mat
[2][2] += (loc
[2] - z
) ** 2
1043 if sum(mat
[0]) < 1e-6 or sum(mat
[1]) < 1e-6 or sum(mat
[2]) < 1e-6:
1044 normal_plurity
= True
1048 if sum(mat
[0]) == 0:
1049 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
1050 elif sum(mat
[1]) == 0:
1051 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
1052 elif sum(mat
[2]) == 0:
1053 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
1055 # warning! this is different from .normalize()
1058 vec
= mathutils
.Vector((1.0, 1.0, 1.0))
1059 vec2
= (mat
@ vec
) / (mat
@ vec
).length
1060 while vec
!= vec2
and iter < itermax
:
1064 if vec2
.length
!= 0:
1066 if vec2
.length
== 0:
1067 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
1069 normals
.append(normal
)
1070 # have plane normals face in the same direction (maximum angle: 90 degrees)
1071 if ((center1
+ normals
[0]) - center2
).length
< \
1072 ((center1
- normals
[0]) - center2
).length
:
1074 if ((center2
+ normals
[1]) - center1
).length
> \
1075 ((center2
- normals
[1]) - center1
).length
:
1078 # rotation matrix, representing the difference between the plane normals
1079 axis
= normals
[0].cross(normals
[1])
1080 axis
= mathutils
.Vector([loc
if abs(loc
) > 1e-8 else 0 for loc
in axis
])
1081 if axis
.angle(mathutils
.Vector((0, 0, 1)), 0) > 1.5707964:
1083 angle
= normals
[0].dot(normals
[1])
1084 rotation_matrix
= mathutils
.Matrix
.Rotation(angle
, 4, axis
)
1086 # if circular, rotate loops so they are aligned
1088 # make sure loop1 is the circular one (or both are circular)
1089 if loop2_circular
and not loop1_circular
:
1090 loop1_circular
, loop2_circular
= True, False
1091 loop1
, loop2
= loop2
, loop1
1093 # match start vertex of loop1 with loop2
1094 target_vector
= bm
.verts
[loop2
[0]].co
- center2
1095 dif_angles
= [[(rotation_matrix
@ (bm
.verts
[vertex
].co
- center1
)
1096 ).angle(target_vector
, 0), False, i
] for
1097 i
, vertex
in enumerate(loop1
)]
1099 if len(loop1
) != len(loop2
):
1100 angle_limit
= dif_angles
[0][0] * 1.2 # 20% margin
1102 [(bm
.verts
[loop2
[0]].co
-
1103 bm
.verts
[loop1
[index
]].co
).length
, angle
, index
] for
1104 angle
, distance
, index
in dif_angles
if angle
<= angle_limit
1107 loop1
= loop1
[dif_angles
[0][2]:] + loop1
[:dif_angles
[0][2]]
1109 # have both loops face the same way
1110 if normal_plurity
and not circular
:
1111 second_to_first
, second_to_second
, second_to_last
= [
1112 (bm
.verts
[loop1
[1]].co
- center1
).angle(
1113 bm
.verts
[loop2
[i
]].co
- center2
) for i
in [0, 1, -1]
1115 last_to_first
, last_to_second
= [
1116 (bm
.verts
[loop1
[-1]].co
-
1117 center1
).angle(bm
.verts
[loop2
[i
]].co
- center2
) for
1120 if (min(last_to_first
, last_to_second
) * 1.1 < min(second_to_first
,
1121 second_to_second
)) or (loop2_circular
and second_to_last
* 1.1 <
1122 min(second_to_first
, second_to_second
)):
1125 loop1
= [loop1
[-1]] + loop1
[:-1]
1127 angle
= (bm
.verts
[loop1
[0]].co
- center1
).\
1128 cross(bm
.verts
[loop1
[1]].co
- center1
).angle(normals
[0], 0)
1129 target_angle
= (bm
.verts
[loop2
[0]].co
- center2
).\
1130 cross(bm
.verts
[loop2
[1]].co
- center2
).angle(normals
[1], 0)
1131 limit
= 1.5707964 # 0.5*pi, 90 degrees
1132 if not ((angle
> limit
and target_angle
> limit
) or
1133 (angle
< limit
and target_angle
< limit
)):
1136 loop1
= [loop1
[-1]] + loop1
[:-1]
1137 elif normals
[0].angle(normals
[1]) > limit
:
1140 loop1
= [loop1
[-1]] + loop1
[:-1]
1142 # both loops have the same length
1143 if len(loop1
) == len(loop2
):
1146 if abs(twist
) < len(loop1
):
1147 loop1
= loop1
[twist
:] + loop1
[:twist
]
1151 lines
.append([loop1
[0], loop2
[0]])
1152 for i
in range(1, len(loop1
)):
1153 lines
.append([loop1
[i
], loop2
[i
]])
1155 # loops of different lengths
1157 # make loop1 longest loop
1158 if len(loop2
) > len(loop1
):
1159 loop1
, loop2
= loop2
, loop1
1160 loop1_circular
, loop2_circular
= loop2_circular
, loop1_circular
1164 if abs(twist
) < len(loop1
):
1165 loop1
= loop1
[twist
:] + loop1
[:twist
]
1169 # shortest angle difference doesn't always give correct start vertex
1170 if loop1_circular
and not loop2_circular
:
1173 if len(loop1
) - shifting
< len(loop2
):
1176 to_last
, to_first
= [
1177 (rotation_matrix
@ (bm
.verts
[loop1
[-1]].co
- center1
)).angle(
1178 (bm
.verts
[loop2
[i
]].co
- center2
), 0) for i
in [-1, 0]
1180 if to_first
< to_last
:
1181 loop1
= [loop1
[-1]] + loop1
[:-1]
1187 # basic shortest side first
1189 lines
.append([loop1
[0], loop2
[0]])
1190 for i
in range(1, len(loop1
)):
1191 if i
>= len(loop2
) - 1:
1193 lines
.append([loop1
[i
], loop2
[-1]])
1196 lines
.append([loop1
[i
], loop2
[i
]])
1198 # shortest edge algorithm
1199 else: # mode == 'shortest'
1200 lines
.append([loop1
[0], loop2
[0]])
1202 for i
in range(len(loop1
) - 1):
1203 if prev_vert2
== len(loop2
) - 1 and not loop2_circular
:
1204 # force triangles, reached end of loop2
1206 elif prev_vert2
== len(loop2
) - 1 and loop2_circular
:
1207 # at end of loop2, but circular, so check with first vert
1208 tri
, quad
= [(bm
.verts
[loop1
[i
+ 1]].co
-
1209 bm
.verts
[loop2
[j
]].co
).length
1210 for j
in [prev_vert2
, 0]]
1212 elif len(loop1
) - 1 - i
== len(loop2
) - 1 - prev_vert2
and \
1214 # force quads, otherwise won't make it to end of loop2
1217 # calculate if tri or quad gives shortest edge
1218 tri
, quad
= [(bm
.verts
[loop1
[i
+ 1]].co
-
1219 bm
.verts
[loop2
[j
]].co
).length
1220 for j
in range(prev_vert2
, prev_vert2
+ 2)]
1224 lines
.append([loop1
[i
+ 1], loop2
[prev_vert2
]])
1225 if circle_full
== 2:
1228 elif not circle_full
:
1229 lines
.append([loop1
[i
+ 1], loop2
[prev_vert2
+ 1]])
1231 # quad to first vertex of loop2
1233 lines
.append([loop1
[i
+ 1], loop2
[0]])
1237 # final face for circular loops
1238 if loop1_circular
and loop2_circular
:
1239 lines
.append([loop1
[0], loop2
[0]])
1244 # calculate number of segments needed
1245 def bridge_calculate_segments(bm
, lines
, loops
, segments
):
1246 # return if amount of segments is set by user
1251 average_edge_length
= [
1252 (bm
.verts
[vertex
].co
-
1253 bm
.verts
[loop
[0][i
+ 1]].co
).length
for loop
in loops
for
1254 i
, vertex
in enumerate(loop
[0][:-1])
1256 # closing edges of circular loops
1257 average_edge_length
+= [
1258 (bm
.verts
[loop
[0][-1]].co
-
1259 bm
.verts
[loop
[0][0]].co
).length
for loop
in loops
if loop
[1]
1263 average_edge_length
= sum(average_edge_length
) / len(average_edge_length
)
1264 average_bridge_length
= sum(
1266 bm
.verts
[v2
].co
).length
for v1
, v2
in lines
]
1269 segments
= max(1, round(average_bridge_length
/ average_edge_length
))
1274 # return dictionary with vertex index as key, and the normal vector as value
1275 def bridge_calculate_virtual_vertex_normals(bm
, lines
, loops
, edge_faces
,
1277 if not edge_faces
: # interpolation isn't set to cubic
1280 # pity reduce() isn't one of the basic functions in python anymore
1281 def average_vector_dictionary(dic
):
1282 for key
, vectors
in dic
.items():
1283 # if type(vectors) == type([]) and len(vectors) > 1:
1284 if len(vectors
) > 1:
1285 average
= mathutils
.Vector()
1286 for vector
in vectors
:
1288 average
/= len(vectors
)
1289 dic
[key
] = [average
]
1292 # get all edges of the loop
1294 [edgekey_to_edge
[tuple(sorted([loops
[j
][0][i
],
1295 loops
[j
][0][i
+ 1]]))] for i
in range(len(loops
[j
][0]) - 1)] for
1298 edges
= edges
[0] + edges
[1]
1300 if loops
[j
][1]: # circular
1301 edges
.append(edgekey_to_edge
[tuple(sorted([loops
[j
][0][0],
1302 loops
[j
][0][-1]]))])
1305 calculation based on face topology (assign edge-normals to vertices)
1307 edge_normal = face_normal x edge_vector
1308 vertex_normal = average(edge_normals)
1310 vertex_normals
= dict([(vertex
, []) for vertex
in loops
[0][0] + loops
[1][0]])
1312 faces
= edge_faces
[edgekey(edge
)] # valid faces connected to edge
1315 # get edge coordinates
1316 v1
, v2
= [bm
.verts
[edgekey(edge
)[i
]].co
for i
in [0, 1]]
1317 edge_vector
= v1
- v2
1318 if edge_vector
.length
< 1e-4:
1319 # zero-length edge, vertices at same location
1321 edge_center
= (v1
+ v2
) / 2
1323 # average face coordinates, if connected to more than 1 valid face
1325 face_normal
= mathutils
.Vector()
1326 face_center
= mathutils
.Vector()
1328 face_normal
+= face
.normal
1329 face_center
+= face
.calc_center_median()
1330 face_normal
/= len(faces
)
1331 face_center
/= len(faces
)
1333 face_normal
= faces
[0].normal
1334 face_center
= faces
[0].calc_center_median()
1335 if face_normal
.length
< 1e-4:
1336 # faces with a surface of 0 have no face normal
1339 # calculate virtual edge normal
1340 edge_normal
= edge_vector
.cross(face_normal
)
1341 edge_normal
.length
= 0.01
1342 if (face_center
- (edge_center
+ edge_normal
)).length
> \
1343 (face_center
- (edge_center
- edge_normal
)).length
:
1344 # make normal face the correct way
1345 edge_normal
.negate()
1346 edge_normal
.normalize()
1347 # add virtual edge normal as entry for both vertices it connects
1348 for vertex
in edgekey(edge
):
1349 vertex_normals
[vertex
].append(edge_normal
)
1352 calculation based on connection with other loop (vertex focused method)
1353 - used for vertices that aren't connected to any valid faces
1355 plane_normal = edge_vector x connection_vector
1356 vertex_normal = plane_normal x edge_vector
1359 vertex
for vertex
, normal
in vertex_normals
.items() if not normal
1363 # edge vectors connected to vertices
1364 edge_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1366 for v
in edgekey(edge
):
1367 if v
in edge_vectors
:
1368 edge_vector
= bm
.verts
[edgekey(edge
)[0]].co
- \
1369 bm
.verts
[edgekey(edge
)[1]].co
1370 if edge_vector
.length
< 1e-4:
1371 # zero-length edge, vertices at same location
1373 edge_vectors
[v
].append(edge_vector
)
1375 # connection vectors between vertices of both loops
1376 connection_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1377 connections
= dict([[vertex
, []] for vertex
in vertices
])
1378 for v1
, v2
in lines
:
1379 if v1
in connection_vectors
or v2
in connection_vectors
:
1380 new_vector
= bm
.verts
[v1
].co
- bm
.verts
[v2
].co
1381 if new_vector
.length
< 1e-4:
1382 # zero-length connection vector,
1383 # vertices in different loops at same location
1385 if v1
in connection_vectors
:
1386 connection_vectors
[v1
].append(new_vector
)
1387 connections
[v1
].append(v2
)
1388 if v2
in connection_vectors
:
1389 connection_vectors
[v2
].append(new_vector
)
1390 connections
[v2
].append(v1
)
1391 connection_vectors
= average_vector_dictionary(connection_vectors
)
1392 connection_vectors
= dict(
1393 [[vertex
, vector
[0]] if vector
else
1394 [vertex
, []] for vertex
, vector
in connection_vectors
.items()]
1397 for vertex
, values
in edge_vectors
.items():
1398 # vertex normal doesn't matter, just assign a random vector to it
1399 if not connection_vectors
[vertex
]:
1400 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1403 # calculate to what location the vertex is connected,
1404 # used to determine what way to flip the normal
1405 connected_center
= mathutils
.Vector()
1406 for v
in connections
[vertex
]:
1407 connected_center
+= bm
.verts
[v
].co
1408 if len(connections
[vertex
]) > 1:
1409 connected_center
/= len(connections
[vertex
])
1410 if len(connections
[vertex
]) == 0:
1411 # shouldn't be possible, but better safe than sorry
1412 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1415 # can't do proper calculations, because of zero-length vector
1417 if (connected_center
- (bm
.verts
[vertex
].co
+
1418 connection_vectors
[vertex
])).length
< (connected_center
-
1419 (bm
.verts
[vertex
].co
- connection_vectors
[vertex
])).length
:
1420 connection_vectors
[vertex
].negate()
1421 vertex_normals
[vertex
] = [connection_vectors
[vertex
].normalized()]
1424 # calculate vertex normals using edge-vectors,
1425 # connection-vectors and the derived plane normal
1426 for edge_vector
in values
:
1427 plane_normal
= edge_vector
.cross(connection_vectors
[vertex
])
1428 vertex_normal
= edge_vector
.cross(plane_normal
)
1429 vertex_normal
.length
= 0.1
1430 if (connected_center
- (bm
.verts
[vertex
].co
+
1431 vertex_normal
)).length
< (connected_center
-
1432 (bm
.verts
[vertex
].co
- vertex_normal
)).length
:
1433 # make normal face the correct way
1434 vertex_normal
.negate()
1435 vertex_normal
.normalize()
1436 vertex_normals
[vertex
].append(vertex_normal
)
1438 # average virtual vertex normals, based on all edges it's connected to
1439 vertex_normals
= average_vector_dictionary(vertex_normals
)
1440 vertex_normals
= dict([[vertex
, vector
[0]] for vertex
, vector
in vertex_normals
.items()])
1442 return(vertex_normals
)
1445 # add vertices to mesh
1446 def bridge_create_vertices(bm
, vertices
):
1447 for i
in range(len(vertices
)):
1448 bm
.verts
.new(vertices
[i
])
1449 bm
.verts
.ensure_lookup_table()
1453 def bridge_create_faces(object, bm
, faces
, twist
):
1454 # have the normal point the correct way
1456 [face
.reverse() for face
in faces
]
1457 faces
= [face
[2:] + face
[:2] if face
[0] == face
[1] else face
for face
in faces
]
1459 # eekadoodle prevention
1460 for i
in range(len(faces
)):
1461 if not faces
[i
][-1]:
1462 if faces
[i
][0] == faces
[i
][-1]:
1463 faces
[i
] = [faces
[i
][1], faces
[i
][2], faces
[i
][3], faces
[i
][1]]
1465 faces
[i
] = [faces
[i
][-1]] + faces
[i
][:-1]
1466 # result of converting from pre-bmesh period
1467 if faces
[i
][-1] == faces
[i
][-2]:
1468 faces
[i
] = faces
[i
][:-1]
1471 for i
in range(len(faces
)):
1472 new_faces
.append(bm
.faces
.new([bm
.verts
[v
] for v
in faces
[i
]]))
1474 object.data
.update(calc_edges
=True) # calc_edges prevents memory-corruption
1476 bm
.verts
.ensure_lookup_table()
1477 bm
.edges
.ensure_lookup_table()
1478 bm
.faces
.ensure_lookup_table()
1483 # calculate input loops
1484 def bridge_get_input(bm
):
1485 # create list of internal edges, which should be skipped
1486 eks_of_selected_faces
= [
1487 item
for sublist
in [face_edgekeys(face
) for
1488 face
in bm
.faces
if face
.select
and not face
.hide
] for item
in sublist
1491 for ek
in eks_of_selected_faces
:
1492 if ek
in edge_count
:
1496 internal_edges
= [ek
for ek
in edge_count
if edge_count
[ek
] > 1]
1498 # sort correct edges into loops
1500 edgekey(edge
) for edge
in bm
.edges
if edge
.select
and
1501 not edge
.hide
and edgekey(edge
) not in internal_edges
1503 loops
= get_connected_selections(selected_edges
)
1508 # return values needed by the bridge operator
1509 def bridge_initialise(bm
, interpolation
):
1510 if interpolation
== 'cubic':
1511 # dict with edge-key as key and list of connected valid faces as value
1513 face
.index
for face
in bm
.faces
if face
.select
or
1517 [[edgekey(edge
), []] for edge
in bm
.edges
if not edge
.hide
]
1519 for face
in bm
.faces
:
1520 if face
.index
in face_blacklist
:
1522 for key
in face_edgekeys(face
):
1523 edge_faces
[key
].append(face
)
1524 # dictionary with the edge-key as key and edge as value
1525 edgekey_to_edge
= dict(
1526 [[edgekey(edge
), edge
] for edge
in bm
.edges
if edge
.select
and not edge
.hide
]
1530 edgekey_to_edge
= False
1532 # selected faces input
1533 old_selected_faces
= [
1534 face
.index
for face
in bm
.faces
if face
.select
and not face
.hide
1537 # find out if faces created by bridging should be smoothed
1540 if sum([face
.smooth
for face
in bm
.faces
]) / len(bm
.faces
) >= 0.5:
1543 return(edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
)
1546 # return a string with the input method
1547 def bridge_input_method(loft
, loft_loop
):
1551 method
= "Loft loop"
1553 method
= "Loft no-loop"
1560 # match up loops in pairs, used for multi-input bridging
1561 def bridge_match_loops(bm
, loops
):
1562 # calculate average loop normals and centers
1565 for vertices
, circular
in loops
:
1566 normal
= mathutils
.Vector()
1567 center
= mathutils
.Vector()
1568 for vertex
in vertices
:
1569 normal
+= bm
.verts
[vertex
].normal
1570 center
+= bm
.verts
[vertex
].co
1571 normals
.append(normal
/ len(vertices
) / 10)
1572 centers
.append(center
/ len(vertices
))
1574 # possible matches if loop normals are faced towards the center
1576 matches
= dict([[i
, []] for i
in range(len(loops
))])
1578 for i
in range(len(loops
) + 1):
1579 for j
in range(i
+ 1, len(loops
)):
1580 if (centers
[i
] - centers
[j
]).length
> \
1581 (centers
[i
] - (centers
[j
] + normals
[j
])).length
and \
1582 (centers
[j
] - centers
[i
]).length
> \
1583 (centers
[j
] - (centers
[i
] + normals
[i
])).length
:
1585 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1586 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1587 # if no loops face each other, just make matches between all the loops
1588 if matches_amount
== 0:
1589 for i
in range(len(loops
) + 1):
1590 for j
in range(i
+ 1, len(loops
)):
1591 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1592 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1593 for key
, value
in matches
.items():
1596 # matches based on distance between centers and number of vertices in loops
1598 for loop_index
in range(len(loops
)):
1599 if loop_index
in new_order
:
1601 loop_matches
= matches
[loop_index
]
1602 if not loop_matches
:
1604 shortest_distance
= loop_matches
[0][0]
1605 shortest_distance
*= 1.1
1607 [abs(len(loops
[loop_index
][0]) -
1608 len(loops
[loop
[2]][0])), loop
[0], loop
[1], loop
[2]] for loop
in
1609 loop_matches
if loop
[0] < shortest_distance
1612 for match
in loop_matches
:
1613 if match
[3] not in new_order
:
1614 new_order
+= [loop_index
, match
[3]]
1617 # reorder loops based on matches
1618 if len(new_order
) >= 2:
1619 loops
= [loops
[i
] for i
in new_order
]
1624 # remove old_selected_faces
1625 def bridge_remove_internal_faces(bm
, old_selected_faces
):
1626 # collect bmesh faces and internal bmesh edges
1627 remove_faces
= [bm
.faces
[face
] for face
in old_selected_faces
]
1628 edges
= collections
.Counter(
1629 [edge
.index
for face
in remove_faces
for edge
in face
.edges
]
1631 remove_edges
= [bm
.edges
[edge
] for edge
in edges
if edges
[edge
] > 1]
1633 # remove internal faces and edges
1634 for face
in remove_faces
:
1635 bm
.faces
.remove(face
)
1636 for edge
in remove_edges
:
1637 bm
.edges
.remove(edge
)
1639 bm
.faces
.ensure_lookup_table()
1640 bm
.edges
.ensure_lookup_table()
1641 bm
.verts
.ensure_lookup_table()
1644 # update list of internal faces that are flagged for removal
1645 def bridge_save_unused_faces(bm
, old_selected_faces
, loops
):
1646 # key: vertex index, value: lists of selected faces using it
1648 vertex_to_face
= dict([[i
, []] for i
in range(len(bm
.verts
))])
1649 [[vertex_to_face
[vertex
.index
].append(face
) for vertex
in
1650 bm
.faces
[face
].verts
] for face
in old_selected_faces
]
1652 # group selected faces that are connected
1655 for face
in old_selected_faces
:
1656 if face
in grouped_faces
:
1658 grouped_faces
.append(face
)
1662 grow_face
= new_faces
[0]
1663 for vertex
in bm
.faces
[grow_face
].verts
:
1664 vertex_face_group
= [
1665 face
for face
in vertex_to_face
[vertex
.index
] if
1666 face
not in grouped_faces
1668 new_faces
+= vertex_face_group
1669 grouped_faces
+= vertex_face_group
1670 group
+= vertex_face_group
1672 groups
.append(group
)
1674 # key: vertex index, value: True/False (is it in a loop that is used)
1675 used_vertices
= dict([[i
, 0] for i
in range(len(bm
.verts
))])
1677 for vertex
in loop
[0]:
1678 used_vertices
[vertex
] = True
1680 # check if group is bridged, if not remove faces from internal faces list
1681 for group
in groups
:
1686 for vertex
in bm
.faces
[face
].verts
:
1687 if used_vertices
[vertex
.index
]:
1692 old_selected_faces
.remove(face
)
1695 # add the newly created faces to the selection
1696 def bridge_select_new_faces(new_faces
, smooth
):
1697 for face
in new_faces
:
1698 face
.select_set(True)
1699 face
.smooth
= smooth
1702 # sort loops, so they are connected in the correct order when lofting
1703 def bridge_sort_loops(bm
, loops
, loft_loop
):
1704 # simplify loops to single points, and prepare for pathfinding
1706 [sum([bm
.verts
[i
].co
[j
] for i
in loop
[0]]) /
1707 len(loop
[0]) for loop
in loops
] for j
in range(3)
1709 nodes
= [mathutils
.Vector((x
[i
], y
[i
], z
[i
])) for i
in range(len(loops
))]
1712 open = [i
for i
in range(1, len(loops
))]
1714 # connect node to path, that is shortest to active_node
1715 while len(open) > 0:
1716 distances
= [(nodes
[active_node
] - nodes
[i
]).length
for i
in open]
1717 active_node
= open[distances
.index(min(distances
))]
1718 open.remove(active_node
)
1719 path
.append([active_node
, min(distances
)])
1720 # check if we didn't start in the middle of the path
1721 for i
in range(2, len(path
)):
1722 if (nodes
[path
[i
][0]] - nodes
[0]).length
< path
[i
][1]:
1725 path
= path
[:-i
] + temp
1729 loops
= [loops
[i
[0]] for i
in path
]
1730 # if requested, duplicate first loop at last position, so loft can loop
1732 loops
= loops
+ [loops
[0]]
1737 # remapping old indices to new position in list
1738 def bridge_update_old_selection(bm
, old_selected_faces
):
1740 old_indices = old_selected_faces[:]
1741 old_selected_faces = []
1742 for i, face in enumerate(bm.faces):
1743 if face.index in old_indices:
1744 old_selected_faces.append(i)
1746 old_selected_faces
= [
1747 i
for i
, face
in enumerate(bm
.faces
) if face
.index
in old_selected_faces
1750 return(old_selected_faces
)
1753 # ########################################
1754 # ##### Circle functions #################
1755 # ########################################
1757 # convert 3d coordinates to 2d coordinates on plane
1758 def circle_3d_to_2d(bm_mod
, loop
, com
, normal
):
1759 # project vertices onto the plane
1760 verts
= [bm_mod
.verts
[v
] for v
in loop
[0]]
1761 verts_projected
= [[v
.co
- (v
.co
- com
).dot(normal
) * normal
, v
.index
]
1764 # calculate two vectors (p and q) along the plane
1765 m
= mathutils
.Vector((normal
[0] + 1.0, normal
[1], normal
[2]))
1766 p
= m
- (m
.dot(normal
) * normal
)
1768 m
= mathutils
.Vector((normal
[0], normal
[1] + 1.0, normal
[2]))
1769 p
= m
- (m
.dot(normal
) * normal
)
1772 # change to 2d coordinates using perpendicular projection
1774 for loc
, vert
in verts_projected
:
1776 x
= p
.dot(vloc
) / p
.dot(p
)
1777 y
= q
.dot(vloc
) / q
.dot(q
)
1778 locs_2d
.append([x
, y
, vert
])
1780 return(locs_2d
, p
, q
)
1783 # calculate a best-fit circle to the 2d locations on the plane
1784 def circle_calculate_best_fit(locs_2d
):
1790 # calculate center and radius (non-linear least squares solution)
1791 for iter in range(500):
1795 d
= (v
[0] ** 2 - 2.0 * x0
* v
[0] + v
[1] ** 2 - 2.0 * y0
* v
[1] + x0
** 2 + y0
** 2) ** 0.5
1796 jmat
.append([(x0
- v
[0]) / d
, (y0
- v
[1]) / d
, -1.0])
1797 k
.append(-(((v
[0] - x0
) ** 2 + (v
[1] - y0
) ** 2) ** 0.5 - r
))
1798 jmat2
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1802 k2
= mathutils
.Vector((0.0, 0.0, 0.0))
1803 for i
in range(len(jmat
)):
1804 k2
+= mathutils
.Vector(jmat
[i
]) * k
[i
]
1805 jmat2
[0][0] += jmat
[i
][0] ** 2
1806 jmat2
[1][0] += jmat
[i
][0] * jmat
[i
][1]
1807 jmat2
[2][0] += jmat
[i
][0] * jmat
[i
][2]
1808 jmat2
[1][1] += jmat
[i
][1] ** 2
1809 jmat2
[2][1] += jmat
[i
][1] * jmat
[i
][2]
1810 jmat2
[2][2] += jmat
[i
][2] ** 2
1811 jmat2
[0][1] = jmat2
[1][0]
1812 jmat2
[0][2] = jmat2
[2][0]
1813 jmat2
[1][2] = jmat2
[2][1]
1818 dx0
, dy0
, dr
= jmat2
@ k2
1822 # stop iterating if we're close enough to optimal solution
1823 if abs(dx0
) < 1e-6 and abs(dy0
) < 1e-6 and abs(dr
) < 1e-6:
1826 # return center of circle and radius
1830 # calculate circle so no vertices have to be moved away from the center
1831 def circle_calculate_min_fit(locs_2d
):
1833 x0
= (min([i
[0] for i
in locs_2d
]) + max([i
[0] for i
in locs_2d
])) / 2.0
1834 y0
= (min([i
[1] for i
in locs_2d
]) + max([i
[1] for i
in locs_2d
])) / 2.0
1835 center
= mathutils
.Vector([x0
, y0
])
1837 r
= min([(mathutils
.Vector([i
[0], i
[1]]) - center
).length
for i
in locs_2d
])
1839 # return center of circle and radius
1843 # calculate the new locations of the vertices that need to be moved
1844 def circle_calculate_verts(flatten
, bm_mod
, locs_2d
, com
, p
, q
, normal
):
1845 # changing 2d coordinates back to 3d coordinates
1848 locs_3d
.append([loc
[2], loc
[0] * p
+ loc
[1] * q
+ com
])
1850 if flatten
: # flat circle
1853 else: # project the locations on the existing mesh
1854 vert_edges
= dict_vert_edges(bm_mod
)
1855 vert_faces
= dict_vert_faces(bm_mod
)
1856 faces
= [f
for f
in bm_mod
.faces
if not f
.hide
]
1857 rays
= [normal
, -normal
]
1861 if bm_mod
.verts
[loc
[0]].co
== loc
[1]: # vertex hasn't moved
1864 dif
= normal
.angle(loc
[1] - bm_mod
.verts
[loc
[0]].co
)
1865 if -1e-6 < dif
< 1e-6 or math
.pi
- 1e-6 < dif
< math
.pi
+ 1e-6:
1866 # original location is already along projection normal
1867 projection
= bm_mod
.verts
[loc
[0]].co
1869 # quick search through adjacent faces
1870 for face
in vert_faces
[loc
[0]]:
1871 verts
= [v
.co
for v
in bm_mod
.faces
[face
].verts
]
1872 if len(verts
) == 3: # triangle
1876 v1
, v2
, v3
, v4
= verts
[:4]
1878 intersect
= mathutils
.geometry
.\
1879 intersect_ray_tri(v1
, v2
, v3
, ray
, loc
[1])
1881 projection
= intersect
1884 intersect
= mathutils
.geometry
.\
1885 intersect_ray_tri(v1
, v3
, v4
, ray
, loc
[1])
1887 projection
= intersect
1892 # check if projection is on adjacent edges
1893 for edgekey
in vert_edges
[loc
[0]]:
1894 line1
= bm_mod
.verts
[edgekey
[0]].co
1895 line2
= bm_mod
.verts
[edgekey
[1]].co
1896 intersect
, dist
= mathutils
.geometry
.intersect_point_line(
1897 loc
[1], line1
, line2
1899 if 1e-6 < dist
< 1 - 1e-6:
1900 projection
= intersect
1903 # full search through the entire mesh
1906 verts
= [v
.co
for v
in face
.verts
]
1907 if len(verts
) == 3: # triangle
1911 v1
, v2
, v3
, v4
= verts
[:4]
1913 intersect
= mathutils
.geometry
.intersect_ray_tri(
1914 v1
, v2
, v3
, ray
, loc
[1]
1917 hits
.append([(loc
[1] - intersect
).length
,
1921 intersect
= mathutils
.geometry
.intersect_ray_tri(
1922 v1
, v3
, v4
, ray
, loc
[1]
1925 hits
.append([(loc
[1] - intersect
).length
,
1929 # if more than 1 hit with mesh, closest hit is new loc
1931 projection
= hits
[0][1]
1933 # nothing to project on, remain at flat location
1935 new_locs
.append([loc
[0], projection
])
1937 # return new positions of projected circle
1941 # check loops and only return valid ones
1942 def circle_check_loops(single_loops
, loops
, mapping
, bm_mod
):
1943 valid_single_loops
= {}
1945 for i
, [loop
, circular
] in enumerate(loops
):
1946 # loop needs to have at least 3 vertices
1949 # loop needs at least 1 vertex in the original, non-mirrored mesh
1953 if mapping
[vert
] > -1:
1958 # loop has to be non-collinear
1960 loc0
= mathutils
.Vector(bm_mod
.verts
[loop
[0]].co
[:])
1961 loc1
= mathutils
.Vector(bm_mod
.verts
[loop
[1]].co
[:])
1963 locn
= mathutils
.Vector(bm_mod
.verts
[v
].co
[:])
1964 if loc0
== loc1
or loc1
== locn
:
1970 if -1e-6 < d1
.angle(d2
, 0) < 1e-6:
1978 # passed all tests, loop is valid
1979 valid_loops
.append([loop
, circular
])
1980 valid_single_loops
[len(valid_loops
) - 1] = single_loops
[i
]
1982 return(valid_single_loops
, valid_loops
)
1985 # calculate the location of single input vertices that need to be flattened
1986 def circle_flatten_singles(bm_mod
, com
, p
, q
, normal
, single_loop
):
1988 for vert
in single_loop
:
1989 loc
= mathutils
.Vector(bm_mod
.verts
[vert
].co
[:])
1990 new_locs
.append([vert
, loc
- (loc
- com
).dot(normal
) * normal
])
1995 # calculate input loops
1996 def circle_get_input(object, bm
):
1997 # get mesh with modifiers applied
1998 derived
, bm_mod
= get_derived_bmesh(object, bm
)
2000 # create list of edge-keys based on selection state
2002 for face
in bm
.faces
:
2003 if face
.select
and not face
.hide
:
2007 # get selected, non-hidden , non-internal edge-keys
2009 key
for keys
in [face_edgekeys(face
) for face
in
2010 bm_mod
.faces
if face
.select
and not face
.hide
] for key
in keys
2013 for ek
in eks_selected
:
2014 if ek
in edge_count
:
2019 edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and
2020 not edge
.hide
and edge_count
.get(edgekey(edge
), 1) == 1
2023 # no faces, so no internal edges either
2025 edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and not edge
.hide
2028 # add edge-keys around single vertices
2029 verts_connected
= dict(
2030 [[vert
, 1] for edge
in [edge
for edge
in
2031 bm_mod
.edges
if edge
.select
and not edge
.hide
] for vert
in
2035 vert
.index
for vert
in bm_mod
.verts
if
2036 vert
.select
and not vert
.hide
and
2037 not verts_connected
.get(vert
.index
, False)
2040 if single_vertices
and len(bm
.faces
) > 0:
2041 vert_to_single
= dict(
2042 [[v
.index
, []] for v
in bm_mod
.verts
if not v
.hide
]
2044 for face
in [face
for face
in bm_mod
.faces
if not face
.select
and not face
.hide
]:
2045 for vert
in face
.verts
:
2047 if vert
in single_vertices
:
2048 for ek
in face_edgekeys(face
):
2050 edge_keys
.append(ek
)
2051 if vert
not in vert_to_single
[ek
[0]]:
2052 vert_to_single
[ek
[0]].append(vert
)
2053 if vert
not in vert_to_single
[ek
[1]]:
2054 vert_to_single
[ek
[1]].append(vert
)
2057 # sort edge-keys into loops
2058 loops
= get_connected_selections(edge_keys
)
2060 # find out to which loops the single vertices belong
2061 single_loops
= dict([[i
, []] for i
in range(len(loops
))])
2062 if single_vertices
and len(bm
.faces
) > 0:
2063 for i
, [loop
, circular
] in enumerate(loops
):
2065 if vert_to_single
[vert
]:
2066 for single
in vert_to_single
[vert
]:
2067 if single
not in single_loops
[i
]:
2068 single_loops
[i
].append(single
)
2070 return(derived
, bm_mod
, single_vertices
, single_loops
, loops
)
2073 # recalculate positions based on the influence of the circle shape
2074 def circle_influence_locs(locs_2d
, new_locs_2d
, influence
):
2075 for i
in range(len(locs_2d
)):
2076 oldx
, oldy
, j
= locs_2d
[i
]
2077 newx
, newy
, k
= new_locs_2d
[i
]
2078 altx
= newx
* (influence
/ 100) + oldx
* ((100 - influence
) / 100)
2079 alty
= newy
* (influence
/ 100) + oldy
* ((100 - influence
) / 100)
2080 locs_2d
[i
] = [altx
, alty
, j
]
2085 # project 2d locations on circle, respecting distance relations between verts
2086 def circle_project_non_regular(locs_2d
, x0
, y0
, r
):
2087 for i
in range(len(locs_2d
)):
2088 x
, y
, j
= locs_2d
[i
]
2089 loc
= mathutils
.Vector([x
- x0
, y
- y0
])
2091 locs_2d
[i
] = [loc
[0], loc
[1], j
]
2096 # project 2d locations on circle, with equal distance between all vertices
2097 def circle_project_regular(locs_2d
, x0
, y0
, r
):
2098 # find offset angle and circling direction
2099 x
, y
, i
= locs_2d
[0]
2100 loc
= mathutils
.Vector([x
- x0
, y
- y0
])
2102 offset_angle
= loc
.angle(mathutils
.Vector([1.0, 0.0]), 0.0)
2103 loca
= mathutils
.Vector([x
- x0
, y
- y0
, 0.0])
2106 x
, y
, j
= locs_2d
[1]
2107 locb
= mathutils
.Vector([x
- x0
, y
- y0
, 0.0])
2108 if loca
.cross(locb
)[2] >= 0:
2112 # distribute vertices along the circle
2113 for i
in range(len(locs_2d
)):
2114 t
= offset_angle
+ ccw
* (i
/ len(locs_2d
) * 2 * math
.pi
)
2117 locs_2d
[i
] = [x
, y
, locs_2d
[i
][2]]
2122 # shift loop, so the first vertex is closest to the center
2123 def circle_shift_loop(bm_mod
, loop
, com
):
2124 verts
, circular
= loop
2126 [(bm_mod
.verts
[vert
].co
- com
).length
, i
] for i
, vert
in enumerate(verts
)
2129 shift
= distances
[0][1]
2130 loop
= [verts
[shift
:] + verts
[:shift
], circular
]
2135 # ########################################
2136 # ##### Curve functions ##################
2137 # ########################################
2139 # create lists with knots and points, all correctly sorted
2140 def curve_calculate_knots(loop
, verts_selected
):
2141 knots
= [v
for v
in loop
[0] if v
in verts_selected
]
2143 # circular loop, potential for weird splines
2145 offset
= int(len(loop
[0]) / 4)
2148 kpos
.append(loop
[0].index(k
))
2150 for i
in range(len(kpos
) - 1):
2151 kdif
.append(kpos
[i
+ 1] - kpos
[i
])
2152 kdif
.append(len(loop
[0]) - kpos
[-1] + kpos
[0])
2156 kadd
.append([kdif
.index(k
), True])
2157 # next 2 lines are optional, they insert
2158 # an extra control point in small gaps
2160 # kadd.append([kdif.index(k), False])
2163 for k
in kadd
: # extra knots to be added
2164 if k
[1]: # big gap (break circular spline)
2165 kpos
= loop
[0].index(knots
[k
[0]]) + offset
2166 if kpos
> len(loop
[0]) - 1:
2167 kpos
-= len(loop
[0])
2168 kins
.append([knots
[k
[0]], loop
[0][kpos
]])
2170 if kpos2
> len(knots
) - 1:
2172 kpos2
= loop
[0].index(knots
[kpos2
]) - offset
2174 kpos2
+= len(loop
[0])
2175 kins
.append([loop
[0][kpos
], loop
[0][kpos2
]])
2176 krot
= loop
[0][kpos2
]
2177 else: # small gap (keep circular spline)
2178 k1
= loop
[0].index(knots
[k
[0]])
2180 if k2
> len(knots
) - 1:
2182 k2
= loop
[0].index(knots
[k2
])
2184 dif
= len(loop
[0]) - 1 - k1
+ k2
2187 kn
= k1
+ int(dif
/ 2)
2188 if kn
> len(loop
[0]) - 1:
2190 kins
.append([loop
[0][k1
], loop
[0][kn
]])
2191 for j
in kins
: # insert new knots
2192 knots
.insert(knots
.index(j
[0]) + 1, j
[1])
2193 if not krot
: # circular loop
2194 knots
.append(knots
[0])
2195 points
= loop
[0][loop
[0].index(knots
[0]):]
2196 points
+= loop
[0][0:loop
[0].index(knots
[0]) + 1]
2197 else: # non-circular loop (broken by script)
2198 krot
= knots
.index(krot
)
2199 knots
= knots
[krot
:] + knots
[0:krot
]
2200 if loop
[0].index(knots
[0]) > loop
[0].index(knots
[-1]):
2201 points
= loop
[0][loop
[0].index(knots
[0]):]
2202 points
+= loop
[0][0:loop
[0].index(knots
[-1]) + 1]
2204 points
= loop
[0][loop
[0].index(knots
[0]):loop
[0].index(knots
[-1]) + 1]
2205 # non-circular loop, add first and last point as knots
2207 if loop
[0][0] not in knots
:
2208 knots
.insert(0, loop
[0][0])
2209 if loop
[0][-1] not in knots
:
2210 knots
.append(loop
[0][-1])
2212 return(knots
, points
)
2215 # calculate relative positions compared to first knot
2216 def curve_calculate_t(bm_mod
, knots
, points
, pknots
, regular
, circular
):
2223 loc
= pknots
[knots
.index(p
)] # use projected knot location
2225 loc
= mathutils
.Vector(bm_mod
.verts
[p
].co
[:])
2228 len_total
+= (loc
- loc_prev
).length
2229 tpoints
.append(len_total
)
2234 tknots
.append(tpoints
[points
.index(p
)])
2236 tknots
[-1] = tpoints
[-1]
2240 tpoints_average
= tpoints
[-1] / (len(tpoints
) - 1)
2241 for i
in range(1, len(tpoints
) - 1):
2242 tpoints
[i
] = i
* tpoints_average
2243 for i
in range(len(knots
)):
2244 tknots
[i
] = tpoints
[points
.index(knots
[i
])]
2246 tknots
[-1] = tpoints
[-1]
2248 return(tknots
, tpoints
)
2251 # change the location of non-selected points to their place on the spline
2252 def curve_calculate_vertices(bm_mod
, knots
, tknots
, points
, tpoints
, splines
,
2253 interpolation
, restriction
):
2260 m
= tpoints
[points
.index(p
)]
2268 if n
> len(splines
) - 1:
2269 n
= len(splines
) - 1
2273 if interpolation
== 'cubic':
2274 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
2275 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
2276 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
2277 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
2278 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
2279 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
2280 newloc
= mathutils
.Vector([x
, y
, z
])
2281 else: # interpolation == 'linear'
2282 a
, d
, t
, u
= splines
[n
]
2283 newloc
= ((m
- t
) / u
) * d
+ a
2285 if restriction
!= 'none': # vertex movement is restricted
2287 else: # set the vertex to its new location
2288 move
.append([p
, newloc
])
2290 if restriction
!= 'none': # vertex movement is restricted
2295 move
.append([p
, bm_mod
.verts
[p
].co
])
2297 oldloc
= bm_mod
.verts
[p
].co
2298 normal
= bm_mod
.verts
[p
].normal
2299 dloc
= newloc
- oldloc
2300 if dloc
.length
< 1e-6:
2301 move
.append([p
, newloc
])
2302 elif restriction
== 'extrude': # only extrusions
2303 if dloc
.angle(normal
, 0) < 0.5 * math
.pi
+ 1e-6:
2304 move
.append([p
, newloc
])
2305 else: # restriction == 'indent' only indentations
2306 if dloc
.angle(normal
) > 0.5 * math
.pi
- 1e-6:
2307 move
.append([p
, newloc
])
2312 # trim loops to part between first and last selected vertices (including)
2313 def curve_cut_boundaries(bm_mod
, loops
):
2315 for loop
, circular
in loops
:
2318 cut_loops
.append([loop
, circular
])
2320 selected
= [bm_mod
.verts
[v
].select
for v
in loop
]
2321 first
= selected
.index(True)
2323 last
= -selected
.index(True)
2325 cut_loops
.append([loop
[first
:], circular
])
2327 cut_loops
.append([loop
[first
:last
], circular
])
2332 # calculate input loops
2333 def curve_get_input(object, bm
, boundaries
):
2334 # get mesh with modifiers applied
2335 derived
, bm_mod
= get_derived_bmesh(object, bm
)
2337 # vertices that still need a loop to run through it
2339 v
.index
for v
in bm_mod
.verts
if v
.select
and not v
.hide
2341 # necessary dictionaries
2342 vert_edges
= dict_vert_edges(bm_mod
)
2343 edge_faces
= dict_edge_faces(bm_mod
)
2345 # find loops through each selected vertex
2346 while len(verts_unsorted
) > 0:
2347 loops
= curve_vertex_loops(bm_mod
, verts_unsorted
[0], vert_edges
,
2349 verts_unsorted
.pop(0)
2351 # check if loop is fully selected
2352 search_perpendicular
= False
2354 for loop
, circular
in loops
:
2356 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2357 if len(selected
) < 2:
2358 # only one selected vertex on loop, don't use
2361 elif len(selected
) == len(loop
):
2362 search_perpendicular
= loop
2364 # entire loop is selected, find perpendicular loops
2365 if search_perpendicular
:
2367 if vert
in verts_unsorted
:
2368 verts_unsorted
.remove(vert
)
2369 perp_loops
= curve_perpendicular_loops(bm_mod
, loop
,
2370 vert_edges
, edge_faces
)
2371 for perp_loop
in perp_loops
:
2372 correct_loops
.append(perp_loop
)
2375 for loop
, circular
in loops
:
2376 correct_loops
.append([loop
, circular
])
2380 correct_loops
= curve_cut_boundaries(bm_mod
, correct_loops
)
2382 return(derived
, bm_mod
, correct_loops
)
2385 # return all loops that are perpendicular to the given one
2386 def curve_perpendicular_loops(bm_mod
, start_loop
, vert_edges
, edge_faces
):
2387 # find perpendicular loops
2389 for start_vert
in start_loop
:
2390 loops
= curve_vertex_loops(bm_mod
, start_vert
, vert_edges
,
2392 for loop
, circular
in loops
:
2393 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2394 if len(selected
) == len(loop
):
2397 perp_loops
.append([loop
, circular
, loop
.index(start_vert
)])
2399 # trim loops to same lengths
2401 [len(loop
[0]), i
] for i
, loop
in enumerate(perp_loops
) if not loop
[1]
2404 # all loops are circular, not trimming
2405 return([[loop
[0], loop
[1]] for loop
in perp_loops
])
2407 shortest
= min(shortest
)
2408 shortest_start
= perp_loops
[shortest
[1]][2]
2409 before_start
= shortest_start
2410 after_start
= shortest
[0] - shortest_start
- 1
2411 bigger_before
= before_start
> after_start
2413 for loop
in perp_loops
:
2414 # have the loop face the same direction as the shortest one
2416 if loop
[2] < len(loop
[0]) / 2:
2418 loop
[2] = len(loop
[0]) - loop
[2] - 1
2420 if loop
[2] > len(loop
[0]) / 2:
2422 loop
[2] = len(loop
[0]) - loop
[2] - 1
2423 # circular loops can shift, to prevent wrong trimming
2425 shift
= shortest_start
- loop
[2]
2426 if loop
[2] + shift
> 0 and loop
[2] + shift
< len(loop
[0]):
2427 loop
[0] = loop
[0][-shift
:] + loop
[0][:-shift
]
2430 loop
[2] += len(loop
[0])
2431 elif loop
[2] > len(loop
[0]) - 1:
2432 loop
[2] -= len(loop
[0])
2434 start
= max(0, loop
[2] - before_start
)
2435 end
= min(len(loop
[0]), loop
[2] + after_start
+ 1)
2436 trimmed_loops
.append([loop
[0][start
:end
], False])
2438 return(trimmed_loops
)
2441 # project knots on non-selected geometry
2442 def curve_project_knots(bm_mod
, verts_selected
, knots
, points
, circular
):
2443 # function to project vertex on edge
2444 def project(v1
, v2
, v3
):
2445 # v1 and v2 are part of a line
2446 # v3 is projected onto it
2452 if circular
: # project all knots
2456 else: # first and last knot shouldn't be projected
2459 pknots
= [mathutils
.Vector(bm_mod
.verts
[knots
[0]].co
[:])]
2460 for knot
in knots
[start
:end
]:
2461 if knot
in verts_selected
:
2462 knot_left
= knot_right
= False
2463 for i
in range(points
.index(knot
) - 1, -1 * len(points
), -1):
2464 if points
[i
] not in knots
:
2465 knot_left
= points
[i
]
2467 for i
in range(points
.index(knot
) + 1, 2 * len(points
)):
2468 if i
> len(points
) - 1:
2470 if points
[i
] not in knots
:
2471 knot_right
= points
[i
]
2473 if knot_left
and knot_right
and knot_left
!= knot_right
:
2474 knot_left
= mathutils
.Vector(bm_mod
.verts
[knot_left
].co
[:])
2475 knot_right
= mathutils
.Vector(bm_mod
.verts
[knot_right
].co
[:])
2476 knot
= mathutils
.Vector(bm_mod
.verts
[knot
].co
[:])
2477 pknots
.append(project(knot_left
, knot_right
, knot
))
2479 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2480 else: # knot isn't selected, so shouldn't be changed
2481 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2483 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knots
[-1]].co
[:]))
2488 # find all loops through a given vertex
2489 def curve_vertex_loops(bm_mod
, start_vert
, vert_edges
, edge_faces
):
2493 for edge
in vert_edges
[start_vert
]:
2494 if edge
in edges_used
:
2499 active_faces
= edge_faces
[edge
]
2504 new_edges
= vert_edges
[new_vert
]
2505 loop
.append(new_vert
)
2507 edges_used
.append(tuple(sorted([loop
[-1], loop
[-2]])))
2508 if len(new_edges
) < 3 or len(new_edges
) > 4:
2513 for new_edge
in new_edges
:
2514 if new_edge
in edges_used
:
2517 for new_face
in edge_faces
[new_edge
]:
2518 if new_face
in active_faces
:
2523 # found correct new edge
2524 active_faces
= edge_faces
[new_edge
]
2530 if new_vert
== loop
[0]:
2538 loops
.append([loop
, circular
])
2543 # ########################################
2544 # ##### Flatten functions ################
2545 # ########################################
2547 # sort input into loops
2548 def flatten_get_input(bm
):
2549 vert_verts
= dict_vert_verts(
2550 [edgekey(edge
) for edge
in bm
.edges
if edge
.select
and not edge
.hide
]
2552 verts
= [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
2554 # no connected verts, consider all selected verts as a single input
2556 return([[verts
, False]])
2559 while len(verts
) > 0:
2563 if loop
[-1] in vert_verts
:
2564 to_grow
= vert_verts
[loop
[-1]]
2568 while len(to_grow
) > 0:
2569 new_vert
= to_grow
[0]
2571 if new_vert
in loop
:
2573 loop
.append(new_vert
)
2574 verts
.remove(new_vert
)
2575 to_grow
+= vert_verts
[new_vert
]
2577 loops
.append([loop
, False])
2582 # calculate position of vertex projections on plane
2583 def flatten_project(bm
, loop
, com
, normal
):
2584 verts
= [bm
.verts
[v
] for v
in loop
[0]]
2586 [v
.index
, mathutils
.Vector(v
.co
[:]) -
2587 (mathutils
.Vector(v
.co
[:]) - com
).dot(normal
) * normal
] for v
in verts
2590 return(verts_projected
)
2593 # ########################################
2594 # ##### Gstretch functions ###############
2595 # ########################################
2597 # fake stroke class, used to create custom strokes if no GP data is found
2598 class gstretch_fake_stroke():
2599 def __init__(self
, points
):
2600 self
.points
= [gstretch_fake_stroke_point(p
) for p
in points
]
2603 # fake stroke point class, used in fake strokes
2604 class gstretch_fake_stroke_point():
2605 def __init__(self
, loc
):
2609 # flips loops, if necessary, to obtain maximum alignment to stroke
2610 def gstretch_align_pairs(ls_pairs
, object, bm_mod
, method
):
2611 # returns total distance between all verts in loop and corresponding stroke
2612 def distance_loop_stroke(loop
, stroke
, object, bm_mod
, method
):
2613 stroke_lengths_cache
= False
2614 loop_length
= len(loop
[0])
2617 if method
!= 'regular':
2618 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2620 for i
, v_index
in enumerate(loop
[0]):
2621 if method
== 'regular':
2622 relative_distance
= i
/ (loop_length
- 1)
2624 relative_distance
= relative_lengths
[i
]
2626 loc1
= object.matrix_world
@ bm_mod
.verts
[v_index
].co
2627 loc2
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2628 relative_distance
, stroke_lengths_cache
)
2629 total_distance
+= (loc2
- loc1
).length
2631 return(total_distance
)
2634 for (loop
, stroke
) in ls_pairs
:
2635 total_dist
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2638 total_dist_rev
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2640 if total_dist_rev
> total_dist
:
2646 # calculate vertex positions on stroke
2647 def gstretch_calculate_verts(loop
, stroke
, object, bm_mod
, method
):
2649 stroke_lengths_cache
= False
2650 loop_length
= len(loop
[0])
2651 matrix_inverse
= object.matrix_world
.inverted()
2653 # return intersection of line with stroke, or None
2654 def intersect_line_stroke(vec1
, vec2
, stroke
):
2655 for i
, p
in enumerate(stroke
.points
[1:]):
2656 intersections
= mathutils
.geometry
.intersect_line_line(vec1
, vec2
,
2657 p
.co
, stroke
.points
[i
].co
)
2658 if intersections
and \
2659 (intersections
[0] - intersections
[1]).length
< 1e-2:
2660 x
, dist
= mathutils
.geometry
.intersect_point_line(
2661 intersections
[0], p
.co
, stroke
.points
[i
].co
)
2663 return(intersections
[0])
2666 if method
== 'project':
2667 vert_edges
= dict_vert_edges(bm_mod
)
2669 for v_index
in loop
[0]:
2671 for ek
in vert_edges
[v_index
]:
2673 v1
= bm_mod
.verts
[v1
]
2674 v2
= bm_mod
.verts
[v2
]
2675 if v1
.select
+ v2
.select
== 1 and not v1
.hide
and not v2
.hide
:
2676 vec1
= object.matrix_world
@ v1
.co
2677 vec2
= object.matrix_world
@ v2
.co
2678 intersection
= intersect_line_stroke(vec1
, vec2
, stroke
)
2681 if not intersection
:
2682 v
= bm_mod
.verts
[v_index
]
2683 intersection
= intersect_line_stroke(v
.co
, v
.co
+ v
.normal
,
2686 move
.append([v_index
, matrix_inverse
@ intersection
])
2689 if method
== 'irregular':
2690 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2692 for i
, v_index
in enumerate(loop
[0]):
2693 if method
== 'regular':
2694 relative_distance
= i
/ (loop_length
- 1)
2695 else: # method == 'irregular'
2696 relative_distance
= relative_lengths
[i
]
2697 loc
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2698 relative_distance
, stroke_lengths_cache
)
2699 loc
= matrix_inverse
@ loc
2700 move
.append([v_index
, loc
])
2705 # create new vertices, based on GP strokes
2706 def gstretch_create_verts(object, bm_mod
, strokes
, method
, conversion
,
2707 conversion_distance
, conversion_max
, conversion_min
, conversion_vertices
):
2710 mat_world
= object.matrix_world
.inverted()
2711 singles
= gstretch_match_single_verts(bm_mod
, strokes
, mat_world
)
2713 for stroke
in strokes
:
2714 stroke_verts
.append([stroke
, []])
2716 if conversion
== 'vertices':
2717 min_end_point
= conversion_vertices
2718 end_point
= conversion_vertices
2719 elif conversion
== 'limit_vertices':
2720 min_end_point
= conversion_min
2721 end_point
= conversion_max
2723 end_point
= len(stroke
.points
)
2724 # creation of new vertices at fixed user-defined distances
2725 if conversion
== 'distance':
2727 prev_point
= stroke
.points
[0]
2728 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
@ prev_point
.co
))
2730 limit
= conversion_distance
2731 for point
in stroke
.points
:
2732 new_distance
= distance
+ (point
.co
- prev_point
.co
).length
2734 while new_distance
> limit
:
2735 to_cover
= limit
- distance
+ (limit
* iteration
)
2736 new_loc
= prev_point
.co
+ to_cover
* \
2737 (point
.co
- prev_point
.co
).normalized()
2738 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* new_loc
))
2739 new_distance
-= limit
2741 distance
= new_distance
2743 # creation of new vertices for other methods
2745 # add vertices at stroke points
2746 for point
in stroke
.points
[:end_point
]:
2747 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
@ point
.co
))
2748 # add more vertices, beyond the points that are available
2749 if min_end_point
> min(len(stroke
.points
), end_point
):
2750 for i
in range(min_end_point
-
2751 (min(len(stroke
.points
), end_point
))):
2752 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
@ point
.co
))
2753 # force even spreading of points, so they are placed on stroke
2755 bm_mod
.verts
.ensure_lookup_table()
2756 bm_mod
.verts
.index_update()
2757 for stroke
, verts_seq
in stroke_verts
:
2758 if len(verts_seq
) < 2:
2760 # spread vertices evenly over the stroke
2761 if method
== 'regular':
2762 loop
= [[vert
.index
for vert
in verts_seq
], False]
2763 move
+= gstretch_calculate_verts(loop
, stroke
, object, bm_mod
,
2766 for i
, vert
in enumerate(verts_seq
):
2768 bm_mod
.edges
.new((verts_seq
[i
- 1], verts_seq
[i
]))
2770 # connect single vertices to the closest stroke
2772 for vert
, m_stroke
, point
in singles
:
2773 if m_stroke
!= stroke
:
2775 bm_mod
.edges
.new((vert
, verts_seq
[point
]))
2776 bm_mod
.edges
.ensure_lookup_table()
2777 bmesh
.update_edit_mesh(object.data
)
2782 # erases the grease pencil stroke
2783 def gstretch_erase_stroke(stroke
, context
):
2784 # change 3d coordinate into a stroke-point
2785 def sp(loc
, context
):
2789 'location': (0, 0, 0),
2791 view3d_utils
.location_3d_to_region_2d(
2792 context
.region
, context
.space_data
.region_3d
, loc
)
2799 if type(stroke
) != bpy
.types
.GPencilStroke
:
2800 # fake stroke, there is nothing to delete
2803 erase_stroke
= [sp(p
.co
, context
) for p
in stroke
.points
]
2805 erase_stroke
[0]['is_start'] = True
2806 #bpy.ops.gpencil.draw(mode='ERASER', stroke=erase_stroke)
2807 bpy
.ops
.gpencil
.data_unlink()
2811 # get point on stroke, given by relative distance (0.0 - 1.0)
2812 def gstretch_eval_stroke(stroke
, distance
, stroke_lengths_cache
=False):
2813 # use cache if available
2814 if not stroke_lengths_cache
:
2816 for i
, p
in enumerate(stroke
.points
[1:]):
2817 lengths
.append((p
.co
- stroke
.points
[i
].co
).length
+ lengths
[-1])
2818 total_length
= max(lengths
[-1], 1e-7)
2819 stroke_lengths_cache
= [length
/ total_length
for length
in
2821 stroke_lengths
= stroke_lengths_cache
[:]
2823 if distance
in stroke_lengths
:
2824 loc
= stroke
.points
[stroke_lengths
.index(distance
)].co
2825 elif distance
> stroke_lengths
[-1]:
2826 # should be impossible, but better safe than sorry
2827 loc
= stroke
.points
[-1].co
2829 stroke_lengths
.append(distance
)
2830 stroke_lengths
.sort()
2831 stroke_index
= stroke_lengths
.index(distance
)
2832 interval_length
= stroke_lengths
[
2833 stroke_index
+ 1] - stroke_lengths
[stroke_index
- 1
2835 distance_relative
= (distance
- stroke_lengths
[stroke_index
- 1]) / interval_length
2836 interval_vector
= stroke
.points
[stroke_index
].co
- stroke
.points
[stroke_index
- 1].co
2837 loc
= stroke
.points
[stroke_index
- 1].co
+ distance_relative
* interval_vector
2839 return(loc
, stroke_lengths_cache
)
2842 # create fake grease pencil strokes for the active object
2843 def gstretch_get_fake_strokes(object, bm_mod
, loops
):
2846 p1
= object.matrix_world
@ bm_mod
.verts
[loop
[0][0]].co
2847 p2
= object.matrix_world
@ bm_mod
.verts
[loop
[0][-1]].co
2848 strokes
.append(gstretch_fake_stroke([p1
, p2
]))
2853 def gstretch_get_strokes(self
, context
):
2854 looptools
= context
.window_manager
.looptools
2855 gp
= get_strokes(self
, context
)
2858 if looptools
.gstretch_use_guide
== "Annotation":
2859 layer
= bpy
.data
.grease_pencils
[0].layers
.active
2860 if looptools
.gstretch_use_guide
== "GPencil" and not looptools
.gstretch_guide
== None:
2861 layer
= looptools
.gstretch_guide
.data
.layers
.active
2864 frame
= layer
.active_frame
2867 strokes
= frame
.strokes
2868 if len(strokes
) < 1:
2873 # returns a list with loop-stroke pairs
2874 def gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
):
2875 if not loops
or not strokes
:
2878 # calculate loop centers
2880 bm_mod
.verts
.ensure_lookup_table()
2882 center
= mathutils
.Vector()
2883 for v_index
in loop
[0]:
2884 center
+= bm_mod
.verts
[v_index
].co
2885 center
/= len(loop
[0])
2886 center
= object.matrix_world
@ center
2887 loop_centers
.append([center
, loop
])
2889 # calculate stroke centers
2891 for stroke
in strokes
:
2892 center
= mathutils
.Vector()
2893 for p
in stroke
.points
:
2895 center
/= len(stroke
.points
)
2896 stroke_centers
.append([center
, stroke
, 0])
2898 # match, first by stroke use count, then by distance
2900 for lc
in loop_centers
:
2902 for i
, sc
in enumerate(stroke_centers
):
2903 distances
.append([sc
[2], (lc
[0] - sc
[0]).length
, i
])
2905 best_stroke
= distances
[0][2]
2906 ls_pairs
.append([lc
[1], stroke_centers
[best_stroke
][1]])
2907 stroke_centers
[best_stroke
][2] += 1 # increase stroke use count
2912 # match single selected vertices to the closest stroke endpoint
2913 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2914 def gstretch_match_single_verts(bm_mod
, strokes
, mat_world
):
2915 # calculate stroke endpoints in object space
2917 for stroke
in strokes
:
2918 endpoints
.append((mat_world
@ stroke
.points
[0].co
, stroke
, 0))
2919 endpoints
.append((mat_world
@ stroke
.points
[-1].co
, stroke
, -1))
2922 # find single vertices (not connected to other selected verts)
2923 for vert
in bm_mod
.verts
:
2927 for edge
in vert
.link_edges
:
2928 if edge
.other_vert(vert
).select
:
2933 # calculate distances from vertex to endpoints
2934 distance
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2935 endpoint_index
) for endpoint_index
, (loc
, stroke
, stroke_point
) in
2936 enumerate(endpoints
)]
2938 distances
.append(distance
[0])
2940 # create matches, based on shortest distance first
2944 singles
.append((distances
[0][1], distances
[0][2], distances
[0][3]))
2945 endpoints
.pop(distances
[0][4])
2948 for (i
, vert
, j
, k
, l
) in distances
:
2949 distance_new
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2950 endpoint_index
) for endpoint_index
, (loc
, stroke
,
2951 stroke_point
) in enumerate(endpoints
)]
2953 distances_new
.append(distance_new
[0])
2954 distances
= distances_new
2959 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2960 def gstretch_relative_lengths(loop
, bm_mod
):
2962 for i
, v_index
in enumerate(loop
[0][1:]):
2964 (bm_mod
.verts
[v_index
].co
-
2965 bm_mod
.verts
[loop
[0][i
]].co
).length
+ lengths
[-1]
2967 total_length
= max(lengths
[-1], 1e-7)
2968 relative_lengths
= [length
/ total_length
for length
in
2971 return(relative_lengths
)
2974 # convert cache-stored strokes into usable (fake) GP strokes
2975 def gstretch_safe_to_true_strokes(safe_strokes
):
2977 for safe_stroke
in safe_strokes
:
2978 strokes
.append(gstretch_fake_stroke(safe_stroke
))
2983 # convert a GP stroke into a list of points which can be stored in cache
2984 def gstretch_true_to_safe_strokes(strokes
):
2986 for stroke
in strokes
:
2987 safe_strokes
.append([p
.co
.copy() for p
in stroke
.points
])
2989 return(safe_strokes
)
2992 # force consistency in GUI, max value can never be lower than min value
2993 def gstretch_update_max(self
, context
):
2994 # called from operator settings (after execution)
2995 if 'conversion_min' in self
.keys():
2996 if self
.conversion_min
> self
.conversion_max
:
2997 self
.conversion_max
= self
.conversion_min
2998 # called from toolbar
3000 lt
= context
.window_manager
.looptools
3001 if lt
.gstretch_conversion_min
> lt
.gstretch_conversion_max
:
3002 lt
.gstretch_conversion_max
= lt
.gstretch_conversion_min
3005 # force consistency in GUI, min value can never be higher than max value
3006 def gstretch_update_min(self
, context
):
3007 # called from operator settings (after execution)
3008 if 'conversion_max' in self
.keys():
3009 if self
.conversion_max
< self
.conversion_min
:
3010 self
.conversion_min
= self
.conversion_max
3011 # called from toolbar
3013 lt
= context
.window_manager
.looptools
3014 if lt
.gstretch_conversion_max
< lt
.gstretch_conversion_min
:
3015 lt
.gstretch_conversion_min
= lt
.gstretch_conversion_max
3018 # ########################################
3019 # ##### Relax functions ##################
3020 # ########################################
3022 # create lists with knots and points, all correctly sorted
3023 def relax_calculate_knots(loops
):
3026 for loop
, circular
in loops
:
3030 if len(loop
) % 2 == 1: # odd
3031 extend
= [False, True, 0, 1, 0, 1]
3033 extend
= [True, False, 0, 1, 1, 2]
3035 if len(loop
) % 2 == 1: # odd
3036 extend
= [False, False, 0, 1, 1, 2]
3038 extend
= [False, False, 0, 1, 1, 2]
3041 loop
= [loop
[-1]] + loop
+ [loop
[0]]
3042 for i
in range(extend
[2 + 2 * j
], len(loop
), 2):
3043 knots
[j
].append(loop
[i
])
3044 for i
in range(extend
[3 + 2 * j
], len(loop
), 2):
3045 if loop
[i
] == loop
[-1] and not circular
:
3047 if len(points
[j
]) == 0:
3048 points
[j
].append(loop
[i
])
3049 elif loop
[i
] != points
[j
][0]:
3050 points
[j
].append(loop
[i
])
3052 if knots
[j
][0] != knots
[j
][-1]:
3053 knots
[j
].append(knots
[j
][0])
3054 if len(points
[1]) == 0:
3060 all_points
.append(p
)
3062 return(all_knots
, all_points
)
3065 # calculate relative positions compared to first knot
3066 def relax_calculate_t(bm_mod
, knots
, points
, regular
):
3069 for i
in range(len(knots
)):
3070 amount
= len(knots
[i
]) + len(points
[i
])
3072 for j
in range(amount
):
3074 mix
.append([True, knots
[i
][round(j
/ 2)]])
3075 elif j
== amount
- 1:
3076 mix
.append([True, knots
[i
][-1]])
3078 mix
.append([False, points
[i
][int(j
/ 2)]])
3084 loc
= mathutils
.Vector(bm_mod
.verts
[m
[1]].co
[:])
3087 len_total
+= (loc
- loc_prev
).length
3089 tknots
.append(len_total
)
3091 tpoints
.append(len_total
)
3095 for p
in range(len(points
[i
])):
3096 tpoints
.append((tknots
[p
] + tknots
[p
+ 1]) / 2)
3097 all_tknots
.append(tknots
)
3098 all_tpoints
.append(tpoints
)
3100 return(all_tknots
, all_tpoints
)
3103 # change the location of the points to their place on the spline
3104 def relax_calculate_verts(bm_mod
, interpolation
, tknots
, knots
, tpoints
,
3108 for i
in range(len(knots
)):
3110 m
= tpoints
[i
][points
[i
].index(p
)]
3112 n
= tknots
[i
].index(m
)
3118 if n
> len(splines
[i
]) - 1:
3119 n
= len(splines
[i
]) - 1
3123 if interpolation
== 'cubic':
3124 ax
, bx
, cx
, dx
, tx
= splines
[i
][n
][0]
3125 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
3126 ay
, by
, cy
, dy
, ty
= splines
[i
][n
][1]
3127 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
3128 az
, bz
, cz
, dz
, tz
= splines
[i
][n
][2]
3129 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
3130 change
.append([p
, mathutils
.Vector([x
, y
, z
])])
3131 else: # interpolation == 'linear'
3132 a
, d
, t
, u
= splines
[i
][n
]
3135 change
.append([p
, ((m
- t
) / u
) * d
+ a
])
3137 move
.append([c
[0], (bm_mod
.verts
[c
[0]].co
+ c
[1]) / 2])
3142 # ########################################
3143 # ##### Space functions ##################
3144 # ########################################
3146 # calculate relative positions compared to first knot
3147 def space_calculate_t(bm_mod
, knots
):
3152 loc
= mathutils
.Vector(bm_mod
.verts
[k
].co
[:])
3155 len_total
+= (loc
- loc_prev
).length
3156 tknots
.append(len_total
)
3159 t_per_segment
= len_total
/ (amount
- 1)
3160 tpoints
= [i
* t_per_segment
for i
in range(amount
)]
3162 return(tknots
, tpoints
)
3165 # change the location of the points to their place on the spline
3166 def space_calculate_verts(bm_mod
, interpolation
, tknots
, tpoints
, points
,
3170 m
= tpoints
[points
.index(p
)]
3178 if n
> len(splines
) - 1:
3179 n
= len(splines
) - 1
3183 if interpolation
== 'cubic':
3184 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
3185 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
3186 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
3187 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
3188 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
3189 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
3190 move
.append([p
, mathutils
.Vector([x
, y
, z
])])
3191 else: # interpolation == 'linear'
3192 a
, d
, t
, u
= splines
[n
]
3193 move
.append([p
, ((m
- t
) / u
) * d
+ a
])
3198 # ########################################
3199 # ##### Operators ########################
3200 # ########################################
3203 class Bridge(Operator
):
3204 bl_idname
= 'mesh.looptools_bridge'
3205 bl_label
= "Bridge / Loft"
3206 bl_description
= "Bridge two, or loft several, loops of vertices"
3207 bl_options
= {'REGISTER', 'UNDO'}
3209 cubic_strength
: FloatProperty(
3211 description
="Higher strength results in more fluid curves",
3216 interpolation
: EnumProperty(
3217 name
="Interpolation mode",
3218 items
=(('cubic', "Cubic", "Gives curved results"),
3219 ('linear', "Linear", "Basic, fast, straight interpolation")),
3220 description
="Interpolation mode: algorithm used when creating "
3226 description
="Loft multiple loops, instead of considering them as "
3227 "a multi-input for bridging",
3230 loft_loop
: BoolProperty(
3232 description
="Connect the first and the last loop with each other",
3235 min_width
: IntProperty(
3236 name
="Minimum width",
3237 description
="Segments with an edge smaller than this are merged "
3238 "(compared to base edge)",
3242 subtype
='PERCENTAGE'
3246 items
=(('basic', "Basic", "Fast algorithm"),
3247 ('shortest', "Shortest edge", "Slower algorithm with better vertex matching")),
3248 description
="Algorithm used for bridging",
3251 remove_faces
: BoolProperty(
3252 name
="Remove faces",
3253 description
="Remove faces that are internal after bridging",
3256 reverse
: BoolProperty(
3258 description
="Manually override the direction in which the loops "
3259 "are bridged. Only use if the tool gives the wrong result",
3262 segments
: IntProperty(
3264 description
="Number of segments used to bridge the gap (0=automatic)",
3271 description
="Twist what vertices are connected to each other",
3276 def poll(cls
, context
):
3277 ob
= context
.active_object
3278 return (ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3280 def draw(self
, context
):
3281 layout
= self
.layout
3282 # layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3285 col_top
= layout
.column(align
=True)
3286 row
= col_top
.row(align
=True)
3287 col_left
= row
.column(align
=True)
3288 col_right
= row
.column(align
=True)
3289 col_right
.active
= self
.segments
!= 1
3290 col_left
.prop(self
, "segments")
3291 col_right
.prop(self
, "min_width", text
="")
3293 bottom_left
= col_left
.row()
3294 bottom_left
.active
= self
.segments
!= 1
3295 bottom_left
.prop(self
, "interpolation", text
="")
3296 bottom_right
= col_right
.row()
3297 bottom_right
.active
= self
.interpolation
== 'cubic'
3298 bottom_right
.prop(self
, "cubic_strength")
3299 # boolean properties
3300 col_top
.prop(self
, "remove_faces")
3302 col_top
.prop(self
, "loft_loop")
3304 # override properties
3306 row
= layout
.row(align
=True)
3307 row
.prop(self
, "twist")
3308 row
.prop(self
, "reverse")
3310 def invoke(self
, context
, event
):
3311 # load custom settings
3312 context
.window_manager
.looptools
.bridge_loft
= self
.loft
3314 return self
.execute(context
)
3316 def execute(self
, context
):
3318 object, bm
= initialise()
3319 edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
= \
3320 bridge_initialise(bm
, self
.interpolation
)
3321 settings_write(self
)
3323 # check cache to see if we can save time
3324 input_method
= bridge_input_method(self
.loft
, self
.loft_loop
)
3325 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Bridge",
3326 object, bm
, input_method
, False)
3329 loops
= bridge_get_input(bm
)
3331 # reorder loops if there are more than 2
3334 loops
= bridge_sort_loops(bm
, loops
, self
.loft_loop
)
3336 loops
= bridge_match_loops(bm
, loops
)
3338 # saving cache for faster execution next time
3340 cache_write("Bridge", object, bm
, input_method
, False, False,
3341 loops
, False, False)
3344 # calculate new geometry
3347 max_vert_index
= len(bm
.verts
) - 1
3348 for i
in range(1, len(loops
)):
3349 if not self
.loft
and i
% 2 == 0:
3351 lines
= bridge_calculate_lines(bm
, loops
[i
- 1:i
+ 1],
3352 self
.mode
, self
.twist
, self
.reverse
)
3353 vertex_normals
= bridge_calculate_virtual_vertex_normals(bm
,
3354 lines
, loops
[i
- 1:i
+ 1], edge_faces
, edgekey_to_edge
)
3355 segments
= bridge_calculate_segments(bm
, lines
,
3356 loops
[i
- 1:i
+ 1], self
.segments
)
3357 new_verts
, new_faces
, max_vert_index
= \
3358 bridge_calculate_geometry(
3359 bm
, lines
, vertex_normals
,
3360 segments
, self
.interpolation
, self
.cubic_strength
,
3361 self
.min_width
, max_vert_index
3364 vertices
+= new_verts
3367 # make sure faces in loops that aren't used, aren't removed
3368 if self
.remove_faces
and old_selected_faces
:
3369 bridge_save_unused_faces(bm
, old_selected_faces
, loops
)
3372 bridge_create_vertices(bm
, vertices
)
3375 new_faces
= bridge_create_faces(object, bm
, faces
, self
.twist
)
3376 old_selected_faces
= [
3377 i
for i
, face
in enumerate(bm
.faces
) if face
.index
in old_selected_faces
3379 bridge_select_new_faces(new_faces
, smooth
)
3380 # edge-data could have changed, can't use cache next run
3381 if faces
and not vertices
:
3382 cache_delete("Bridge")
3383 # delete internal faces
3384 if self
.remove_faces
and old_selected_faces
:
3385 bridge_remove_internal_faces(bm
, old_selected_faces
)
3386 # make sure normals are facing outside
3387 bmesh
.update_edit_mesh(object.data
, loop_triangles
=False,
3389 bpy
.ops
.mesh
.normals_make_consistent()
3398 class Circle(Operator
):
3399 bl_idname
= "mesh.looptools_circle"
3401 bl_description
= "Move selected vertices into a circle shape"
3402 bl_options
= {'REGISTER', 'UNDO'}
3404 custom_radius
: BoolProperty(
3406 description
="Force a custom radius",
3411 items
=(("best", "Best fit", "Non-linear least squares"),
3412 ("inside", "Fit inside", "Only move vertices towards the center")),
3413 description
="Method used for fitting a circle to the vertices",
3416 flatten
: BoolProperty(
3418 description
="Flatten the circle, instead of projecting it on the mesh",
3421 influence
: FloatProperty(
3423 description
="Force of the tool",
3428 subtype
='PERCENTAGE'
3430 lock_x
: BoolProperty(
3432 description
="Lock editing of the x-coordinate",
3435 lock_y
: BoolProperty(
3437 description
="Lock editing of the y-coordinate",
3440 lock_z
: BoolProperty(name
="Lock Z",
3441 description
="Lock editing of the z-coordinate",
3444 radius
: FloatProperty(
3446 description
="Custom radius for circle",
3451 regular
: BoolProperty(
3453 description
="Distribute vertices at constant distances along the circle",
3458 def poll(cls
, context
):
3459 ob
= context
.active_object
3460 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3462 def draw(self
, context
):
3463 layout
= self
.layout
3464 col
= layout
.column()
3466 col
.prop(self
, "fit")
3469 col
.prop(self
, "flatten")
3470 row
= col
.row(align
=True)
3471 row
.prop(self
, "custom_radius")
3472 row_right
= row
.row(align
=True)
3473 row_right
.active
= self
.custom_radius
3474 row_right
.prop(self
, "radius", text
="")
3475 col
.prop(self
, "regular")
3478 col_move
= col
.column(align
=True)
3479 row
= col_move
.row(align
=True)
3481 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3483 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3485 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3487 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3489 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3491 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3492 col_move
.prop(self
, "influence")
3494 def invoke(self
, context
, event
):
3495 # load custom settings
3497 return self
.execute(context
)
3499 def execute(self
, context
):
3501 object, bm
= initialise()
3502 settings_write(self
)
3503 # check cache to see if we can save time
3504 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Circle",
3505 object, bm
, False, False)
3507 derived
, bm_mod
= get_derived_bmesh(object, bm
)
3510 derived
, bm_mod
, single_vertices
, single_loops
, loops
= \
3511 circle_get_input(object, bm
)
3512 mapping
= get_mapping(derived
, bm
, bm_mod
, single_vertices
,
3514 single_loops
, loops
= circle_check_loops(single_loops
, loops
,
3517 # saving cache for faster execution next time
3519 cache_write("Circle", object, bm
, False, False, single_loops
,
3520 loops
, derived
, mapping
)
3523 for i
, loop
in enumerate(loops
):
3524 # best fitting flat plane
3525 com
, normal
= calculate_plane(bm_mod
, loop
)
3526 # if circular, shift loop so we get a good starting vertex
3528 loop
= circle_shift_loop(bm_mod
, loop
, com
)
3529 # flatten vertices on plane
3530 locs_2d
, p
, q
= circle_3d_to_2d(bm_mod
, loop
, com
, normal
)
3532 if self
.fit
== 'best':
3533 x0
, y0
, r
= circle_calculate_best_fit(locs_2d
)
3534 else: # self.fit == 'inside'
3535 x0
, y0
, r
= circle_calculate_min_fit(locs_2d
)
3537 if self
.custom_radius
:
3538 r
= self
.radius
/ p
.length
3539 # calculate positions on circle
3541 new_locs_2d
= circle_project_regular(locs_2d
[:], x0
, y0
, r
)
3543 new_locs_2d
= circle_project_non_regular(locs_2d
[:], x0
, y0
, r
)
3544 # take influence into account
3545 locs_2d
= circle_influence_locs(locs_2d
, new_locs_2d
,
3547 # calculate 3d positions of the created 2d input
3548 move
.append(circle_calculate_verts(self
.flatten
, bm_mod
,
3549 locs_2d
, com
, p
, q
, normal
))
3550 # flatten single input vertices on plane defined by loop
3551 if self
.flatten
and single_loops
:
3552 move
.append(circle_flatten_singles(bm_mod
, com
, p
, q
,
3553 normal
, single_loops
[i
]))
3555 # move vertices to new locations
3556 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3557 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3560 move_verts(object, bm
, mapping
, move
, lock
, -1)
3571 class Curve(Operator
):
3572 bl_idname
= "mesh.looptools_curve"
3574 bl_description
= "Turn a loop into a smooth curve"
3575 bl_options
= {'REGISTER', 'UNDO'}
3577 boundaries
: BoolProperty(
3579 description
="Limit the tool to work within the boundaries of the selected vertices",
3582 influence
: FloatProperty(
3584 description
="Force of the tool",
3589 subtype
='PERCENTAGE'
3591 interpolation
: EnumProperty(
3592 name
="Interpolation",
3593 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
3594 ("linear", "Linear", "Simple and fast linear algorithm")),
3595 description
="Algorithm used for interpolation",
3598 lock_x
: BoolProperty(
3600 description
="Lock editing of the x-coordinate",
3603 lock_y
: BoolProperty(
3605 description
="Lock editing of the y-coordinate",
3608 lock_z
: BoolProperty(
3610 description
="Lock editing of the z-coordinate",
3613 regular
: BoolProperty(
3615 description
="Distribute vertices at constant distances along the curve",
3618 restriction
: EnumProperty(
3620 items
=(("none", "None", "No restrictions on vertex movement"),
3621 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
3622 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
3623 description
="Restrictions on how the vertices can be moved",
3628 def poll(cls
, context
):
3629 ob
= context
.active_object
3630 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3632 def draw(self
, context
):
3633 layout
= self
.layout
3634 col
= layout
.column()
3636 col
.prop(self
, "interpolation")
3637 col
.prop(self
, "restriction")
3638 col
.prop(self
, "boundaries")
3639 col
.prop(self
, "regular")
3642 col_move
= col
.column(align
=True)
3643 row
= col_move
.row(align
=True)
3645 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3647 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3649 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3651 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3653 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3655 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3656 col_move
.prop(self
, "influence")
3658 def invoke(self
, context
, event
):
3659 # load custom settings
3661 return self
.execute(context
)
3663 def execute(self
, context
):
3665 object, bm
= initialise()
3666 settings_write(self
)
3667 # check cache to see if we can save time
3668 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Curve",
3669 object, bm
, False, self
.boundaries
)
3671 derived
, bm_mod
= get_derived_bmesh(object, bm
)
3674 derived
, bm_mod
, loops
= curve_get_input(object, bm
, self
.boundaries
)
3675 mapping
= get_mapping(derived
, bm
, bm_mod
, False, True, loops
)
3676 loops
= check_loops(loops
, mapping
, bm_mod
)
3678 v
.index
for v
in bm_mod
.verts
if v
.select
and not v
.hide
3681 # saving cache for faster execution next time
3683 cache_write("Curve", object, bm
, False, self
.boundaries
, False,
3684 loops
, derived
, mapping
)
3688 knots
, points
= curve_calculate_knots(loop
, verts_selected
)
3689 pknots
= curve_project_knots(bm_mod
, verts_selected
, knots
,
3691 tknots
, tpoints
= curve_calculate_t(bm_mod
, knots
, points
,
3692 pknots
, self
.regular
, loop
[1])
3693 splines
= calculate_splines(self
.interpolation
, bm_mod
,
3695 move
.append(curve_calculate_vertices(bm_mod
, knots
, tknots
,
3696 points
, tpoints
, splines
, self
.interpolation
,
3699 # move vertices to new locations
3700 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3701 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3704 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
3715 class Flatten(Operator
):
3716 bl_idname
= "mesh.looptools_flatten"
3717 bl_label
= "Flatten"
3718 bl_description
= "Flatten vertices on a best-fitting plane"
3719 bl_options
= {'REGISTER', 'UNDO'}
3721 influence
: FloatProperty(
3723 description
="Force of the tool",
3728 subtype
='PERCENTAGE'
3730 lock_x
: BoolProperty(
3732 description
="Lock editing of the x-coordinate",
3735 lock_y
: BoolProperty(
3737 description
="Lock editing of the y-coordinate",
3740 lock_z
: BoolProperty(name
="Lock Z",
3741 description
="Lock editing of the z-coordinate",
3744 plane
: EnumProperty(
3746 items
=(("best_fit", "Best fit", "Calculate a best fitting plane"),
3747 ("normal", "Normal", "Derive plane from averaging vertex normals"),
3748 ("view", "View", "Flatten on a plane perpendicular to the viewing angle")),
3749 description
="Plane on which vertices are flattened",
3752 restriction
: EnumProperty(
3754 items
=(("none", "None", "No restrictions on vertex movement"),
3755 ("bounding_box", "Bounding box", "Vertices are restricted to "
3756 "movement inside the bounding box of the selection")),
3757 description
="Restrictions on how the vertices can be moved",
3762 def poll(cls
, context
):
3763 ob
= context
.active_object
3764 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3766 def draw(self
, context
):
3767 layout
= self
.layout
3768 col
= layout
.column()
3770 col
.prop(self
, "plane")
3771 # col.prop(self, "restriction")
3774 col_move
= col
.column(align
=True)
3775 row
= col_move
.row(align
=True)
3777 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3779 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3781 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3783 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3785 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3787 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3788 col_move
.prop(self
, "influence")
3790 def invoke(self
, context
, event
):
3791 # load custom settings
3793 return self
.execute(context
)
3795 def execute(self
, context
):
3797 object, bm
= initialise()
3798 settings_write(self
)
3799 # check cache to see if we can save time
3800 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Flatten",
3801 object, bm
, False, False)
3803 # order input into virtual loops
3804 loops
= flatten_get_input(bm
)
3805 loops
= check_loops(loops
, mapping
, bm
)
3807 # saving cache for faster execution next time
3809 cache_write("Flatten", object, bm
, False, False, False, loops
,
3814 # calculate plane and position of vertices on them
3815 com
, normal
= calculate_plane(bm
, loop
, method
=self
.plane
,
3817 to_move
= flatten_project(bm
, loop
, com
, normal
)
3818 if self
.restriction
== 'none':
3819 move
.append(to_move
)
3821 move
.append(to_move
)
3823 # move vertices to new locations
3824 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3825 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3828 move_verts(object, bm
, False, move
, lock
, self
.influence
)
3836 # Annotation operator
3837 class RemoveAnnotation(Operator
):
3838 bl_idname
= "remove.annotation"
3839 bl_label
= "Remove Annotation"
3840 bl_description
= "Remove all Annotation Strokes"
3841 bl_options
= {'REGISTER', 'UNDO'}
3843 def execute(self
, context
):
3846 bpy
.data
.grease_pencils
[0].layers
.active
.clear()
3848 self
.report({'INFO'}, "No Annotation data to Unlink")
3849 return {'CANCELLED'}
3854 class RemoveGPencil(Operator
):
3855 bl_idname
= "remove.gp"
3856 bl_label
= "Remove GPencil"
3857 bl_description
= "Remove all GPencil Strokes"
3858 bl_options
= {'REGISTER', 'UNDO'}
3860 def execute(self
, context
):
3863 looptools
= context
.window_manager
.looptools
3864 looptools
.gstretch_guide
.data
.layers
.data
.clear()
3865 looptools
.gstretch_guide
.data
.update_tag()
3867 self
.report({'INFO'}, "No GPencil data to Unlink")
3868 return {'CANCELLED'}
3873 class GStretch(Operator
):
3874 bl_idname
= "mesh.looptools_gstretch"
3875 bl_label
= "Gstretch"
3876 bl_description
= "Stretch selected vertices to active stroke"
3877 bl_options
= {'REGISTER', 'UNDO'}
3879 conversion
: EnumProperty(
3881 items
=(("distance", "Distance", "Set the distance between vertices "
3882 "of the converted stroke"),
3883 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
3884 "number of vertices that converted strokes will have"),
3885 ("vertices", "Exact vertices", "Set the exact number of vertices "
3886 "that converted strokes will have. Short strokes "
3887 "with few points may contain less vertices than this number."),
3888 ("none", "No simplification", "Convert each point "
3890 description
="If strokes are converted to geometry, "
3891 "use this simplification method",
3892 default
='limit_vertices'
3894 conversion_distance
: FloatProperty(
3896 description
="Absolute distance between vertices along the converted "
3903 conversion_max
: IntProperty(
3904 name
="Max Vertices",
3905 description
="Maximum number of vertices strokes will "
3906 "have, when they are converted to geomtery",
3910 update
=gstretch_update_min
3912 conversion_min
: IntProperty(
3913 name
="Min Vertices",
3914 description
="Minimum number of vertices strokes will "
3915 "have, when they are converted to geomtery",
3919 update
=gstretch_update_max
3921 conversion_vertices
: IntProperty(
3923 description
="Number of vertices strokes will "
3924 "have, when they are converted to geometry. If strokes have less "
3925 "points than required, the 'Spread evenly' method is used",
3930 delete_strokes
: BoolProperty(
3931 name
="Delete strokes",
3932 description
="Remove strokes if they have been used."
3933 "WARNING: DOES NOT SUPPORT UNDO",
3936 influence
: FloatProperty(
3938 description
="Force of the tool",
3943 subtype
='PERCENTAGE'
3945 lock_x
: BoolProperty(
3947 description
="Lock editing of the x-coordinate",
3950 lock_y
: BoolProperty(
3952 description
="Lock editing of the y-coordinate",
3955 lock_z
: BoolProperty(
3957 description
="Lock editing of the z-coordinate",
3960 method
: EnumProperty(
3962 items
=(("project", "Project", "Project vertices onto the stroke, "
3963 "using vertex normals and connected edges"),
3964 ("irregular", "Spread", "Distribute vertices along the full "
3965 "stroke, retaining relative distances between the vertices"),
3966 ("regular", "Spread evenly", "Distribute vertices at regular "
3967 "distances along the full stroke")),
3968 description
="Method of distributing the vertices over the "
3974 def poll(cls
, context
):
3975 ob
= context
.active_object
3976 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3978 def draw(self
, context
):
3979 looptools
= context
.window_manager
.looptools
3980 layout
= self
.layout
3981 col
= layout
.column()
3983 col
.prop(self
, "method")
3986 col_conv
= col
.column(align
=True)
3987 col_conv
.prop(self
, "conversion", text
="")
3988 if self
.conversion
== 'distance':
3989 col_conv
.prop(self
, "conversion_distance")
3990 elif self
.conversion
== 'limit_vertices':
3991 row
= col_conv
.row(align
=True)
3992 row
.prop(self
, "conversion_min", text
="Min")
3993 row
.prop(self
, "conversion_max", text
="Max")
3994 elif self
.conversion
== 'vertices':
3995 col_conv
.prop(self
, "conversion_vertices")
3998 col_move
= col
.column(align
=True)
3999 row
= col_move
.row(align
=True)
4001 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
4003 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
4005 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
4007 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
4009 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
4011 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
4012 col_move
.prop(self
, "influence")
4014 if looptools
.gstretch_use_guide
== "Annotation":
4015 col
.operator("remove.annotation", text
="Delete annotation strokes")
4016 if looptools
.gstretch_use_guide
== "GPencil":
4017 col
.operator("remove.gp", text
="Delete GPencil strokes")
4019 def invoke(self
, context
, event
):
4020 # flush cached strokes
4021 if 'Gstretch' in looptools_cache
:
4022 looptools_cache
['Gstretch']['single_loops'] = []
4023 # load custom settings
4025 return self
.execute(context
)
4027 def execute(self
, context
):
4029 object, bm
= initialise()
4030 settings_write(self
)
4032 # check cache to see if we can save time
4033 cached
, safe_strokes
, loops
, derived
, mapping
= cache_read("Gstretch",
4034 object, bm
, False, False)
4036 straightening
= False
4038 strokes
= gstretch_safe_to_true_strokes(safe_strokes
)
4039 # cached strokes were flushed (see operator's invoke function)
4040 elif get_strokes(self
, context
):
4041 strokes
= gstretch_get_strokes(self
, context
)
4043 # straightening function (no GP) -> loops ignore modifiers
4044 straightening
= True
4047 bm_mod
.verts
.ensure_lookup_table()
4048 bm_mod
.edges
.ensure_lookup_table()
4049 bm_mod
.faces
.ensure_lookup_table()
4050 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
4051 if not straightening
:
4052 derived
, bm_mod
= get_derived_bmesh(object, bm
)
4054 # get loops and strokes
4055 if get_strokes(self
, context
):
4057 derived
, bm_mod
, loops
= get_connected_input(object, bm
, input='selected')
4058 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4059 loops
= check_loops(loops
, mapping
, bm_mod
)
4061 strokes
= gstretch_get_strokes(self
, context
)
4063 # straightening function (no GP) -> loops ignore modifiers
4067 bm_mod
.verts
.ensure_lookup_table()
4068 bm_mod
.edges
.ensure_lookup_table()
4069 bm_mod
.faces
.ensure_lookup_table()
4071 edgekey(edge
) for edge
in bm_mod
.edges
if
4072 edge
.select
and not edge
.hide
4074 loops
= get_connected_selections(edge_keys
)
4075 loops
= check_loops(loops
, mapping
, bm_mod
)
4076 # create fake strokes
4077 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
4079 # saving cache for faster execution next time
4082 safe_strokes
= gstretch_true_to_safe_strokes(strokes
)
4085 cache_write("Gstretch", object, bm
, False, False,
4086 safe_strokes
, loops
, derived
, mapping
)
4088 # pair loops and strokes
4089 ls_pairs
= gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
)
4090 ls_pairs
= gstretch_align_pairs(ls_pairs
, object, bm_mod
, self
.method
)
4094 # no selected geometry, convert GP to verts
4096 move
.append(gstretch_create_verts(object, bm
, strokes
,
4097 self
.method
, self
.conversion
, self
.conversion_distance
,
4098 self
.conversion_max
, self
.conversion_min
,
4099 self
.conversion_vertices
))
4100 for stroke
in strokes
:
4101 gstretch_erase_stroke(stroke
, context
)
4103 for (loop
, stroke
) in ls_pairs
:
4104 move
.append(gstretch_calculate_verts(loop
, stroke
, object,
4105 bm_mod
, self
.method
))
4106 if self
.delete_strokes
:
4107 if type(stroke
) != bpy
.types
.GPencilStroke
:
4108 # in case of cached fake stroke, get the real one
4109 if get_strokes(self
, context
):
4110 strokes
= gstretch_get_strokes(self
, context
)
4111 if loops
and strokes
:
4112 ls_pairs
= gstretch_match_loops_strokes(loops
,
4113 strokes
, object, bm_mod
)
4114 ls_pairs
= gstretch_align_pairs(ls_pairs
,
4115 object, bm_mod
, self
.method
)
4116 for (l
, s
) in ls_pairs
:
4120 gstretch_erase_stroke(stroke
, context
)
4122 # move vertices to new locations
4123 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4124 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4127 bmesh
.update_edit_mesh(object.data
, loop_triangles
=True, destructive
=True)
4128 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4139 class Relax(Operator
):
4140 bl_idname
= "mesh.looptools_relax"
4142 bl_description
= "Relax the loop, so it is smoother"
4143 bl_options
= {'REGISTER', 'UNDO'}
4145 input: EnumProperty(
4147 items
=(("all", "Parallel (all)", "Also use non-selected "
4148 "parallel loops as input"),
4149 ("selected", "Selection", "Only use selected vertices as input")),
4150 description
="Loops that are relaxed",
4153 interpolation
: EnumProperty(
4154 name
="Interpolation",
4155 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4156 ("linear", "Linear", "Simple and fast linear algorithm")),
4157 description
="Algorithm used for interpolation",
4160 iterations
: EnumProperty(
4162 items
=(("1", "1", "One"),
4163 ("3", "3", "Three"),
4165 ("10", "10", "Ten"),
4166 ("25", "25", "Twenty-five")),
4167 description
="Number of times the loop is relaxed",
4170 regular
: BoolProperty(
4172 description
="Distribute vertices at constant distances along the loop",
4177 def poll(cls
, context
):
4178 ob
= context
.active_object
4179 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4181 def draw(self
, context
):
4182 layout
= self
.layout
4183 col
= layout
.column()
4185 col
.prop(self
, "interpolation")
4186 col
.prop(self
, "input")
4187 col
.prop(self
, "iterations")
4188 col
.prop(self
, "regular")
4190 def invoke(self
, context
, event
):
4191 # load custom settings
4193 return self
.execute(context
)
4195 def execute(self
, context
):
4197 object, bm
= initialise()
4198 settings_write(self
)
4199 # check cache to see if we can save time
4200 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Relax",
4201 object, bm
, self
.input, False)
4203 derived
, bm_mod
= get_derived_bmesh(object, bm
)
4206 derived
, bm_mod
, loops
= get_connected_input(object, bm
, self
.input)
4207 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4208 loops
= check_loops(loops
, mapping
, bm_mod
)
4209 knots
, points
= relax_calculate_knots(loops
)
4211 # saving cache for faster execution next time
4213 cache_write("Relax", object, bm
, self
.input, False, False, loops
,
4216 for iteration
in range(int(self
.iterations
)):
4217 # calculate splines and new positions
4218 tknots
, tpoints
= relax_calculate_t(bm_mod
, knots
, points
,
4221 for i
in range(len(knots
)):
4222 splines
.append(calculate_splines(self
.interpolation
, bm_mod
,
4223 tknots
[i
], knots
[i
]))
4224 move
= [relax_calculate_verts(bm_mod
, self
.interpolation
,
4225 tknots
, knots
, tpoints
, points
, splines
)]
4226 move_verts(object, bm
, mapping
, move
, False, -1)
4237 class Space(Operator
):
4238 bl_idname
= "mesh.looptools_space"
4240 bl_description
= "Space the vertices in a regular distribution on the loop"
4241 bl_options
= {'REGISTER', 'UNDO'}
4243 influence
: FloatProperty(
4245 description
="Force of the tool",
4250 subtype
='PERCENTAGE'
4252 input: EnumProperty(
4254 items
=(("all", "Parallel (all)", "Also use non-selected "
4255 "parallel loops as input"),
4256 ("selected", "Selection", "Only use selected vertices as input")),
4257 description
="Loops that are spaced",
4260 interpolation
: EnumProperty(
4261 name
="Interpolation",
4262 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4263 ("linear", "Linear", "Vertices are projected on existing edges")),
4264 description
="Algorithm used for interpolation",
4267 lock_x
: BoolProperty(
4269 description
="Lock editing of the x-coordinate",
4272 lock_y
: BoolProperty(
4274 description
="Lock editing of the y-coordinate",
4277 lock_z
: BoolProperty(
4279 description
="Lock editing of the z-coordinate",
4284 def poll(cls
, context
):
4285 ob
= context
.active_object
4286 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4288 def draw(self
, context
):
4289 layout
= self
.layout
4290 col
= layout
.column()
4292 col
.prop(self
, "interpolation")
4293 col
.prop(self
, "input")
4296 col_move
= col
.column(align
=True)
4297 row
= col_move
.row(align
=True)
4299 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
4301 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
4303 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
4305 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
4307 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
4309 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
4310 col_move
.prop(self
, "influence")
4312 def invoke(self
, context
, event
):
4313 # load custom settings
4315 return self
.execute(context
)
4317 def execute(self
, context
):
4319 object, bm
= initialise()
4320 settings_write(self
)
4321 # check cache to see if we can save time
4322 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Space",
4323 object, bm
, self
.input, False)
4325 derived
, bm_mod
= get_derived_bmesh(object, bm
)
4328 derived
, bm_mod
, loops
= get_connected_input(object, bm
, self
.input)
4329 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4330 loops
= check_loops(loops
, mapping
, bm_mod
)
4332 # saving cache for faster execution next time
4334 cache_write("Space", object, bm
, self
.input, False, False, loops
,
4339 # calculate splines and new positions
4340 if loop
[1]: # circular
4341 loop
[0].append(loop
[0][0])
4342 tknots
, tpoints
= space_calculate_t(bm_mod
, loop
[0][:])
4343 splines
= calculate_splines(self
.interpolation
, bm_mod
,
4345 move
.append(space_calculate_verts(bm_mod
, self
.interpolation
,
4346 tknots
, tpoints
, loop
[0][:-1], splines
))
4347 # move vertices to new locations
4348 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4349 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4352 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4362 # ########################################
4363 # ##### GUI and registration #############
4364 # ########################################
4366 # menu containing all tools
4367 class VIEW3D_MT_edit_mesh_looptools(Menu
):
4368 bl_label
= "LoopTools"
4370 def draw(self
, context
):
4371 layout
= self
.layout
4373 layout
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4374 layout
.operator("mesh.looptools_circle")
4375 layout
.operator("mesh.looptools_curve")
4376 layout
.operator("mesh.looptools_flatten")
4377 layout
.operator("mesh.looptools_gstretch")
4378 layout
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4379 layout
.operator("mesh.looptools_relax")
4380 layout
.operator("mesh.looptools_space")
4383 # panel containing all tools
4384 class VIEW3D_PT_tools_looptools(Panel
):
4385 bl_space_type
= 'VIEW_3D'
4386 bl_region_type
= 'UI'
4387 bl_category
= 'Edit'
4388 bl_context
= "mesh_edit"
4389 bl_label
= "LoopTools"
4390 bl_options
= {'DEFAULT_CLOSED'}
4392 def draw(self
, context
):
4393 layout
= self
.layout
4394 col
= layout
.column(align
=True)
4395 lt
= context
.window_manager
.looptools
4397 # bridge - first line
4398 split
= col
.split(factor
=0.15, align
=True)
4399 if lt
.display_bridge
:
4400 split
.prop(lt
, "display_bridge", text
="", icon
='DOWNARROW_HLT')
4402 split
.prop(lt
, "display_bridge", text
="", icon
='RIGHTARROW')
4403 split
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4405 if lt
.display_bridge
:
4406 box
= col
.column(align
=True).box().column()
4407 # box.prop(self, "mode")
4410 col_top
= box
.column(align
=True)
4411 row
= col_top
.row(align
=True)
4412 col_left
= row
.column(align
=True)
4413 col_right
= row
.column(align
=True)
4414 col_right
.active
= lt
.bridge_segments
!= 1
4415 col_left
.prop(lt
, "bridge_segments")
4416 col_right
.prop(lt
, "bridge_min_width", text
="")
4418 bottom_left
= col_left
.row()
4419 bottom_left
.active
= lt
.bridge_segments
!= 1
4420 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4421 bottom_right
= col_right
.row()
4422 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4423 bottom_right
.prop(lt
, "bridge_cubic_strength")
4424 # boolean properties
4425 col_top
.prop(lt
, "bridge_remove_faces")
4427 # override properties
4429 row
= box
.row(align
=True)
4430 row
.prop(lt
, "bridge_twist")
4431 row
.prop(lt
, "bridge_reverse")
4433 # circle - first line
4434 split
= col
.split(factor
=0.15, align
=True)
4435 if lt
.display_circle
:
4436 split
.prop(lt
, "display_circle", text
="", icon
='DOWNARROW_HLT')
4438 split
.prop(lt
, "display_circle", text
="", icon
='RIGHTARROW')
4439 split
.operator("mesh.looptools_circle")
4441 if lt
.display_circle
:
4442 box
= col
.column(align
=True).box().column()
4443 box
.prop(lt
, "circle_fit")
4446 box
.prop(lt
, "circle_flatten")
4447 row
= box
.row(align
=True)
4448 row
.prop(lt
, "circle_custom_radius")
4449 row_right
= row
.row(align
=True)
4450 row_right
.active
= lt
.circle_custom_radius
4451 row_right
.prop(lt
, "circle_radius", text
="")
4452 box
.prop(lt
, "circle_regular")
4455 col_move
= box
.column(align
=True)
4456 row
= col_move
.row(align
=True)
4457 if lt
.circle_lock_x
:
4458 row
.prop(lt
, "circle_lock_x", text
="X", icon
='LOCKED')
4460 row
.prop(lt
, "circle_lock_x", text
="X", icon
='UNLOCKED')
4461 if lt
.circle_lock_y
:
4462 row
.prop(lt
, "circle_lock_y", text
="Y", icon
='LOCKED')
4464 row
.prop(lt
, "circle_lock_y", text
="Y", icon
='UNLOCKED')
4465 if lt
.circle_lock_z
:
4466 row
.prop(lt
, "circle_lock_z", text
="Z", icon
='LOCKED')
4468 row
.prop(lt
, "circle_lock_z", text
="Z", icon
='UNLOCKED')
4469 col_move
.prop(lt
, "circle_influence")
4471 # curve - first line
4472 split
= col
.split(factor
=0.15, align
=True)
4473 if lt
.display_curve
:
4474 split
.prop(lt
, "display_curve", text
="", icon
='DOWNARROW_HLT')
4476 split
.prop(lt
, "display_curve", text
="", icon
='RIGHTARROW')
4477 split
.operator("mesh.looptools_curve")
4479 if lt
.display_curve
:
4480 box
= col
.column(align
=True).box().column()
4481 box
.prop(lt
, "curve_interpolation")
4482 box
.prop(lt
, "curve_restriction")
4483 box
.prop(lt
, "curve_boundaries")
4484 box
.prop(lt
, "curve_regular")
4487 col_move
= box
.column(align
=True)
4488 row
= col_move
.row(align
=True)
4490 row
.prop(lt
, "curve_lock_x", text
="X", icon
='LOCKED')
4492 row
.prop(lt
, "curve_lock_x", text
="X", icon
='UNLOCKED')
4494 row
.prop(lt
, "curve_lock_y", text
="Y", icon
='LOCKED')
4496 row
.prop(lt
, "curve_lock_y", text
="Y", icon
='UNLOCKED')
4498 row
.prop(lt
, "curve_lock_z", text
="Z", icon
='LOCKED')
4500 row
.prop(lt
, "curve_lock_z", text
="Z", icon
='UNLOCKED')
4501 col_move
.prop(lt
, "curve_influence")
4503 # flatten - first line
4504 split
= col
.split(factor
=0.15, align
=True)
4505 if lt
.display_flatten
:
4506 split
.prop(lt
, "display_flatten", text
="", icon
='DOWNARROW_HLT')
4508 split
.prop(lt
, "display_flatten", text
="", icon
='RIGHTARROW')
4509 split
.operator("mesh.looptools_flatten")
4510 # flatten - settings
4511 if lt
.display_flatten
:
4512 box
= col
.column(align
=True).box().column()
4513 box
.prop(lt
, "flatten_plane")
4514 # box.prop(lt, "flatten_restriction")
4517 col_move
= box
.column(align
=True)
4518 row
= col_move
.row(align
=True)
4519 if lt
.flatten_lock_x
:
4520 row
.prop(lt
, "flatten_lock_x", text
="X", icon
='LOCKED')
4522 row
.prop(lt
, "flatten_lock_x", text
="X", icon
='UNLOCKED')
4523 if lt
.flatten_lock_y
:
4524 row
.prop(lt
, "flatten_lock_y", text
="Y", icon
='LOCKED')
4526 row
.prop(lt
, "flatten_lock_y", text
="Y", icon
='UNLOCKED')
4527 if lt
.flatten_lock_z
:
4528 row
.prop(lt
, "flatten_lock_z", text
="Z", icon
='LOCKED')
4530 row
.prop(lt
, "flatten_lock_z", text
="Z", icon
='UNLOCKED')
4531 col_move
.prop(lt
, "flatten_influence")
4533 # gstretch - first line
4534 split
= col
.split(factor
=0.15, align
=True)
4535 if lt
.display_gstretch
:
4536 split
.prop(lt
, "display_gstretch", text
="", icon
='DOWNARROW_HLT')
4538 split
.prop(lt
, "display_gstretch", text
="", icon
='RIGHTARROW')
4539 split
.operator("mesh.looptools_gstretch")
4541 if lt
.display_gstretch
:
4542 box
= col
.column(align
=True).box().column()
4543 box
.prop(lt
, "gstretch_use_guide")
4544 if lt
.gstretch_use_guide
== "GPencil":
4545 box
.prop(lt
, "gstretch_guide")
4546 box
.prop(lt
, "gstretch_method")
4548 col_conv
= box
.column(align
=True)
4549 col_conv
.prop(lt
, "gstretch_conversion", text
="")
4550 if lt
.gstretch_conversion
== 'distance':
4551 col_conv
.prop(lt
, "gstretch_conversion_distance")
4552 elif lt
.gstretch_conversion
== 'limit_vertices':
4553 row
= col_conv
.row(align
=True)
4554 row
.prop(lt
, "gstretch_conversion_min", text
="Min")
4555 row
.prop(lt
, "gstretch_conversion_max", text
="Max")
4556 elif lt
.gstretch_conversion
== 'vertices':
4557 col_conv
.prop(lt
, "gstretch_conversion_vertices")
4560 col_move
= box
.column(align
=True)
4561 row
= col_move
.row(align
=True)
4562 if lt
.gstretch_lock_x
:
4563 row
.prop(lt
, "gstretch_lock_x", text
="X", icon
='LOCKED')
4565 row
.prop(lt
, "gstretch_lock_x", text
="X", icon
='UNLOCKED')
4566 if lt
.gstretch_lock_y
:
4567 row
.prop(lt
, "gstretch_lock_y", text
="Y", icon
='LOCKED')
4569 row
.prop(lt
, "gstretch_lock_y", text
="Y", icon
='UNLOCKED')
4570 if lt
.gstretch_lock_z
:
4571 row
.prop(lt
, "gstretch_lock_z", text
="Z", icon
='LOCKED')
4573 row
.prop(lt
, "gstretch_lock_z", text
="Z", icon
='UNLOCKED')
4574 col_move
.prop(lt
, "gstretch_influence")
4575 if lt
.gstretch_use_guide
== "Annotation":
4576 box
.operator("remove.annotation", text
="Delete Annotation Strokes")
4577 if lt
.gstretch_use_guide
== "GPencil":
4578 box
.operator("remove.gp", text
="Delete GPencil Strokes")
4581 split
= col
.split(factor
=0.15, align
=True)
4583 split
.prop(lt
, "display_loft", text
="", icon
='DOWNARROW_HLT')
4585 split
.prop(lt
, "display_loft", text
="", icon
='RIGHTARROW')
4586 split
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4589 box
= col
.column(align
=True).box().column()
4590 # box.prop(self, "mode")
4593 col_top
= box
.column(align
=True)
4594 row
= col_top
.row(align
=True)
4595 col_left
= row
.column(align
=True)
4596 col_right
= row
.column(align
=True)
4597 col_right
.active
= lt
.bridge_segments
!= 1
4598 col_left
.prop(lt
, "bridge_segments")
4599 col_right
.prop(lt
, "bridge_min_width", text
="")
4601 bottom_left
= col_left
.row()
4602 bottom_left
.active
= lt
.bridge_segments
!= 1
4603 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4604 bottom_right
= col_right
.row()
4605 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4606 bottom_right
.prop(lt
, "bridge_cubic_strength")
4607 # boolean properties
4608 col_top
.prop(lt
, "bridge_remove_faces")
4609 col_top
.prop(lt
, "bridge_loft_loop")
4611 # override properties
4613 row
= box
.row(align
=True)
4614 row
.prop(lt
, "bridge_twist")
4615 row
.prop(lt
, "bridge_reverse")
4617 # relax - first line
4618 split
= col
.split(factor
=0.15, align
=True)
4619 if lt
.display_relax
:
4620 split
.prop(lt
, "display_relax", text
="", icon
='DOWNARROW_HLT')
4622 split
.prop(lt
, "display_relax", text
="", icon
='RIGHTARROW')
4623 split
.operator("mesh.looptools_relax")
4625 if lt
.display_relax
:
4626 box
= col
.column(align
=True).box().column()
4627 box
.prop(lt
, "relax_interpolation")
4628 box
.prop(lt
, "relax_input")
4629 box
.prop(lt
, "relax_iterations")
4630 box
.prop(lt
, "relax_regular")
4632 # space - first line
4633 split
= col
.split(factor
=0.15, align
=True)
4634 if lt
.display_space
:
4635 split
.prop(lt
, "display_space", text
="", icon
='DOWNARROW_HLT')
4637 split
.prop(lt
, "display_space", text
="", icon
='RIGHTARROW')
4638 split
.operator("mesh.looptools_space")
4640 if lt
.display_space
:
4641 box
= col
.column(align
=True).box().column()
4642 box
.prop(lt
, "space_interpolation")
4643 box
.prop(lt
, "space_input")
4646 col_move
= box
.column(align
=True)
4647 row
= col_move
.row(align
=True)
4649 row
.prop(lt
, "space_lock_x", text
="X", icon
='LOCKED')
4651 row
.prop(lt
, "space_lock_x", text
="X", icon
='UNLOCKED')
4653 row
.prop(lt
, "space_lock_y", text
="Y", icon
='LOCKED')
4655 row
.prop(lt
, "space_lock_y", text
="Y", icon
='UNLOCKED')
4657 row
.prop(lt
, "space_lock_z", text
="Z", icon
='LOCKED')
4659 row
.prop(lt
, "space_lock_z", text
="Z", icon
='UNLOCKED')
4660 col_move
.prop(lt
, "space_influence")
4663 # property group containing all properties for the gui in the panel
4664 class LoopToolsProps(PropertyGroup
):
4666 Fake module like class
4667 bpy.context.window_manager.looptools
4669 # general display properties
4670 display_bridge
: BoolProperty(
4671 name
="Bridge settings",
4672 description
="Display settings of the Bridge tool",
4675 display_circle
: BoolProperty(
4676 name
="Circle settings",
4677 description
="Display settings of the Circle tool",
4680 display_curve
: BoolProperty(
4681 name
="Curve settings",
4682 description
="Display settings of the Curve tool",
4685 display_flatten
: BoolProperty(
4686 name
="Flatten settings",
4687 description
="Display settings of the Flatten tool",
4690 display_gstretch
: BoolProperty(
4691 name
="Gstretch settings",
4692 description
="Display settings of the Gstretch tool",
4695 display_loft
: BoolProperty(
4696 name
="Loft settings",
4697 description
="Display settings of the Loft tool",
4700 display_relax
: BoolProperty(
4701 name
="Relax settings",
4702 description
="Display settings of the Relax tool",
4705 display_space
: BoolProperty(
4706 name
="Space settings",
4707 description
="Display settings of the Space tool",
4712 bridge_cubic_strength
: FloatProperty(
4714 description
="Higher strength results in more fluid curves",
4719 bridge_interpolation
: EnumProperty(
4720 name
="Interpolation mode",
4721 items
=(('cubic', "Cubic", "Gives curved results"),
4722 ('linear', "Linear", "Basic, fast, straight interpolation")),
4723 description
="Interpolation mode: algorithm used when creating segments",
4726 bridge_loft
: BoolProperty(
4728 description
="Loft multiple loops, instead of considering them as "
4729 "a multi-input for bridging",
4732 bridge_loft_loop
: BoolProperty(
4734 description
="Connect the first and the last loop with each other",
4737 bridge_min_width
: IntProperty(
4738 name
="Minimum width",
4739 description
="Segments with an edge smaller than this are merged "
4740 "(compared to base edge)",
4744 subtype
='PERCENTAGE'
4746 bridge_mode
: EnumProperty(
4748 items
=(('basic', "Basic", "Fast algorithm"),
4749 ('shortest', "Shortest edge", "Slower algorithm with "
4750 "better vertex matching")),
4751 description
="Algorithm used for bridging",
4754 bridge_remove_faces
: BoolProperty(
4755 name
="Remove faces",
4756 description
="Remove faces that are internal after bridging",
4759 bridge_reverse
: BoolProperty(
4761 description
="Manually override the direction in which the loops "
4762 "are bridged. Only use if the tool gives the wrong result",
4765 bridge_segments
: IntProperty(
4767 description
="Number of segments used to bridge the gap (0=automatic)",
4772 bridge_twist
: IntProperty(
4774 description
="Twist what vertices are connected to each other",
4779 circle_custom_radius
: BoolProperty(
4781 description
="Force a custom radius",
4784 circle_fit
: EnumProperty(
4786 items
=(("best", "Best fit", "Non-linear least squares"),
4787 ("inside", "Fit inside", "Only move vertices towards the center")),
4788 description
="Method used for fitting a circle to the vertices",
4791 circle_flatten
: BoolProperty(
4793 description
="Flatten the circle, instead of projecting it on the mesh",
4796 circle_influence
: FloatProperty(
4798 description
="Force of the tool",
4803 subtype
='PERCENTAGE'
4805 circle_lock_x
: BoolProperty(
4807 description
="Lock editing of the x-coordinate",
4810 circle_lock_y
: BoolProperty(
4812 description
="Lock editing of the y-coordinate",
4815 circle_lock_z
: BoolProperty(
4817 description
="Lock editing of the z-coordinate",
4820 circle_radius
: FloatProperty(
4822 description
="Custom radius for circle",
4827 circle_regular
: BoolProperty(
4829 description
="Distribute vertices at constant distances along the circle",
4833 curve_boundaries
: BoolProperty(
4835 description
="Limit the tool to work within the boundaries of the "
4836 "selected vertices",
4839 curve_influence
: FloatProperty(
4841 description
="Force of the tool",
4846 subtype
='PERCENTAGE'
4848 curve_interpolation
: EnumProperty(
4849 name
="Interpolation",
4850 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4851 ("linear", "Linear", "Simple and fast linear algorithm")),
4852 description
="Algorithm used for interpolation",
4855 curve_lock_x
: BoolProperty(
4857 description
="Lock editing of the x-coordinate",
4860 curve_lock_y
: BoolProperty(
4862 description
="Lock editing of the y-coordinate",
4865 curve_lock_z
: BoolProperty(
4867 description
="Lock editing of the z-coordinate",
4870 curve_regular
: BoolProperty(
4872 description
="Distribute vertices at constant distances along the curve",
4875 curve_restriction
: EnumProperty(
4877 items
=(("none", "None", "No restrictions on vertex movement"),
4878 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
4879 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
4880 description
="Restrictions on how the vertices can be moved",
4884 # flatten properties
4885 flatten_influence
: FloatProperty(
4887 description
="Force of the tool",
4892 subtype
='PERCENTAGE'
4894 flatten_lock_x
: BoolProperty(
4896 description
="Lock editing of the x-coordinate",
4898 flatten_lock_y
: BoolProperty(name
="Lock Y",
4899 description
="Lock editing of the y-coordinate",
4902 flatten_lock_z
: BoolProperty(
4904 description
="Lock editing of the z-coordinate",
4907 flatten_plane
: EnumProperty(
4909 items
=(("best_fit", "Best fit", "Calculate a best fitting plane"),
4910 ("normal", "Normal", "Derive plane from averaging vertex "
4912 ("view", "View", "Flatten on a plane perpendicular to the "
4914 description
="Plane on which vertices are flattened",
4917 flatten_restriction
: EnumProperty(
4919 items
=(("none", "None", "No restrictions on vertex movement"),
4920 ("bounding_box", "Bounding box", "Vertices are restricted to "
4921 "movement inside the bounding box of the selection")),
4922 description
="Restrictions on how the vertices can be moved",
4926 # gstretch properties
4927 gstretch_conversion
: EnumProperty(
4929 items
=(("distance", "Distance", "Set the distance between vertices "
4930 "of the converted stroke"),
4931 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
4932 "number of vertices that converted GP strokes will have"),
4933 ("vertices", "Exact vertices", "Set the exact number of vertices "
4934 "that converted strokes will have. Short strokes "
4935 "with few points may contain less vertices than this number."),
4936 ("none", "No simplification", "Convert each point "
4938 description
="If strokes are converted to geometry, "
4939 "use this simplification method",
4940 default
='limit_vertices'
4942 gstretch_conversion_distance
: FloatProperty(
4944 description
="Absolute distance between vertices along the converted "
4951 gstretch_conversion_max
: IntProperty(
4952 name
="Max Vertices",
4953 description
="Maximum number of vertices strokes will "
4954 "have, when they are converted to geomtery",
4958 update
=gstretch_update_min
4960 gstretch_conversion_min
: IntProperty(
4961 name
="Min Vertices",
4962 description
="Minimum number of vertices strokes will "
4963 "have, when they are converted to geomtery",
4967 update
=gstretch_update_max
4969 gstretch_conversion_vertices
: IntProperty(
4971 description
="Number of vertices strokes will "
4972 "have, when they are converted to geometry. If strokes have less "
4973 "points than required, the 'Spread evenly' method is used",
4978 gstretch_delete_strokes
: BoolProperty(
4979 name
="Delete strokes",
4980 description
="Remove Grease Pencil strokes if they have been used "
4981 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
4984 gstretch_influence
: FloatProperty(
4986 description
="Force of the tool",
4991 subtype
='PERCENTAGE'
4993 gstretch_lock_x
: BoolProperty(
4995 description
="Lock editing of the x-coordinate",
4998 gstretch_lock_y
: BoolProperty(
5000 description
="Lock editing of the y-coordinate",
5003 gstretch_lock_z
: BoolProperty(
5005 description
="Lock editing of the z-coordinate",
5008 gstretch_method
: EnumProperty(
5010 items
=(("project", "Project", "Project vertices onto the stroke, "
5011 "using vertex normals and connected edges"),
5012 ("irregular", "Spread", "Distribute vertices along the full "
5013 "stroke, retaining relative distances between the vertices"),
5014 ("regular", "Spread evenly", "Distribute vertices at regular "
5015 "distances along the full stroke")),
5016 description
="Method of distributing the vertices over the Grease "
5020 gstretch_use_guide
: EnumProperty(
5022 items
=(("None", "None", "None"),
5023 ("Annotation", "Annotation", "Annotation"),
5024 ("GPencil", "GPencil", "GPencil")),
5027 gstretch_guide
: PointerProperty(
5028 name
="GPencil object",
5029 description
="Set GPencil object",
5030 type=bpy
.types
.Object
5034 relax_input
: EnumProperty(name
="Input",
5035 items
=(("all", "Parallel (all)", "Also use non-selected "
5036 "parallel loops as input"),
5037 ("selected", "Selection", "Only use selected vertices as input")),
5038 description
="Loops that are relaxed",
5041 relax_interpolation
: EnumProperty(
5042 name
="Interpolation",
5043 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5044 ("linear", "Linear", "Simple and fast linear algorithm")),
5045 description
="Algorithm used for interpolation",
5048 relax_iterations
: EnumProperty(name
="Iterations",
5049 items
=(("1", "1", "One"),
5050 ("3", "3", "Three"),
5052 ("10", "10", "Ten"),
5053 ("25", "25", "Twenty-five")),
5054 description
="Number of times the loop is relaxed",
5057 relax_regular
: BoolProperty(
5059 description
="Distribute vertices at constant distances along the loop",
5064 space_influence
: FloatProperty(
5066 description
="Force of the tool",
5071 subtype
='PERCENTAGE'
5073 space_input
: EnumProperty(
5075 items
=(("all", "Parallel (all)", "Also use non-selected "
5076 "parallel loops as input"),
5077 ("selected", "Selection", "Only use selected vertices as input")),
5078 description
="Loops that are spaced",
5081 space_interpolation
: EnumProperty(
5082 name
="Interpolation",
5083 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5084 ("linear", "Linear", "Vertices are projected on existing edges")),
5085 description
="Algorithm used for interpolation",
5088 space_lock_x
: BoolProperty(
5090 description
="Lock editing of the x-coordinate",
5093 space_lock_y
: BoolProperty(
5095 description
="Lock editing of the y-coordinate",
5098 space_lock_z
: BoolProperty(
5100 description
="Lock editing of the z-coordinate",
5104 # draw function for integration in menus
5105 def menu_func(self
, context
):
5106 self
.layout
.menu("VIEW3D_MT_edit_mesh_looptools")
5107 self
.layout
.separator()
5110 # Add-ons Preferences Update Panel
5112 # Define Panel classes for updating
5114 VIEW3D_PT_tools_looptools
,
5118 def update_panel(self
, context
):
5119 message
= "LoopTools: Updating Panel locations has failed"
5121 for panel
in panels
:
5122 if "bl_rna" in panel
.__dict
__:
5123 bpy
.utils
.unregister_class(panel
)
5125 for panel
in panels
:
5126 panel
.bl_category
= context
.preferences
.addons
[__name__
].preferences
.category
5127 bpy
.utils
.register_class(panel
)
5129 except Exception as e
:
5130 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
5134 class LoopPreferences(AddonPreferences
):
5135 # this must match the addon name, use '__package__'
5136 # when defining this in a submodule of a python package.
5137 bl_idname
= __name__
5139 category
: StringProperty(
5140 name
="Tab Category",
5141 description
="Choose a name for the category of the panel",
5146 def draw(self
, context
):
5147 layout
= self
.layout
5151 col
.label(text
="Tab Category:")
5152 col
.prop(self
, "category", text
="")
5155 # define classes for registration
5157 VIEW3D_MT_edit_mesh_looptools
,
5158 VIEW3D_PT_tools_looptools
,
5173 # registering and menu integration
5176 bpy
.utils
.register_class(cls
)
5177 bpy
.types
.VIEW3D_MT_edit_mesh_context_menu
.prepend(menu_func
)
5178 bpy
.types
.WindowManager
.looptools
= PointerProperty(type=LoopToolsProps
)
5179 update_panel(None, bpy
.context
)
5182 # unregistering and removing menus
5184 for cls
in reversed(classes
):
5185 bpy
.utils
.unregister_class(cls
)
5186 bpy
.types
.VIEW3D_MT_edit_mesh_context_menu
.remove(menu_func
)
5188 del bpy
.types
.WindowManager
.looptools
5189 except Exception as e
:
5190 print('unregister fail:\n', e
)
5194 if __name__
== "__main__":