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, 72, 2),
24 "location": "View3D > Toolbar and View3D > Specials (W-key)",
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_grease_pencil(object, context
):
64 gp
= object.grease_pencil
66 gp
= context
.scene
.grease_pencil
70 # force a full recalculation next time
71 def cache_delete(tool
):
72 if tool
in looptools_cache
:
73 del looptools_cache
[tool
]
76 # check cache for stored information
77 def cache_read(tool
, object, bm
, input_method
, boundaries
):
78 # current tool not cached yet
79 if tool
not in looptools_cache
:
80 return(False, False, False, False, False)
81 # check if selected object didn't change
82 if object.name
!= looptools_cache
[tool
]["object"]:
83 return(False, False, False, False, False)
84 # check if input didn't change
85 if input_method
!= looptools_cache
[tool
]["input_method"]:
86 return(False, False, False, False, False)
87 if boundaries
!= looptools_cache
[tool
]["boundaries"]:
88 return(False, False, False, False, False)
89 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
and
91 if modifiers
!= looptools_cache
[tool
]["modifiers"]:
92 return(False, False, False, False, False)
93 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
94 if input != looptools_cache
[tool
]["input"]:
95 return(False, False, False, False, False)
97 single_loops
= looptools_cache
[tool
]["single_loops"]
98 loops
= looptools_cache
[tool
]["loops"]
99 derived
= looptools_cache
[tool
]["derived"]
100 mapping
= looptools_cache
[tool
]["mapping"]
102 return(True, single_loops
, loops
, derived
, mapping
)
105 # store information in the cache
106 def cache_write(tool
, object, bm
, input_method
, boundaries
, single_loops
,
107 loops
, derived
, mapping
):
108 # clear cache of current tool
109 if tool
in looptools_cache
:
110 del looptools_cache
[tool
]
111 # prepare values to be saved to cache
112 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
113 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
and
114 mod
.type == 'MIRROR']
116 looptools_cache
[tool
] = {"input": input, "object": object.name
,
117 "input_method": input_method
, "boundaries": boundaries
,
118 "single_loops": single_loops
, "loops": loops
,
119 "derived": derived
, "mapping": mapping
, "modifiers": modifiers
}
122 # calculates natural cubic splines through all given knots
123 def calculate_cubic_splines(bm_mod
, tknots
, knots
):
124 # hack for circular loops
125 if knots
[0] == knots
[-1] and len(knots
) > 1:
128 for k
in range(-1, -5, -1):
129 if k
- 1 < -len(knots
):
131 k_new1
.append(knots
[k
- 1])
134 if k
+ 1 > len(knots
) - 1:
136 k_new2
.append(knots
[k
+ 1])
143 for t
in range(-1, -5, -1):
144 if t
- 1 < -len(tknots
):
146 total1
+= tknots
[t
] - tknots
[t
- 1]
147 t_new1
.append(tknots
[0] - total1
)
151 if t
+ 1 > len(tknots
) - 1:
153 total2
+= tknots
[t
+ 1] - tknots
[t
]
154 t_new2
.append(tknots
[-1] + total2
)
167 locs
= [bm_mod
.verts
[k
].co
[:] for k
in knots
]
174 for i
in range(n
- 1):
175 if x
[i
+ 1] - x
[i
] == 0:
178 h
.append(x
[i
+ 1] - x
[i
])
180 for i
in range(1, n
- 1):
181 q
.append(3 / h
[i
] * (a
[i
+ 1] - a
[i
]) - 3 / h
[i
- 1] * (a
[i
] - a
[i
- 1]))
185 for i
in range(1, n
- 1):
186 l
.append(2 * (x
[i
+ 1] - x
[i
- 1]) - h
[i
- 1] * u
[i
- 1])
189 u
.append(h
[i
] / l
[i
])
190 z
.append((q
[i
] - h
[i
- 1] * z
[i
- 1]) / l
[i
])
193 b
= [False for i
in range(n
- 1)]
194 c
= [False for i
in range(n
)]
195 d
= [False for i
in range(n
- 1)]
197 for i
in range(n
- 2, -1, -1):
198 c
[i
] = z
[i
] - u
[i
] * c
[i
+ 1]
199 b
[i
] = (a
[i
+ 1] - a
[i
]) / h
[i
] - h
[i
] * (c
[i
+ 1] + 2 * c
[i
]) / 3
200 d
[i
] = (c
[i
+ 1] - c
[i
]) / (3 * h
[i
])
201 for i
in range(n
- 1):
202 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
204 for i
in range(len(knots
) - 1):
205 splines
.append([result
[i
], result
[i
+ n
- 1], result
[i
+ (n
- 1) * 2]])
206 if circular
: # cleaning up after hack
208 tknots
= tknots
[4:-4]
213 # calculates linear splines through all given knots
214 def calculate_linear_splines(bm_mod
, tknots
, knots
):
216 for i
in range(len(knots
) - 1):
217 a
= bm_mod
.verts
[knots
[i
]].co
218 b
= bm_mod
.verts
[knots
[i
+ 1]].co
221 u
= tknots
[i
+ 1] - t
222 splines
.append([a
, d
, t
, u
]) # [locStart, locDif, tStart, tDif]
227 # calculate a best-fit plane to the given vertices
228 def calculate_plane(bm_mod
, loop
, method
="best_fit", object=False):
229 # getting the vertex locations
230 locs
= [bm_mod
.verts
[v
].co
.copy() for v
in loop
[0]]
232 # calculating the center of masss
233 com
= mathutils
.Vector()
239 if method
== 'best_fit':
240 # creating the covariance matrix
241 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
246 mat
[0][0] += (loc
[0] - x
) ** 2
247 mat
[1][0] += (loc
[0] - x
) * (loc
[1] - y
)
248 mat
[2][0] += (loc
[0] - x
) * (loc
[2] - z
)
249 mat
[0][1] += (loc
[1] - y
) * (loc
[0] - x
)
250 mat
[1][1] += (loc
[1] - y
) ** 2
251 mat
[2][1] += (loc
[1] - y
) * (loc
[2] - z
)
252 mat
[0][2] += (loc
[2] - z
) * (loc
[0] - x
)
253 mat
[1][2] += (loc
[2] - z
) * (loc
[1] - y
)
254 mat
[2][2] += (loc
[2] - z
) ** 2
256 # calculating the normal to the plane
259 mat
= matrix_invert(mat
)
262 if math
.fabs(sum(mat
[0])) < math
.fabs(sum(mat
[1])):
263 if math
.fabs(sum(mat
[0])) < math
.fabs(sum(mat
[2])):
265 elif math
.fabs(sum(mat
[1])) < math
.fabs(sum(mat
[2])):
268 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
270 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
272 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
274 # warning! this is different from .normalize()
276 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
277 for i
in range(itermax
):
285 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
288 elif method
== 'normal':
289 # averaging the vertex normals
290 v_normals
= [bm_mod
.verts
[v
].normal
for v
in loop
[0]]
291 normal
= mathutils
.Vector()
292 for v_normal
in v_normals
:
294 normal
/= len(v_normals
)
297 elif method
== 'view':
298 # calculate view normal
299 rotation
= bpy
.context
.space_data
.region_3d
.view_matrix
.to_3x3().\
301 normal
= rotation
* mathutils
.Vector((0.0, 0.0, 1.0))
303 normal
= object.matrix_world
.inverted().to_euler().to_matrix() * \
309 # calculate splines based on given interpolation method (controller function)
310 def calculate_splines(interpolation
, bm_mod
, tknots
, knots
):
311 if interpolation
== 'cubic':
312 splines
= calculate_cubic_splines(bm_mod
, tknots
, knots
[:])
313 else: # interpolations == 'linear'
314 splines
= calculate_linear_splines(bm_mod
, tknots
, knots
[:])
319 # check loops and only return valid ones
320 def check_loops(loops
, mapping
, bm_mod
):
322 for loop
, circular
in loops
:
323 # loop needs to have at least 3 vertices
326 # loop needs at least 1 vertex in the original, non-mirrored mesh
330 if mapping
[vert
] > -1:
335 # vertices can not all be at the same location
337 for i
in range(len(loop
) - 1):
338 if (bm_mod
.verts
[loop
[i
]].co
- bm_mod
.verts
[loop
[i
+ 1]].co
).length
> 1e-6:
343 # passed all tests, loop is valid
344 valid_loops
.append([loop
, circular
])
349 # input: bmesh, output: dict with the edge-key as key and face-index as value
350 def dict_edge_faces(bm
):
351 edge_faces
= dict([[edgekey(edge
), []] for edge
in bm
.edges
if not edge
.hide
])
352 for face
in bm
.faces
:
355 for key
in face_edgekeys(face
):
356 edge_faces
[key
].append(face
.index
)
361 # input: bmesh (edge-faces optional), output: dict with face-face connections
362 def dict_face_faces(bm
, edge_faces
=False):
364 edge_faces
= dict_edge_faces(bm
)
366 connected_faces
= dict([[face
.index
, []] for face
in bm
.faces
if not face
.hide
])
367 for face
in bm
.faces
:
370 for edge_key
in face_edgekeys(face
):
371 for connected_face
in edge_faces
[edge_key
]:
372 if connected_face
== face
.index
:
374 connected_faces
[face
.index
].append(connected_face
)
376 return(connected_faces
)
379 # input: bmesh, output: dict with the vert index as key and edge-keys as value
380 def dict_vert_edges(bm
):
381 vert_edges
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
382 for edge
in bm
.edges
:
387 vert_edges
[vert
].append(ek
)
392 # input: bmesh, output: dict with the vert index as key and face index as value
393 def dict_vert_faces(bm
):
394 vert_faces
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
395 for face
in bm
.faces
:
397 for vert
in face
.verts
:
398 vert_faces
[vert
.index
].append(face
.index
)
403 # input: list of edge-keys, output: dictionary with vertex-vertex connections
404 def dict_vert_verts(edge_keys
):
405 # create connection data
409 if ek
[i
] in vert_verts
:
410 vert_verts
[ek
[i
]].append(ek
[1 - i
])
412 vert_verts
[ek
[i
]] = [ek
[1 - i
]]
417 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
419 return(tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])))
422 # returns the edgekeys of a bmesh face
423 def face_edgekeys(face
):
424 return([tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])) for edge
in face
.edges
])
427 # calculate input loops
428 def get_connected_input(object, bm
, scene
, input):
429 # get mesh with modifiers applied
430 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
432 # calculate selected loops
433 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and not edge
.hide
]
434 loops
= get_connected_selections(edge_keys
)
436 # if only selected loops are needed, we're done
437 if input == 'selected':
438 return(derived
, bm_mod
, loops
)
439 # elif input == 'all':
440 loops
= get_parallel_loops(bm_mod
, loops
)
442 return(derived
, bm_mod
, loops
)
445 # sorts all edge-keys into a list of loops
446 def get_connected_selections(edge_keys
):
447 # create connection data
448 vert_verts
= dict_vert_verts(edge_keys
)
450 # find loops consisting of connected selected edges
452 while len(vert_verts
) > 0:
453 loop
= [iter(vert_verts
.keys()).__next
__()]
459 # no more connection data for current vertex
460 if loop
[-1] not in vert_verts
:
468 for i
, next_vert
in enumerate(vert_verts
[loop
[-1]]):
469 if next_vert
not in loop
:
470 vert_verts
[loop
[-1]].pop(i
)
471 if len(vert_verts
[loop
[-1]]) == 0:
472 del vert_verts
[loop
[-1]]
473 # remove connection both ways
474 if next_vert
in vert_verts
:
475 if len(vert_verts
[next_vert
]) == 1:
476 del vert_verts
[next_vert
]
478 vert_verts
[next_vert
].remove(loop
[-1])
479 loop
.append(next_vert
)
483 # found one end of the loop, continue with next
487 # found both ends of the loop, stop growing
491 # check if loop is circular
492 if loop
[0] in vert_verts
:
493 if loop
[-1] in vert_verts
[loop
[0]]:
495 if len(vert_verts
[loop
[0]]) == 1:
496 del vert_verts
[loop
[0]]
498 vert_verts
[loop
[0]].remove(loop
[-1])
499 if len(vert_verts
[loop
[-1]]) == 1:
500 del vert_verts
[loop
[-1]]
502 vert_verts
[loop
[-1]].remove(loop
[0])
516 # get the derived mesh data, if there is a mirror modifier
517 def get_derived_bmesh(object, bm
, scene
):
518 # check for mirror modifiers
519 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
521 # disable other modifiers
522 show_viewport
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport
]
523 for mod
in object.modifiers
:
524 if mod
.type != 'MIRROR':
525 mod
.show_viewport
= False
528 mesh_mod
= object.to_mesh(scene
, True, 'PREVIEW')
529 bm_mod
.from_mesh(mesh_mod
)
530 bpy
.context
.blend_data
.meshes
.remove(mesh_mod
)
531 # re-enable other modifiers
532 for mod_name
in show_viewport
:
533 object.modifiers
[mod_name
].show_viewport
= True
534 # no mirror modifiers, so no derived mesh necessary
539 bm_mod
.verts
.ensure_lookup_table()
540 bm_mod
.edges
.ensure_lookup_table()
541 bm_mod
.faces
.ensure_lookup_table()
543 return(derived
, bm_mod
)
546 # return a mapping of derived indices to indices
547 def get_mapping(derived
, bm
, bm_mod
, single_vertices
, full_search
, loops
):
552 verts
= [v
for v
in bm
.verts
if not v
.hide
]
554 verts
= [v
for v
in bm
.verts
if v
.select
and not v
.hide
]
556 # non-selected vertices around single vertices also need to be mapped
558 mapping
= dict([[vert
, -1] for vert
in single_vertices
])
559 verts_mod
= [bm_mod
.verts
[vert
] for vert
in single_vertices
]
561 for v_mod
in verts_mod
:
562 if (v
.co
- v_mod
.co
).length
< 1e-6:
563 mapping
[v_mod
.index
] = v
.index
565 real_singles
= [v_real
for v_real
in mapping
.values() if v_real
> -1]
567 verts_indices
= [vert
.index
for vert
in verts
]
568 for face
in [face
for face
in bm
.faces
if not face
.select
and not face
.hide
]:
569 for vert
in face
.verts
:
570 if vert
.index
in real_singles
:
572 if v
.index
not in verts_indices
:
577 # create mapping of derived indices to indices
578 mapping
= dict([[vert
, -1] for loop
in loops
for vert
in loop
[0]])
580 for single
in single_vertices
:
582 verts_mod
= [bm_mod
.verts
[i
] for i
in mapping
.keys()]
584 for v_mod
in verts_mod
:
585 if (v
.co
- v_mod
.co
).length
< 1e-6:
586 mapping
[v_mod
.index
] = v
.index
587 verts_mod
.remove(v_mod
)
593 # calculate the determinant of a matrix
594 def matrix_determinant(m
):
595 determinant
= m
[0][0] * m
[1][1] * m
[2][2] + m
[0][1] * m
[1][2] * m
[2][0] \
596 + m
[0][2] * m
[1][0] * m
[2][1] - m
[0][2] * m
[1][1] * m
[2][0] \
597 - m
[0][1] * m
[1][0] * m
[2][2] - m
[0][0] * m
[1][2] * m
[2][1]
602 # custom matrix inversion, to provide higher precision than the built-in one
603 def matrix_invert(m
):
604 r
= mathutils
.Matrix((
605 (m
[1][1] * m
[2][2] - m
[1][2] * m
[2][1], m
[0][2] * m
[2][1] - m
[0][1] * m
[2][2],
606 m
[0][1] * m
[1][2] - m
[0][2] * m
[1][1]),
607 (m
[1][2] * m
[2][0] - m
[1][0] * m
[2][2], m
[0][0] * m
[2][2] - m
[0][2] * m
[2][0],
608 m
[0][2] * m
[1][0] - m
[0][0] * m
[1][2]),
609 (m
[1][0] * m
[2][1] - m
[1][1] * m
[2][0], m
[0][1] * m
[2][0] - m
[0][0] * m
[2][1],
610 m
[0][0] * m
[1][1] - m
[0][1] * m
[1][0])))
612 return (r
* (1 / matrix_determinant(m
)))
615 # returns a list of all loops parallel to the input, input included
616 def get_parallel_loops(bm_mod
, loops
):
617 # get required dictionaries
618 edge_faces
= dict_edge_faces(bm_mod
)
619 connected_faces
= dict_face_faces(bm_mod
, edge_faces
)
620 # turn vertex loops into edge loops
623 edgeloop
= [[sorted([loop
[0][i
], loop
[0][i
+ 1]]) for i
in
624 range(len(loop
[0]) - 1)], loop
[1]]
625 if loop
[1]: # circular
626 edgeloop
[0].append(sorted([loop
[0][-1], loop
[0][0]]))
627 edgeloops
.append(edgeloop
[:])
628 # variables to keep track while iterating
632 for loop
in edgeloops
:
633 # initialise with original loop
634 all_edgeloops
.append(loop
[0])
638 if edge
[0] not in verts_used
:
639 verts_used
.append(edge
[0])
640 if edge
[1] not in verts_used
:
641 verts_used
.append(edge
[1])
643 # find parallel loops
644 while len(newloops
) > 0:
647 for i
in newloops
[-1]:
649 forbidden_side
= False
650 if i
not in edge_faces
:
651 # weird input with branches
654 for face
in edge_faces
[i
]:
655 if len(side_a
) == 0 and forbidden_side
!= "a":
661 elif side_a
[-1] in connected_faces
[face
] and \
662 forbidden_side
!= "a":
668 if len(side_b
) == 0 and forbidden_side
!= "b":
674 elif side_b
[-1] in connected_faces
[face
] and \
675 forbidden_side
!= "b":
683 # weird input with branches
696 for key
in face_edgekeys(bm_mod
.faces
[fi
]):
697 if key
[0] not in verts_used
and key
[1] not in \
699 extraloop
.append(key
)
702 for key
in extraloop
:
704 if new_vert
not in verts_used
:
705 verts_used
.append(new_vert
)
706 newloops
.append(extraloop
)
707 all_edgeloops
.append(extraloop
)
709 # input contains branches, only return selected loop
713 # change edgeloops into normal loops
715 for edgeloop
in all_edgeloops
:
717 # grow loop by comparing vertices between consecutive edge-keys
718 for i
in range(len(edgeloop
) - 1):
719 for vert
in range(2):
720 if edgeloop
[i
][vert
] in edgeloop
[i
+ 1]:
721 loop
.append(edgeloop
[i
][vert
])
724 # add starting vertex
725 for vert
in range(2):
726 if edgeloop
[0][vert
] != loop
[0]:
727 loop
= [edgeloop
[0][vert
]] + loop
730 for vert
in range(2):
731 if edgeloop
[-1][vert
] != loop
[-1]:
732 loop
.append(edgeloop
[-1][vert
])
734 # check if loop is circular
735 if loop
[0] == loop
[-1]:
740 loops
.append([loop
, circular
])
745 # gather initial data
747 global_undo
= bpy
.context
.user_preferences
.edit
.use_global_undo
748 bpy
.context
.user_preferences
.edit
.use_global_undo
= False
749 object = bpy
.context
.active_object
750 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
751 # ensure that selection is synced for the derived mesh
752 bpy
.ops
.object.mode_set(mode
='OBJECT')
753 bpy
.ops
.object.mode_set(mode
='EDIT')
754 bm
= bmesh
.from_edit_mesh(object.data
)
756 bm
.verts
.ensure_lookup_table()
757 bm
.edges
.ensure_lookup_table()
758 bm
.faces
.ensure_lookup_table()
760 return(global_undo
, object, bm
)
763 # move the vertices to their new locations
764 def move_verts(object, bm
, mapping
, move
, lock
, influence
):
766 lock_x
, lock_y
, lock_z
= lock
767 orientation
= bpy
.context
.space_data
.transform_orientation
768 custom
= bpy
.context
.space_data
.current_orientation
770 mat
= custom
.matrix
.to_4x4().inverted() * object.matrix_world
.copy()
771 elif orientation
== 'LOCAL':
772 mat
= mathutils
.Matrix
.Identity(4)
773 elif orientation
== 'VIEW':
774 mat
= bpy
.context
.region_data
.view_matrix
.copy() * \
775 object.matrix_world
.copy()
776 else: # orientation == 'GLOBAL'
777 mat
= object.matrix_world
.copy()
778 mat_inv
= mat
.inverted()
781 for index
, loc
in loop
:
783 if mapping
[index
] == -1:
786 index
= mapping
[index
]
788 delta
= (loc
- bm
.verts
[index
].co
) * mat_inv
796 loc
= bm
.verts
[index
].co
+ delta
800 new_loc
= loc
* (influence
/ 100) + \
801 bm
.verts
[index
].co
* ((100 - influence
) / 100)
802 bm
.verts
[index
].co
= new_loc
806 bm
.verts
.ensure_lookup_table()
807 bm
.edges
.ensure_lookup_table()
808 bm
.faces
.ensure_lookup_table()
811 # load custom tool settings
812 def settings_load(self
):
813 lt
= bpy
.context
.window_manager
.looptools
814 tool
= self
.name
.split()[0].lower()
815 keys
= self
.as_keywords().keys()
817 setattr(self
, key
, getattr(lt
, tool
+ "_" + key
))
820 # store custom tool settings
821 def settings_write(self
):
822 lt
= bpy
.context
.window_manager
.looptools
823 tool
= self
.name
.split()[0].lower()
824 keys
= self
.as_keywords().keys()
826 setattr(lt
, tool
+ "_" + key
, getattr(self
, key
))
829 # clean up and set settings back to original state
830 def terminate(global_undo
):
831 # update editmesh cached data
832 obj
= bpy
.context
.active_object
833 if obj
.mode
== 'EDIT':
834 bmesh
.update_edit_mesh(obj
.data
, tessface
=True, destructive
=True)
836 bpy
.context
.user_preferences
.edit
.use_global_undo
= global_undo
839 # ########################################
840 # ##### Bridge functions #################
841 # ########################################
843 # calculate a cubic spline through the middle section of 4 given coordinates
844 def bridge_calculate_cubic_spline(bm
, coordinates
):
850 for i
in coordinates
:
851 a
.append(float(i
[j
]))
854 h
.append(x
[i
+ 1] - x
[i
])
856 for i
in range(1, 3):
857 q
.append(3.0 / h
[i
] * (a
[i
+ 1] - a
[i
]) - 3.0 / h
[i
- 1] * (a
[i
] - a
[i
- 1]))
861 for i
in range(1, 3):
862 l
.append(2.0 * (x
[i
+ 1] - x
[i
- 1]) - h
[i
- 1] * u
[i
- 1])
863 u
.append(h
[i
] / l
[i
])
864 z
.append((q
[i
] - h
[i
- 1] * z
[i
- 1]) / l
[i
])
867 b
= [False for i
in range(3)]
868 c
= [False for i
in range(4)]
869 d
= [False for i
in range(3)]
871 for i
in range(2, -1, -1):
872 c
[i
] = z
[i
] - u
[i
] * c
[i
+ 1]
873 b
[i
] = (a
[i
+ 1] - a
[i
]) / h
[i
] - h
[i
] * (c
[i
+ 1] + 2.0 * c
[i
]) / 3.0
874 d
[i
] = (c
[i
+ 1] - c
[i
]) / (3.0 * h
[i
])
876 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
877 spline
= [result
[1], result
[4], result
[7]]
882 # return a list with new vertex location vectors, a list with face vertex
883 # integers, and the highest vertex integer in the virtual mesh
884 def bridge_calculate_geometry(bm
, lines
, vertex_normals
, segments
,
885 interpolation
, cubic_strength
, min_width
, max_vert_index
):
889 # calculate location based on interpolation method
890 def get_location(line
, segment
, splines
):
891 v1
= bm
.verts
[lines
[line
][0]].co
892 v2
= bm
.verts
[lines
[line
][1]].co
893 if interpolation
== 'linear':
894 return v1
+ (segment
/ segments
) * (v2
- v1
)
895 else: # interpolation == 'cubic'
896 m
= (segment
/ segments
)
897 ax
, bx
, cx
, dx
, tx
= splines
[line
][0]
898 x
= ax
+ bx
* m
+ cx
* m
** 2 + dx
* m
** 3
899 ay
, by
, cy
, dy
, ty
= splines
[line
][1]
900 y
= ay
+ by
* m
+ cy
* m
** 2 + dy
* m
** 3
901 az
, bz
, cz
, dz
, tz
= splines
[line
][2]
902 z
= az
+ bz
* m
+ cz
* m
** 2 + dz
* m
** 3
903 return mathutils
.Vector((x
, y
, z
))
905 # no interpolation needed
907 for i
, line
in enumerate(lines
):
908 if i
< len(lines
) - 1:
909 faces
.append([line
[0], lines
[i
+ 1][0], lines
[i
+ 1][1], line
[1]])
910 # more than 1 segment, interpolate
912 # calculate splines (if necessary) once, so no recalculations needed
913 if interpolation
== 'cubic':
916 v1
= bm
.verts
[line
[0]].co
917 v2
= bm
.verts
[line
[1]].co
918 size
= (v2
- v1
).length
* cubic_strength
919 splines
.append(bridge_calculate_cubic_spline(bm
,
920 [v1
+ size
* vertex_normals
[line
[0]], v1
, v2
,
921 v2
+ size
* vertex_normals
[line
[1]]]))
925 # create starting situation
926 virtual_width
= [(bm
.verts
[lines
[i
][0]].co
-
927 bm
.verts
[lines
[i
+ 1][0]].co
).length
for i
928 in range(len(lines
) - 1)]
929 new_verts
= [get_location(0, seg
, splines
) for seg
in range(1,
931 first_line_indices
= [i
for i
in range(max_vert_index
+ 1,
932 max_vert_index
+ segments
)]
934 prev_verts
= new_verts
[:] # vertex locations of verts on previous line
935 prev_vert_indices
= first_line_indices
[:]
936 max_vert_index
+= segments
- 1 # highest vertex index in virtual mesh
937 next_verts
= [] # vertex locations of verts on current line
938 next_vert_indices
= []
940 for i
, line
in enumerate(lines
):
941 if i
< len(lines
) - 1:
945 for seg
in range(1, segments
):
946 loc1
= prev_verts
[seg
- 1]
947 loc2
= get_location(i
+ 1, seg
, splines
)
948 if (loc1
- loc2
).length
< (min_width
/ 100) * virtual_width
[i
] \
949 and line
[1] == lines
[i
+ 1][1]:
950 # triangle, no new vertex
951 faces
.append([v1
, v2
, prev_vert_indices
[seg
- 1],
952 prev_vert_indices
[seg
- 1]])
953 next_verts
+= prev_verts
[seg
- 1:]
954 next_vert_indices
+= prev_vert_indices
[seg
- 1:]
958 if i
== len(lines
) - 2 and lines
[0] == lines
[-1]:
959 # quad with first line, no new vertex
960 faces
.append([v1
, v2
, first_line_indices
[seg
- 1],
961 prev_vert_indices
[seg
- 1]])
962 v2
= first_line_indices
[seg
- 1]
963 v1
= prev_vert_indices
[seg
- 1]
965 # quad, add new vertex
967 faces
.append([v1
, v2
, max_vert_index
,
968 prev_vert_indices
[seg
- 1]])
970 v1
= prev_vert_indices
[seg
- 1]
971 new_verts
.append(loc2
)
972 next_verts
.append(loc2
)
973 next_vert_indices
.append(max_vert_index
)
975 faces
.append([v1
, v2
, lines
[i
+ 1][1], line
[1]])
977 prev_verts
= next_verts
[:]
978 prev_vert_indices
= next_vert_indices
[:]
980 next_vert_indices
= []
982 return(new_verts
, faces
, max_vert_index
)
985 # calculate lines (list of lists, vertex indices) that are used for bridging
986 def bridge_calculate_lines(bm
, loops
, mode
, twist
, reverse
):
988 loop1
, loop2
= [i
[0] for i
in loops
]
989 loop1_circular
, loop2_circular
= [i
[1] for i
in loops
]
990 circular
= loop1_circular
or loop2_circular
993 # calculate loop centers
995 for loop
in [loop1
, loop2
]:
996 center
= mathutils
.Vector()
998 center
+= bm
.verts
[vertex
].co
1000 centers
.append(center
)
1001 for i
, loop
in enumerate([loop1
, loop2
]):
1003 if bm
.verts
[vertex
].co
== centers
[i
]:
1004 # prevent zero-length vectors in angle comparisons
1005 centers
[i
] += mathutils
.Vector((0.01, 0, 0))
1007 center1
, center2
= centers
1009 # calculate the normals of the virtual planes that the loops are on
1011 normal_plurity
= False
1012 for i
, loop
in enumerate([loop1
, loop2
]):
1014 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1017 x
, y
, z
= centers
[i
]
1018 for loc
in [bm
.verts
[vertex
].co
for vertex
in loop
]:
1019 mat
[0][0] += (loc
[0] - x
) ** 2
1020 mat
[1][0] += (loc
[0] - x
) * (loc
[1] - y
)
1021 mat
[2][0] += (loc
[0] - x
) * (loc
[2] - z
)
1022 mat
[0][1] += (loc
[1] - y
) * (loc
[0] - x
)
1023 mat
[1][1] += (loc
[1] - y
) ** 2
1024 mat
[2][1] += (loc
[1] - y
) * (loc
[2] - z
)
1025 mat
[0][2] += (loc
[2] - z
) * (loc
[0] - x
)
1026 mat
[1][2] += (loc
[2] - z
) * (loc
[1] - y
)
1027 mat
[2][2] += (loc
[2] - z
) ** 2
1030 if sum(mat
[0]) < 1e-6 or sum(mat
[1]) < 1e-6 or sum(mat
[2]) < 1e-6:
1031 normal_plurity
= True
1035 if sum(mat
[0]) == 0:
1036 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
1037 elif sum(mat
[1]) == 0:
1038 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
1039 elif sum(mat
[2]) == 0:
1040 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
1042 # warning! this is different from .normalize()
1045 vec
= mathutils
.Vector((1.0, 1.0, 1.0))
1046 vec2
= (mat
* vec
) / (mat
* vec
).length
1047 while vec
!= vec2
and iter < itermax
:
1051 if vec2
.length
!= 0:
1053 if vec2
.length
== 0:
1054 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
1056 normals
.append(normal
)
1057 # have plane normals face in the same direction (maximum angle: 90 degrees)
1058 if ((center1
+ normals
[0]) - center2
).length
< \
1059 ((center1
- normals
[0]) - center2
).length
:
1061 if ((center2
+ normals
[1]) - center1
).length
> \
1062 ((center2
- normals
[1]) - center1
).length
:
1065 # rotation matrix, representing the difference between the plane normals
1066 axis
= normals
[0].cross(normals
[1])
1067 axis
= mathutils
.Vector([loc
if abs(loc
) > 1e-8 else 0 for loc
in axis
])
1068 if axis
.angle(mathutils
.Vector((0, 0, 1)), 0) > 1.5707964:
1070 angle
= normals
[0].dot(normals
[1])
1071 rotation_matrix
= mathutils
.Matrix
.Rotation(angle
, 4, axis
)
1073 # if circular, rotate loops so they are aligned
1075 # make sure loop1 is the circular one (or both are circular)
1076 if loop2_circular
and not loop1_circular
:
1077 loop1_circular
, loop2_circular
= True, False
1078 loop1
, loop2
= loop2
, loop1
1080 # match start vertex of loop1 with loop2
1081 target_vector
= bm
.verts
[loop2
[0]].co
- center2
1082 dif_angles
= [[(rotation_matrix
* (bm
.verts
[vertex
].co
- center1
)
1083 ).angle(target_vector
, 0), False, i
] for
1084 i
, vertex
in enumerate(loop1
)]
1086 if len(loop1
) != len(loop2
):
1087 angle_limit
= dif_angles
[0][0] * 1.2 # 20% margin
1089 [(bm
.verts
[loop2
[0]].co
-
1090 bm
.verts
[loop1
[index
]].co
).length
, angle
, index
] for
1091 angle
, distance
, index
in dif_angles
if angle
<= angle_limit
1094 loop1
= loop1
[dif_angles
[0][2]:] + loop1
[:dif_angles
[0][2]]
1096 # have both loops face the same way
1097 if normal_plurity
and not circular
:
1098 second_to_first
, second_to_second
, second_to_last
= [
1099 (bm
.verts
[loop1
[1]].co
- center1
).angle(
1100 bm
.verts
[loop2
[i
]].co
- center2
) for i
in [0, 1, -1]
1102 last_to_first
, last_to_second
= [
1103 (bm
.verts
[loop1
[-1]].co
-
1104 center1
).angle(bm
.verts
[loop2
[i
]].co
- center2
) for
1107 if (min(last_to_first
, last_to_second
) * 1.1 < min(second_to_first
,
1108 second_to_second
)) or (loop2_circular
and second_to_last
* 1.1 <
1109 min(second_to_first
, second_to_second
)):
1112 loop1
= [loop1
[-1]] + loop1
[:-1]
1114 angle
= (bm
.verts
[loop1
[0]].co
- center1
).\
1115 cross(bm
.verts
[loop1
[1]].co
- center1
).angle(normals
[0], 0)
1116 target_angle
= (bm
.verts
[loop2
[0]].co
- center2
).\
1117 cross(bm
.verts
[loop2
[1]].co
- center2
).angle(normals
[1], 0)
1118 limit
= 1.5707964 # 0.5*pi, 90 degrees
1119 if not ((angle
> limit
and target_angle
> limit
) or
1120 (angle
< limit
and target_angle
< limit
)):
1123 loop1
= [loop1
[-1]] + loop1
[:-1]
1124 elif normals
[0].angle(normals
[1]) > limit
:
1127 loop1
= [loop1
[-1]] + loop1
[:-1]
1129 # both loops have the same length
1130 if len(loop1
) == len(loop2
):
1133 if abs(twist
) < len(loop1
):
1134 loop1
= loop1
[twist
:] + loop1
[:twist
]
1138 lines
.append([loop1
[0], loop2
[0]])
1139 for i
in range(1, len(loop1
)):
1140 lines
.append([loop1
[i
], loop2
[i
]])
1142 # loops of different lengths
1144 # make loop1 longest loop
1145 if len(loop2
) > len(loop1
):
1146 loop1
, loop2
= loop2
, loop1
1147 loop1_circular
, loop2_circular
= loop2_circular
, loop1_circular
1151 if abs(twist
) < len(loop1
):
1152 loop1
= loop1
[twist
:] + loop1
[:twist
]
1156 # shortest angle difference doesn't always give correct start vertex
1157 if loop1_circular
and not loop2_circular
:
1160 if len(loop1
) - shifting
< len(loop2
):
1163 to_last
, to_first
= [
1164 (rotation_matrix
* (bm
.verts
[loop1
[-1]].co
- center1
)).angle(
1165 (bm
.verts
[loop2
[i
]].co
- center2
), 0) for i
in [-1, 0]
1167 if to_first
< to_last
:
1168 loop1
= [loop1
[-1]] + loop1
[:-1]
1174 # basic shortest side first
1176 lines
.append([loop1
[0], loop2
[0]])
1177 for i
in range(1, len(loop1
)):
1178 if i
>= len(loop2
) - 1:
1180 lines
.append([loop1
[i
], loop2
[-1]])
1183 lines
.append([loop1
[i
], loop2
[i
]])
1185 # shortest edge algorithm
1186 else: # mode == 'shortest'
1187 lines
.append([loop1
[0], loop2
[0]])
1189 for i
in range(len(loop1
) - 1):
1190 if prev_vert2
== len(loop2
) - 1 and not loop2_circular
:
1191 # force triangles, reached end of loop2
1193 elif prev_vert2
== len(loop2
) - 1 and loop2_circular
:
1194 # at end of loop2, but circular, so check with first vert
1195 tri
, quad
= [(bm
.verts
[loop1
[i
+ 1]].co
-
1196 bm
.verts
[loop2
[j
]].co
).length
1197 for j
in [prev_vert2
, 0]]
1199 elif len(loop1
) - 1 - i
== len(loop2
) - 1 - prev_vert2
and \
1201 # force quads, otherwise won't make it to end of loop2
1204 # calculate if tri or quad gives shortest edge
1205 tri
, quad
= [(bm
.verts
[loop1
[i
+ 1]].co
-
1206 bm
.verts
[loop2
[j
]].co
).length
1207 for j
in range(prev_vert2
, prev_vert2
+ 2)]
1211 lines
.append([loop1
[i
+ 1], loop2
[prev_vert2
]])
1212 if circle_full
== 2:
1215 elif not circle_full
:
1216 lines
.append([loop1
[i
+ 1], loop2
[prev_vert2
+ 1]])
1218 # quad to first vertex of loop2
1220 lines
.append([loop1
[i
+ 1], loop2
[0]])
1224 # final face for circular loops
1225 if loop1_circular
and loop2_circular
:
1226 lines
.append([loop1
[0], loop2
[0]])
1231 # calculate number of segments needed
1232 def bridge_calculate_segments(bm
, lines
, loops
, segments
):
1233 # return if amount of segments is set by user
1238 average_edge_length
= [
1239 (bm
.verts
[vertex
].co
-
1240 bm
.verts
[loop
[0][i
+ 1]].co
).length
for loop
in loops
for
1241 i
, vertex
in enumerate(loop
[0][:-1])
1243 # closing edges of circular loops
1244 average_edge_length
+= [
1245 (bm
.verts
[loop
[0][-1]].co
-
1246 bm
.verts
[loop
[0][0]].co
).length
for loop
in loops
if loop
[1]
1250 average_edge_length
= sum(average_edge_length
) / len(average_edge_length
)
1251 average_bridge_length
= sum(
1253 bm
.verts
[v2
].co
).length
for v1
, v2
in lines
]
1256 segments
= max(1, round(average_bridge_length
/ average_edge_length
))
1261 # return dictionary with vertex index as key, and the normal vector as value
1262 def bridge_calculate_virtual_vertex_normals(bm
, lines
, loops
, edge_faces
,
1264 if not edge_faces
: # interpolation isn't set to cubic
1267 # pity reduce() isn't one of the basic functions in python anymore
1268 def average_vector_dictionary(dic
):
1269 for key
, vectors
in dic
.items():
1270 # if type(vectors) == type([]) and len(vectors) > 1:
1271 if len(vectors
) > 1:
1272 average
= mathutils
.Vector()
1273 for vector
in vectors
:
1275 average
/= len(vectors
)
1276 dic
[key
] = [average
]
1279 # get all edges of the loop
1281 [edgekey_to_edge
[tuple(sorted([loops
[j
][0][i
],
1282 loops
[j
][0][i
+ 1]]))] for i
in range(len(loops
[j
][0]) - 1)] for
1285 edges
= edges
[0] + edges
[1]
1287 if loops
[j
][1]: # circular
1288 edges
.append(edgekey_to_edge
[tuple(sorted([loops
[j
][0][0],
1289 loops
[j
][0][-1]]))])
1292 calculation based on face topology (assign edge-normals to vertices)
1294 edge_normal = face_normal x edge_vector
1295 vertex_normal = average(edge_normals)
1297 vertex_normals
= dict([(vertex
, []) for vertex
in loops
[0][0] + loops
[1][0]])
1299 faces
= edge_faces
[edgekey(edge
)] # valid faces connected to edge
1302 # get edge coordinates
1303 v1
, v2
= [bm
.verts
[edgekey(edge
)[i
]].co
for i
in [0, 1]]
1304 edge_vector
= v1
- v2
1305 if edge_vector
.length
< 1e-4:
1306 # zero-length edge, vertices at same location
1308 edge_center
= (v1
+ v2
) / 2
1310 # average face coordinates, if connected to more than 1 valid face
1312 face_normal
= mathutils
.Vector()
1313 face_center
= mathutils
.Vector()
1315 face_normal
+= face
.normal
1316 face_center
+= face
.calc_center_median()
1317 face_normal
/= len(faces
)
1318 face_center
/= len(faces
)
1320 face_normal
= faces
[0].normal
1321 face_center
= faces
[0].calc_center_median()
1322 if face_normal
.length
< 1e-4:
1323 # faces with a surface of 0 have no face normal
1326 # calculate virtual edge normal
1327 edge_normal
= edge_vector
.cross(face_normal
)
1328 edge_normal
.length
= 0.01
1329 if (face_center
- (edge_center
+ edge_normal
)).length
> \
1330 (face_center
- (edge_center
- edge_normal
)).length
:
1331 # make normal face the correct way
1332 edge_normal
.negate()
1333 edge_normal
.normalize()
1334 # add virtual edge normal as entry for both vertices it connects
1335 for vertex
in edgekey(edge
):
1336 vertex_normals
[vertex
].append(edge_normal
)
1339 calculation based on connection with other loop (vertex focused method)
1340 - used for vertices that aren't connected to any valid faces
1342 plane_normal = edge_vector x connection_vector
1343 vertex_normal = plane_normal x edge_vector
1346 vertex
for vertex
, normal
in vertex_normals
.items() if not normal
1350 # edge vectors connected to vertices
1351 edge_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1353 for v
in edgekey(edge
):
1354 if v
in edge_vectors
:
1355 edge_vector
= bm
.verts
[edgekey(edge
)[0]].co
- \
1356 bm
.verts
[edgekey(edge
)[1]].co
1357 if edge_vector
.length
< 1e-4:
1358 # zero-length edge, vertices at same location
1360 edge_vectors
[v
].append(edge_vector
)
1362 # connection vectors between vertices of both loops
1363 connection_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1364 connections
= dict([[vertex
, []] for vertex
in vertices
])
1365 for v1
, v2
in lines
:
1366 if v1
in connection_vectors
or v2
in connection_vectors
:
1367 new_vector
= bm
.verts
[v1
].co
- bm
.verts
[v2
].co
1368 if new_vector
.length
< 1e-4:
1369 # zero-length connection vector,
1370 # vertices in different loops at same location
1372 if v1
in connection_vectors
:
1373 connection_vectors
[v1
].append(new_vector
)
1374 connections
[v1
].append(v2
)
1375 if v2
in connection_vectors
:
1376 connection_vectors
[v2
].append(new_vector
)
1377 connections
[v2
].append(v1
)
1378 connection_vectors
= average_vector_dictionary(connection_vectors
)
1379 connection_vectors
= dict(
1380 [[vertex
, vector
[0]] if vector
else
1381 [vertex
, []] for vertex
, vector
in connection_vectors
.items()]
1384 for vertex
, values
in edge_vectors
.items():
1385 # vertex normal doesn't matter, just assign a random vector to it
1386 if not connection_vectors
[vertex
]:
1387 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1390 # calculate to what location the vertex is connected,
1391 # used to determine what way to flip the normal
1392 connected_center
= mathutils
.Vector()
1393 for v
in connections
[vertex
]:
1394 connected_center
+= bm
.verts
[v
].co
1395 if len(connections
[vertex
]) > 1:
1396 connected_center
/= len(connections
[vertex
])
1397 if len(connections
[vertex
]) == 0:
1398 # shouldn't be possible, but better safe than sorry
1399 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1402 # can't do proper calculations, because of zero-length vector
1404 if (connected_center
- (bm
.verts
[vertex
].co
+
1405 connection_vectors
[vertex
])).length
< (connected_center
-
1406 (bm
.verts
[vertex
].co
- connection_vectors
[vertex
])).length
:
1407 connection_vectors
[vertex
].negate()
1408 vertex_normals
[vertex
] = [connection_vectors
[vertex
].normalized()]
1411 # calculate vertex normals using edge-vectors,
1412 # connection-vectors and the derived plane normal
1413 for edge_vector
in values
:
1414 plane_normal
= edge_vector
.cross(connection_vectors
[vertex
])
1415 vertex_normal
= edge_vector
.cross(plane_normal
)
1416 vertex_normal
.length
= 0.1
1417 if (connected_center
- (bm
.verts
[vertex
].co
+
1418 vertex_normal
)).length
< (connected_center
-
1419 (bm
.verts
[vertex
].co
- vertex_normal
)).length
:
1420 # make normal face the correct way
1421 vertex_normal
.negate()
1422 vertex_normal
.normalize()
1423 vertex_normals
[vertex
].append(vertex_normal
)
1425 # average virtual vertex normals, based on all edges it's connected to
1426 vertex_normals
= average_vector_dictionary(vertex_normals
)
1427 vertex_normals
= dict([[vertex
, vector
[0]] for vertex
, vector
in vertex_normals
.items()])
1429 return(vertex_normals
)
1432 # add vertices to mesh
1433 def bridge_create_vertices(bm
, vertices
):
1434 for i
in range(len(vertices
)):
1435 bm
.verts
.new(vertices
[i
])
1436 bm
.verts
.ensure_lookup_table()
1440 def bridge_create_faces(object, bm
, faces
, twist
):
1441 # have the normal point the correct way
1443 [face
.reverse() for face
in faces
]
1444 faces
= [face
[2:] + face
[:2] if face
[0] == face
[1] else face
for face
in faces
]
1446 # eekadoodle prevention
1447 for i
in range(len(faces
)):
1448 if not faces
[i
][-1]:
1449 if faces
[i
][0] == faces
[i
][-1]:
1450 faces
[i
] = [faces
[i
][1], faces
[i
][2], faces
[i
][3], faces
[i
][1]]
1452 faces
[i
] = [faces
[i
][-1]] + faces
[i
][:-1]
1453 # result of converting from pre-bmesh period
1454 if faces
[i
][-1] == faces
[i
][-2]:
1455 faces
[i
] = faces
[i
][:-1]
1458 for i
in range(len(faces
)):
1459 new_faces
.append(bm
.faces
.new([bm
.verts
[v
] for v
in faces
[i
]]))
1461 object.data
.update(calc_edges
=True) # calc_edges prevents memory-corruption
1463 bm
.verts
.ensure_lookup_table()
1464 bm
.edges
.ensure_lookup_table()
1465 bm
.faces
.ensure_lookup_table()
1470 # calculate input loops
1471 def bridge_get_input(bm
):
1472 # create list of internal edges, which should be skipped
1473 eks_of_selected_faces
= [
1474 item
for sublist
in [face_edgekeys(face
) for
1475 face
in bm
.faces
if face
.select
and not face
.hide
] for item
in sublist
1478 for ek
in eks_of_selected_faces
:
1479 if ek
in edge_count
:
1483 internal_edges
= [ek
for ek
in edge_count
if edge_count
[ek
] > 1]
1485 # sort correct edges into loops
1487 edgekey(edge
) for edge
in bm
.edges
if edge
.select
and
1488 not edge
.hide
and edgekey(edge
) not in internal_edges
1490 loops
= get_connected_selections(selected_edges
)
1495 # return values needed by the bridge operator
1496 def bridge_initialise(bm
, interpolation
):
1497 if interpolation
== 'cubic':
1498 # dict with edge-key as key and list of connected valid faces as value
1500 face
.index
for face
in bm
.faces
if face
.select
or
1504 [[edgekey(edge
), []] for edge
in bm
.edges
if not edge
.hide
]
1506 for face
in bm
.faces
:
1507 if face
.index
in face_blacklist
:
1509 for key
in face_edgekeys(face
):
1510 edge_faces
[key
].append(face
)
1511 # dictionary with the edge-key as key and edge as value
1512 edgekey_to_edge
= dict(
1513 [[edgekey(edge
), edge
] for edge
in bm
.edges
if edge
.select
and not edge
.hide
]
1517 edgekey_to_edge
= False
1519 # selected faces input
1520 old_selected_faces
= [
1521 face
.index
for face
in bm
.faces
if face
.select
and not face
.hide
1524 # find out if faces created by bridging should be smoothed
1527 if sum([face
.smooth
for face
in bm
.faces
]) / len(bm
.faces
) >= 0.5:
1530 return(edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
)
1533 # return a string with the input method
1534 def bridge_input_method(loft
, loft_loop
):
1538 method
= "Loft loop"
1540 method
= "Loft no-loop"
1547 # match up loops in pairs, used for multi-input bridging
1548 def bridge_match_loops(bm
, loops
):
1549 # calculate average loop normals and centers
1552 for vertices
, circular
in loops
:
1553 normal
= mathutils
.Vector()
1554 center
= mathutils
.Vector()
1555 for vertex
in vertices
:
1556 normal
+= bm
.verts
[vertex
].normal
1557 center
+= bm
.verts
[vertex
].co
1558 normals
.append(normal
/ len(vertices
) / 10)
1559 centers
.append(center
/ len(vertices
))
1561 # possible matches if loop normals are faced towards the center
1563 matches
= dict([[i
, []] for i
in range(len(loops
))])
1565 for i
in range(len(loops
) + 1):
1566 for j
in range(i
+ 1, len(loops
)):
1567 if (centers
[i
] - centers
[j
]).length
> \
1568 (centers
[i
] - (centers
[j
] + normals
[j
])).length
and \
1569 (centers
[j
] - centers
[i
]).length
> \
1570 (centers
[j
] - (centers
[i
] + normals
[i
])).length
:
1572 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1573 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1574 # if no loops face each other, just make matches between all the loops
1575 if matches_amount
== 0:
1576 for i
in range(len(loops
) + 1):
1577 for j
in range(i
+ 1, len(loops
)):
1578 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1579 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1580 for key
, value
in matches
.items():
1583 # matches based on distance between centers and number of vertices in loops
1585 for loop_index
in range(len(loops
)):
1586 if loop_index
in new_order
:
1588 loop_matches
= matches
[loop_index
]
1589 if not loop_matches
:
1591 shortest_distance
= loop_matches
[0][0]
1592 shortest_distance
*= 1.1
1594 [abs(len(loops
[loop_index
][0]) -
1595 len(loops
[loop
[2]][0])), loop
[0], loop
[1], loop
[2]] for loop
in
1596 loop_matches
if loop
[0] < shortest_distance
1599 for match
in loop_matches
:
1600 if match
[3] not in new_order
:
1601 new_order
+= [loop_index
, match
[3]]
1604 # reorder loops based on matches
1605 if len(new_order
) >= 2:
1606 loops
= [loops
[i
] for i
in new_order
]
1611 # remove old_selected_faces
1612 def bridge_remove_internal_faces(bm
, old_selected_faces
):
1613 # collect bmesh faces and internal bmesh edges
1614 remove_faces
= [bm
.faces
[face
] for face
in old_selected_faces
]
1615 edges
= collections
.Counter(
1616 [edge
.index
for face
in remove_faces
for edge
in face
.edges
]
1618 remove_edges
= [bm
.edges
[edge
] for edge
in edges
if edges
[edge
] > 1]
1620 # remove internal faces and edges
1621 for face
in remove_faces
:
1622 bm
.faces
.remove(face
)
1623 for edge
in remove_edges
:
1624 bm
.edges
.remove(edge
)
1626 bm
.faces
.ensure_lookup_table()
1627 bm
.edges
.ensure_lookup_table()
1628 bm
.verts
.ensure_lookup_table()
1631 # update list of internal faces that are flagged for removal
1632 def bridge_save_unused_faces(bm
, old_selected_faces
, loops
):
1633 # key: vertex index, value: lists of selected faces using it
1635 vertex_to_face
= dict([[i
, []] for i
in range(len(bm
.verts
))])
1636 [[vertex_to_face
[vertex
.index
].append(face
) for vertex
in
1637 bm
.faces
[face
].verts
] for face
in old_selected_faces
]
1639 # group selected faces that are connected
1642 for face
in old_selected_faces
:
1643 if face
in grouped_faces
:
1645 grouped_faces
.append(face
)
1649 grow_face
= new_faces
[0]
1650 for vertex
in bm
.faces
[grow_face
].verts
:
1651 vertex_face_group
= [
1652 face
for face
in vertex_to_face
[vertex
.index
] if
1653 face
not in grouped_faces
1655 new_faces
+= vertex_face_group
1656 grouped_faces
+= vertex_face_group
1657 group
+= vertex_face_group
1659 groups
.append(group
)
1661 # key: vertex index, value: True/False (is it in a loop that is used)
1662 used_vertices
= dict([[i
, 0] for i
in range(len(bm
.verts
))])
1664 for vertex
in loop
[0]:
1665 used_vertices
[vertex
] = True
1667 # check if group is bridged, if not remove faces from internal faces list
1668 for group
in groups
:
1673 for vertex
in bm
.faces
[face
].verts
:
1674 if used_vertices
[vertex
.index
]:
1679 old_selected_faces
.remove(face
)
1682 # add the newly created faces to the selection
1683 def bridge_select_new_faces(new_faces
, smooth
):
1684 for face
in new_faces
:
1685 face
.select_set(True)
1686 face
.smooth
= smooth
1689 # sort loops, so they are connected in the correct order when lofting
1690 def bridge_sort_loops(bm
, loops
, loft_loop
):
1691 # simplify loops to single points, and prepare for pathfinding
1693 [sum([bm
.verts
[i
].co
[j
] for i
in loop
[0]]) /
1694 len(loop
[0]) for loop
in loops
] for j
in range(3)
1696 nodes
= [mathutils
.Vector((x
[i
], y
[i
], z
[i
])) for i
in range(len(loops
))]
1699 open = [i
for i
in range(1, len(loops
))]
1701 # connect node to path, that is shortest to active_node
1702 while len(open) > 0:
1703 distances
= [(nodes
[active_node
] - nodes
[i
]).length
for i
in open]
1704 active_node
= open[distances
.index(min(distances
))]
1705 open.remove(active_node
)
1706 path
.append([active_node
, min(distances
)])
1707 # check if we didn't start in the middle of the path
1708 for i
in range(2, len(path
)):
1709 if (nodes
[path
[i
][0]] - nodes
[0]).length
< path
[i
][1]:
1712 path
= path
[:-i
] + temp
1716 loops
= [loops
[i
[0]] for i
in path
]
1717 # if requested, duplicate first loop at last position, so loft can loop
1719 loops
= loops
+ [loops
[0]]
1724 # remapping old indices to new position in list
1725 def bridge_update_old_selection(bm
, old_selected_faces
):
1727 old_indices = old_selected_faces[:]
1728 old_selected_faces = []
1729 for i, face in enumerate(bm.faces):
1730 if face.index in old_indices:
1731 old_selected_faces.append(i)
1733 old_selected_faces
= [
1734 i
for i
, face
in enumerate(bm
.faces
) if face
.index
in old_selected_faces
1737 return(old_selected_faces
)
1740 # ########################################
1741 # ##### Circle functions #################
1742 # ########################################
1744 # convert 3d coordinates to 2d coordinates on plane
1745 def circle_3d_to_2d(bm_mod
, loop
, com
, normal
):
1746 # project vertices onto the plane
1747 verts
= [bm_mod
.verts
[v
] for v
in loop
[0]]
1748 verts_projected
= [[v
.co
- (v
.co
- com
).dot(normal
) * normal
, v
.index
]
1751 # calculate two vectors (p and q) along the plane
1752 m
= mathutils
.Vector((normal
[0] + 1.0, normal
[1], normal
[2]))
1753 p
= m
- (m
.dot(normal
) * normal
)
1755 m
= mathutils
.Vector((normal
[0], normal
[1] + 1.0, normal
[2]))
1756 p
= m
- (m
.dot(normal
) * normal
)
1759 # change to 2d coordinates using perpendicular projection
1761 for loc
, vert
in verts_projected
:
1763 x
= p
.dot(vloc
) / p
.dot(p
)
1764 y
= q
.dot(vloc
) / q
.dot(q
)
1765 locs_2d
.append([x
, y
, vert
])
1767 return(locs_2d
, p
, q
)
1770 # calculate a best-fit circle to the 2d locations on the plane
1771 def circle_calculate_best_fit(locs_2d
):
1777 # calculate center and radius (non-linear least squares solution)
1778 for iter in range(500):
1782 d
= (v
[0] ** 2 - 2.0 * x0
* v
[0] + v
[1] ** 2 - 2.0 * y0
* v
[1] + x0
** 2 + y0
** 2) ** 0.5
1783 jmat
.append([(x0
- v
[0]) / d
, (y0
- v
[1]) / d
, -1.0])
1784 k
.append(-(((v
[0] - x0
) ** 2 + (v
[1] - y0
) ** 2) ** 0.5 - r
))
1785 jmat2
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1789 k2
= mathutils
.Vector((0.0, 0.0, 0.0))
1790 for i
in range(len(jmat
)):
1791 k2
+= mathutils
.Vector(jmat
[i
]) * k
[i
]
1792 jmat2
[0][0] += jmat
[i
][0] ** 2
1793 jmat2
[1][0] += jmat
[i
][0] * jmat
[i
][1]
1794 jmat2
[2][0] += jmat
[i
][0] * jmat
[i
][2]
1795 jmat2
[1][1] += jmat
[i
][1] ** 2
1796 jmat2
[2][1] += jmat
[i
][1] * jmat
[i
][2]
1797 jmat2
[2][2] += jmat
[i
][2] ** 2
1798 jmat2
[0][1] = jmat2
[1][0]
1799 jmat2
[0][2] = jmat2
[2][0]
1800 jmat2
[1][2] = jmat2
[2][1]
1805 dx0
, dy0
, dr
= jmat2
* k2
1809 # stop iterating if we're close enough to optimal solution
1810 if abs(dx0
) < 1e-6 and abs(dy0
) < 1e-6 and abs(dr
) < 1e-6:
1813 # return center of circle and radius
1817 # calculate circle so no vertices have to be moved away from the center
1818 def circle_calculate_min_fit(locs_2d
):
1820 x0
= (min([i
[0] for i
in locs_2d
]) + max([i
[0] for i
in locs_2d
])) / 2.0
1821 y0
= (min([i
[1] for i
in locs_2d
]) + max([i
[1] for i
in locs_2d
])) / 2.0
1822 center
= mathutils
.Vector([x0
, y0
])
1824 r
= min([(mathutils
.Vector([i
[0], i
[1]]) - center
).length
for i
in locs_2d
])
1826 # return center of circle and radius
1830 # calculate the new locations of the vertices that need to be moved
1831 def circle_calculate_verts(flatten
, bm_mod
, locs_2d
, com
, p
, q
, normal
):
1832 # changing 2d coordinates back to 3d coordinates
1835 locs_3d
.append([loc
[2], loc
[0] * p
+ loc
[1] * q
+ com
])
1837 if flatten
: # flat circle
1840 else: # project the locations on the existing mesh
1841 vert_edges
= dict_vert_edges(bm_mod
)
1842 vert_faces
= dict_vert_faces(bm_mod
)
1843 faces
= [f
for f
in bm_mod
.faces
if not f
.hide
]
1844 rays
= [normal
, -normal
]
1848 if bm_mod
.verts
[loc
[0]].co
== loc
[1]: # vertex hasn't moved
1851 dif
= normal
.angle(loc
[1] - bm_mod
.verts
[loc
[0]].co
)
1852 if -1e-6 < dif
< 1e-6 or math
.pi
- 1e-6 < dif
< math
.pi
+ 1e-6:
1853 # original location is already along projection normal
1854 projection
= bm_mod
.verts
[loc
[0]].co
1856 # quick search through adjacent faces
1857 for face
in vert_faces
[loc
[0]]:
1858 verts
= [v
.co
for v
in bm_mod
.faces
[face
].verts
]
1859 if len(verts
) == 3: # triangle
1863 v1
, v2
, v3
, v4
= verts
[:4]
1865 intersect
= mathutils
.geometry
.\
1866 intersect_ray_tri(v1
, v2
, v3
, ray
, loc
[1])
1868 projection
= intersect
1871 intersect
= mathutils
.geometry
.\
1872 intersect_ray_tri(v1
, v3
, v4
, ray
, loc
[1])
1874 projection
= intersect
1879 # check if projection is on adjacent edges
1880 for edgekey
in vert_edges
[loc
[0]]:
1881 line1
= bm_mod
.verts
[edgekey
[0]].co
1882 line2
= bm_mod
.verts
[edgekey
[1]].co
1883 intersect
, dist
= mathutils
.geometry
.intersect_point_line(
1884 loc
[1], line1
, line2
1886 if 1e-6 < dist
< 1 - 1e-6:
1887 projection
= intersect
1890 # full search through the entire mesh
1893 verts
= [v
.co
for v
in face
.verts
]
1894 if len(verts
) == 3: # triangle
1898 v1
, v2
, v3
, v4
= verts
[:4]
1900 intersect
= mathutils
.geometry
.intersect_ray_tri(
1901 v1
, v2
, v3
, ray
, loc
[1]
1904 hits
.append([(loc
[1] - intersect
).length
,
1908 intersect
= mathutils
.geometry
.intersect_ray_tri(
1909 v1
, v3
, v4
, ray
, loc
[1]
1912 hits
.append([(loc
[1] - intersect
).length
,
1916 # if more than 1 hit with mesh, closest hit is new loc
1918 projection
= hits
[0][1]
1920 # nothing to project on, remain at flat location
1922 new_locs
.append([loc
[0], projection
])
1924 # return new positions of projected circle
1928 # check loops and only return valid ones
1929 def circle_check_loops(single_loops
, loops
, mapping
, bm_mod
):
1930 valid_single_loops
= {}
1932 for i
, [loop
, circular
] in enumerate(loops
):
1933 # loop needs to have at least 3 vertices
1936 # loop needs at least 1 vertex in the original, non-mirrored mesh
1940 if mapping
[vert
] > -1:
1945 # loop has to be non-collinear
1947 loc0
= mathutils
.Vector(bm_mod
.verts
[loop
[0]].co
[:])
1948 loc1
= mathutils
.Vector(bm_mod
.verts
[loop
[1]].co
[:])
1950 locn
= mathutils
.Vector(bm_mod
.verts
[v
].co
[:])
1951 if loc0
== loc1
or loc1
== locn
:
1957 if -1e-6 < d1
.angle(d2
, 0) < 1e-6:
1965 # passed all tests, loop is valid
1966 valid_loops
.append([loop
, circular
])
1967 valid_single_loops
[len(valid_loops
) - 1] = single_loops
[i
]
1969 return(valid_single_loops
, valid_loops
)
1972 # calculate the location of single input vertices that need to be flattened
1973 def circle_flatten_singles(bm_mod
, com
, p
, q
, normal
, single_loop
):
1975 for vert
in single_loop
:
1976 loc
= mathutils
.Vector(bm_mod
.verts
[vert
].co
[:])
1977 new_locs
.append([vert
, loc
- (loc
- com
).dot(normal
) * normal
])
1982 # calculate input loops
1983 def circle_get_input(object, bm
, scene
):
1984 # get mesh with modifiers applied
1985 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
1987 # create list of edge-keys based on selection state
1989 for face
in bm
.faces
:
1990 if face
.select
and not face
.hide
:
1994 # get selected, non-hidden , non-internal edge-keys
1996 key
for keys
in [face_edgekeys(face
) for face
in
1997 bm_mod
.faces
if face
.select
and not face
.hide
] for key
in keys
2000 for ek
in eks_selected
:
2001 if ek
in edge_count
:
2006 edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and
2007 not edge
.hide
and edge_count
.get(edgekey(edge
), 1) == 1
2010 # no faces, so no internal edges either
2012 edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select
and not edge
.hide
2015 # add edge-keys around single vertices
2016 verts_connected
= dict(
2017 [[vert
, 1] for edge
in [edge
for edge
in
2018 bm_mod
.edges
if edge
.select
and not edge
.hide
] for vert
in
2022 vert
.index
for vert
in bm_mod
.verts
if
2023 vert
.select
and not vert
.hide
and
2024 not verts_connected
.get(vert
.index
, False)
2027 if single_vertices
and len(bm
.faces
) > 0:
2028 vert_to_single
= dict(
2029 [[v
.index
, []] for v
in bm_mod
.verts
if not v
.hide
]
2031 for face
in [face
for face
in bm_mod
.faces
if not face
.select
and not face
.hide
]:
2032 for vert
in face
.verts
:
2034 if vert
in single_vertices
:
2035 for ek
in face_edgekeys(face
):
2037 edge_keys
.append(ek
)
2038 if vert
not in vert_to_single
[ek
[0]]:
2039 vert_to_single
[ek
[0]].append(vert
)
2040 if vert
not in vert_to_single
[ek
[1]]:
2041 vert_to_single
[ek
[1]].append(vert
)
2044 # sort edge-keys into loops
2045 loops
= get_connected_selections(edge_keys
)
2047 # find out to which loops the single vertices belong
2048 single_loops
= dict([[i
, []] for i
in range(len(loops
))])
2049 if single_vertices
and len(bm
.faces
) > 0:
2050 for i
, [loop
, circular
] in enumerate(loops
):
2052 if vert_to_single
[vert
]:
2053 for single
in vert_to_single
[vert
]:
2054 if single
not in single_loops
[i
]:
2055 single_loops
[i
].append(single
)
2057 return(derived
, bm_mod
, single_vertices
, single_loops
, loops
)
2060 # recalculate positions based on the influence of the circle shape
2061 def circle_influence_locs(locs_2d
, new_locs_2d
, influence
):
2062 for i
in range(len(locs_2d
)):
2063 oldx
, oldy
, j
= locs_2d
[i
]
2064 newx
, newy
, k
= new_locs_2d
[i
]
2065 altx
= newx
* (influence
/ 100) + oldx
* ((100 - influence
) / 100)
2066 alty
= newy
* (influence
/ 100) + oldy
* ((100 - influence
) / 100)
2067 locs_2d
[i
] = [altx
, alty
, j
]
2072 # project 2d locations on circle, respecting distance relations between verts
2073 def circle_project_non_regular(locs_2d
, x0
, y0
, r
):
2074 for i
in range(len(locs_2d
)):
2075 x
, y
, j
= locs_2d
[i
]
2076 loc
= mathutils
.Vector([x
- x0
, y
- y0
])
2078 locs_2d
[i
] = [loc
[0], loc
[1], j
]
2083 # project 2d locations on circle, with equal distance between all vertices
2084 def circle_project_regular(locs_2d
, x0
, y0
, r
):
2085 # find offset angle and circling direction
2086 x
, y
, i
= locs_2d
[0]
2087 loc
= mathutils
.Vector([x
- x0
, y
- y0
])
2089 offset_angle
= loc
.angle(mathutils
.Vector([1.0, 0.0]), 0.0)
2090 loca
= mathutils
.Vector([x
- x0
, y
- y0
, 0.0])
2093 x
, y
, j
= locs_2d
[1]
2094 locb
= mathutils
.Vector([x
- x0
, y
- y0
, 0.0])
2095 if loca
.cross(locb
)[2] >= 0:
2099 # distribute vertices along the circle
2100 for i
in range(len(locs_2d
)):
2101 t
= offset_angle
+ ccw
* (i
/ len(locs_2d
) * 2 * math
.pi
)
2104 locs_2d
[i
] = [x
, y
, locs_2d
[i
][2]]
2109 # shift loop, so the first vertex is closest to the center
2110 def circle_shift_loop(bm_mod
, loop
, com
):
2111 verts
, circular
= loop
2113 [(bm_mod
.verts
[vert
].co
- com
).length
, i
] for i
, vert
in enumerate(verts
)
2116 shift
= distances
[0][1]
2117 loop
= [verts
[shift
:] + verts
[:shift
], circular
]
2122 # ########################################
2123 # ##### Curve functions ##################
2124 # ########################################
2126 # create lists with knots and points, all correctly sorted
2127 def curve_calculate_knots(loop
, verts_selected
):
2128 knots
= [v
for v
in loop
[0] if v
in verts_selected
]
2130 # circular loop, potential for weird splines
2132 offset
= int(len(loop
[0]) / 4)
2135 kpos
.append(loop
[0].index(k
))
2137 for i
in range(len(kpos
) - 1):
2138 kdif
.append(kpos
[i
+ 1] - kpos
[i
])
2139 kdif
.append(len(loop
[0]) - kpos
[-1] + kpos
[0])
2143 kadd
.append([kdif
.index(k
), True])
2144 # next 2 lines are optional, they insert
2145 # an extra control point in small gaps
2147 # kadd.append([kdif.index(k), False])
2150 for k
in kadd
: # extra knots to be added
2151 if k
[1]: # big gap (break circular spline)
2152 kpos
= loop
[0].index(knots
[k
[0]]) + offset
2153 if kpos
> len(loop
[0]) - 1:
2154 kpos
-= len(loop
[0])
2155 kins
.append([knots
[k
[0]], loop
[0][kpos
]])
2157 if kpos2
> len(knots
) - 1:
2159 kpos2
= loop
[0].index(knots
[kpos2
]) - offset
2161 kpos2
+= len(loop
[0])
2162 kins
.append([loop
[0][kpos
], loop
[0][kpos2
]])
2163 krot
= loop
[0][kpos2
]
2164 else: # small gap (keep circular spline)
2165 k1
= loop
[0].index(knots
[k
[0]])
2167 if k2
> len(knots
) - 1:
2169 k2
= loop
[0].index(knots
[k2
])
2171 dif
= len(loop
[0]) - 1 - k1
+ k2
2174 kn
= k1
+ int(dif
/ 2)
2175 if kn
> len(loop
[0]) - 1:
2177 kins
.append([loop
[0][k1
], loop
[0][kn
]])
2178 for j
in kins
: # insert new knots
2179 knots
.insert(knots
.index(j
[0]) + 1, j
[1])
2180 if not krot
: # circular loop
2181 knots
.append(knots
[0])
2182 points
= loop
[0][loop
[0].index(knots
[0]):]
2183 points
+= loop
[0][0:loop
[0].index(knots
[0]) + 1]
2184 else: # non-circular loop (broken by script)
2185 krot
= knots
.index(krot
)
2186 knots
= knots
[krot
:] + knots
[0:krot
]
2187 if loop
[0].index(knots
[0]) > loop
[0].index(knots
[-1]):
2188 points
= loop
[0][loop
[0].index(knots
[0]):]
2189 points
+= loop
[0][0:loop
[0].index(knots
[-1]) + 1]
2191 points
= loop
[0][loop
[0].index(knots
[0]):loop
[0].index(knots
[-1]) + 1]
2192 # non-circular loop, add first and last point as knots
2194 if loop
[0][0] not in knots
:
2195 knots
.insert(0, loop
[0][0])
2196 if loop
[0][-1] not in knots
:
2197 knots
.append(loop
[0][-1])
2199 return(knots
, points
)
2202 # calculate relative positions compared to first knot
2203 def curve_calculate_t(bm_mod
, knots
, points
, pknots
, regular
, circular
):
2210 loc
= pknots
[knots
.index(p
)] # use projected knot location
2212 loc
= mathutils
.Vector(bm_mod
.verts
[p
].co
[:])
2215 len_total
+= (loc
- loc_prev
).length
2216 tpoints
.append(len_total
)
2221 tknots
.append(tpoints
[points
.index(p
)])
2223 tknots
[-1] = tpoints
[-1]
2227 tpoints_average
= tpoints
[-1] / (len(tpoints
) - 1)
2228 for i
in range(1, len(tpoints
) - 1):
2229 tpoints
[i
] = i
* tpoints_average
2230 for i
in range(len(knots
)):
2231 tknots
[i
] = tpoints
[points
.index(knots
[i
])]
2233 tknots
[-1] = tpoints
[-1]
2235 return(tknots
, tpoints
)
2238 # change the location of non-selected points to their place on the spline
2239 def curve_calculate_vertices(bm_mod
, knots
, tknots
, points
, tpoints
, splines
,
2240 interpolation
, restriction
):
2247 m
= tpoints
[points
.index(p
)]
2255 if n
> len(splines
) - 1:
2256 n
= len(splines
) - 1
2260 if interpolation
== 'cubic':
2261 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
2262 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
2263 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
2264 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
2265 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
2266 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
2267 newloc
= mathutils
.Vector([x
, y
, z
])
2268 else: # interpolation == 'linear'
2269 a
, d
, t
, u
= splines
[n
]
2270 newloc
= ((m
- t
) / u
) * d
+ a
2272 if restriction
!= 'none': # vertex movement is restricted
2274 else: # set the vertex to its new location
2275 move
.append([p
, newloc
])
2277 if restriction
!= 'none': # vertex movement is restricted
2282 move
.append([p
, bm_mod
.verts
[p
].co
])
2284 oldloc
= bm_mod
.verts
[p
].co
2285 normal
= bm_mod
.verts
[p
].normal
2286 dloc
= newloc
- oldloc
2287 if dloc
.length
< 1e-6:
2288 move
.append([p
, newloc
])
2289 elif restriction
== 'extrude': # only extrusions
2290 if dloc
.angle(normal
, 0) < 0.5 * math
.pi
+ 1e-6:
2291 move
.append([p
, newloc
])
2292 else: # restriction == 'indent' only indentations
2293 if dloc
.angle(normal
) > 0.5 * math
.pi
- 1e-6:
2294 move
.append([p
, newloc
])
2299 # trim loops to part between first and last selected vertices (including)
2300 def curve_cut_boundaries(bm_mod
, loops
):
2302 for loop
, circular
in loops
:
2305 cut_loops
.append([loop
, circular
])
2307 selected
= [bm_mod
.verts
[v
].select
for v
in loop
]
2308 first
= selected
.index(True)
2310 last
= -selected
.index(True)
2312 cut_loops
.append([loop
[first
:], circular
])
2314 cut_loops
.append([loop
[first
:last
], circular
])
2319 # calculate input loops
2320 def curve_get_input(object, bm
, boundaries
, scene
):
2321 # get mesh with modifiers applied
2322 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
2324 # vertices that still need a loop to run through it
2326 v
.index
for v
in bm_mod
.verts
if v
.select
and not v
.hide
2328 # necessary dictionaries
2329 vert_edges
= dict_vert_edges(bm_mod
)
2330 edge_faces
= dict_edge_faces(bm_mod
)
2332 # find loops through each selected vertex
2333 while len(verts_unsorted
) > 0:
2334 loops
= curve_vertex_loops(bm_mod
, verts_unsorted
[0], vert_edges
,
2336 verts_unsorted
.pop(0)
2338 # check if loop is fully selected
2339 search_perpendicular
= False
2341 for loop
, circular
in loops
:
2343 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2344 if len(selected
) < 2:
2345 # only one selected vertex on loop, don't use
2348 elif len(selected
) == len(loop
):
2349 search_perpendicular
= loop
2351 # entire loop is selected, find perpendicular loops
2352 if search_perpendicular
:
2354 if vert
in verts_unsorted
:
2355 verts_unsorted
.remove(vert
)
2356 perp_loops
= curve_perpendicular_loops(bm_mod
, loop
,
2357 vert_edges
, edge_faces
)
2358 for perp_loop
in perp_loops
:
2359 correct_loops
.append(perp_loop
)
2362 for loop
, circular
in loops
:
2363 correct_loops
.append([loop
, circular
])
2367 correct_loops
= curve_cut_boundaries(bm_mod
, correct_loops
)
2369 return(derived
, bm_mod
, correct_loops
)
2372 # return all loops that are perpendicular to the given one
2373 def curve_perpendicular_loops(bm_mod
, start_loop
, vert_edges
, edge_faces
):
2374 # find perpendicular loops
2376 for start_vert
in start_loop
:
2377 loops
= curve_vertex_loops(bm_mod
, start_vert
, vert_edges
,
2379 for loop
, circular
in loops
:
2380 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2381 if len(selected
) == len(loop
):
2384 perp_loops
.append([loop
, circular
, loop
.index(start_vert
)])
2386 # trim loops to same lengths
2388 [len(loop
[0]), i
] for i
, loop
in enumerate(perp_loops
) if not loop
[1]
2391 # all loops are circular, not trimming
2392 return([[loop
[0], loop
[1]] for loop
in perp_loops
])
2394 shortest
= min(shortest
)
2395 shortest_start
= perp_loops
[shortest
[1]][2]
2396 before_start
= shortest_start
2397 after_start
= shortest
[0] - shortest_start
- 1
2398 bigger_before
= before_start
> after_start
2400 for loop
in perp_loops
:
2401 # have the loop face the same direction as the shortest one
2403 if loop
[2] < len(loop
[0]) / 2:
2405 loop
[2] = len(loop
[0]) - loop
[2] - 1
2407 if loop
[2] > len(loop
[0]) / 2:
2409 loop
[2] = len(loop
[0]) - loop
[2] - 1
2410 # circular loops can shift, to prevent wrong trimming
2412 shift
= shortest_start
- loop
[2]
2413 if loop
[2] + shift
> 0 and loop
[2] + shift
< len(loop
[0]):
2414 loop
[0] = loop
[0][-shift
:] + loop
[0][:-shift
]
2417 loop
[2] += len(loop
[0])
2418 elif loop
[2] > len(loop
[0]) - 1:
2419 loop
[2] -= len(loop
[0])
2421 start
= max(0, loop
[2] - before_start
)
2422 end
= min(len(loop
[0]), loop
[2] + after_start
+ 1)
2423 trimmed_loops
.append([loop
[0][start
:end
], False])
2425 return(trimmed_loops
)
2428 # project knots on non-selected geometry
2429 def curve_project_knots(bm_mod
, verts_selected
, knots
, points
, circular
):
2430 # function to project vertex on edge
2431 def project(v1
, v2
, v3
):
2432 # v1 and v2 are part of a line
2433 # v3 is projected onto it
2439 if circular
: # project all knots
2443 else: # first and last knot shouldn't be projected
2446 pknots
= [mathutils
.Vector(bm_mod
.verts
[knots
[0]].co
[:])]
2447 for knot
in knots
[start
:end
]:
2448 if knot
in verts_selected
:
2449 knot_left
= knot_right
= False
2450 for i
in range(points
.index(knot
) - 1, -1 * len(points
), -1):
2451 if points
[i
] not in knots
:
2452 knot_left
= points
[i
]
2454 for i
in range(points
.index(knot
) + 1, 2 * len(points
)):
2455 if i
> len(points
) - 1:
2457 if points
[i
] not in knots
:
2458 knot_right
= points
[i
]
2460 if knot_left
and knot_right
and knot_left
!= knot_right
:
2461 knot_left
= mathutils
.Vector(bm_mod
.verts
[knot_left
].co
[:])
2462 knot_right
= mathutils
.Vector(bm_mod
.verts
[knot_right
].co
[:])
2463 knot
= mathutils
.Vector(bm_mod
.verts
[knot
].co
[:])
2464 pknots
.append(project(knot_left
, knot_right
, knot
))
2466 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2467 else: # knot isn't selected, so shouldn't be changed
2468 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2470 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knots
[-1]].co
[:]))
2475 # find all loops through a given vertex
2476 def curve_vertex_loops(bm_mod
, start_vert
, vert_edges
, edge_faces
):
2480 for edge
in vert_edges
[start_vert
]:
2481 if edge
in edges_used
:
2486 active_faces
= edge_faces
[edge
]
2491 new_edges
= vert_edges
[new_vert
]
2492 loop
.append(new_vert
)
2494 edges_used
.append(tuple(sorted([loop
[-1], loop
[-2]])))
2495 if len(new_edges
) < 3 or len(new_edges
) > 4:
2500 for new_edge
in new_edges
:
2501 if new_edge
in edges_used
:
2504 for new_face
in edge_faces
[new_edge
]:
2505 if new_face
in active_faces
:
2510 # found correct new edge
2511 active_faces
= edge_faces
[new_edge
]
2517 if new_vert
== loop
[0]:
2525 loops
.append([loop
, circular
])
2530 # ########################################
2531 # ##### Flatten functions ################
2532 # ########################################
2534 # sort input into loops
2535 def flatten_get_input(bm
):
2536 vert_verts
= dict_vert_verts(
2537 [edgekey(edge
) for edge
in bm
.edges
if edge
.select
and not edge
.hide
]
2539 verts
= [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
2541 # no connected verts, consider all selected verts as a single input
2543 return([[verts
, False]])
2546 while len(verts
) > 0:
2550 if loop
[-1] in vert_verts
:
2551 to_grow
= vert_verts
[loop
[-1]]
2555 while len(to_grow
) > 0:
2556 new_vert
= to_grow
[0]
2558 if new_vert
in loop
:
2560 loop
.append(new_vert
)
2561 verts
.remove(new_vert
)
2562 to_grow
+= vert_verts
[new_vert
]
2564 loops
.append([loop
, False])
2569 # calculate position of vertex projections on plane
2570 def flatten_project(bm
, loop
, com
, normal
):
2571 verts
= [bm
.verts
[v
] for v
in loop
[0]]
2573 [v
.index
, mathutils
.Vector(v
.co
[:]) -
2574 (mathutils
.Vector(v
.co
[:]) - com
).dot(normal
) * normal
] for v
in verts
2577 return(verts_projected
)
2580 # ########################################
2581 # ##### Gstretch functions ###############
2582 # ########################################
2584 # fake stroke class, used to create custom strokes if no GP data is found
2585 class gstretch_fake_stroke():
2586 def __init__(self
, points
):
2587 self
.points
= [gstretch_fake_stroke_point(p
) for p
in points
]
2590 # fake stroke point class, used in fake strokes
2591 class gstretch_fake_stroke_point():
2592 def __init__(self
, loc
):
2596 # flips loops, if necessary, to obtain maximum alignment to stroke
2597 def gstretch_align_pairs(ls_pairs
, object, bm_mod
, method
):
2598 # returns total distance between all verts in loop and corresponding stroke
2599 def distance_loop_stroke(loop
, stroke
, object, bm_mod
, method
):
2600 stroke_lengths_cache
= False
2601 loop_length
= len(loop
[0])
2604 if method
!= 'regular':
2605 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2607 for i
, v_index
in enumerate(loop
[0]):
2608 if method
== 'regular':
2609 relative_distance
= i
/ (loop_length
- 1)
2611 relative_distance
= relative_lengths
[i
]
2613 loc1
= object.matrix_world
* bm_mod
.verts
[v_index
].co
2614 loc2
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2615 relative_distance
, stroke_lengths_cache
)
2616 total_distance
+= (loc2
- loc1
).length
2618 return(total_distance
)
2621 for (loop
, stroke
) in ls_pairs
:
2622 total_dist
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2625 total_dist_rev
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2627 if total_dist_rev
> total_dist
:
2633 # calculate vertex positions on stroke
2634 def gstretch_calculate_verts(loop
, stroke
, object, bm_mod
, method
):
2636 stroke_lengths_cache
= False
2637 loop_length
= len(loop
[0])
2638 matrix_inverse
= object.matrix_world
.inverted()
2640 # return intersection of line with stroke, or None
2641 def intersect_line_stroke(vec1
, vec2
, stroke
):
2642 for i
, p
in enumerate(stroke
.points
[1:]):
2643 intersections
= mathutils
.geometry
.intersect_line_line(vec1
, vec2
,
2644 p
.co
, stroke
.points
[i
].co
)
2645 if intersections
and \
2646 (intersections
[0] - intersections
[1]).length
< 1e-2:
2647 x
, dist
= mathutils
.geometry
.intersect_point_line(
2648 intersections
[0], p
.co
, stroke
.points
[i
].co
)
2650 return(intersections
[0])
2653 if method
== 'project':
2654 vert_edges
= dict_vert_edges(bm_mod
)
2656 for v_index
in loop
[0]:
2658 for ek
in vert_edges
[v_index
]:
2660 v1
= bm_mod
.verts
[v1
]
2661 v2
= bm_mod
.verts
[v2
]
2662 if v1
.select
+ v2
.select
== 1 and not v1
.hide
and not v2
.hide
:
2663 vec1
= object.matrix_world
* v1
.co
2664 vec2
= object.matrix_world
* v2
.co
2665 intersection
= intersect_line_stroke(vec1
, vec2
, stroke
)
2668 if not intersection
:
2669 v
= bm_mod
.verts
[v_index
]
2670 intersection
= intersect_line_stroke(v
.co
, v
.co
+ v
.normal
,
2673 move
.append([v_index
, matrix_inverse
* intersection
])
2676 if method
== 'irregular':
2677 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2679 for i
, v_index
in enumerate(loop
[0]):
2680 if method
== 'regular':
2681 relative_distance
= i
/ (loop_length
- 1)
2682 else: # method == 'irregular'
2683 relative_distance
= relative_lengths
[i
]
2684 loc
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2685 relative_distance
, stroke_lengths_cache
)
2686 loc
= matrix_inverse
* loc
2687 move
.append([v_index
, loc
])
2692 # create new vertices, based on GP strokes
2693 def gstretch_create_verts(object, bm_mod
, strokes
, method
, conversion
,
2694 conversion_distance
, conversion_max
, conversion_min
, conversion_vertices
):
2697 mat_world
= object.matrix_world
.inverted()
2698 singles
= gstretch_match_single_verts(bm_mod
, strokes
, mat_world
)
2700 for stroke
in strokes
:
2701 stroke_verts
.append([stroke
, []])
2703 if conversion
== 'vertices':
2704 min_end_point
= conversion_vertices
2705 end_point
= conversion_vertices
2706 elif conversion
== 'limit_vertices':
2707 min_end_point
= conversion_min
2708 end_point
= conversion_max
2710 end_point
= len(stroke
.points
)
2711 # creation of new vertices at fixed user-defined distances
2712 if conversion
== 'distance':
2714 prev_point
= stroke
.points
[0]
2715 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* prev_point
.co
))
2717 limit
= conversion_distance
2718 for point
in stroke
.points
:
2719 new_distance
= distance
+ (point
.co
- prev_point
.co
).length
2721 while new_distance
> limit
:
2722 to_cover
= limit
- distance
+ (limit
* iteration
)
2723 new_loc
= prev_point
.co
+ to_cover
* \
2724 (point
.co
- prev_point
.co
).normalized()
2725 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* new_loc
))
2726 new_distance
-= limit
2728 distance
= new_distance
2730 # creation of new vertices for other methods
2732 # add vertices at stroke points
2733 for point
in stroke
.points
[:end_point
]:
2734 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* point
.co
))
2735 # add more vertices, beyond the points that are available
2736 if min_end_point
> min(len(stroke
.points
), end_point
):
2737 for i
in range(min_end_point
-
2738 (min(len(stroke
.points
), end_point
))):
2739 stroke_verts
[-1][1].append(bm_mod
.verts
.new(mat_world
* point
.co
))
2740 # force even spreading of points, so they are placed on stroke
2742 bm_mod
.verts
.ensure_lookup_table()
2743 bm_mod
.verts
.index_update()
2744 for stroke
, verts_seq
in stroke_verts
:
2745 if len(verts_seq
) < 2:
2747 # spread vertices evenly over the stroke
2748 if method
== 'regular':
2749 loop
= [[vert
.index
for vert
in verts_seq
], False]
2750 move
+= gstretch_calculate_verts(loop
, stroke
, object, bm_mod
,
2753 for i
, vert
in enumerate(verts_seq
):
2755 bm_mod
.edges
.new((verts_seq
[i
- 1], verts_seq
[i
]))
2757 # connect single vertices to the closest stroke
2759 for vert
, m_stroke
, point
in singles
:
2760 if m_stroke
!= stroke
:
2762 bm_mod
.edges
.new((vert
, verts_seq
[point
]))
2763 bm_mod
.edges
.ensure_lookup_table()
2764 bmesh
.update_edit_mesh(object.data
)
2769 # erases the grease pencil stroke
2770 def gstretch_erase_stroke(stroke
, context
):
2771 # change 3d coordinate into a stroke-point
2772 def sp(loc
, context
):
2776 'location': (0, 0, 0),
2778 view3d_utils
.location_3d_to_region_2d(
2779 context
.region
, context
.space_data
.region_3d
, loc
)
2786 if type(stroke
) != bpy
.types
.GPencilStroke
:
2787 # fake stroke, there is nothing to delete
2790 erase_stroke
= [sp(p
.co
, context
) for p
in stroke
.points
]
2792 erase_stroke
[0]['is_start'] = True
2793 bpy
.ops
.gpencil
.draw(mode
='ERASER', stroke
=erase_stroke
)
2796 # get point on stroke, given by relative distance (0.0 - 1.0)
2797 def gstretch_eval_stroke(stroke
, distance
, stroke_lengths_cache
=False):
2798 # use cache if available
2799 if not stroke_lengths_cache
:
2801 for i
, p
in enumerate(stroke
.points
[1:]):
2802 lengths
.append((p
.co
- stroke
.points
[i
].co
).length
+ lengths
[-1])
2803 total_length
= max(lengths
[-1], 1e-7)
2804 stroke_lengths_cache
= [length
/ total_length
for length
in
2806 stroke_lengths
= stroke_lengths_cache
[:]
2808 if distance
in stroke_lengths
:
2809 loc
= stroke
.points
[stroke_lengths
.index(distance
)].co
2810 elif distance
> stroke_lengths
[-1]:
2811 # should be impossible, but better safe than sorry
2812 loc
= stroke
.points
[-1].co
2814 stroke_lengths
.append(distance
)
2815 stroke_lengths
.sort()
2816 stroke_index
= stroke_lengths
.index(distance
)
2817 interval_length
= stroke_lengths
[
2818 stroke_index
+ 1] - stroke_lengths
[stroke_index
- 1
2820 distance_relative
= (distance
- stroke_lengths
[stroke_index
- 1]) / interval_length
2821 interval_vector
= stroke
.points
[stroke_index
].co
- stroke
.points
[stroke_index
- 1].co
2822 loc
= stroke
.points
[stroke_index
- 1].co
+ distance_relative
* interval_vector
2824 return(loc
, stroke_lengths_cache
)
2827 # create fake grease pencil strokes for the active object
2828 def gstretch_get_fake_strokes(object, bm_mod
, loops
):
2831 p1
= object.matrix_world
* bm_mod
.verts
[loop
[0][0]].co
2832 p2
= object.matrix_world
* bm_mod
.verts
[loop
[0][-1]].co
2833 strokes
.append(gstretch_fake_stroke([p1
, p2
]))
2838 # get grease pencil strokes for the active object
2839 def gstretch_get_strokes(object, context
):
2840 gp
= get_grease_pencil(object, context
)
2843 layer
= gp
.layers
.active
2846 frame
= layer
.active_frame
2849 strokes
= frame
.strokes
2850 if len(strokes
) < 1:
2856 # returns a list with loop-stroke pairs
2857 def gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
):
2858 if not loops
or not strokes
:
2861 # calculate loop centers
2863 bm_mod
.verts
.ensure_lookup_table()
2865 center
= mathutils
.Vector()
2866 for v_index
in loop
[0]:
2867 center
+= bm_mod
.verts
[v_index
].co
2868 center
/= len(loop
[0])
2869 center
= object.matrix_world
* center
2870 loop_centers
.append([center
, loop
])
2872 # calculate stroke centers
2874 for stroke
in strokes
:
2875 center
= mathutils
.Vector()
2876 for p
in stroke
.points
:
2878 center
/= len(stroke
.points
)
2879 stroke_centers
.append([center
, stroke
, 0])
2881 # match, first by stroke use count, then by distance
2883 for lc
in loop_centers
:
2885 for i
, sc
in enumerate(stroke_centers
):
2886 distances
.append([sc
[2], (lc
[0] - sc
[0]).length
, i
])
2888 best_stroke
= distances
[0][2]
2889 ls_pairs
.append([lc
[1], stroke_centers
[best_stroke
][1]])
2890 stroke_centers
[best_stroke
][2] += 1 # increase stroke use count
2895 # match single selected vertices to the closest stroke endpoint
2896 # returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
2897 def gstretch_match_single_verts(bm_mod
, strokes
, mat_world
):
2898 # calculate stroke endpoints in object space
2900 for stroke
in strokes
:
2901 endpoints
.append((mat_world
* stroke
.points
[0].co
, stroke
, 0))
2902 endpoints
.append((mat_world
* stroke
.points
[-1].co
, stroke
, -1))
2905 # find single vertices (not connected to other selected verts)
2906 for vert
in bm_mod
.verts
:
2910 for edge
in vert
.link_edges
:
2911 if edge
.other_vert(vert
).select
:
2916 # calculate distances from vertex to endpoints
2917 distance
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2918 endpoint_index
) for endpoint_index
, (loc
, stroke
, stroke_point
) in
2919 enumerate(endpoints
)]
2921 distances
.append(distance
[0])
2923 # create matches, based on shortest distance first
2927 singles
.append((distances
[0][1], distances
[0][2], distances
[0][3]))
2928 endpoints
.pop(distances
[0][4])
2931 for (i
, vert
, j
, k
, l
) in distances
:
2932 distance_new
= [((vert
.co
- loc
).length
, vert
, stroke
, stroke_point
,
2933 endpoint_index
) for endpoint_index
, (loc
, stroke
,
2934 stroke_point
) in enumerate(endpoints
)]
2936 distances_new
.append(distance_new
[0])
2937 distances
= distances_new
2942 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2943 def gstretch_relative_lengths(loop
, bm_mod
):
2945 for i
, v_index
in enumerate(loop
[0][1:]):
2947 (bm_mod
.verts
[v_index
].co
-
2948 bm_mod
.verts
[loop
[0][i
]].co
).length
+ lengths
[-1]
2950 total_length
= max(lengths
[-1], 1e-7)
2951 relative_lengths
= [length
/ total_length
for length
in
2954 return(relative_lengths
)
2957 # convert cache-stored strokes into usable (fake) GP strokes
2958 def gstretch_safe_to_true_strokes(safe_strokes
):
2960 for safe_stroke
in safe_strokes
:
2961 strokes
.append(gstretch_fake_stroke(safe_stroke
))
2966 # convert a GP stroke into a list of points which can be stored in cache
2967 def gstretch_true_to_safe_strokes(strokes
):
2969 for stroke
in strokes
:
2970 safe_strokes
.append([p
.co
.copy() for p
in stroke
.points
])
2972 return(safe_strokes
)
2975 # force consistency in GUI, max value can never be lower than min value
2976 def gstretch_update_max(self
, context
):
2977 # called from operator settings (after execution)
2978 if 'conversion_min' in self
.keys():
2979 if self
.conversion_min
> self
.conversion_max
:
2980 self
.conversion_max
= self
.conversion_min
2981 # called from toolbar
2983 lt
= context
.window_manager
.looptools
2984 if lt
.gstretch_conversion_min
> lt
.gstretch_conversion_max
:
2985 lt
.gstretch_conversion_max
= lt
.gstretch_conversion_min
2988 # force consistency in GUI, min value can never be higher than max value
2989 def gstretch_update_min(self
, context
):
2990 # called from operator settings (after execution)
2991 if 'conversion_max' in self
.keys():
2992 if self
.conversion_max
< self
.conversion_min
:
2993 self
.conversion_min
= self
.conversion_max
2994 # called from toolbar
2996 lt
= context
.window_manager
.looptools
2997 if lt
.gstretch_conversion_max
< lt
.gstretch_conversion_min
:
2998 lt
.gstretch_conversion_min
= lt
.gstretch_conversion_max
3001 # ########################################
3002 # ##### Relax functions ##################
3003 # ########################################
3005 # create lists with knots and points, all correctly sorted
3006 def relax_calculate_knots(loops
):
3009 for loop
, circular
in loops
:
3013 if len(loop
) % 2 == 1: # odd
3014 extend
= [False, True, 0, 1, 0, 1]
3016 extend
= [True, False, 0, 1, 1, 2]
3018 if len(loop
) % 2 == 1: # odd
3019 extend
= [False, False, 0, 1, 1, 2]
3021 extend
= [False, False, 0, 1, 1, 2]
3024 loop
= [loop
[-1]] + loop
+ [loop
[0]]
3025 for i
in range(extend
[2 + 2 * j
], len(loop
), 2):
3026 knots
[j
].append(loop
[i
])
3027 for i
in range(extend
[3 + 2 * j
], len(loop
), 2):
3028 if loop
[i
] == loop
[-1] and not circular
:
3030 if len(points
[j
]) == 0:
3031 points
[j
].append(loop
[i
])
3032 elif loop
[i
] != points
[j
][0]:
3033 points
[j
].append(loop
[i
])
3035 if knots
[j
][0] != knots
[j
][-1]:
3036 knots
[j
].append(knots
[j
][0])
3037 if len(points
[1]) == 0:
3043 all_points
.append(p
)
3045 return(all_knots
, all_points
)
3048 # calculate relative positions compared to first knot
3049 def relax_calculate_t(bm_mod
, knots
, points
, regular
):
3052 for i
in range(len(knots
)):
3053 amount
= len(knots
[i
]) + len(points
[i
])
3055 for j
in range(amount
):
3057 mix
.append([True, knots
[i
][round(j
/ 2)]])
3058 elif j
== amount
- 1:
3059 mix
.append([True, knots
[i
][-1]])
3061 mix
.append([False, points
[i
][int(j
/ 2)]])
3067 loc
= mathutils
.Vector(bm_mod
.verts
[m
[1]].co
[:])
3070 len_total
+= (loc
- loc_prev
).length
3072 tknots
.append(len_total
)
3074 tpoints
.append(len_total
)
3078 for p
in range(len(points
[i
])):
3079 tpoints
.append((tknots
[p
] + tknots
[p
+ 1]) / 2)
3080 all_tknots
.append(tknots
)
3081 all_tpoints
.append(tpoints
)
3083 return(all_tknots
, all_tpoints
)
3086 # change the location of the points to their place on the spline
3087 def relax_calculate_verts(bm_mod
, interpolation
, tknots
, knots
, tpoints
,
3091 for i
in range(len(knots
)):
3093 m
= tpoints
[i
][points
[i
].index(p
)]
3095 n
= tknots
[i
].index(m
)
3101 if n
> len(splines
[i
]) - 1:
3102 n
= len(splines
[i
]) - 1
3106 if interpolation
== 'cubic':
3107 ax
, bx
, cx
, dx
, tx
= splines
[i
][n
][0]
3108 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
3109 ay
, by
, cy
, dy
, ty
= splines
[i
][n
][1]
3110 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
3111 az
, bz
, cz
, dz
, tz
= splines
[i
][n
][2]
3112 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
3113 change
.append([p
, mathutils
.Vector([x
, y
, z
])])
3114 else: # interpolation == 'linear'
3115 a
, d
, t
, u
= splines
[i
][n
]
3118 change
.append([p
, ((m
- t
) / u
) * d
+ a
])
3120 move
.append([c
[0], (bm_mod
.verts
[c
[0]].co
+ c
[1]) / 2])
3125 # ########################################
3126 # ##### Space functions ##################
3127 # ########################################
3129 # calculate relative positions compared to first knot
3130 def space_calculate_t(bm_mod
, knots
):
3135 loc
= mathutils
.Vector(bm_mod
.verts
[k
].co
[:])
3138 len_total
+= (loc
- loc_prev
).length
3139 tknots
.append(len_total
)
3142 t_per_segment
= len_total
/ (amount
- 1)
3143 tpoints
= [i
* t_per_segment
for i
in range(amount
)]
3145 return(tknots
, tpoints
)
3148 # change the location of the points to their place on the spline
3149 def space_calculate_verts(bm_mod
, interpolation
, tknots
, tpoints
, points
,
3153 m
= tpoints
[points
.index(p
)]
3161 if n
> len(splines
) - 1:
3162 n
= len(splines
) - 1
3166 if interpolation
== 'cubic':
3167 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
3168 x
= ax
+ bx
* (m
- tx
) + cx
* (m
- tx
) ** 2 + dx
* (m
- tx
) ** 3
3169 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
3170 y
= ay
+ by
* (m
- ty
) + cy
* (m
- ty
) ** 2 + dy
* (m
- ty
) ** 3
3171 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
3172 z
= az
+ bz
* (m
- tz
) + cz
* (m
- tz
) ** 2 + dz
* (m
- tz
) ** 3
3173 move
.append([p
, mathutils
.Vector([x
, y
, z
])])
3174 else: # interpolation == 'linear'
3175 a
, d
, t
, u
= splines
[n
]
3176 move
.append([p
, ((m
- t
) / u
) * d
+ a
])
3181 # ########################################
3182 # ##### Operators ########################
3183 # ########################################
3186 class Bridge(Operator
):
3187 bl_idname
= 'mesh.looptools_bridge'
3188 bl_label
= "Bridge / Loft"
3189 bl_description
= "Bridge two, or loft several, loops of vertices"
3190 bl_options
= {'REGISTER', 'UNDO'}
3192 cubic_strength
= FloatProperty(
3194 description
="Higher strength results in more fluid curves",
3199 interpolation
= EnumProperty(
3200 name
="Interpolation mode",
3201 items
=(('cubic', "Cubic", "Gives curved results"),
3202 ('linear', "Linear", "Basic, fast, straight interpolation")),
3203 description
="Interpolation mode: algorithm used when creating "
3207 loft
= BoolProperty(
3209 description
="Loft multiple loops, instead of considering them as "
3210 "a multi-input for bridging",
3213 loft_loop
= BoolProperty(
3215 description
="Connect the first and the last loop with each other",
3218 min_width
= IntProperty(
3219 name
="Minimum width",
3220 description
="Segments with an edge smaller than this are merged "
3221 "(compared to base edge)",
3225 subtype
='PERCENTAGE'
3227 mode
= EnumProperty(
3229 items
=(('basic', "Basic", "Fast algorithm"),
3230 ('shortest', "Shortest edge", "Slower algorithm with better vertex matching")),
3231 description
="Algorithm used for bridging",
3234 remove_faces
= BoolProperty(
3235 name
="Remove faces",
3236 description
="Remove faces that are internal after bridging",
3239 reverse
= BoolProperty(
3241 description
="Manually override the direction in which the loops "
3242 "are bridged. Only use if the tool gives the wrong result",
3245 segments
= IntProperty(
3247 description
="Number of segments used to bridge the gap (0=automatic)",
3252 twist
= IntProperty(
3254 description
="Twist what vertices are connected to each other",
3259 def poll(cls
, context
):
3260 ob
= context
.active_object
3261 return (ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3263 def draw(self
, context
):
3264 layout
= self
.layout
3265 # layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
3268 col_top
= layout
.column(align
=True)
3269 row
= col_top
.row(align
=True)
3270 col_left
= row
.column(align
=True)
3271 col_right
= row
.column(align
=True)
3272 col_right
.active
= self
.segments
!= 1
3273 col_left
.prop(self
, "segments")
3274 col_right
.prop(self
, "min_width", text
="")
3276 bottom_left
= col_left
.row()
3277 bottom_left
.active
= self
.segments
!= 1
3278 bottom_left
.prop(self
, "interpolation", text
="")
3279 bottom_right
= col_right
.row()
3280 bottom_right
.active
= self
.interpolation
== 'cubic'
3281 bottom_right
.prop(self
, "cubic_strength")
3282 # boolean properties
3283 col_top
.prop(self
, "remove_faces")
3285 col_top
.prop(self
, "loft_loop")
3287 # override properties
3289 row
= layout
.row(align
=True)
3290 row
.prop(self
, "twist")
3291 row
.prop(self
, "reverse")
3293 def invoke(self
, context
, event
):
3294 # load custom settings
3295 context
.window_manager
.looptools
.bridge_loft
= self
.loft
3297 return self
.execute(context
)
3299 def execute(self
, context
):
3301 global_undo
, object, bm
= initialise()
3302 edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
= \
3303 bridge_initialise(bm
, self
.interpolation
)
3304 settings_write(self
)
3306 # check cache to see if we can save time
3307 input_method
= bridge_input_method(self
.loft
, self
.loft_loop
)
3308 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Bridge",
3309 object, bm
, input_method
, False)
3312 loops
= bridge_get_input(bm
)
3314 # reorder loops if there are more than 2
3317 loops
= bridge_sort_loops(bm
, loops
, self
.loft_loop
)
3319 loops
= bridge_match_loops(bm
, loops
)
3321 # saving cache for faster execution next time
3323 cache_write("Bridge", object, bm
, input_method
, False, False,
3324 loops
, False, False)
3327 # calculate new geometry
3330 max_vert_index
= len(bm
.verts
) - 1
3331 for i
in range(1, len(loops
)):
3332 if not self
.loft
and i
% 2 == 0:
3334 lines
= bridge_calculate_lines(bm
, loops
[i
- 1:i
+ 1],
3335 self
.mode
, self
.twist
, self
.reverse
)
3336 vertex_normals
= bridge_calculate_virtual_vertex_normals(bm
,
3337 lines
, loops
[i
- 1:i
+ 1], edge_faces
, edgekey_to_edge
)
3338 segments
= bridge_calculate_segments(bm
, lines
,
3339 loops
[i
- 1:i
+ 1], self
.segments
)
3340 new_verts
, new_faces
, max_vert_index
= \
3341 bridge_calculate_geometry(
3342 bm
, lines
, vertex_normals
,
3343 segments
, self
.interpolation
, self
.cubic_strength
,
3344 self
.min_width
, max_vert_index
3347 vertices
+= new_verts
3350 # make sure faces in loops that aren't used, aren't removed
3351 if self
.remove_faces
and old_selected_faces
:
3352 bridge_save_unused_faces(bm
, old_selected_faces
, loops
)
3355 bridge_create_vertices(bm
, vertices
)
3358 new_faces
= bridge_create_faces(object, bm
, faces
, self
.twist
)
3359 old_selected_faces
= [
3360 i
for i
, face
in enumerate(bm
.faces
) if face
.index
in old_selected_faces
3362 bridge_select_new_faces(new_faces
, smooth
)
3363 # edge-data could have changed, can't use cache next run
3364 if faces
and not vertices
:
3365 cache_delete("Bridge")
3366 # delete internal faces
3367 if self
.remove_faces
and old_selected_faces
:
3368 bridge_remove_internal_faces(bm
, old_selected_faces
)
3369 # make sure normals are facing outside
3370 bmesh
.update_edit_mesh(object.data
, tessface
=False,
3372 bpy
.ops
.mesh
.normals_make_consistent()
3375 terminate(global_undo
)
3381 class Circle(Operator
):
3382 bl_idname
= "mesh.looptools_circle"
3384 bl_description
= "Move selected vertices into a circle shape"
3385 bl_options
= {'REGISTER', 'UNDO'}
3387 custom_radius
= BoolProperty(
3389 description
="Force a custom radius",
3394 items
=(("best", "Best fit", "Non-linear least squares"),
3395 ("inside", "Fit inside", "Only move vertices towards the center")),
3396 description
="Method used for fitting a circle to the vertices",
3399 flatten
= BoolProperty(
3401 description
="Flatten the circle, instead of projecting it on the mesh",
3404 influence
= FloatProperty(
3406 description
="Force of the tool",
3411 subtype
='PERCENTAGE'
3413 lock_x
= BoolProperty(
3415 description
="Lock editing of the x-coordinate",
3418 lock_y
= BoolProperty(
3420 description
="Lock editing of the y-coordinate",
3423 lock_z
= BoolProperty(name
="Lock Z",
3424 description
="Lock editing of the z-coordinate",
3427 radius
= FloatProperty(
3429 description
="Custom radius for circle",
3434 regular
= BoolProperty(
3436 description
="Distribute vertices at constant distances along the circle",
3441 def poll(cls
, context
):
3442 ob
= context
.active_object
3443 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3445 def draw(self
, context
):
3446 layout
= self
.layout
3447 col
= layout
.column()
3449 col
.prop(self
, "fit")
3452 col
.prop(self
, "flatten")
3453 row
= col
.row(align
=True)
3454 row
.prop(self
, "custom_radius")
3455 row_right
= row
.row(align
=True)
3456 row_right
.active
= self
.custom_radius
3457 row_right
.prop(self
, "radius", text
="")
3458 col
.prop(self
, "regular")
3461 col_move
= col
.column(align
=True)
3462 row
= col_move
.row(align
=True)
3464 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3466 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3468 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3470 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3472 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3474 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3475 col_move
.prop(self
, "influence")
3477 def invoke(self
, context
, event
):
3478 # load custom settings
3480 return self
.execute(context
)
3482 def execute(self
, context
):
3484 global_undo
, object, bm
= initialise()
3485 settings_write(self
)
3486 # check cache to see if we can save time
3487 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Circle",
3488 object, bm
, False, False)
3490 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3493 derived
, bm_mod
, single_vertices
, single_loops
, loops
= \
3494 circle_get_input(object, bm
, context
.scene
)
3495 mapping
= get_mapping(derived
, bm
, bm_mod
, single_vertices
,
3497 single_loops
, loops
= circle_check_loops(single_loops
, loops
,
3500 # saving cache for faster execution next time
3502 cache_write("Circle", object, bm
, False, False, single_loops
,
3503 loops
, derived
, mapping
)
3506 for i
, loop
in enumerate(loops
):
3507 # best fitting flat plane
3508 com
, normal
= calculate_plane(bm_mod
, loop
)
3509 # if circular, shift loop so we get a good starting vertex
3511 loop
= circle_shift_loop(bm_mod
, loop
, com
)
3512 # flatten vertices on plane
3513 locs_2d
, p
, q
= circle_3d_to_2d(bm_mod
, loop
, com
, normal
)
3515 if self
.fit
== 'best':
3516 x0
, y0
, r
= circle_calculate_best_fit(locs_2d
)
3517 else: # self.fit == 'inside'
3518 x0
, y0
, r
= circle_calculate_min_fit(locs_2d
)
3520 if self
.custom_radius
:
3521 r
= self
.radius
/ p
.length
3522 # calculate positions on circle
3524 new_locs_2d
= circle_project_regular(locs_2d
[:], x0
, y0
, r
)
3526 new_locs_2d
= circle_project_non_regular(locs_2d
[:], x0
, y0
, r
)
3527 # take influence into account
3528 locs_2d
= circle_influence_locs(locs_2d
, new_locs_2d
,
3530 # calculate 3d positions of the created 2d input
3531 move
.append(circle_calculate_verts(self
.flatten
, bm_mod
,
3532 locs_2d
, com
, p
, q
, normal
))
3533 # flatten single input vertices on plane defined by loop
3534 if self
.flatten
and single_loops
:
3535 move
.append(circle_flatten_singles(bm_mod
, com
, p
, q
,
3536 normal
, single_loops
[i
]))
3538 # move vertices to new locations
3539 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3540 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3543 move_verts(object, bm
, mapping
, move
, lock
, -1)
3548 terminate(global_undo
)
3554 class Curve(Operator
):
3555 bl_idname
= "mesh.looptools_curve"
3557 bl_description
= "Turn a loop into a smooth curve"
3558 bl_options
= {'REGISTER', 'UNDO'}
3560 boundaries
= BoolProperty(
3562 description
="Limit the tool to work within the boundaries of the selected vertices",
3565 influence
= FloatProperty(
3567 description
="Force of the tool",
3572 subtype
='PERCENTAGE'
3574 interpolation
= EnumProperty(
3575 name
="Interpolation",
3576 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
3577 ("linear", "Linear", "Simple and fast linear algorithm")),
3578 description
="Algorithm used for interpolation",
3581 lock_x
= BoolProperty(
3583 description
="Lock editing of the x-coordinate",
3586 lock_y
= BoolProperty(
3588 description
="Lock editing of the y-coordinate",
3591 lock_z
= BoolProperty(
3593 description
="Lock editing of the z-coordinate",
3596 regular
= BoolProperty(
3598 description
="Distribute vertices at constant distances along the curve",
3601 restriction
= EnumProperty(
3603 items
=(("none", "None", "No restrictions on vertex movement"),
3604 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
3605 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
3606 description
="Restrictions on how the vertices can be moved",
3611 def poll(cls
, context
):
3612 ob
= context
.active_object
3613 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3615 def draw(self
, context
):
3616 layout
= self
.layout
3617 col
= layout
.column()
3619 col
.prop(self
, "interpolation")
3620 col
.prop(self
, "restriction")
3621 col
.prop(self
, "boundaries")
3622 col
.prop(self
, "regular")
3625 col_move
= col
.column(align
=True)
3626 row
= col_move
.row(align
=True)
3628 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3630 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3632 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3634 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3636 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3638 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3639 col_move
.prop(self
, "influence")
3641 def invoke(self
, context
, event
):
3642 # load custom settings
3644 return self
.execute(context
)
3646 def execute(self
, context
):
3648 global_undo
, object, bm
= initialise()
3649 settings_write(self
)
3650 # check cache to see if we can save time
3651 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Curve",
3652 object, bm
, False, self
.boundaries
)
3654 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3657 derived
, bm_mod
, loops
= curve_get_input(object, bm
,
3658 self
.boundaries
, context
.scene
)
3659 mapping
= get_mapping(derived
, bm
, bm_mod
, False, True, loops
)
3660 loops
= check_loops(loops
, mapping
, bm_mod
)
3662 v
.index
for v
in bm_mod
.verts
if v
.select
and not v
.hide
3665 # saving cache for faster execution next time
3667 cache_write("Curve", object, bm
, False, self
.boundaries
, False,
3668 loops
, derived
, mapping
)
3672 knots
, points
= curve_calculate_knots(loop
, verts_selected
)
3673 pknots
= curve_project_knots(bm_mod
, verts_selected
, knots
,
3675 tknots
, tpoints
= curve_calculate_t(bm_mod
, knots
, points
,
3676 pknots
, self
.regular
, loop
[1])
3677 splines
= calculate_splines(self
.interpolation
, bm_mod
,
3679 move
.append(curve_calculate_vertices(bm_mod
, knots
, tknots
,
3680 points
, tpoints
, splines
, self
.interpolation
,
3683 # move vertices to new locations
3684 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3685 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3688 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
3693 terminate(global_undo
)
3699 class Flatten(Operator
):
3700 bl_idname
= "mesh.looptools_flatten"
3701 bl_label
= "Flatten"
3702 bl_description
= "Flatten vertices on a best-fitting plane"
3703 bl_options
= {'REGISTER', 'UNDO'}
3705 influence
= FloatProperty(
3707 description
="Force of the tool",
3712 subtype
='PERCENTAGE'
3714 lock_x
= BoolProperty(
3716 description
="Lock editing of the x-coordinate",
3719 lock_y
= BoolProperty(
3721 description
="Lock editing of the y-coordinate",
3724 lock_z
= BoolProperty(name
="Lock Z",
3725 description
="Lock editing of the z-coordinate",
3728 plane
= EnumProperty(
3730 items
=(("best_fit", "Best fit", "Calculate a best fitting plane"),
3731 ("normal", "Normal", "Derive plane from averaging vertex normals"),
3732 ("view", "View", "Flatten on a plane perpendicular to the viewing angle")),
3733 description
="Plane on which vertices are flattened",
3736 restriction
= EnumProperty(
3738 items
=(("none", "None", "No restrictions on vertex movement"),
3739 ("bounding_box", "Bounding box", "Vertices are restricted to "
3740 "movement inside the bounding box of the selection")),
3741 description
="Restrictions on how the vertices can be moved",
3746 def poll(cls
, context
):
3747 ob
= context
.active_object
3748 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3750 def draw(self
, context
):
3751 layout
= self
.layout
3752 col
= layout
.column()
3754 col
.prop(self
, "plane")
3755 # col.prop(self, "restriction")
3758 col_move
= col
.column(align
=True)
3759 row
= col_move
.row(align
=True)
3761 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3763 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3765 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3767 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3769 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3771 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3772 col_move
.prop(self
, "influence")
3774 def invoke(self
, context
, event
):
3775 # load custom settings
3777 return self
.execute(context
)
3779 def execute(self
, context
):
3781 global_undo
, object, bm
= initialise()
3782 settings_write(self
)
3783 # check cache to see if we can save time
3784 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Flatten",
3785 object, bm
, False, False)
3787 # order input into virtual loops
3788 loops
= flatten_get_input(bm
)
3789 loops
= check_loops(loops
, mapping
, bm
)
3791 # saving cache for faster execution next time
3793 cache_write("Flatten", object, bm
, False, False, False, loops
,
3798 # calculate plane and position of vertices on them
3799 com
, normal
= calculate_plane(bm
, loop
, method
=self
.plane
,
3801 to_move
= flatten_project(bm
, loop
, com
, normal
)
3802 if self
.restriction
== 'none':
3803 move
.append(to_move
)
3805 move
.append(to_move
)
3807 # move vertices to new locations
3808 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
3809 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
3812 move_verts(object, bm
, False, move
, lock
, self
.influence
)
3815 terminate(global_undo
)
3821 class RemoveGP(Operator
):
3822 bl_idname
= "remove.gp"
3823 bl_label
= "Remove GP"
3824 bl_description
= "Remove all Grease Pencil Strokes"
3825 bl_options
= {'REGISTER', 'UNDO'}
3827 def execute(self
, context
):
3829 if context
.gpencil_data
is not None:
3830 bpy
.ops
.gpencil
.data_unlink()
3832 self
.report({'INFO'}, "No Grease Pencil data to Unlink")
3833 return {'CANCELLED'}
3838 class GStretch(Operator
):
3839 bl_idname
= "mesh.looptools_gstretch"
3840 bl_label
= "Gstretch"
3841 bl_description
= "Stretch selected vertices to Grease Pencil stroke"
3842 bl_options
= {'REGISTER', 'UNDO'}
3844 conversion
= EnumProperty(
3846 items
=(("distance", "Distance", "Set the distance between vertices "
3847 "of the converted grease pencil stroke"),
3848 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
3849 "number of vertices that converted GP strokes will have"),
3850 ("vertices", "Exact vertices", "Set the exact number of vertices "
3851 "that converted grease pencil strokes will have. Short strokes "
3852 "with few points may contain less vertices than this number."),
3853 ("none", "No simplification", "Convert each grease pencil point "
3855 description
="If grease pencil strokes are converted to geometry, "
3856 "use this simplification method",
3857 default
='limit_vertices'
3859 conversion_distance
= FloatProperty(
3861 description
="Absolute distance between vertices along the converted "
3862 "grease pencil stroke",
3868 conversion_max
= IntProperty(
3869 name
="Max Vertices",
3870 description
="Maximum number of vertices grease pencil strokes will "
3871 "have, when they are converted to geomtery",
3875 update
=gstretch_update_min
3877 conversion_min
= IntProperty(
3878 name
="Min Vertices",
3879 description
="Minimum number of vertices grease pencil strokes will "
3880 "have, when they are converted to geomtery",
3884 update
=gstretch_update_max
3886 conversion_vertices
= IntProperty(
3888 description
="Number of vertices grease pencil strokes will "
3889 "have, when they are converted to geometry. If strokes have less "
3890 "points than required, the 'Spread evenly' method is used",
3895 delete_strokes
= BoolProperty(
3896 name
="Delete strokes",
3897 description
="Remove Grease Pencil strokes if they have been used "
3898 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
3901 influence
= FloatProperty(
3903 description
="Force of the tool",
3908 subtype
='PERCENTAGE'
3910 lock_x
= BoolProperty(
3912 description
="Lock editing of the x-coordinate",
3915 lock_y
= BoolProperty(
3917 description
="Lock editing of the y-coordinate",
3920 lock_z
= BoolProperty(
3922 description
="Lock editing of the z-coordinate",
3925 method
= EnumProperty(
3927 items
=(("project", "Project", "Project vertices onto the stroke, "
3928 "using vertex normals and connected edges"),
3929 ("irregular", "Spread", "Distribute vertices along the full "
3930 "stroke, retaining relative distances between the vertices"),
3931 ("regular", "Spread evenly", "Distribute vertices at regular "
3932 "distances along the full stroke")),
3933 description
="Method of distributing the vertices over the Grease "
3939 def poll(cls
, context
):
3940 ob
= context
.active_object
3941 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3943 def draw(self
, context
):
3944 layout
= self
.layout
3945 col
= layout
.column()
3947 col
.prop(self
, "method")
3950 col_conv
= col
.column(align
=True)
3951 col_conv
.prop(self
, "conversion", text
="")
3952 if self
.conversion
== 'distance':
3953 col_conv
.prop(self
, "conversion_distance")
3954 elif self
.conversion
== 'limit_vertices':
3955 row
= col_conv
.row(align
=True)
3956 row
.prop(self
, "conversion_min", text
="Min")
3957 row
.prop(self
, "conversion_max", text
="Max")
3958 elif self
.conversion
== 'vertices':
3959 col_conv
.prop(self
, "conversion_vertices")
3962 col_move
= col
.column(align
=True)
3963 row
= col_move
.row(align
=True)
3965 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
3967 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
3969 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
3971 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
3973 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
3975 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
3976 col_move
.prop(self
, "influence")
3978 col
.operator("remove.gp", text
="Delete GP Strokes")
3980 def invoke(self
, context
, event
):
3981 # flush cached strokes
3982 if 'Gstretch' in looptools_cache
:
3983 looptools_cache
['Gstretch']['single_loops'] = []
3984 # load custom settings
3986 return self
.execute(context
)
3988 def execute(self
, context
):
3990 global_undo
, object, bm
= initialise()
3991 settings_write(self
)
3993 # check cache to see if we can save time
3994 cached
, safe_strokes
, loops
, derived
, mapping
= cache_read("Gstretch",
3995 object, bm
, False, False)
3997 straightening
= False
3999 strokes
= gstretch_safe_to_true_strokes(safe_strokes
)
4000 # cached strokes were flushed (see operator's invoke function)
4001 elif get_grease_pencil(object, context
):
4002 strokes
= gstretch_get_strokes(object, context
)
4004 # straightening function (no GP) -> loops ignore modifiers
4005 straightening
= True
4008 bm_mod
.verts
.ensure_lookup_table()
4009 bm_mod
.edges
.ensure_lookup_table()
4010 bm_mod
.faces
.ensure_lookup_table()
4011 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
4012 if not straightening
:
4013 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
4015 # get loops and strokes
4016 if get_grease_pencil(object, context
):
4018 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
4019 context
.scene
, input='selected')
4020 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4021 loops
= check_loops(loops
, mapping
, bm_mod
)
4023 strokes
= gstretch_get_strokes(object, context
)
4025 # straightening function (no GP) -> loops ignore modifiers
4029 bm_mod
.verts
.ensure_lookup_table()
4030 bm_mod
.edges
.ensure_lookup_table()
4031 bm_mod
.faces
.ensure_lookup_table()
4033 edgekey(edge
) for edge
in bm_mod
.edges
if
4034 edge
.select
and not edge
.hide
4036 loops
= get_connected_selections(edge_keys
)
4037 loops
= check_loops(loops
, mapping
, bm_mod
)
4038 # create fake strokes
4039 strokes
= gstretch_get_fake_strokes(object, bm_mod
, loops
)
4041 # saving cache for faster execution next time
4044 safe_strokes
= gstretch_true_to_safe_strokes(strokes
)
4047 cache_write("Gstretch", object, bm
, False, False,
4048 safe_strokes
, loops
, derived
, mapping
)
4050 # pair loops and strokes
4051 ls_pairs
= gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
)
4052 ls_pairs
= gstretch_align_pairs(ls_pairs
, object, bm_mod
, self
.method
)
4056 # no selected geometry, convert GP to verts
4058 move
.append(gstretch_create_verts(object, bm
, strokes
,
4059 self
.method
, self
.conversion
, self
.conversion_distance
,
4060 self
.conversion_max
, self
.conversion_min
,
4061 self
.conversion_vertices
))
4062 for stroke
in strokes
:
4063 gstretch_erase_stroke(stroke
, context
)
4065 for (loop
, stroke
) in ls_pairs
:
4066 move
.append(gstretch_calculate_verts(loop
, stroke
, object,
4067 bm_mod
, self
.method
))
4068 if self
.delete_strokes
:
4069 if type(stroke
) != bpy
.types
.GPencilStroke
:
4070 # in case of cached fake stroke, get the real one
4071 if get_grease_pencil(object, context
):
4072 strokes
= gstretch_get_strokes(object, context
)
4073 if loops
and strokes
:
4074 ls_pairs
= gstretch_match_loops_strokes(loops
,
4075 strokes
, object, bm_mod
)
4076 ls_pairs
= gstretch_align_pairs(ls_pairs
,
4077 object, bm_mod
, self
.method
)
4078 for (l
, s
) in ls_pairs
:
4082 gstretch_erase_stroke(stroke
, context
)
4084 # move vertices to new locations
4085 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4086 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4089 bmesh
.update_edit_mesh(object.data
, tessface
=True, destructive
=True)
4090 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4095 terminate(global_undo
)
4101 class Relax(Operator
):
4102 bl_idname
= "mesh.looptools_relax"
4104 bl_description
= "Relax the loop, so it is smoother"
4105 bl_options
= {'REGISTER', 'UNDO'}
4107 input = EnumProperty(
4109 items
=(("all", "Parallel (all)", "Also use non-selected "
4110 "parallel loops as input"),
4111 ("selected", "Selection", "Only use selected vertices as input")),
4112 description
="Loops that are relaxed",
4115 interpolation
= EnumProperty(
4116 name
="Interpolation",
4117 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4118 ("linear", "Linear", "Simple and fast linear algorithm")),
4119 description
="Algorithm used for interpolation",
4122 iterations
= EnumProperty(
4124 items
=(("1", "1", "One"),
4125 ("3", "3", "Three"),
4127 ("10", "10", "Ten"),
4128 ("25", "25", "Twenty-five")),
4129 description
="Number of times the loop is relaxed",
4132 regular
= BoolProperty(
4134 description
="Distribute vertices at constant distances along the loop",
4139 def poll(cls
, context
):
4140 ob
= context
.active_object
4141 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4143 def draw(self
, context
):
4144 layout
= self
.layout
4145 col
= layout
.column()
4147 col
.prop(self
, "interpolation")
4148 col
.prop(self
, "input")
4149 col
.prop(self
, "iterations")
4150 col
.prop(self
, "regular")
4152 def invoke(self
, context
, event
):
4153 # load custom settings
4155 return self
.execute(context
)
4157 def execute(self
, context
):
4159 global_undo
, object, bm
= initialise()
4160 settings_write(self
)
4161 # check cache to see if we can save time
4162 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Relax",
4163 object, bm
, self
.input, False)
4165 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
4168 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
4169 context
.scene
, self
.input)
4170 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4171 loops
= check_loops(loops
, mapping
, bm_mod
)
4172 knots
, points
= relax_calculate_knots(loops
)
4174 # saving cache for faster execution next time
4176 cache_write("Relax", object, bm
, self
.input, False, False, loops
,
4179 for iteration
in range(int(self
.iterations
)):
4180 # calculate splines and new positions
4181 tknots
, tpoints
= relax_calculate_t(bm_mod
, knots
, points
,
4184 for i
in range(len(knots
)):
4185 splines
.append(calculate_splines(self
.interpolation
, bm_mod
,
4186 tknots
[i
], knots
[i
]))
4187 move
= [relax_calculate_verts(bm_mod
, self
.interpolation
,
4188 tknots
, knots
, tpoints
, points
, splines
)]
4189 move_verts(object, bm
, mapping
, move
, False, -1)
4194 terminate(global_undo
)
4200 class Space(Operator
):
4201 bl_idname
= "mesh.looptools_space"
4203 bl_description
= "Space the vertices in a regular distrubtion on the loop"
4204 bl_options
= {'REGISTER', 'UNDO'}
4206 influence
= FloatProperty(
4208 description
="Force of the tool",
4213 subtype
='PERCENTAGE'
4215 input = EnumProperty(
4217 items
=(("all", "Parallel (all)", "Also use non-selected "
4218 "parallel loops as input"),
4219 ("selected", "Selection", "Only use selected vertices as input")),
4220 description
="Loops that are spaced",
4223 interpolation
= EnumProperty(
4224 name
="Interpolation",
4225 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4226 ("linear", "Linear", "Vertices are projected on existing edges")),
4227 description
="Algorithm used for interpolation",
4230 lock_x
= BoolProperty(
4232 description
="Lock editing of the x-coordinate",
4235 lock_y
= BoolProperty(
4237 description
="Lock editing of the y-coordinate",
4240 lock_z
= BoolProperty(
4242 description
="Lock editing of the z-coordinate",
4247 def poll(cls
, context
):
4248 ob
= context
.active_object
4249 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
4251 def draw(self
, context
):
4252 layout
= self
.layout
4253 col
= layout
.column()
4255 col
.prop(self
, "interpolation")
4256 col
.prop(self
, "input")
4259 col_move
= col
.column(align
=True)
4260 row
= col_move
.row(align
=True)
4262 row
.prop(self
, "lock_x", text
="X", icon
='LOCKED')
4264 row
.prop(self
, "lock_x", text
="X", icon
='UNLOCKED')
4266 row
.prop(self
, "lock_y", text
="Y", icon
='LOCKED')
4268 row
.prop(self
, "lock_y", text
="Y", icon
='UNLOCKED')
4270 row
.prop(self
, "lock_z", text
="Z", icon
='LOCKED')
4272 row
.prop(self
, "lock_z", text
="Z", icon
='UNLOCKED')
4273 col_move
.prop(self
, "influence")
4275 def invoke(self
, context
, event
):
4276 # load custom settings
4278 return self
.execute(context
)
4280 def execute(self
, context
):
4282 global_undo
, object, bm
= initialise()
4283 settings_write(self
)
4284 # check cache to see if we can save time
4285 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Space",
4286 object, bm
, self
.input, False)
4288 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
4291 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
4292 context
.scene
, self
.input)
4293 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
4294 loops
= check_loops(loops
, mapping
, bm_mod
)
4296 # saving cache for faster execution next time
4298 cache_write("Space", object, bm
, self
.input, False, False, loops
,
4303 # calculate splines and new positions
4304 if loop
[1]: # circular
4305 loop
[0].append(loop
[0][0])
4306 tknots
, tpoints
= space_calculate_t(bm_mod
, loop
[0][:])
4307 splines
= calculate_splines(self
.interpolation
, bm_mod
,
4309 move
.append(space_calculate_verts(bm_mod
, self
.interpolation
,
4310 tknots
, tpoints
, loop
[0][:-1], splines
))
4311 # move vertices to new locations
4312 if self
.lock_x
or self
.lock_y
or self
.lock_z
:
4313 lock
= [self
.lock_x
, self
.lock_y
, self
.lock_z
]
4316 move_verts(object, bm
, mapping
, move
, lock
, self
.influence
)
4321 terminate(global_undo
)
4326 # ########################################
4327 # ##### GUI and registration #############
4328 # ########################################
4330 # menu containing all tools
4331 class VIEW3D_MT_edit_mesh_looptools(Menu
):
4332 bl_label
= "LoopTools"
4334 def draw(self
, context
):
4335 layout
= self
.layout
4337 layout
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4338 layout
.operator("mesh.looptools_circle")
4339 layout
.operator("mesh.looptools_curve")
4340 layout
.operator("mesh.looptools_flatten")
4341 layout
.operator("mesh.looptools_gstretch")
4342 layout
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4343 layout
.operator("mesh.looptools_relax")
4344 layout
.operator("mesh.looptools_space")
4347 # panel containing all tools
4348 class VIEW3D_PT_tools_looptools(Panel
):
4349 bl_space_type
= 'VIEW_3D'
4350 bl_region_type
= 'TOOLS'
4351 bl_category
= 'Tools'
4352 bl_context
= "mesh_edit"
4353 bl_label
= "LoopTools"
4354 bl_options
= {'DEFAULT_CLOSED'}
4356 def draw(self
, context
):
4357 layout
= self
.layout
4358 col
= layout
.column(align
=True)
4359 lt
= context
.window_manager
.looptools
4361 # bridge - first line
4362 split
= col
.split(percentage
=0.15, align
=True)
4363 if lt
.display_bridge
:
4364 split
.prop(lt
, "display_bridge", text
="", icon
='DOWNARROW_HLT')
4366 split
.prop(lt
, "display_bridge", text
="", icon
='RIGHTARROW')
4367 split
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
4369 if lt
.display_bridge
:
4370 box
= col
.column(align
=True).box().column()
4371 # box.prop(self, "mode")
4374 col_top
= box
.column(align
=True)
4375 row
= col_top
.row(align
=True)
4376 col_left
= row
.column(align
=True)
4377 col_right
= row
.column(align
=True)
4378 col_right
.active
= lt
.bridge_segments
!= 1
4379 col_left
.prop(lt
, "bridge_segments")
4380 col_right
.prop(lt
, "bridge_min_width", text
="")
4382 bottom_left
= col_left
.row()
4383 bottom_left
.active
= lt
.bridge_segments
!= 1
4384 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4385 bottom_right
= col_right
.row()
4386 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4387 bottom_right
.prop(lt
, "bridge_cubic_strength")
4388 # boolean properties
4389 col_top
.prop(lt
, "bridge_remove_faces")
4391 # override properties
4393 row
= box
.row(align
=True)
4394 row
.prop(lt
, "bridge_twist")
4395 row
.prop(lt
, "bridge_reverse")
4397 # circle - first line
4398 split
= col
.split(percentage
=0.15, align
=True)
4399 if lt
.display_circle
:
4400 split
.prop(lt
, "display_circle", text
="", icon
='DOWNARROW_HLT')
4402 split
.prop(lt
, "display_circle", text
="", icon
='RIGHTARROW')
4403 split
.operator("mesh.looptools_circle")
4405 if lt
.display_circle
:
4406 box
= col
.column(align
=True).box().column()
4407 box
.prop(lt
, "circle_fit")
4410 box
.prop(lt
, "circle_flatten")
4411 row
= box
.row(align
=True)
4412 row
.prop(lt
, "circle_custom_radius")
4413 row_right
= row
.row(align
=True)
4414 row_right
.active
= lt
.circle_custom_radius
4415 row_right
.prop(lt
, "circle_radius", text
="")
4416 box
.prop(lt
, "circle_regular")
4419 col_move
= box
.column(align
=True)
4420 row
= col_move
.row(align
=True)
4421 if lt
.circle_lock_x
:
4422 row
.prop(lt
, "circle_lock_x", text
="X", icon
='LOCKED')
4424 row
.prop(lt
, "circle_lock_x", text
="X", icon
='UNLOCKED')
4425 if lt
.circle_lock_y
:
4426 row
.prop(lt
, "circle_lock_y", text
="Y", icon
='LOCKED')
4428 row
.prop(lt
, "circle_lock_y", text
="Y", icon
='UNLOCKED')
4429 if lt
.circle_lock_z
:
4430 row
.prop(lt
, "circle_lock_z", text
="Z", icon
='LOCKED')
4432 row
.prop(lt
, "circle_lock_z", text
="Z", icon
='UNLOCKED')
4433 col_move
.prop(lt
, "circle_influence")
4435 # curve - first line
4436 split
= col
.split(percentage
=0.15, align
=True)
4437 if lt
.display_curve
:
4438 split
.prop(lt
, "display_curve", text
="", icon
='DOWNARROW_HLT')
4440 split
.prop(lt
, "display_curve", text
="", icon
='RIGHTARROW')
4441 split
.operator("mesh.looptools_curve")
4443 if lt
.display_curve
:
4444 box
= col
.column(align
=True).box().column()
4445 box
.prop(lt
, "curve_interpolation")
4446 box
.prop(lt
, "curve_restriction")
4447 box
.prop(lt
, "curve_boundaries")
4448 box
.prop(lt
, "curve_regular")
4451 col_move
= box
.column(align
=True)
4452 row
= col_move
.row(align
=True)
4454 row
.prop(lt
, "curve_lock_x", text
="X", icon
='LOCKED')
4456 row
.prop(lt
, "curve_lock_x", text
="X", icon
='UNLOCKED')
4458 row
.prop(lt
, "curve_lock_y", text
="Y", icon
='LOCKED')
4460 row
.prop(lt
, "curve_lock_y", text
="Y", icon
='UNLOCKED')
4462 row
.prop(lt
, "curve_lock_z", text
="Z", icon
='LOCKED')
4464 row
.prop(lt
, "curve_lock_z", text
="Z", icon
='UNLOCKED')
4465 col_move
.prop(lt
, "curve_influence")
4467 # flatten - first line
4468 split
= col
.split(percentage
=0.15, align
=True)
4469 if lt
.display_flatten
:
4470 split
.prop(lt
, "display_flatten", text
="", icon
='DOWNARROW_HLT')
4472 split
.prop(lt
, "display_flatten", text
="", icon
='RIGHTARROW')
4473 split
.operator("mesh.looptools_flatten")
4474 # flatten - settings
4475 if lt
.display_flatten
:
4476 box
= col
.column(align
=True).box().column()
4477 box
.prop(lt
, "flatten_plane")
4478 # box.prop(lt, "flatten_restriction")
4481 col_move
= box
.column(align
=True)
4482 row
= col_move
.row(align
=True)
4483 if lt
.flatten_lock_x
:
4484 row
.prop(lt
, "flatten_lock_x", text
="X", icon
='LOCKED')
4486 row
.prop(lt
, "flatten_lock_x", text
="X", icon
='UNLOCKED')
4487 if lt
.flatten_lock_y
:
4488 row
.prop(lt
, "flatten_lock_y", text
="Y", icon
='LOCKED')
4490 row
.prop(lt
, "flatten_lock_y", text
="Y", icon
='UNLOCKED')
4491 if lt
.flatten_lock_z
:
4492 row
.prop(lt
, "flatten_lock_z", text
="Z", icon
='LOCKED')
4494 row
.prop(lt
, "flatten_lock_z", text
="Z", icon
='UNLOCKED')
4495 col_move
.prop(lt
, "flatten_influence")
4497 # gstretch - first line
4498 split
= col
.split(percentage
=0.15, align
=True)
4499 if lt
.display_gstretch
:
4500 split
.prop(lt
, "display_gstretch", text
="", icon
='DOWNARROW_HLT')
4502 split
.prop(lt
, "display_gstretch", text
="", icon
='RIGHTARROW')
4503 split
.operator("mesh.looptools_gstretch")
4505 if lt
.display_gstretch
:
4506 box
= col
.column(align
=True).box().column()
4507 box
.prop(lt
, "gstretch_method")
4509 col_conv
= box
.column(align
=True)
4510 col_conv
.prop(lt
, "gstretch_conversion", text
="")
4511 if lt
.gstretch_conversion
== 'distance':
4512 col_conv
.prop(lt
, "gstretch_conversion_distance")
4513 elif lt
.gstretch_conversion
== 'limit_vertices':
4514 row
= col_conv
.row(align
=True)
4515 row
.prop(lt
, "gstretch_conversion_min", text
="Min")
4516 row
.prop(lt
, "gstretch_conversion_max", text
="Max")
4517 elif lt
.gstretch_conversion
== 'vertices':
4518 col_conv
.prop(lt
, "gstretch_conversion_vertices")
4521 col_move
= box
.column(align
=True)
4522 row
= col_move
.row(align
=True)
4523 if lt
.gstretch_lock_x
:
4524 row
.prop(lt
, "gstretch_lock_x", text
="X", icon
='LOCKED')
4526 row
.prop(lt
, "gstretch_lock_x", text
="X", icon
='UNLOCKED')
4527 if lt
.gstretch_lock_y
:
4528 row
.prop(lt
, "gstretch_lock_y", text
="Y", icon
='LOCKED')
4530 row
.prop(lt
, "gstretch_lock_y", text
="Y", icon
='UNLOCKED')
4531 if lt
.gstretch_lock_z
:
4532 row
.prop(lt
, "gstretch_lock_z", text
="Z", icon
='LOCKED')
4534 row
.prop(lt
, "gstretch_lock_z", text
="Z", icon
='UNLOCKED')
4535 col_move
.prop(lt
, "gstretch_influence")
4536 box
.operator("remove.gp", text
="Delete GP Strokes")
4539 split
= col
.split(percentage
=0.15, align
=True)
4541 split
.prop(lt
, "display_loft", text
="", icon
='DOWNARROW_HLT')
4543 split
.prop(lt
, "display_loft", text
="", icon
='RIGHTARROW')
4544 split
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
4547 box
= col
.column(align
=True).box().column()
4548 # box.prop(self, "mode")
4551 col_top
= box
.column(align
=True)
4552 row
= col_top
.row(align
=True)
4553 col_left
= row
.column(align
=True)
4554 col_right
= row
.column(align
=True)
4555 col_right
.active
= lt
.bridge_segments
!= 1
4556 col_left
.prop(lt
, "bridge_segments")
4557 col_right
.prop(lt
, "bridge_min_width", text
="")
4559 bottom_left
= col_left
.row()
4560 bottom_left
.active
= lt
.bridge_segments
!= 1
4561 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
4562 bottom_right
= col_right
.row()
4563 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
4564 bottom_right
.prop(lt
, "bridge_cubic_strength")
4565 # boolean properties
4566 col_top
.prop(lt
, "bridge_remove_faces")
4567 col_top
.prop(lt
, "bridge_loft_loop")
4569 # override properties
4571 row
= box
.row(align
=True)
4572 row
.prop(lt
, "bridge_twist")
4573 row
.prop(lt
, "bridge_reverse")
4575 # relax - first line
4576 split
= col
.split(percentage
=0.15, align
=True)
4577 if lt
.display_relax
:
4578 split
.prop(lt
, "display_relax", text
="", icon
='DOWNARROW_HLT')
4580 split
.prop(lt
, "display_relax", text
="", icon
='RIGHTARROW')
4581 split
.operator("mesh.looptools_relax")
4583 if lt
.display_relax
:
4584 box
= col
.column(align
=True).box().column()
4585 box
.prop(lt
, "relax_interpolation")
4586 box
.prop(lt
, "relax_input")
4587 box
.prop(lt
, "relax_iterations")
4588 box
.prop(lt
, "relax_regular")
4590 # space - first line
4591 split
= col
.split(percentage
=0.15, align
=True)
4592 if lt
.display_space
:
4593 split
.prop(lt
, "display_space", text
="", icon
='DOWNARROW_HLT')
4595 split
.prop(lt
, "display_space", text
="", icon
='RIGHTARROW')
4596 split
.operator("mesh.looptools_space")
4598 if lt
.display_space
:
4599 box
= col
.column(align
=True).box().column()
4600 box
.prop(lt
, "space_interpolation")
4601 box
.prop(lt
, "space_input")
4604 col_move
= box
.column(align
=True)
4605 row
= col_move
.row(align
=True)
4607 row
.prop(lt
, "space_lock_x", text
="X", icon
='LOCKED')
4609 row
.prop(lt
, "space_lock_x", text
="X", icon
='UNLOCKED')
4611 row
.prop(lt
, "space_lock_y", text
="Y", icon
='LOCKED')
4613 row
.prop(lt
, "space_lock_y", text
="Y", icon
='UNLOCKED')
4615 row
.prop(lt
, "space_lock_z", text
="Z", icon
='LOCKED')
4617 row
.prop(lt
, "space_lock_z", text
="Z", icon
='UNLOCKED')
4618 col_move
.prop(lt
, "space_influence")
4621 # property group containing all properties for the gui in the panel
4622 class LoopToolsProps(PropertyGroup
):
4624 Fake module like class
4625 bpy.context.window_manager.looptools
4627 # general display properties
4628 display_bridge
= BoolProperty(
4629 name
="Bridge settings",
4630 description
="Display settings of the Bridge tool",
4633 display_circle
= BoolProperty(
4634 name
="Circle settings",
4635 description
="Display settings of the Circle tool",
4638 display_curve
= BoolProperty(
4639 name
="Curve settings",
4640 description
="Display settings of the Curve tool",
4643 display_flatten
= BoolProperty(
4644 name
="Flatten settings",
4645 description
="Display settings of the Flatten tool",
4648 display_gstretch
= BoolProperty(
4649 name
="Gstretch settings",
4650 description
="Display settings of the Gstretch tool",
4653 display_loft
= BoolProperty(
4654 name
="Loft settings",
4655 description
="Display settings of the Loft tool",
4658 display_relax
= BoolProperty(
4659 name
="Relax settings",
4660 description
="Display settings of the Relax tool",
4663 display_space
= BoolProperty(
4664 name
="Space settings",
4665 description
="Display settings of the Space tool",
4670 bridge_cubic_strength
= FloatProperty(
4672 description
="Higher strength results in more fluid curves",
4677 bridge_interpolation
= EnumProperty(
4678 name
="Interpolation mode",
4679 items
=(('cubic', "Cubic", "Gives curved results"),
4680 ('linear', "Linear", "Basic, fast, straight interpolation")),
4681 description
="Interpolation mode: algorithm used when creating segments",
4684 bridge_loft
= BoolProperty(
4686 description
="Loft multiple loops, instead of considering them as "
4687 "a multi-input for bridging",
4690 bridge_loft_loop
= BoolProperty(
4692 description
="Connect the first and the last loop with each other",
4695 bridge_min_width
= IntProperty(
4696 name
="Minimum width",
4697 description
="Segments with an edge smaller than this are merged "
4698 "(compared to base edge)",
4702 subtype
='PERCENTAGE'
4704 bridge_mode
= EnumProperty(
4706 items
=(('basic', "Basic", "Fast algorithm"),
4707 ('shortest', "Shortest edge", "Slower algorithm with "
4708 "better vertex matching")),
4709 description
="Algorithm used for bridging",
4712 bridge_remove_faces
= BoolProperty(
4713 name
="Remove faces",
4714 description
="Remove faces that are internal after bridging",
4717 bridge_reverse
= BoolProperty(
4719 description
="Manually override the direction in which the loops "
4720 "are bridged. Only use if the tool gives the wrong result",
4723 bridge_segments
= IntProperty(
4725 description
="Number of segments used to bridge the gap (0=automatic)",
4730 bridge_twist
= IntProperty(
4732 description
="Twist what vertices are connected to each other",
4737 circle_custom_radius
= BoolProperty(
4739 description
="Force a custom radius",
4742 circle_fit
= EnumProperty(
4744 items
=(("best", "Best fit", "Non-linear least squares"),
4745 ("inside", "Fit inside", "Only move vertices towards the center")),
4746 description
="Method used for fitting a circle to the vertices",
4749 circle_flatten
= BoolProperty(
4751 description
="Flatten the circle, instead of projecting it on the mesh",
4754 circle_influence
= FloatProperty(
4756 description
="Force of the tool",
4761 subtype
='PERCENTAGE'
4763 circle_lock_x
= BoolProperty(
4765 description
="Lock editing of the x-coordinate",
4768 circle_lock_y
= BoolProperty(
4770 description
="Lock editing of the y-coordinate",
4773 circle_lock_z
= BoolProperty(
4775 description
="Lock editing of the z-coordinate",
4778 circle_radius
= FloatProperty(
4780 description
="Custom radius for circle",
4785 circle_regular
= BoolProperty(
4787 description
="Distribute vertices at constant distances along the circle",
4791 curve_boundaries
= BoolProperty(
4793 description
="Limit the tool to work within the boundaries of the "
4794 "selected vertices",
4797 curve_influence
= FloatProperty(
4799 description
="Force of the tool",
4804 subtype
='PERCENTAGE'
4806 curve_interpolation
= EnumProperty(
4807 name
="Interpolation",
4808 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4809 ("linear", "Linear", "Simple and fast linear algorithm")),
4810 description
="Algorithm used for interpolation",
4813 curve_lock_x
= BoolProperty(
4815 description
="Lock editing of the x-coordinate",
4818 curve_lock_y
= BoolProperty(
4820 description
="Lock editing of the y-coordinate",
4823 curve_lock_z
= BoolProperty(
4825 description
="Lock editing of the z-coordinate",
4828 curve_regular
= BoolProperty(
4830 description
="Distribute vertices at constant distances along the curve",
4833 curve_restriction
= EnumProperty(
4835 items
=(("none", "None", "No restrictions on vertex movement"),
4836 ("extrude", "Extrude only", "Only allow extrusions (no indentations)"),
4837 ("indent", "Indent only", "Only allow indentation (no extrusions)")),
4838 description
="Restrictions on how the vertices can be moved",
4842 # flatten properties
4843 flatten_influence
= FloatProperty(
4845 description
="Force of the tool",
4850 subtype
='PERCENTAGE'
4852 flatten_lock_x
= BoolProperty(
4854 description
="Lock editing of the x-coordinate",
4856 flatten_lock_y
= BoolProperty(name
="Lock Y",
4857 description
="Lock editing of the y-coordinate",
4860 flatten_lock_z
= BoolProperty(
4862 description
="Lock editing of the z-coordinate",
4865 flatten_plane
= EnumProperty(
4867 items
=(("best_fit", "Best fit", "Calculate a best fitting plane"),
4868 ("normal", "Normal", "Derive plane from averaging vertex "
4870 ("view", "View", "Flatten on a plane perpendicular to the "
4872 description
="Plane on which vertices are flattened",
4875 flatten_restriction
= EnumProperty(
4877 items
=(("none", "None", "No restrictions on vertex movement"),
4878 ("bounding_box", "Bounding box", "Vertices are restricted to "
4879 "movement inside the bounding box of the selection")),
4880 description
="Restrictions on how the vertices can be moved",
4884 # gstretch properties
4885 gstretch_conversion
= EnumProperty(
4887 items
=(("distance", "Distance", "Set the distance between vertices "
4888 "of the converted grease pencil stroke"),
4889 ("limit_vertices", "Limit vertices", "Set the minimum and maximum "
4890 "number of vertices that converted GP strokes will have"),
4891 ("vertices", "Exact vertices", "Set the exact number of vertices "
4892 "that converted grease pencil strokes will have. Short strokes "
4893 "with few points may contain less vertices than this number."),
4894 ("none", "No simplification", "Convert each grease pencil point "
4896 description
="If grease pencil strokes are converted to geometry, "
4897 "use this simplification method",
4898 default
='limit_vertices'
4900 gstretch_conversion_distance
= FloatProperty(
4902 description
="Absolute distance between vertices along the converted "
4903 "grease pencil stroke",
4909 gstretch_conversion_max
= IntProperty(
4910 name
="Max Vertices",
4911 description
="Maximum number of vertices grease pencil strokes will "
4912 "have, when they are converted to geomtery",
4916 update
=gstretch_update_min
4918 gstretch_conversion_min
= IntProperty(
4919 name
="Min Vertices",
4920 description
="Minimum number of vertices grease pencil strokes will "
4921 "have, when they are converted to geomtery",
4925 update
=gstretch_update_max
4927 gstretch_conversion_vertices
= IntProperty(
4929 description
="Number of vertices grease pencil strokes will "
4930 "have, when they are converted to geometry. If strokes have less "
4931 "points than required, the 'Spread evenly' method is used",
4936 gstretch_delete_strokes
= BoolProperty(
4937 name
="Delete strokes",
4938 description
="Remove Grease Pencil strokes if they have been used "
4939 "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
4942 gstretch_influence
= FloatProperty(
4944 description
="Force of the tool",
4949 subtype
='PERCENTAGE'
4951 gstretch_lock_x
= BoolProperty(
4953 description
="Lock editing of the x-coordinate",
4956 gstretch_lock_y
= BoolProperty(
4958 description
="Lock editing of the y-coordinate",
4961 gstretch_lock_z
= BoolProperty(
4963 description
="Lock editing of the z-coordinate",
4966 gstretch_method
= EnumProperty(
4968 items
=(("project", "Project", "Project vertices onto the stroke, "
4969 "using vertex normals and connected edges"),
4970 ("irregular", "Spread", "Distribute vertices along the full "
4971 "stroke, retaining relative distances between the vertices"),
4972 ("regular", "Spread evenly", "Distribute vertices at regular "
4973 "distances along the full stroke")),
4974 description
="Method of distributing the vertices over the Grease "
4980 relax_input
= EnumProperty(name
="Input",
4981 items
=(("all", "Parallel (all)", "Also use non-selected "
4982 "parallel loops as input"),
4983 ("selected", "Selection", "Only use selected vertices as input")),
4984 description
="Loops that are relaxed",
4987 relax_interpolation
= EnumProperty(
4988 name
="Interpolation",
4989 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
4990 ("linear", "Linear", "Simple and fast linear algorithm")),
4991 description
="Algorithm used for interpolation",
4994 relax_iterations
= EnumProperty(name
="Iterations",
4995 items
=(("1", "1", "One"),
4996 ("3", "3", "Three"),
4998 ("10", "10", "Ten"),
4999 ("25", "25", "Twenty-five")),
5000 description
="Number of times the loop is relaxed",
5003 relax_regular
= BoolProperty(
5005 description
="Distribute vertices at constant distances along the loop",
5010 space_influence
= FloatProperty(
5012 description
="Force of the tool",
5017 subtype
='PERCENTAGE'
5019 space_input
= EnumProperty(
5021 items
=(("all", "Parallel (all)", "Also use non-selected "
5022 "parallel loops as input"),
5023 ("selected", "Selection", "Only use selected vertices as input")),
5024 description
="Loops that are spaced",
5027 space_interpolation
= EnumProperty(
5028 name
="Interpolation",
5029 items
=(("cubic", "Cubic", "Natural cubic spline, smooth results"),
5030 ("linear", "Linear", "Vertices are projected on existing edges")),
5031 description
="Algorithm used for interpolation",
5034 space_lock_x
= BoolProperty(
5036 description
="Lock editing of the x-coordinate",
5039 space_lock_y
= BoolProperty(
5041 description
="Lock editing of the y-coordinate",
5044 space_lock_z
= BoolProperty(
5046 description
="Lock editing of the z-coordinate",
5051 # draw function for integration in menus
5052 def menu_func(self
, context
):
5053 self
.layout
.menu("VIEW3D_MT_edit_mesh_looptools")
5054 self
.layout
.separator()
5057 # Add-ons Preferences Update Panel
5059 # Define Panel classes for updating
5061 VIEW3D_PT_tools_looptools
,
5065 def update_panel(self
, context
):
5066 message
= "LoopTools: Updating Panel locations has failed"
5068 for panel
in panels
:
5069 if "bl_rna" in panel
.__dict
__:
5070 bpy
.utils
.unregister_class(panel
)
5072 for panel
in panels
:
5073 panel
.bl_category
= context
.user_preferences
.addons
[__name__
].preferences
.category
5074 bpy
.utils
.register_class(panel
)
5076 except Exception as e
:
5077 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
5081 class LoopPreferences(AddonPreferences
):
5082 # this must match the addon name, use '__package__'
5083 # when defining this in a submodule of a python package.
5084 bl_idname
= __name__
5086 category
= StringProperty(
5087 name
="Tab Category",
5088 description
="Choose a name for the category of the panel",
5093 def draw(self
, context
):
5094 layout
= self
.layout
5098 col
.label(text
="Tab Category:")
5099 col
.prop(self
, "category", text
="")
5102 # define classes for registration
5104 VIEW3D_MT_edit_mesh_looptools
,
5105 VIEW3D_PT_tools_looptools
,
5119 # registering and menu integration
5122 bpy
.utils
.register_class(c
)
5123 bpy
.types
.VIEW3D_MT_edit_mesh_specials
.prepend(menu_func
)
5124 bpy
.types
.WindowManager
.looptools
= PointerProperty(type=LoopToolsProps
)
5125 update_panel(None, bpy
.context
)
5128 # unregistering and removing menus
5131 bpy
.utils
.unregister_class(c
)
5132 bpy
.types
.VIEW3D_MT_edit_mesh_specials
.remove(menu_func
)
5134 del bpy
.types
.WindowManager
.looptools
5139 if __name__
== "__main__":