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, 63, 0),
24 "location": "View3D > Toolbar and View3D > Specials (W-key)",
26 "description": "Mesh modelling toolkit. Several tools to aid modelling",
27 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
28 "Scripts/Modeling/LoopTools",
29 "tracker_url": "http://projects.blender.org/tracker/index.php?"
30 "func=detail&aid=26189",
39 from bpy_extras
import view3d_utils
42 ##########################################
43 ####### General functions ################
44 ##########################################
47 # used by all tools to improve speed on reruns
51 # force a full recalculation next time
52 def cache_delete(tool
):
53 if tool
in looptools_cache
:
54 del looptools_cache
[tool
]
57 # check cache for stored information
58 def cache_read(tool
, object, bm
, input_method
, boundaries
):
59 # current tool not cached yet
60 if tool
not in looptools_cache
:
61 return(False, False, False, False, False)
62 # check if selected object didn't change
63 if object.name
!= looptools_cache
[tool
]["object"]:
64 return(False, False, False, False, False)
65 # check if input didn't change
66 if input_method
!= looptools_cache
[tool
]["input_method"]:
67 return(False, False, False, False, False)
68 if boundaries
!= looptools_cache
[tool
]["boundaries"]:
69 return(False, False, False, False, False)
70 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport \
71 and mod
.type == 'MIRROR']
72 if modifiers
!= looptools_cache
[tool
]["modifiers"]:
73 return(False, False, False, False, False)
74 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
75 if input != looptools_cache
[tool
]["input"]:
76 return(False, False, False, False, False)
78 single_loops
= looptools_cache
[tool
]["single_loops"]
79 loops
= looptools_cache
[tool
]["loops"]
80 derived
= looptools_cache
[tool
]["derived"]
81 mapping
= looptools_cache
[tool
]["mapping"]
83 return(True, single_loops
, loops
, derived
, mapping
)
86 # store information in the cache
87 def cache_write(tool
, object, bm
, input_method
, boundaries
, single_loops
,
88 loops
, derived
, mapping
):
89 # clear cache of current tool
90 if tool
in looptools_cache
:
91 del looptools_cache
[tool
]
92 # prepare values to be saved to cache
93 input = [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
94 modifiers
= [mod
.name
for mod
in object.modifiers
if mod
.show_viewport \
95 and mod
.type == 'MIRROR']
97 looptools_cache
[tool
] = {"input": input, "object": object.name
,
98 "input_method": input_method
, "boundaries": boundaries
,
99 "single_loops": single_loops
, "loops": loops
,
100 "derived": derived
, "mapping": mapping
, "modifiers": modifiers
}
103 # calculates natural cubic splines through all given knots
104 def calculate_cubic_splines(bm_mod
, tknots
, knots
):
105 # hack for circular loops
106 if knots
[0] == knots
[-1] and len(knots
) > 1:
109 for k
in range(-1, -5, -1):
110 if k
- 1 < -len(knots
):
112 k_new1
.append(knots
[k
-1])
115 if k
+ 1 > len(knots
) - 1:
117 k_new2
.append(knots
[k
+1])
124 for t
in range(-1, -5, -1):
125 if t
- 1 < -len(tknots
):
127 total1
+= tknots
[t
] - tknots
[t
-1]
128 t_new1
.append(tknots
[0] - total1
)
132 if t
+ 1 > len(tknots
) - 1:
134 total2
+= tknots
[t
+1] - tknots
[t
]
135 t_new2
.append(tknots
[-1] + total2
)
148 locs
= [bm_mod
.verts
[k
].co
[:] for k
in knots
]
156 if x
[i
+1] - x
[i
] == 0:
159 h
.append(x
[i
+1] - x
[i
])
161 for i
in range(1, n
-1):
162 q
.append(3/h
[i
]*(a
[i
+1]-a
[i
]) - 3/h
[i
-1]*(a
[i
]-a
[i
-1]))
166 for i
in range(1, n
-1):
167 l
.append(2*(x
[i
+1]-x
[i
-1]) - h
[i
-1]*u
[i
-1])
170 u
.append(h
[i
] / l
[i
])
171 z
.append((q
[i
] - h
[i
-1] * z
[i
-1]) / l
[i
])
174 b
= [False for i
in range(n
-1)]
175 c
= [False for i
in range(n
)]
176 d
= [False for i
in range(n
-1)]
178 for i
in range(n
-2, -1, -1):
179 c
[i
] = z
[i
] - u
[i
]*c
[i
+1]
180 b
[i
] = (a
[i
+1]-a
[i
])/h
[i
] - h
[i
]*(c
[i
+1]+2*c
[i
])/3
181 d
[i
] = (c
[i
+1]-c
[i
]) / (3*h
[i
])
183 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
185 for i
in range(len(knots
)-1):
186 splines
.append([result
[i
], result
[i
+n
-1], result
[i
+(n
-1)*2]])
187 if circular
: # cleaning up after hack
189 tknots
= tknots
[4:-4]
194 # calculates linear splines through all given knots
195 def calculate_linear_splines(bm_mod
, tknots
, knots
):
197 for i
in range(len(knots
)-1):
198 a
= bm_mod
.verts
[knots
[i
]].co
199 b
= bm_mod
.verts
[knots
[i
+1]].co
203 splines
.append([a
, d
, t
, u
]) # [locStart, locDif, tStart, tDif]
208 # calculate a best-fit plane to the given vertices
209 def calculate_plane(bm_mod
, loop
, method
="best_fit", object=False):
210 # getting the vertex locations
211 locs
= [bm_mod
.verts
[v
].co
.copy() for v
in loop
[0]]
213 # calculating the center of masss
214 com
= mathutils
.Vector()
220 if method
== 'best_fit':
221 # creating the covariance matrix
222 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
227 mat
[0][0] += (loc
[0]-x
)**2
228 mat
[1][0] += (loc
[0]-x
)*(loc
[1]-y
)
229 mat
[2][0] += (loc
[0]-x
)*(loc
[2]-z
)
230 mat
[0][1] += (loc
[1]-y
)*(loc
[0]-x
)
231 mat
[1][1] += (loc
[1]-y
)**2
232 mat
[2][1] += (loc
[1]-y
)*(loc
[2]-z
)
233 mat
[0][2] += (loc
[2]-z
)*(loc
[0]-x
)
234 mat
[1][2] += (loc
[2]-z
)*(loc
[1]-y
)
235 mat
[2][2] += (loc
[2]-z
)**2
237 # calculating the normal to the plane
242 if sum(mat
[0]) == 0.0:
243 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
244 elif sum(mat
[1]) == 0.0:
245 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
246 elif sum(mat
[2]) == 0.0:
247 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
249 # warning! this is different from .normalize()
252 vec
= mathutils
.Vector((1.0, 1.0, 1.0))
253 vec2
= (mat
* vec
)/(mat
* vec
).length
254 while vec
!= vec2
and iter<itermax
:
261 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
264 elif method
== 'normal':
265 # averaging the vertex normals
266 v_normals
= [bm_mod
.verts
[v
].normal
for v
in loop
[0]]
267 normal
= mathutils
.Vector()
268 for v_normal
in v_normals
:
270 normal
/= len(v_normals
)
273 elif method
== 'view':
274 # calculate view normal
275 rotation
= bpy
.context
.space_data
.region_3d
.view_matrix
.to_3x3().\
277 normal
= rotation
* mathutils
.Vector((0.0, 0.0, 1.0))
279 normal
= object.matrix_world
.inverted().to_euler().to_matrix() * \
285 # calculate splines based on given interpolation method (controller function)
286 def calculate_splines(interpolation
, bm_mod
, tknots
, knots
):
287 if interpolation
== 'cubic':
288 splines
= calculate_cubic_splines(bm_mod
, tknots
, knots
[:])
289 else: # interpolations == 'linear'
290 splines
= calculate_linear_splines(bm_mod
, tknots
, knots
[:])
295 # check loops and only return valid ones
296 def check_loops(loops
, mapping
, bm_mod
):
298 for loop
, circular
in loops
:
299 # loop needs to have at least 3 vertices
302 # loop needs at least 1 vertex in the original, non-mirrored mesh
306 if mapping
[vert
] > -1:
311 # vertices can not all be at the same location
313 for i
in range(len(loop
) - 1):
314 if (bm_mod
.verts
[loop
[i
]].co
- \
315 bm_mod
.verts
[loop
[i
+1]].co
).length
> 1e-6:
320 # passed all tests, loop is valid
321 valid_loops
.append([loop
, circular
])
326 # input: bmesh, output: dict with the edge-key as key and face-index as value
327 def dict_edge_faces(bm
):
328 edge_faces
= dict([[edgekey(edge
), []] for edge
in bm
.edges
if \
330 for face
in bm
.faces
:
333 for key
in face_edgekeys(face
):
334 edge_faces
[key
].append(face
.index
)
339 # input: bmesh (edge-faces optional), output: dict with face-face connections
340 def dict_face_faces(bm
, edge_faces
=False):
342 edge_faces
= dict_edge_faces(bm
)
344 connected_faces
= dict([[face
.index
, []] for face
in bm
.faces
if \
346 for face
in bm
.faces
:
349 for edge_key
in face_edgekeys(face
):
350 for connected_face
in edge_faces
[edge_key
]:
351 if connected_face
== face
.index
:
353 connected_faces
[face
.index
].append(connected_face
)
355 return(connected_faces
)
358 # input: bmesh, output: dict with the vert index as key and edge-keys as value
359 def dict_vert_edges(bm
):
360 vert_edges
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
361 for edge
in bm
.edges
:
366 vert_edges
[vert
].append(ek
)
371 # input: bmesh, output: dict with the vert index as key and face index as value
372 def dict_vert_faces(bm
):
373 vert_faces
= dict([[v
.index
, []] for v
in bm
.verts
if not v
.hide
])
374 for face
in bm
.faces
:
376 for vert
in face
.verts
:
377 vert_faces
[vert
.index
].append(face
.index
)
382 # input: list of edge-keys, output: dictionary with vertex-vertex connections
383 def dict_vert_verts(edge_keys
):
384 # create connection data
388 if ek
[i
] in vert_verts
:
389 vert_verts
[ek
[i
]].append(ek
[1-i
])
391 vert_verts
[ek
[i
]] = [ek
[1-i
]]
396 # return the edgekey ([v1.index, v2.index]) of a bmesh edge
398 return(tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])))
401 # returns the edgekeys of a bmesh face
402 def face_edgekeys(face
):
403 return([tuple(sorted([edge
.verts
[0].index
, edge
.verts
[1].index
])) for \
407 # calculate input loops
408 def get_connected_input(object, bm
, scene
, input):
409 # get mesh with modifiers applied
410 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
412 # calculate selected loops
413 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if \
414 edge
.select
and not edge
.hide
]
415 loops
= get_connected_selections(edge_keys
)
417 # if only selected loops are needed, we're done
418 if input == 'selected':
419 return(derived
, bm_mod
, loops
)
420 # elif input == 'all':
421 loops
= get_parallel_loops(bm_mod
, loops
)
423 return(derived
, bm_mod
, loops
)
426 # sorts all edge-keys into a list of loops
427 def get_connected_selections(edge_keys
):
428 # create connection data
429 vert_verts
= dict_vert_verts(edge_keys
)
431 # find loops consisting of connected selected edges
433 while len(vert_verts
) > 0:
434 loop
= [iter(vert_verts
.keys()).__next
__()]
440 # no more connection data for current vertex
441 if loop
[-1] not in vert_verts
:
449 for i
, next_vert
in enumerate(vert_verts
[loop
[-1]]):
450 if next_vert
not in loop
:
451 vert_verts
[loop
[-1]].pop(i
)
452 if len(vert_verts
[loop
[-1]]) == 0:
453 del vert_verts
[loop
[-1]]
454 # remove connection both ways
455 if next_vert
in vert_verts
:
456 if len(vert_verts
[next_vert
]) == 1:
457 del vert_verts
[next_vert
]
459 vert_verts
[next_vert
].remove(loop
[-1])
460 loop
.append(next_vert
)
464 # found one end of the loop, continue with next
468 # found both ends of the loop, stop growing
472 # check if loop is circular
473 if loop
[0] in vert_verts
:
474 if loop
[-1] in vert_verts
[loop
[0]]:
476 if len(vert_verts
[loop
[0]]) == 1:
477 del vert_verts
[loop
[0]]
479 vert_verts
[loop
[0]].remove(loop
[-1])
480 if len(vert_verts
[loop
[-1]]) == 1:
481 del vert_verts
[loop
[-1]]
483 vert_verts
[loop
[-1]].remove(loop
[0])
497 # get the derived mesh data, if there is a mirror modifier
498 def get_derived_bmesh(object, bm
, scene
):
499 # check for mirror modifiers
500 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
502 # disable other modifiers
503 show_viewport
= [mod
.name
for mod
in object.modifiers
if \
505 for mod
in object.modifiers
:
506 if mod
.type != 'MIRROR':
507 mod
.show_viewport
= False
510 mesh_mod
= object.to_mesh(scene
, True, 'PREVIEW')
511 bm_mod
.from_mesh(mesh_mod
)
512 bpy
.context
.blend_data
.meshes
.remove(mesh_mod
)
513 # re-enable other modifiers
514 for mod_name
in show_viewport
:
515 object.modifiers
[mod_name
].show_viewport
= True
516 # no mirror modifiers, so no derived mesh necessary
521 return(derived
, bm_mod
)
524 # return a mapping of derived indices to indices
525 def get_mapping(derived
, bm
, bm_mod
, single_vertices
, full_search
, loops
):
530 verts
= [v
for v
in bm
.verts
if not v
.hide
]
532 verts
= [v
for v
in bm
.verts
if v
.select
and not v
.hide
]
534 # non-selected vertices around single vertices also need to be mapped
536 mapping
= dict([[vert
, -1] for vert
in single_vertices
])
537 verts_mod
= [bm_mod
.verts
[vert
] for vert
in single_vertices
]
539 for v_mod
in verts_mod
:
540 if (v
.co
- v_mod
.co
).length
< 1e-6:
541 mapping
[v_mod
.index
] = v
.index
543 real_singles
= [v_real
for v_real
in mapping
.values() if v_real
>-1]
545 verts_indices
= [vert
.index
for vert
in verts
]
546 for face
in [face
for face
in bm
.faces
if not face
.select \
548 for vert
in face
.verts
:
549 if vert
.index
in real_singles
:
551 if not v
.index
in verts_indices
:
556 # create mapping of derived indices to indices
557 mapping
= dict([[vert
, -1] for loop
in loops
for vert
in loop
[0]])
559 for single
in single_vertices
:
561 verts_mod
= [bm_mod
.verts
[i
] for i
in mapping
.keys()]
563 for v_mod
in verts_mod
:
564 if (v
.co
- v_mod
.co
).length
< 1e-6:
565 mapping
[v_mod
.index
] = v
.index
566 verts_mod
.remove(v_mod
)
572 # returns a list of all loops parallel to the input, input included
573 def get_parallel_loops(bm_mod
, loops
):
574 # get required dictionaries
575 edge_faces
= dict_edge_faces(bm_mod
)
576 connected_faces
= dict_face_faces(bm_mod
, edge_faces
)
577 # turn vertex loops into edge loops
580 edgeloop
= [[sorted([loop
[0][i
], loop
[0][i
+1]]) for i
in \
581 range(len(loop
[0])-1)], loop
[1]]
582 if loop
[1]: # circular
583 edgeloop
[0].append(sorted([loop
[0][-1], loop
[0][0]]))
584 edgeloops
.append(edgeloop
[:])
585 # variables to keep track while iterating
589 for loop
in edgeloops
:
590 # initialise with original loop
591 all_edgeloops
.append(loop
[0])
595 if edge
[0] not in verts_used
:
596 verts_used
.append(edge
[0])
597 if edge
[1] not in verts_used
:
598 verts_used
.append(edge
[1])
600 # find parallel loops
601 while len(newloops
) > 0:
604 for i
in newloops
[-1]:
606 forbidden_side
= False
607 if not i
in edge_faces
:
608 # weird input with branches
611 for face
in edge_faces
[i
]:
612 if len(side_a
) == 0 and forbidden_side
!= "a":
618 elif side_a
[-1] in connected_faces
[face
] and \
619 forbidden_side
!= "a":
625 if len(side_b
) == 0 and forbidden_side
!= "b":
631 elif side_b
[-1] in connected_faces
[face
] and \
632 forbidden_side
!= "b":
640 # weird input with branches
653 for key
in face_edgekeys(bm_mod
.faces
[fi
]):
654 if key
[0] not in verts_used
and key
[1] not in \
656 extraloop
.append(key
)
659 for key
in extraloop
:
661 if new_vert
not in verts_used
:
662 verts_used
.append(new_vert
)
663 newloops
.append(extraloop
)
664 all_edgeloops
.append(extraloop
)
666 # input contains branches, only return selected loop
670 # change edgeloops into normal loops
672 for edgeloop
in all_edgeloops
:
674 # grow loop by comparing vertices between consecutive edge-keys
675 for i
in range(len(edgeloop
)-1):
676 for vert
in range(2):
677 if edgeloop
[i
][vert
] in edgeloop
[i
+1]:
678 loop
.append(edgeloop
[i
][vert
])
681 # add starting vertex
682 for vert
in range(2):
683 if edgeloop
[0][vert
] != loop
[0]:
684 loop
= [edgeloop
[0][vert
]] + loop
687 for vert
in range(2):
688 if edgeloop
[-1][vert
] != loop
[-1]:
689 loop
.append(edgeloop
[-1][vert
])
691 # check if loop is circular
692 if loop
[0] == loop
[-1]:
697 loops
.append([loop
, circular
])
702 # gather initial data
704 global_undo
= bpy
.context
.user_preferences
.edit
.use_global_undo
705 bpy
.context
.user_preferences
.edit
.use_global_undo
= False
706 object = bpy
.context
.active_object
707 if 'MIRROR' in [mod
.type for mod
in object.modifiers
if mod
.show_viewport
]:
708 # ensure that selection is synced for the derived mesh
709 bpy
.ops
.object.mode_set(mode
='OBJECT')
710 bpy
.ops
.object.mode_set(mode
='EDIT')
711 bm
= bmesh
.from_edit_mesh(object.data
)
713 return(global_undo
, object, bm
)
716 # move the vertices to their new locations
717 def move_verts(object, bm
, mapping
, move
, influence
):
719 for index
, loc
in loop
:
721 if mapping
[index
] == -1:
724 index
= mapping
[index
]
726 bm
.verts
[index
].co
= loc
*(influence
/100) + \
727 bm
.verts
[index
].co
*((100-influence
)/100)
729 bm
.verts
[index
].co
= loc
734 # load custom tool settings
735 def settings_load(self
):
736 lt
= bpy
.context
.window_manager
.looptools
737 tool
= self
.name
.split()[0].lower()
738 keys
= self
.as_keywords().keys()
740 setattr(self
, key
, getattr(lt
, tool
+ "_" + key
))
743 # store custom tool settings
744 def settings_write(self
):
745 lt
= bpy
.context
.window_manager
.looptools
746 tool
= self
.name
.split()[0].lower()
747 keys
= self
.as_keywords().keys()
749 setattr(lt
, tool
+ "_" + key
, getattr(self
, key
))
752 # clean up and set settings back to original state
753 def terminate(global_undo
):
754 context
= bpy
.context
756 # update editmesh cached data
757 obj
= context
.active_object
758 if obj
.mode
== 'EDIT':
759 bmesh
.update_edit_mesh(obj
.data
, tessface
=True, destructive
=True)
761 context
.user_preferences
.edit
.use_global_undo
= global_undo
764 ##########################################
765 ####### Bridge functions #################
766 ##########################################
768 # calculate a cubic spline through the middle section of 4 given coordinates
769 def bridge_calculate_cubic_spline(bm
, coordinates
):
775 for i
in coordinates
:
776 a
.append(float(i
[j
]))
779 h
.append(x
[i
+1]-x
[i
])
782 q
.append(3.0/h
[i
]*(a
[i
+1]-a
[i
])-3.0/h
[i
-1]*(a
[i
]-a
[i
-1]))
787 l
.append(2.0*(x
[i
+1]-x
[i
-1])-h
[i
-1]*u
[i
-1])
789 z
.append((q
[i
]-h
[i
-1]*z
[i
-1])/l
[i
])
792 b
= [False for i
in range(3)]
793 c
= [False for i
in range(4)]
794 d
= [False for i
in range(3)]
796 for i
in range(2,-1,-1):
797 c
[i
] = z
[i
]-u
[i
]*c
[i
+1]
798 b
[i
] = (a
[i
+1]-a
[i
])/h
[i
]-h
[i
]*(c
[i
+1]+2.0*c
[i
])/3.0
799 d
[i
] = (c
[i
+1]-c
[i
])/(3.0*h
[i
])
801 result
.append([a
[i
], b
[i
], c
[i
], d
[i
], x
[i
]])
802 spline
= [result
[1], result
[4], result
[7]]
807 # return a list with new vertex location vectors, a list with face vertex
808 # integers, and the highest vertex integer in the virtual mesh
809 def bridge_calculate_geometry(bm
, lines
, vertex_normals
, segments
,
810 interpolation
, cubic_strength
, min_width
, max_vert_index
):
814 # calculate location based on interpolation method
815 def get_location(line
, segment
, splines
):
816 v1
= bm
.verts
[lines
[line
][0]].co
817 v2
= bm
.verts
[lines
[line
][1]].co
818 if interpolation
== 'linear':
819 return v1
+ (segment
/segments
) * (v2
-v1
)
820 else: # interpolation == 'cubic'
821 m
= (segment
/segments
)
822 ax
,bx
,cx
,dx
,tx
= splines
[line
][0]
823 x
= ax
+bx
*m
+cx
*m
**2+dx
*m
**3
824 ay
,by
,cy
,dy
,ty
= splines
[line
][1]
825 y
= ay
+by
*m
+cy
*m
**2+dy
*m
**3
826 az
,bz
,cz
,dz
,tz
= splines
[line
][2]
827 z
= az
+bz
*m
+cz
*m
**2+dz
*m
**3
828 return mathutils
.Vector((x
, y
, z
))
830 # no interpolation needed
832 for i
, line
in enumerate(lines
):
834 faces
.append([line
[0], lines
[i
+1][0], lines
[i
+1][1], line
[1]])
835 # more than 1 segment, interpolate
837 # calculate splines (if necessary) once, so no recalculations needed
838 if interpolation
== 'cubic':
841 v1
= bm
.verts
[line
[0]].co
842 v2
= bm
.verts
[line
[1]].co
843 size
= (v2
-v1
).length
* cubic_strength
844 splines
.append(bridge_calculate_cubic_spline(bm
,
845 [v1
+size
*vertex_normals
[line
[0]], v1
, v2
,
846 v2
+size
*vertex_normals
[line
[1]]]))
850 # create starting situation
851 virtual_width
= [(bm
.verts
[lines
[i
][0]].co
-
852 bm
.verts
[lines
[i
+1][0]].co
).length
for i
853 in range(len(lines
)-1)]
854 new_verts
= [get_location(0, seg
, splines
) for seg
in range(1,
856 first_line_indices
= [i
for i
in range(max_vert_index
+1,
857 max_vert_index
+segments
)]
859 prev_verts
= new_verts
[:] # vertex locations of verts on previous line
860 prev_vert_indices
= first_line_indices
[:]
861 max_vert_index
+= segments
- 1 # highest vertex index in virtual mesh
862 next_verts
= [] # vertex locations of verts on current line
863 next_vert_indices
= []
865 for i
, line
in enumerate(lines
):
870 for seg
in range(1, segments
):
871 loc1
= prev_verts
[seg
-1]
872 loc2
= get_location(i
+1, seg
, splines
)
873 if (loc1
-loc2
).length
< (min_width
/100)*virtual_width
[i
] \
874 and line
[1]==lines
[i
+1][1]:
875 # triangle, no new vertex
876 faces
.append([v1
, v2
, prev_vert_indices
[seg
-1],
877 prev_vert_indices
[seg
-1]])
878 next_verts
+= prev_verts
[seg
-1:]
879 next_vert_indices
+= prev_vert_indices
[seg
-1:]
883 if i
== len(lines
)-2 and lines
[0] == lines
[-1]:
884 # quad with first line, no new vertex
885 faces
.append([v1
, v2
, first_line_indices
[seg
-1],
886 prev_vert_indices
[seg
-1]])
887 v2
= first_line_indices
[seg
-1]
888 v1
= prev_vert_indices
[seg
-1]
890 # quad, add new vertex
892 faces
.append([v1
, v2
, max_vert_index
,
893 prev_vert_indices
[seg
-1]])
895 v1
= prev_vert_indices
[seg
-1]
896 new_verts
.append(loc2
)
897 next_verts
.append(loc2
)
898 next_vert_indices
.append(max_vert_index
)
900 faces
.append([v1
, v2
, lines
[i
+1][1], line
[1]])
902 prev_verts
= next_verts
[:]
903 prev_vert_indices
= next_vert_indices
[:]
905 next_vert_indices
= []
907 return(new_verts
, faces
, max_vert_index
)
910 # calculate lines (list of lists, vertex indices) that are used for bridging
911 def bridge_calculate_lines(bm
, loops
, mode
, twist
, reverse
):
913 loop1
, loop2
= [i
[0] for i
in loops
]
914 loop1_circular
, loop2_circular
= [i
[1] for i
in loops
]
915 circular
= loop1_circular
or loop2_circular
918 # calculate loop centers
920 for loop
in [loop1
, loop2
]:
921 center
= mathutils
.Vector()
923 center
+= bm
.verts
[vertex
].co
925 centers
.append(center
)
926 for i
, loop
in enumerate([loop1
, loop2
]):
928 if bm
.verts
[vertex
].co
== centers
[i
]:
929 # prevent zero-length vectors in angle comparisons
930 centers
[i
] += mathutils
.Vector((0.01, 0, 0))
932 center1
, center2
= centers
934 # calculate the normals of the virtual planes that the loops are on
936 normal_plurity
= False
937 for i
, loop
in enumerate([loop1
, loop2
]):
939 mat
= mathutils
.Matrix(((0.0, 0.0, 0.0),
943 for loc
in [bm
.verts
[vertex
].co
for vertex
in loop
]:
944 mat
[0][0] += (loc
[0]-x
)**2
945 mat
[1][0] += (loc
[0]-x
)*(loc
[1]-y
)
946 mat
[2][0] += (loc
[0]-x
)*(loc
[2]-z
)
947 mat
[0][1] += (loc
[1]-y
)*(loc
[0]-x
)
948 mat
[1][1] += (loc
[1]-y
)**2
949 mat
[2][1] += (loc
[1]-y
)*(loc
[2]-z
)
950 mat
[0][2] += (loc
[2]-z
)*(loc
[0]-x
)
951 mat
[1][2] += (loc
[2]-z
)*(loc
[1]-y
)
952 mat
[2][2] += (loc
[2]-z
)**2
955 if sum(mat
[0]) < 1e-6 or sum(mat
[1]) < 1e-6 or sum(mat
[2]) < 1e-6:
956 normal_plurity
= True
961 normal
= mathutils
.Vector((1.0, 0.0, 0.0))
962 elif sum(mat
[1]) == 0:
963 normal
= mathutils
.Vector((0.0, 1.0, 0.0))
964 elif sum(mat
[2]) == 0:
965 normal
= mathutils
.Vector((0.0, 0.0, 1.0))
967 # warning! this is different from .normalize()
970 vec
= mathutils
.Vector((1.0, 1.0, 1.0))
971 vec2
= (mat
* vec
)/(mat
* vec
).length
972 while vec
!= vec2
and iter<itermax
:
979 vec2
= mathutils
.Vector((1.0, 1.0, 1.0))
981 normals
.append(normal
)
982 # have plane normals face in the same direction (maximum angle: 90 degrees)
983 if ((center1
+ normals
[0]) - center2
).length
< \
984 ((center1
- normals
[0]) - center2
).length
:
986 if ((center2
+ normals
[1]) - center1
).length
> \
987 ((center2
- normals
[1]) - center1
).length
:
990 # rotation matrix, representing the difference between the plane normals
991 axis
= normals
[0].cross(normals
[1])
992 axis
= mathutils
.Vector([loc
if abs(loc
) > 1e-8 else 0 for loc
in axis
])
993 if axis
.angle(mathutils
.Vector((0, 0, 1)), 0) > 1.5707964:
995 angle
= normals
[0].dot(normals
[1])
996 rotation_matrix
= mathutils
.Matrix
.Rotation(angle
, 4, axis
)
998 # if circular, rotate loops so they are aligned
1000 # make sure loop1 is the circular one (or both are circular)
1001 if loop2_circular
and not loop1_circular
:
1002 loop1_circular
, loop2_circular
= True, False
1003 loop1
, loop2
= loop2
, loop1
1005 # match start vertex of loop1 with loop2
1006 target_vector
= bm
.verts
[loop2
[0]].co
- center2
1007 dif_angles
= [[(rotation_matrix
* (bm
.verts
[vertex
].co
- center1
)
1008 ).angle(target_vector
, 0), False, i
] for
1009 i
, vertex
in enumerate(loop1
)]
1011 if len(loop1
) != len(loop2
):
1012 angle_limit
= dif_angles
[0][0] * 1.2 # 20% margin
1013 dif_angles
= [[(bm
.verts
[loop2
[0]].co
- \
1014 bm
.verts
[loop1
[index
]].co
).length
, angle
, index
] for \
1015 angle
, distance
, index
in dif_angles
if angle
<= angle_limit
]
1017 loop1
= loop1
[dif_angles
[0][2]:] + loop1
[:dif_angles
[0][2]]
1019 # have both loops face the same way
1020 if normal_plurity
and not circular
:
1021 second_to_first
, second_to_second
, second_to_last
= \
1022 [(bm
.verts
[loop1
[1]].co
- center1
).\
1023 angle(bm
.verts
[loop2
[i
]].co
- center2
) for i
in [0, 1, -1]]
1024 last_to_first
, last_to_second
= [(bm
.verts
[loop1
[-1]].co
- \
1025 center1
).angle(bm
.verts
[loop2
[i
]].co
- center2
) for \
1027 if (min(last_to_first
, last_to_second
)*1.1 < min(second_to_first
, \
1028 second_to_second
)) or (loop2_circular
and second_to_last
*1.1 < \
1029 min(second_to_first
, second_to_second
)):
1032 loop1
= [loop1
[-1]] + loop1
[:-1]
1034 angle
= (bm
.verts
[loop1
[0]].co
- center1
).\
1035 cross(bm
.verts
[loop1
[1]].co
- center1
).angle(normals
[0], 0)
1036 target_angle
= (bm
.verts
[loop2
[0]].co
- center2
).\
1037 cross(bm
.verts
[loop2
[1]].co
- center2
).angle(normals
[1], 0)
1038 limit
= 1.5707964 # 0.5*pi, 90 degrees
1039 if not ((angle
> limit
and target_angle
> limit
) or \
1040 (angle
< limit
and target_angle
< limit
)):
1043 loop1
= [loop1
[-1]] + loop1
[:-1]
1044 elif normals
[0].angle(normals
[1]) > limit
:
1047 loop1
= [loop1
[-1]] + loop1
[:-1]
1049 # both loops have the same length
1050 if len(loop1
) == len(loop2
):
1053 if abs(twist
) < len(loop1
):
1054 loop1
= loop1
[twist
:]+loop1
[:twist
]
1058 lines
.append([loop1
[0], loop2
[0]])
1059 for i
in range(1, len(loop1
)):
1060 lines
.append([loop1
[i
], loop2
[i
]])
1062 # loops of different lengths
1064 # make loop1 longest loop
1065 if len(loop2
) > len(loop1
):
1066 loop1
, loop2
= loop2
, loop1
1067 loop1_circular
, loop2_circular
= loop2_circular
, loop1_circular
1071 if abs(twist
) < len(loop1
):
1072 loop1
= loop1
[twist
:]+loop1
[:twist
]
1076 # shortest angle difference doesn't always give correct start vertex
1077 if loop1_circular
and not loop2_circular
:
1080 if len(loop1
) - shifting
< len(loop2
):
1083 to_last
, to_first
= [(rotation_matrix
*
1084 (bm
.verts
[loop1
[-1]].co
- center1
)).angle((bm
.\
1085 verts
[loop2
[i
]].co
- center2
), 0) for i
in [-1, 0]]
1086 if to_first
< to_last
:
1087 loop1
= [loop1
[-1]] + loop1
[:-1]
1093 # basic shortest side first
1095 lines
.append([loop1
[0], loop2
[0]])
1096 for i
in range(1, len(loop1
)):
1097 if i
>= len(loop2
) - 1:
1099 lines
.append([loop1
[i
], loop2
[-1]])
1102 lines
.append([loop1
[i
], loop2
[i
]])
1104 # shortest edge algorithm
1105 else: # mode == 'shortest'
1106 lines
.append([loop1
[0], loop2
[0]])
1108 for i
in range(len(loop1
) -1):
1109 if prev_vert2
== len(loop2
) - 1 and not loop2_circular
:
1110 # force triangles, reached end of loop2
1112 elif prev_vert2
== len(loop2
) - 1 and loop2_circular
:
1113 # at end of loop2, but circular, so check with first vert
1114 tri
, quad
= [(bm
.verts
[loop1
[i
+1]].co
-
1115 bm
.verts
[loop2
[j
]].co
).length
1116 for j
in [prev_vert2
, 0]]
1119 elif len(loop1
) - 1 - i
== len(loop2
) - 1 - prev_vert2
and \
1121 # force quads, otherwise won't make it to end of loop2
1124 # calculate if tri or quad gives shortest edge
1125 tri
, quad
= [(bm
.verts
[loop1
[i
+1]].co
-
1126 bm
.verts
[loop2
[j
]].co
).length
1127 for j
in range(prev_vert2
, prev_vert2
+2)]
1131 lines
.append([loop1
[i
+1], loop2
[prev_vert2
]])
1132 if circle_full
== 2:
1135 elif not circle_full
:
1136 lines
.append([loop1
[i
+1], loop2
[prev_vert2
+1]])
1138 # quad to first vertex of loop2
1140 lines
.append([loop1
[i
+1], loop2
[0]])
1144 # final face for circular loops
1145 if loop1_circular
and loop2_circular
:
1146 lines
.append([loop1
[0], loop2
[0]])
1151 # calculate number of segments needed
1152 def bridge_calculate_segments(bm
, lines
, loops
, segments
):
1153 # return if amount of segments is set by user
1158 average_edge_length
= [(bm
.verts
[vertex
].co
- \
1159 bm
.verts
[loop
[0][i
+1]].co
).length
for loop
in loops
for \
1160 i
, vertex
in enumerate(loop
[0][:-1])]
1161 # closing edges of circular loops
1162 average_edge_length
+= [(bm
.verts
[loop
[0][-1]].co
- \
1163 bm
.verts
[loop
[0][0]].co
).length
for loop
in loops
if loop
[1]]
1166 average_edge_length
= sum(average_edge_length
) / len(average_edge_length
)
1167 average_bridge_length
= sum([(bm
.verts
[v1
].co
- \
1168 bm
.verts
[v2
].co
).length
for v1
, v2
in lines
]) / len(lines
)
1170 segments
= max(1, round(average_bridge_length
/ average_edge_length
))
1175 # return dictionary with vertex index as key, and the normal vector as value
1176 def bridge_calculate_virtual_vertex_normals(bm
, lines
, loops
, edge_faces
,
1178 if not edge_faces
: # interpolation isn't set to cubic
1181 # pity reduce() isn't one of the basic functions in python anymore
1182 def average_vector_dictionary(dic
):
1183 for key
, vectors
in dic
.items():
1184 #if type(vectors) == type([]) and len(vectors) > 1:
1185 if len(vectors
) > 1:
1186 average
= mathutils
.Vector()
1187 for vector
in vectors
:
1189 average
/= len(vectors
)
1190 dic
[key
] = [average
]
1193 # get all edges of the loop
1194 edges
= [[edgekey_to_edge
[tuple(sorted([loops
[j
][0][i
],
1195 loops
[j
][0][i
+1]]))] for i
in range(len(loops
[j
][0])-1)] for \
1197 edges
= edges
[0] + edges
[1]
1199 if loops
[j
][1]: # circular
1200 edges
.append(edgekey_to_edge
[tuple(sorted([loops
[j
][0][0],
1201 loops
[j
][0][-1]]))])
1204 calculation based on face topology (assign edge-normals to vertices)
1206 edge_normal = face_normal x edge_vector
1207 vertex_normal = average(edge_normals)
1209 vertex_normals
= dict([(vertex
, []) for vertex
in loops
[0][0]+loops
[1][0]])
1211 faces
= edge_faces
[edgekey(edge
)] # valid faces connected to edge
1214 # get edge coordinates
1215 v1
, v2
= [bm
.verts
[edgekey(edge
)[i
]].co
for i
in [0,1]]
1216 edge_vector
= v1
- v2
1217 if edge_vector
.length
< 1e-4:
1218 # zero-length edge, vertices at same location
1220 edge_center
= (v1
+ v2
) / 2
1222 # average face coordinates, if connected to more than 1 valid face
1224 face_normal
= mathutils
.Vector()
1225 face_center
= mathutils
.Vector()
1227 face_normal
+= face
.normal
1228 face_center
+= face
.calc_center_median()
1229 face_normal
/= len(faces
)
1230 face_center
/= len(faces
)
1232 face_normal
= faces
[0].normal
1233 face_center
= faces
[0].calc_center_median()
1234 if face_normal
.length
< 1e-4:
1235 # faces with a surface of 0 have no face normal
1238 # calculate virtual edge normal
1239 edge_normal
= edge_vector
.cross(face_normal
)
1240 edge_normal
.length
= 0.01
1241 if (face_center
- (edge_center
+ edge_normal
)).length
> \
1242 (face_center
- (edge_center
- edge_normal
)).length
:
1243 # make normal face the correct way
1244 edge_normal
.negate()
1245 edge_normal
.normalize()
1246 # add virtual edge normal as entry for both vertices it connects
1247 for vertex
in edgekey(edge
):
1248 vertex_normals
[vertex
].append(edge_normal
)
1251 calculation based on connection with other loop (vertex focused method)
1252 - used for vertices that aren't connected to any valid faces
1254 plane_normal = edge_vector x connection_vector
1255 vertex_normal = plane_normal x edge_vector
1257 vertices
= [vertex
for vertex
, normal
in vertex_normals
.items() if not \
1261 # edge vectors connected to vertices
1262 edge_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1264 for v
in edgekey(edge
):
1265 if v
in edge_vectors
:
1266 edge_vector
= bm
.verts
[edgekey(edge
)[0]].co
- \
1267 bm
.verts
[edgekey(edge
)[1]].co
1268 if edge_vector
.length
< 1e-4:
1269 # zero-length edge, vertices at same location
1271 edge_vectors
[v
].append(edge_vector
)
1273 # connection vectors between vertices of both loops
1274 connection_vectors
= dict([[vertex
, []] for vertex
in vertices
])
1275 connections
= dict([[vertex
, []] for vertex
in vertices
])
1276 for v1
, v2
in lines
:
1277 if v1
in connection_vectors
or v2
in connection_vectors
:
1278 new_vector
= bm
.verts
[v1
].co
- bm
.verts
[v2
].co
1279 if new_vector
.length
< 1e-4:
1280 # zero-length connection vector,
1281 # vertices in different loops at same location
1283 if v1
in connection_vectors
:
1284 connection_vectors
[v1
].append(new_vector
)
1285 connections
[v1
].append(v2
)
1286 if v2
in connection_vectors
:
1287 connection_vectors
[v2
].append(new_vector
)
1288 connections
[v2
].append(v1
)
1289 connection_vectors
= average_vector_dictionary(connection_vectors
)
1290 connection_vectors
= dict([[vertex
, vector
[0]] if vector
else \
1291 [vertex
, []] for vertex
, vector
in connection_vectors
.items()])
1293 for vertex
, values
in edge_vectors
.items():
1294 # vertex normal doesn't matter, just assign a random vector to it
1295 if not connection_vectors
[vertex
]:
1296 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1299 # calculate to what location the vertex is connected,
1300 # used to determine what way to flip the normal
1301 connected_center
= mathutils
.Vector()
1302 for v
in connections
[vertex
]:
1303 connected_center
+= bm
.verts
[v
].co
1304 if len(connections
[vertex
]) > 1:
1305 connected_center
/= len(connections
[vertex
])
1306 if len(connections
[vertex
]) == 0:
1307 # shouldn't be possible, but better safe than sorry
1308 vertex_normals
[vertex
] = [mathutils
.Vector((1, 0, 0))]
1311 # can't do proper calculations, because of zero-length vector
1313 if (connected_center
- (bm
.verts
[vertex
].co
+ \
1314 connection_vectors
[vertex
])).length
< (connected_center
- \
1315 (bm
.verts
[vertex
].co
- connection_vectors
[vertex
])).\
1317 connection_vectors
[vertex
].negate()
1318 vertex_normals
[vertex
] = [connection_vectors
[vertex
].\
1322 # calculate vertex normals using edge-vectors,
1323 # connection-vectors and the derived plane normal
1324 for edge_vector
in values
:
1325 plane_normal
= edge_vector
.cross(connection_vectors
[vertex
])
1326 vertex_normal
= edge_vector
.cross(plane_normal
)
1327 vertex_normal
.length
= 0.1
1328 if (connected_center
- (bm
.verts
[vertex
].co
+ \
1329 vertex_normal
)).length
< (connected_center
- \
1330 (bm
.verts
[vertex
].co
- vertex_normal
)).length
:
1331 # make normal face the correct way
1332 vertex_normal
.negate()
1333 vertex_normal
.normalize()
1334 vertex_normals
[vertex
].append(vertex_normal
)
1336 # average virtual vertex normals, based on all edges it's connected to
1337 vertex_normals
= average_vector_dictionary(vertex_normals
)
1338 vertex_normals
= dict([[vertex
, vector
[0]] for vertex
, vector
in \
1339 vertex_normals
.items()])
1341 return(vertex_normals
)
1344 # add vertices to mesh
1345 def bridge_create_vertices(bm
, vertices
):
1346 for i
in range(len(vertices
)):
1347 bm
.verts
.new(vertices
[i
])
1351 def bridge_create_faces(object, bm
, faces
, twist
):
1352 # have the normal point the correct way
1354 [face
.reverse() for face
in faces
]
1355 faces
= [face
[2:]+face
[:2] if face
[0]==face
[1] else face
for \
1358 # eekadoodle prevention
1359 for i
in range(len(faces
)):
1360 if not faces
[i
][-1]:
1361 if faces
[i
][0] == faces
[i
][-1]:
1362 faces
[i
] = [faces
[i
][1], faces
[i
][2], faces
[i
][3], faces
[i
][1]]
1364 faces
[i
] = [faces
[i
][-1]] + faces
[i
][:-1]
1365 # result of converting from pre-bmesh period
1366 if faces
[i
][-1] == faces
[i
][-2]:
1367 faces
[i
] = faces
[i
][:-1]
1369 for i
in range(len(faces
)):
1370 bm
.faces
.new([bm
.verts
[v
] for v
in faces
[i
]])
1372 object.data
.update(calc_edges
=True) # calc_edges prevents memory-corruption
1375 # calculate input loops
1376 def bridge_get_input(bm
):
1377 # create list of internal edges, which should be skipped
1378 eks_of_selected_faces
= [item
for sublist
in [face_edgekeys(face
) for \
1379 face
in bm
.faces
if face
.select
and not face
.hide
] for item
in sublist
]
1381 for ek
in eks_of_selected_faces
:
1382 if ek
in edge_count
:
1386 internal_edges
= [ek
for ek
in edge_count
if edge_count
[ek
] > 1]
1388 # sort correct edges into loops
1389 selected_edges
= [edgekey(edge
) for edge
in bm
.edges
if edge
.select \
1390 and not edge
.hide
and edgekey(edge
) not in internal_edges
]
1391 loops
= get_connected_selections(selected_edges
)
1396 # return values needed by the bridge operator
1397 def bridge_initialise(bm
, interpolation
):
1398 if interpolation
== 'cubic':
1399 # dict with edge-key as key and list of connected valid faces as value
1400 face_blacklist
= [face
.index
for face
in bm
.faces
if face
.select
or \
1402 edge_faces
= dict([[edgekey(edge
), []] for edge
in bm
.edges
if not \
1404 for face
in bm
.faces
:
1405 if face
.index
in face_blacklist
:
1407 for key
in face_edgekeys(face
):
1408 edge_faces
[key
].append(face
)
1409 # dictionary with the edge-key as key and edge as value
1410 edgekey_to_edge
= dict([[edgekey(edge
), edge
] for edge
in \
1411 bm
.edges
if edge
.select
and not edge
.hide
])
1414 edgekey_to_edge
= False
1416 # selected faces input
1417 old_selected_faces
= [face
.index
for face
in bm
.faces
if face
.select \
1420 # find out if faces created by bridging should be smoothed
1423 if sum([face
.smooth
for face
in bm
.faces
])/len(bm
.faces
) \
1427 return(edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
)
1430 # return a string with the input method
1431 def bridge_input_method(loft
, loft_loop
):
1435 method
= "Loft loop"
1437 method
= "Loft no-loop"
1444 # match up loops in pairs, used for multi-input bridging
1445 def bridge_match_loops(bm
, loops
):
1446 # calculate average loop normals and centers
1449 for vertices
, circular
in loops
:
1450 normal
= mathutils
.Vector()
1451 center
= mathutils
.Vector()
1452 for vertex
in vertices
:
1453 normal
+= bm
.verts
[vertex
].normal
1454 center
+= bm
.verts
[vertex
].co
1455 normals
.append(normal
/ len(vertices
) / 10)
1456 centers
.append(center
/ len(vertices
))
1458 # possible matches if loop normals are faced towards the center
1460 matches
= dict([[i
, []] for i
in range(len(loops
))])
1462 for i
in range(len(loops
) + 1):
1463 for j
in range(i
+1, len(loops
)):
1464 if (centers
[i
] - centers
[j
]).length
> (centers
[i
] - (centers
[j
] \
1465 + normals
[j
])).length
and (centers
[j
] - centers
[i
]).length
> \
1466 (centers
[j
] - (centers
[i
] + normals
[i
])).length
:
1468 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1469 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1470 # if no loops face each other, just make matches between all the loops
1471 if matches_amount
== 0:
1472 for i
in range(len(loops
) + 1):
1473 for j
in range(i
+1, len(loops
)):
1474 matches
[i
].append([(centers
[i
] - centers
[j
]).length
, i
, j
])
1475 matches
[j
].append([(centers
[i
] - centers
[j
]).length
, j
, i
])
1476 for key
, value
in matches
.items():
1479 # matches based on distance between centers and number of vertices in loops
1481 for loop_index
in range(len(loops
)):
1482 if loop_index
in new_order
:
1484 loop_matches
= matches
[loop_index
]
1485 if not loop_matches
:
1487 shortest_distance
= loop_matches
[0][0]
1488 shortest_distance
*= 1.1
1489 loop_matches
= [[abs(len(loops
[loop_index
][0]) - \
1490 len(loops
[loop
[2]][0])), loop
[0], loop
[1], loop
[2]] for loop
in \
1491 loop_matches
if loop
[0] < shortest_distance
]
1493 for match
in loop_matches
:
1494 if match
[3] not in new_order
:
1495 new_order
+= [loop_index
, match
[3]]
1498 # reorder loops based on matches
1499 if len(new_order
) >= 2:
1500 loops
= [loops
[i
] for i
in new_order
]
1505 # remove old_selected_faces
1506 def bridge_remove_internal_faces(bm
, old_selected_faces
):
1507 # collect bmesh faces and internal bmesh edges
1508 remove_faces
= [bm
.faces
[face
] for face
in old_selected_faces
]
1509 edges
= collections
.Counter([edge
.index
for face
in remove_faces
for \
1510 edge
in face
.edges
])
1511 remove_edges
= [bm
.edges
[edge
] for edge
in edges
if edges
[edge
] > 1]
1513 # remove internal faces and edges
1514 for face
in remove_faces
:
1515 bm
.faces
.remove(face
)
1516 for edge
in remove_edges
:
1517 bm
.edges
.remove(edge
)
1520 # update list of internal faces that are flagged for removal
1521 def bridge_save_unused_faces(bm
, old_selected_faces
, loops
):
1522 # key: vertex index, value: lists of selected faces using it
1523 vertex_to_face
= dict([[i
, []] for i
in range(len(bm
.verts
))])
1524 [[vertex_to_face
[vertex
.index
].append(face
) for vertex
in \
1525 bm
.faces
[face
].verts
] for face
in old_selected_faces
]
1527 # group selected faces that are connected
1530 for face
in old_selected_faces
:
1531 if face
in grouped_faces
:
1533 grouped_faces
.append(face
)
1537 grow_face
= new_faces
[0]
1538 for vertex
in bm
.faces
[grow_face
].verts
:
1539 vertex_face_group
= [face
for face
in vertex_to_face
[\
1540 vertex
.index
] if face
not in grouped_faces
]
1541 new_faces
+= vertex_face_group
1542 grouped_faces
+= vertex_face_group
1543 group
+= vertex_face_group
1545 groups
.append(group
)
1547 # key: vertex index, value: True/False (is it in a loop that is used)
1548 used_vertices
= dict([[i
, 0] for i
in range(len(bm
.verts
))])
1550 for vertex
in loop
[0]:
1551 used_vertices
[vertex
] = True
1553 # check if group is bridged, if not remove faces from internal faces list
1554 for group
in groups
:
1559 for vertex
in bm
.faces
[face
].verts
:
1560 if used_vertices
[vertex
.index
]:
1565 old_selected_faces
.remove(face
)
1568 # add the newly created faces to the selection
1569 def bridge_select_new_faces(bm
, amount
, smooth
):
1570 for i
in range(amount
):
1571 bm
.faces
[-(i
+1)].select_set(True)
1572 bm
.faces
[-(i
+1)].smooth
= smooth
1575 # sort loops, so they are connected in the correct order when lofting
1576 def bridge_sort_loops(bm
, loops
, loft_loop
):
1577 # simplify loops to single points, and prepare for pathfinding
1578 x
, y
, z
= [[sum([bm
.verts
[i
].co
[j
] for i
in loop
[0]]) / \
1579 len(loop
[0]) for loop
in loops
] for j
in range(3)]
1580 nodes
= [mathutils
.Vector((x
[i
], y
[i
], z
[i
])) for i
in range(len(loops
))]
1583 open = [i
for i
in range(1, len(loops
))]
1585 # connect node to path, that is shortest to active_node
1586 while len(open) > 0:
1587 distances
= [(nodes
[active_node
] - nodes
[i
]).length
for i
in open]
1588 active_node
= open[distances
.index(min(distances
))]
1589 open.remove(active_node
)
1590 path
.append([active_node
, min(distances
)])
1591 # check if we didn't start in the middle of the path
1592 for i
in range(2, len(path
)):
1593 if (nodes
[path
[i
][0]]-nodes
[0]).length
< path
[i
][1]:
1596 path
= path
[:-i
] + temp
1600 loops
= [loops
[i
[0]] for i
in path
]
1601 # if requested, duplicate first loop at last position, so loft can loop
1603 loops
= loops
+ [loops
[0]]
1608 ##########################################
1609 ####### Circle functions #################
1610 ##########################################
1612 # convert 3d coordinates to 2d coordinates on plane
1613 def circle_3d_to_2d(bm_mod
, loop
, com
, normal
):
1614 # project vertices onto the plane
1615 verts
= [bm_mod
.verts
[v
] for v
in loop
[0]]
1616 verts_projected
= [[v
.co
- (v
.co
- com
).dot(normal
) * normal
, v
.index
]
1619 # calculate two vectors (p and q) along the plane
1620 m
= mathutils
.Vector((normal
[0] + 1.0, normal
[1], normal
[2]))
1621 p
= m
- (m
.dot(normal
) * normal
)
1623 m
= mathutils
.Vector((normal
[0], normal
[1] + 1.0, normal
[2]))
1624 p
= m
- (m
.dot(normal
) * normal
)
1627 # change to 2d coordinates using perpendicular projection
1629 for loc
, vert
in verts_projected
:
1631 x
= p
.dot(vloc
) / p
.dot(p
)
1632 y
= q
.dot(vloc
) / q
.dot(q
)
1633 locs_2d
.append([x
, y
, vert
])
1635 return(locs_2d
, p
, q
)
1638 # calculate a best-fit circle to the 2d locations on the plane
1639 def circle_calculate_best_fit(locs_2d
):
1645 # calculate center and radius (non-linear least squares solution)
1646 for iter in range(500):
1650 d
= (v
[0]**2-2.0*x0
*v
[0]+v
[1]**2-2.0*y0
*v
[1]+x0
**2+y0
**2)**0.5
1651 jmat
.append([(x0
-v
[0])/d
, (y0
-v
[1])/d
, -1.0])
1652 k
.append(-(((v
[0]-x0
)**2+(v
[1]-y0
)**2)**0.5-r
))
1653 jmat2
= mathutils
.Matrix(((0.0, 0.0, 0.0),
1657 k2
= mathutils
.Vector((0.0, 0.0, 0.0))
1658 for i
in range(len(jmat
)):
1659 k2
+= mathutils
.Vector(jmat
[i
])*k
[i
]
1660 jmat2
[0][0] += jmat
[i
][0]**2
1661 jmat2
[1][0] += jmat
[i
][0]*jmat
[i
][1]
1662 jmat2
[2][0] += jmat
[i
][0]*jmat
[i
][2]
1663 jmat2
[1][1] += jmat
[i
][1]**2
1664 jmat2
[2][1] += jmat
[i
][1]*jmat
[i
][2]
1665 jmat2
[2][2] += jmat
[i
][2]**2
1666 jmat2
[0][1] = jmat2
[1][0]
1667 jmat2
[0][2] = jmat2
[2][0]
1668 jmat2
[1][2] = jmat2
[2][1]
1673 dx0
, dy0
, dr
= jmat2
* k2
1677 # stop iterating if we're close enough to optimal solution
1678 if abs(dx0
)<1e-6 and abs(dy0
)<1e-6 and abs(dr
)<1e-6:
1681 # return center of circle and radius
1685 # calculate circle so no vertices have to be moved away from the center
1686 def circle_calculate_min_fit(locs_2d
):
1688 x0
= (min([i
[0] for i
in locs_2d
])+max([i
[0] for i
in locs_2d
]))/2.0
1689 y0
= (min([i
[1] for i
in locs_2d
])+max([i
[1] for i
in locs_2d
]))/2.0
1690 center
= mathutils
.Vector([x0
, y0
])
1692 r
= min([(mathutils
.Vector([i
[0], i
[1]])-center
).length
for i
in locs_2d
])
1694 # return center of circle and radius
1698 # calculate the new locations of the vertices that need to be moved
1699 def circle_calculate_verts(flatten
, bm_mod
, locs_2d
, com
, p
, q
, normal
):
1700 # changing 2d coordinates back to 3d coordinates
1703 locs_3d
.append([loc
[2], loc
[0]*p
+ loc
[1]*q
+ com
])
1705 if flatten
: # flat circle
1708 else: # project the locations on the existing mesh
1709 vert_edges
= dict_vert_edges(bm_mod
)
1710 vert_faces
= dict_vert_faces(bm_mod
)
1711 faces
= [f
for f
in bm_mod
.faces
if not f
.hide
]
1712 rays
= [normal
, -normal
]
1716 if bm_mod
.verts
[loc
[0]].co
== loc
[1]: # vertex hasn't moved
1719 dif
= normal
.angle(loc
[1]-bm_mod
.verts
[loc
[0]].co
)
1720 if -1e-6 < dif
< 1e-6 or math
.pi
-1e-6 < dif
< math
.pi
+1e-6:
1721 # original location is already along projection normal
1722 projection
= bm_mod
.verts
[loc
[0]].co
1724 # quick search through adjacent faces
1725 for face
in vert_faces
[loc
[0]]:
1726 verts
= [v
.co
for v
in bm_mod
.faces
[face
].verts
]
1727 if len(verts
) == 3: # triangle
1731 v1
, v2
, v3
, v4
= verts
[:4]
1733 intersect
= mathutils
.geometry
.\
1734 intersect_ray_tri(v1
, v2
, v3
, ray
, loc
[1])
1736 projection
= intersect
1739 intersect
= mathutils
.geometry
.\
1740 intersect_ray_tri(v1
, v3
, v4
, ray
, loc
[1])
1742 projection
= intersect
1747 # check if projection is on adjacent edges
1748 for edgekey
in vert_edges
[loc
[0]]:
1749 line1
= bm_mod
.verts
[edgekey
[0]].co
1750 line2
= bm_mod
.verts
[edgekey
[1]].co
1751 intersect
, dist
= mathutils
.geometry
.intersect_point_line(\
1752 loc
[1], line1
, line2
)
1753 if 1e-6 < dist
< 1 - 1e-6:
1754 projection
= intersect
1757 # full search through the entire mesh
1760 verts
= [v
.co
for v
in face
.verts
]
1761 if len(verts
) == 3: # triangle
1765 v1
, v2
, v3
, v4
= verts
[:4]
1767 intersect
= mathutils
.geometry
.intersect_ray_tri(\
1768 v1
, v2
, v3
, ray
, loc
[1])
1770 hits
.append([(loc
[1] - intersect
).length
,
1774 intersect
= mathutils
.geometry
.intersect_ray_tri(\
1775 v1
, v3
, v4
, ray
, loc
[1])
1777 hits
.append([(loc
[1] - intersect
).length
,
1781 # if more than 1 hit with mesh, closest hit is new loc
1783 projection
= hits
[0][1]
1785 # nothing to project on, remain at flat location
1787 new_locs
.append([loc
[0], projection
])
1789 # return new positions of projected circle
1793 # check loops and only return valid ones
1794 def circle_check_loops(single_loops
, loops
, mapping
, bm_mod
):
1795 valid_single_loops
= {}
1797 for i
, [loop
, circular
] in enumerate(loops
):
1798 # loop needs to have at least 3 vertices
1801 # loop needs at least 1 vertex in the original, non-mirrored mesh
1805 if mapping
[vert
] > -1:
1810 # loop has to be non-collinear
1812 loc0
= mathutils
.Vector(bm_mod
.verts
[loop
[0]].co
[:])
1813 loc1
= mathutils
.Vector(bm_mod
.verts
[loop
[1]].co
[:])
1815 locn
= mathutils
.Vector(bm_mod
.verts
[v
].co
[:])
1816 if loc0
== loc1
or loc1
== locn
:
1822 if -1e-6 < d1
.angle(d2
, 0) < 1e-6:
1830 # passed all tests, loop is valid
1831 valid_loops
.append([loop
, circular
])
1832 valid_single_loops
[len(valid_loops
)-1] = single_loops
[i
]
1834 return(valid_single_loops
, valid_loops
)
1837 # calculate the location of single input vertices that need to be flattened
1838 def circle_flatten_singles(bm_mod
, com
, p
, q
, normal
, single_loop
):
1840 for vert
in single_loop
:
1841 loc
= mathutils
.Vector(bm_mod
.verts
[vert
].co
[:])
1842 new_locs
.append([vert
, loc
- (loc
-com
).dot(normal
)*normal
])
1847 # calculate input loops
1848 def circle_get_input(object, bm
, scene
):
1849 # get mesh with modifiers applied
1850 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
1852 # create list of edge-keys based on selection state
1854 for face
in bm
.faces
:
1855 if face
.select
and not face
.hide
:
1859 # get selected, non-hidden , non-internal edge-keys
1860 eks_selected
= [key
for keys
in [face_edgekeys(face
) for face
in \
1861 bm_mod
.faces
if face
.select
and not face
.hide
] for key
in keys
]
1863 for ek
in eks_selected
:
1864 if ek
in edge_count
:
1868 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select \
1869 and not edge
.hide
and edge_count
.get(edgekey(edge
), 1)==1]
1871 # no faces, so no internal edges either
1872 edge_keys
= [edgekey(edge
) for edge
in bm_mod
.edges
if edge
.select \
1875 # add edge-keys around single vertices
1876 verts_connected
= dict([[vert
, 1] for edge
in [edge
for edge
in \
1877 bm_mod
.edges
if edge
.select
and not edge
.hide
] for vert
in \
1879 single_vertices
= [vert
.index
for vert
in bm_mod
.verts
if \
1880 vert
.select
and not vert
.hide
and not \
1881 verts_connected
.get(vert
.index
, False)]
1883 if single_vertices
and len(bm
.faces
)>0:
1884 vert_to_single
= dict([[v
.index
, []] for v
in bm_mod
.verts \
1886 for face
in [face
for face
in bm_mod
.faces
if not face
.select \
1888 for vert
in face
.verts
:
1890 if vert
in single_vertices
:
1891 for ek
in face_edgekeys(face
):
1893 edge_keys
.append(ek
)
1894 if vert
not in vert_to_single
[ek
[0]]:
1895 vert_to_single
[ek
[0]].append(vert
)
1896 if vert
not in vert_to_single
[ek
[1]]:
1897 vert_to_single
[ek
[1]].append(vert
)
1900 # sort edge-keys into loops
1901 loops
= get_connected_selections(edge_keys
)
1903 # find out to which loops the single vertices belong
1904 single_loops
= dict([[i
, []] for i
in range(len(loops
))])
1905 if single_vertices
and len(bm
.faces
)>0:
1906 for i
, [loop
, circular
] in enumerate(loops
):
1908 if vert_to_single
[vert
]:
1909 for single
in vert_to_single
[vert
]:
1910 if single
not in single_loops
[i
]:
1911 single_loops
[i
].append(single
)
1913 return(derived
, bm_mod
, single_vertices
, single_loops
, loops
)
1916 # recalculate positions based on the influence of the circle shape
1917 def circle_influence_locs(locs_2d
, new_locs_2d
, influence
):
1918 for i
in range(len(locs_2d
)):
1919 oldx
, oldy
, j
= locs_2d
[i
]
1920 newx
, newy
, k
= new_locs_2d
[i
]
1921 altx
= newx
*(influence
/100)+ oldx
*((100-influence
)/100)
1922 alty
= newy
*(influence
/100)+ oldy
*((100-influence
)/100)
1923 locs_2d
[i
] = [altx
, alty
, j
]
1928 # project 2d locations on circle, respecting distance relations between verts
1929 def circle_project_non_regular(locs_2d
, x0
, y0
, r
):
1930 for i
in range(len(locs_2d
)):
1931 x
, y
, j
= locs_2d
[i
]
1932 loc
= mathutils
.Vector([x
-x0
, y
-y0
])
1934 locs_2d
[i
] = [loc
[0], loc
[1], j
]
1939 # project 2d locations on circle, with equal distance between all vertices
1940 def circle_project_regular(locs_2d
, x0
, y0
, r
):
1941 # find offset angle and circling direction
1942 x
, y
, i
= locs_2d
[0]
1943 loc
= mathutils
.Vector([x
-x0
, y
-y0
])
1945 offset_angle
= loc
.angle(mathutils
.Vector([1.0, 0.0]), 0.0)
1946 loca
= mathutils
.Vector([x
-x0
, y
-y0
, 0.0])
1949 x
, y
, j
= locs_2d
[1]
1950 locb
= mathutils
.Vector([x
-x0
, y
-y0
, 0.0])
1951 if loca
.cross(locb
)[2] >= 0:
1955 # distribute vertices along the circle
1956 for i
in range(len(locs_2d
)):
1957 t
= offset_angle
+ ccw
* (i
/ len(locs_2d
) * 2 * math
.pi
)
1960 locs_2d
[i
] = [x
, y
, locs_2d
[i
][2]]
1965 # shift loop, so the first vertex is closest to the center
1966 def circle_shift_loop(bm_mod
, loop
, com
):
1967 verts
, circular
= loop
1968 distances
= [[(bm_mod
.verts
[vert
].co
- com
).length
, i
] \
1969 for i
, vert
in enumerate(verts
)]
1971 shift
= distances
[0][1]
1972 loop
= [verts
[shift
:] + verts
[:shift
], circular
]
1977 ##########################################
1978 ####### Curve functions ##################
1979 ##########################################
1981 # create lists with knots and points, all correctly sorted
1982 def curve_calculate_knots(loop
, verts_selected
):
1983 knots
= [v
for v
in loop
[0] if v
in verts_selected
]
1985 # circular loop, potential for weird splines
1987 offset
= int(len(loop
[0]) / 4)
1990 kpos
.append(loop
[0].index(k
))
1992 for i
in range(len(kpos
) - 1):
1993 kdif
.append(kpos
[i
+1] - kpos
[i
])
1994 kdif
.append(len(loop
[0]) - kpos
[-1] + kpos
[0])
1998 kadd
.append([kdif
.index(k
), True])
1999 # next 2 lines are optional, they insert
2000 # an extra control point in small gaps
2002 # kadd.append([kdif.index(k), False])
2005 for k
in kadd
: # extra knots to be added
2006 if k
[1]: # big gap (break circular spline)
2007 kpos
= loop
[0].index(knots
[k
[0]]) + offset
2008 if kpos
> len(loop
[0]) - 1:
2009 kpos
-= len(loop
[0])
2010 kins
.append([knots
[k
[0]], loop
[0][kpos
]])
2012 if kpos2
> len(knots
)-1:
2014 kpos2
= loop
[0].index(knots
[kpos2
]) - offset
2016 kpos2
+= len(loop
[0])
2017 kins
.append([loop
[0][kpos
], loop
[0][kpos2
]])
2018 krot
= loop
[0][kpos2
]
2019 else: # small gap (keep circular spline)
2020 k1
= loop
[0].index(knots
[k
[0]])
2022 if k2
> len(knots
)-1:
2024 k2
= loop
[0].index(knots
[k2
])
2026 dif
= len(loop
[0]) - 1 - k1
+ k2
2029 kn
= k1
+ int(dif
/2)
2030 if kn
> len(loop
[0]) - 1:
2032 kins
.append([loop
[0][k1
], loop
[0][kn
]])
2033 for j
in kins
: # insert new knots
2034 knots
.insert(knots
.index(j
[0]) + 1, j
[1])
2035 if not krot
: # circular loop
2036 knots
.append(knots
[0])
2037 points
= loop
[0][loop
[0].index(knots
[0]):]
2038 points
+= loop
[0][0:loop
[0].index(knots
[0]) + 1]
2039 else: # non-circular loop (broken by script)
2040 krot
= knots
.index(krot
)
2041 knots
= knots
[krot
:] + knots
[0:krot
]
2042 if loop
[0].index(knots
[0]) > loop
[0].index(knots
[-1]):
2043 points
= loop
[0][loop
[0].index(knots
[0]):]
2044 points
+= loop
[0][0:loop
[0].index(knots
[-1])+1]
2046 points
= loop
[0][loop
[0].index(knots
[0]):\
2047 loop
[0].index(knots
[-1]) + 1]
2048 # non-circular loop, add first and last point as knots
2050 if loop
[0][0] not in knots
:
2051 knots
.insert(0, loop
[0][0])
2052 if loop
[0][-1] not in knots
:
2053 knots
.append(loop
[0][-1])
2055 return(knots
, points
)
2058 # calculate relative positions compared to first knot
2059 def curve_calculate_t(bm_mod
, knots
, points
, pknots
, regular
, circular
):
2066 loc
= pknots
[knots
.index(p
)] # use projected knot location
2068 loc
= mathutils
.Vector(bm_mod
.verts
[p
].co
[:])
2071 len_total
+= (loc
-loc_prev
).length
2072 tpoints
.append(len_total
)
2077 tknots
.append(tpoints
[points
.index(p
)])
2079 tknots
[-1] = tpoints
[-1]
2083 tpoints_average
= tpoints
[-1] / (len(tpoints
) - 1)
2084 for i
in range(1, len(tpoints
) - 1):
2085 tpoints
[i
] = i
* tpoints_average
2086 for i
in range(len(knots
)):
2087 tknots
[i
] = tpoints
[points
.index(knots
[i
])]
2089 tknots
[-1] = tpoints
[-1]
2092 return(tknots
, tpoints
)
2095 # change the location of non-selected points to their place on the spline
2096 def curve_calculate_vertices(bm_mod
, knots
, tknots
, points
, tpoints
, splines
,
2097 interpolation
, restriction
):
2104 m
= tpoints
[points
.index(p
)]
2112 if n
> len(splines
) - 1:
2113 n
= len(splines
) - 1
2117 if interpolation
== 'cubic':
2118 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
2119 x
= ax
+ bx
*(m
-tx
) + cx
*(m
-tx
)**2 + dx
*(m
-tx
)**3
2120 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
2121 y
= ay
+ by
*(m
-ty
) + cy
*(m
-ty
)**2 + dy
*(m
-ty
)**3
2122 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
2123 z
= az
+ bz
*(m
-tz
) + cz
*(m
-tz
)**2 + dz
*(m
-tz
)**3
2124 newloc
= mathutils
.Vector([x
,y
,z
])
2125 else: # interpolation == 'linear'
2126 a
, d
, t
, u
= splines
[n
]
2127 newloc
= ((m
-t
)/u
)*d
+ a
2129 if restriction
!= 'none': # vertex movement is restricted
2131 else: # set the vertex to its new location
2132 move
.append([p
, newloc
])
2134 if restriction
!= 'none': # vertex movement is restricted
2139 move
.append([p
, bm_mod
.verts
[p
].co
])
2141 oldloc
= bm_mod
.verts
[p
].co
2142 normal
= bm_mod
.verts
[p
].normal
2143 dloc
= newloc
- oldloc
2144 if dloc
.length
< 1e-6:
2145 move
.append([p
, newloc
])
2146 elif restriction
== 'extrude': # only extrusions
2147 if dloc
.angle(normal
, 0) < 0.5 * math
.pi
+ 1e-6:
2148 move
.append([p
, newloc
])
2149 else: # restriction == 'indent' only indentations
2150 if dloc
.angle(normal
) > 0.5 * math
.pi
- 1e-6:
2151 move
.append([p
, newloc
])
2156 # trim loops to part between first and last selected vertices (including)
2157 def curve_cut_boundaries(bm_mod
, loops
):
2159 for loop
, circular
in loops
:
2162 cut_loops
.append([loop
, circular
])
2164 selected
= [bm_mod
.verts
[v
].select
for v
in loop
]
2165 first
= selected
.index(True)
2167 last
= -selected
.index(True)
2169 cut_loops
.append([loop
[first
:], circular
])
2171 cut_loops
.append([loop
[first
:last
], circular
])
2176 # calculate input loops
2177 def curve_get_input(object, bm
, boundaries
, scene
):
2178 # get mesh with modifiers applied
2179 derived
, bm_mod
= get_derived_bmesh(object, bm
, scene
)
2181 # vertices that still need a loop to run through it
2182 verts_unsorted
= [v
.index
for v
in bm_mod
.verts
if \
2183 v
.select
and not v
.hide
]
2184 # necessary dictionaries
2185 vert_edges
= dict_vert_edges(bm_mod
)
2186 edge_faces
= dict_edge_faces(bm_mod
)
2189 # find loops through each selected vertex
2190 while len(verts_unsorted
) > 0:
2191 loops
= curve_vertex_loops(bm_mod
, verts_unsorted
[0], vert_edges
,
2193 verts_unsorted
.pop(0)
2195 # check if loop is fully selected
2196 search_perpendicular
= False
2198 for loop
, circular
in loops
:
2200 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2201 if len(selected
) < 2:
2202 # only one selected vertex on loop, don't use
2205 elif len(selected
) == len(loop
):
2206 search_perpendicular
= loop
2208 # entire loop is selected, find perpendicular loops
2209 if search_perpendicular
:
2211 if vert
in verts_unsorted
:
2212 verts_unsorted
.remove(vert
)
2213 perp_loops
= curve_perpendicular_loops(bm_mod
, loop
,
2214 vert_edges
, edge_faces
)
2215 for perp_loop
in perp_loops
:
2216 correct_loops
.append(perp_loop
)
2219 for loop
, circular
in loops
:
2220 correct_loops
.append([loop
, circular
])
2224 correct_loops
= curve_cut_boundaries(bm_mod
, correct_loops
)
2226 return(derived
, bm_mod
, correct_loops
)
2229 # return all loops that are perpendicular to the given one
2230 def curve_perpendicular_loops(bm_mod
, start_loop
, vert_edges
, edge_faces
):
2231 # find perpendicular loops
2233 for start_vert
in start_loop
:
2234 loops
= curve_vertex_loops(bm_mod
, start_vert
, vert_edges
,
2236 for loop
, circular
in loops
:
2237 selected
= [v
for v
in loop
if bm_mod
.verts
[v
].select
]
2238 if len(selected
) == len(loop
):
2241 perp_loops
.append([loop
, circular
, loop
.index(start_vert
)])
2243 # trim loops to same lengths
2244 shortest
= [[len(loop
[0]), i
] for i
, loop
in enumerate(perp_loops
)\
2247 # all loops are circular, not trimming
2248 return([[loop
[0], loop
[1]] for loop
in perp_loops
])
2250 shortest
= min(shortest
)
2251 shortest_start
= perp_loops
[shortest
[1]][2]
2252 before_start
= shortest_start
2253 after_start
= shortest
[0] - shortest_start
- 1
2254 bigger_before
= before_start
> after_start
2256 for loop
in perp_loops
:
2257 # have the loop face the same direction as the shortest one
2259 if loop
[2] < len(loop
[0]) / 2:
2261 loop
[2] = len(loop
[0]) - loop
[2] - 1
2263 if loop
[2] > len(loop
[0]) / 2:
2265 loop
[2] = len(loop
[0]) - loop
[2] - 1
2266 # circular loops can shift, to prevent wrong trimming
2268 shift
= shortest_start
- loop
[2]
2269 if loop
[2] + shift
> 0 and loop
[2] + shift
< len(loop
[0]):
2270 loop
[0] = loop
[0][-shift
:] + loop
[0][:-shift
]
2273 loop
[2] += len(loop
[0])
2274 elif loop
[2] > len(loop
[0]) -1:
2275 loop
[2] -= len(loop
[0])
2277 start
= max(0, loop
[2] - before_start
)
2278 end
= min(len(loop
[0]), loop
[2] + after_start
+ 1)
2279 trimmed_loops
.append([loop
[0][start
:end
], False])
2281 return(trimmed_loops
)
2284 # project knots on non-selected geometry
2285 def curve_project_knots(bm_mod
, verts_selected
, knots
, points
, circular
):
2286 # function to project vertex on edge
2287 def project(v1
, v2
, v3
):
2288 # v1 and v2 are part of a line
2289 # v3 is projected onto it
2295 if circular
: # project all knots
2299 else: # first and last knot shouldn't be projected
2302 pknots
= [mathutils
.Vector(bm_mod
.verts
[knots
[0]].co
[:])]
2303 for knot
in knots
[start
:end
]:
2304 if knot
in verts_selected
:
2305 knot_left
= knot_right
= False
2306 for i
in range(points
.index(knot
)-1, -1*len(points
), -1):
2307 if points
[i
] not in knots
:
2308 knot_left
= points
[i
]
2310 for i
in range(points
.index(knot
)+1, 2*len(points
)):
2311 if i
> len(points
) - 1:
2313 if points
[i
] not in knots
:
2314 knot_right
= points
[i
]
2316 if knot_left
and knot_right
and knot_left
!= knot_right
:
2317 knot_left
= mathutils
.Vector(\
2318 bm_mod
.verts
[knot_left
].co
[:])
2319 knot_right
= mathutils
.Vector(\
2320 bm_mod
.verts
[knot_right
].co
[:])
2321 knot
= mathutils
.Vector(bm_mod
.verts
[knot
].co
[:])
2322 pknots
.append(project(knot_left
, knot_right
, knot
))
2324 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2325 else: # knot isn't selected, so shouldn't be changed
2326 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knot
].co
[:]))
2328 pknots
.append(mathutils
.Vector(bm_mod
.verts
[knots
[-1]].co
[:]))
2333 # find all loops through a given vertex
2334 def curve_vertex_loops(bm_mod
, start_vert
, vert_edges
, edge_faces
):
2338 for edge
in vert_edges
[start_vert
]:
2339 if edge
in edges_used
:
2344 active_faces
= edge_faces
[edge
]
2349 new_edges
= vert_edges
[new_vert
]
2350 loop
.append(new_vert
)
2352 edges_used
.append(tuple(sorted([loop
[-1], loop
[-2]])))
2353 if len(new_edges
) < 3 or len(new_edges
) > 4:
2358 for new_edge
in new_edges
:
2359 if new_edge
in edges_used
:
2362 for new_face
in edge_faces
[new_edge
]:
2363 if new_face
in active_faces
:
2368 # found correct new edge
2369 active_faces
= edge_faces
[new_edge
]
2375 if new_vert
== loop
[0]:
2383 loops
.append([loop
, circular
])
2388 ##########################################
2389 ####### Flatten functions ################
2390 ##########################################
2392 # sort input into loops
2393 def flatten_get_input(bm
):
2394 vert_verts
= dict_vert_verts([edgekey(edge
) for edge
in bm
.edges \
2395 if edge
.select
and not edge
.hide
])
2396 verts
= [v
.index
for v
in bm
.verts
if v
.select
and not v
.hide
]
2398 # no connected verts, consider all selected verts as a single input
2400 return([[verts
, False]])
2403 while len(verts
) > 0:
2407 if loop
[-1] in vert_verts
:
2408 to_grow
= vert_verts
[loop
[-1]]
2412 while len(to_grow
) > 0:
2413 new_vert
= to_grow
[0]
2415 if new_vert
in loop
:
2417 loop
.append(new_vert
)
2418 verts
.remove(new_vert
)
2419 to_grow
+= vert_verts
[new_vert
]
2421 loops
.append([loop
, False])
2426 # calculate position of vertex projections on plane
2427 def flatten_project(bm
, loop
, com
, normal
):
2428 verts
= [bm
.verts
[v
] for v
in loop
[0]]
2429 verts_projected
= [[v
.index
, mathutils
.Vector(v
.co
[:]) - \
2430 (mathutils
.Vector(v
.co
[:])-com
).dot(normal
)*normal
] for v
in verts
]
2432 return(verts_projected
)
2437 ##########################################
2438 ####### Gstretch functions ###############
2439 ##########################################
2441 # flips loops, if necessary, to obtain maximum alignment to stroke
2442 def gstretch_align_pairs(ls_pairs
, object, bm_mod
, method
):
2443 # returns total distance between all verts in loop and corresponding stroke
2444 def distance_loop_stroke(loop
, stroke
, object, bm_mod
, method
):
2445 stroke_lengths_cache
= False
2446 loop_length
= len(loop
[0])
2449 if method
!= 'regular':
2450 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2452 for i
, v_index
in enumerate(loop
[0]):
2453 if method
== 'regular':
2454 relative_distance
= i
/ (loop_length
- 1)
2456 relative_distance
= relative_lengths
[i
]
2458 loc1
= object.matrix_world
* bm_mod
.verts
[v_index
].co
2459 loc2
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2460 relative_distance
, stroke_lengths_cache
)
2461 total_distance
+= (loc2
- loc1
).length
2463 return(total_distance
)
2466 for (loop
, stroke
) in ls_pairs
:
2467 distance_loop_stroke
2468 total_dist
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2471 total_dist_rev
= distance_loop_stroke(loop
, stroke
, object, bm_mod
,
2473 if total_dist_rev
> total_dist
:
2479 # calculate vertex positions on stroke
2480 def gstretch_calculate_verts(loop
, stroke
, object, bm_mod
, method
):
2482 stroke_lengths_cache
= False
2483 loop_length
= len(loop
[0])
2484 matrix_inverse
= object.matrix_world
.inverted()
2486 # return intersection of line with stroke, or None
2487 def intersect_line_stroke(vec1
, vec2
, stroke
):
2488 for i
, p
in enumerate(stroke
.points
[1:]):
2489 intersections
= mathutils
.geometry
.intersect_line_line(vec1
, vec2
,
2490 p
.co
, stroke
.points
[i
].co
)
2491 if intersections
and \
2492 (intersections
[0] - intersections
[1]).length
< 1e-2:
2493 x
, dist
= mathutils
.geometry
.intersect_point_line(
2494 intersections
[0], p
.co
, stroke
.points
[i
].co
)
2496 return(intersections
[0])
2499 if method
== 'project':
2500 projection_vectors
= []
2501 vert_edges
= dict_vert_edges(bm_mod
)
2503 for v_index
in loop
[0]:
2504 for ek
in vert_edges
[v_index
]:
2506 v1
= bm_mod
.verts
[v1
]
2507 v2
= bm_mod
.verts
[v2
]
2508 if v1
.select
+ v2
.select
== 1 and not v1
.hide
and not v2
.hide
:
2509 vec1
= object.matrix_world
* v1
.co
2510 vec2
= object.matrix_world
* v2
.co
2511 intersection
= intersect_line_stroke(vec1
, vec2
, stroke
)
2514 if not intersection
:
2515 v
= bm_mod
.verts
[v_index
]
2516 intersection
= intersect_line_stroke(v
.co
, v
.co
+ v
.normal
,
2519 move
.append([v_index
, matrix_inverse
* intersection
])
2522 if method
== 'irregular':
2523 relative_lengths
= gstretch_relative_lengths(loop
, bm_mod
)
2525 for i
, v_index
in enumerate(loop
[0]):
2526 if method
== 'regular':
2527 relative_distance
= i
/ (loop_length
- 1)
2528 else: # method == 'irregular'
2529 relative_distance
= relative_lengths
[i
]
2530 loc
, stroke_lengths_cache
= gstretch_eval_stroke(stroke
,
2531 relative_distance
, stroke_lengths_cache
)
2532 loc
= matrix_inverse
* loc
2533 move
.append([v_index
, loc
])
2538 # erases the grease pencil stroke
2539 def gstretch_erase_stroke(stroke
, context
):
2540 # change 3d coordinate into a stroke-point
2541 def sp(loc
, context
):
2545 'location': (0, 0, 0),
2546 'mouse': (view3d_utils
.location_3d_to_region_2d(\
2547 context
.region
, context
.space_data
.region_3d
, loc
)),
2552 erase_stroke
= [sp(p
.co
, context
) for p
in stroke
.points
]
2554 erase_stroke
[0]['is_start'] = True
2555 bpy
.ops
.gpencil
.draw(mode
='ERASER', stroke
=erase_stroke
)
2558 # get point on stroke, given by relative distance (0.0 - 1.0)
2559 def gstretch_eval_stroke(stroke
, distance
, stroke_lengths_cache
=False):
2560 # use cache if available
2561 if not stroke_lengths_cache
:
2563 for i
, p
in enumerate(stroke
.points
[1:]):
2564 lengths
.append((p
.co
- stroke
.points
[i
].co
).length
+ \
2566 total_length
= max(lengths
[-1], 1e-7)
2567 stroke_lengths_cache
= [length
/ total_length
for length
in
2569 stroke_lengths
= stroke_lengths_cache
[:]
2571 if distance
in stroke_lengths
:
2572 loc
= stroke
.points
[stroke_lengths
.index(distance
)].co
2573 elif distance
> stroke_lengths
[-1]:
2574 # should be impossible, but better safe than sorry
2575 loc
= stroke
.points
[-1].co
2577 stroke_lengths
.append(distance
)
2578 stroke_lengths
.sort()
2579 stroke_index
= stroke_lengths
.index(distance
)
2580 interval_length
= stroke_lengths
[stroke_index
+1] - \
2581 stroke_lengths
[stroke_index
-1]
2582 distance_relative
= (distance
- stroke_lengths
[stroke_index
-1]) / \
2584 interval_vector
= stroke
.points
[stroke_index
].co
- \
2585 stroke
.points
[stroke_index
-1].co
2586 loc
= stroke
.points
[stroke_index
-1].co
+ \
2587 distance_relative
* interval_vector
2589 return(loc
, stroke_lengths_cache
)
2592 # get grease pencil strokes for the active object
2593 def gstretch_get_strokes(object):
2594 gp
= object.grease_pencil
2597 layer
= gp
.layers
.active
2600 frame
= layer
.active_frame
2603 strokes
= frame
.strokes
2604 if len(strokes
) < 1:
2610 # returns a list with loop-stroke pairs
2611 def gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
):
2612 if not loops
or not strokes
:
2615 # calculate loop centers
2618 center
= mathutils
.Vector()
2619 for v_index
in loop
[0]:
2620 center
+= bm_mod
.verts
[v_index
].co
2621 center
/= len(loop
[0])
2622 center
= object.matrix_world
* center
2623 loop_centers
.append([center
, loop
])
2625 # calculate stroke centers
2627 for stroke
in strokes
:
2628 center
= mathutils
.Vector()
2629 for p
in stroke
.points
:
2631 center
/= len(stroke
.points
)
2632 stroke_centers
.append([center
, stroke
, 0])
2634 # match, first by stroke use count, then by distance
2636 for lc
in loop_centers
:
2638 for i
, sc
in enumerate(stroke_centers
):
2639 distances
.append([sc
[2], (lc
[0] - sc
[0]).length
, i
])
2641 best_stroke
= distances
[0][2]
2642 ls_pairs
.append([lc
[1], stroke_centers
[best_stroke
][1]])
2643 stroke_centers
[best_stroke
][2] += 1 # increase stroke use count
2648 # returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
2649 def gstretch_relative_lengths(loop
, bm_mod
):
2651 for i
, v_index
in enumerate(loop
[0][1:]):
2652 lengths
.append((bm_mod
.verts
[v_index
].co
- \
2653 bm_mod
.verts
[loop
[0][i
]].co
).length
+ lengths
[-1])
2654 total_length
= max(lengths
[-1], 1e-7)
2655 relative_lengths
= [length
/ total_length
for length
in
2658 return(relative_lengths
)
2661 ##########################################
2662 ####### Relax functions ##################
2663 ##########################################
2665 # create lists with knots and points, all correctly sorted
2666 def relax_calculate_knots(loops
):
2669 for loop
, circular
in loops
:
2673 if len(loop
)%2 == 1: # odd
2674 extend
= [False, True, 0, 1, 0, 1]
2676 extend
= [True, False, 0, 1, 1, 2]
2678 if len(loop
)%2 == 1: # odd
2679 extend
= [False, False, 0, 1, 1, 2]
2681 extend
= [False, False, 0, 1, 1, 2]
2684 loop
= [loop
[-1]] + loop
+ [loop
[0]]
2685 for i
in range(extend
[2+2*j
], len(loop
), 2):
2686 knots
[j
].append(loop
[i
])
2687 for i
in range(extend
[3+2*j
], len(loop
), 2):
2688 if loop
[i
] == loop
[-1] and not circular
:
2690 if len(points
[j
]) == 0:
2691 points
[j
].append(loop
[i
])
2692 elif loop
[i
] != points
[j
][0]:
2693 points
[j
].append(loop
[i
])
2695 if knots
[j
][0] != knots
[j
][-1]:
2696 knots
[j
].append(knots
[j
][0])
2697 if len(points
[1]) == 0:
2703 all_points
.append(p
)
2705 return(all_knots
, all_points
)
2708 # calculate relative positions compared to first knot
2709 def relax_calculate_t(bm_mod
, knots
, points
, regular
):
2712 for i
in range(len(knots
)):
2713 amount
= len(knots
[i
]) + len(points
[i
])
2715 for j
in range(amount
):
2717 mix
.append([True, knots
[i
][round(j
/2)]])
2719 mix
.append([True, knots
[i
][-1]])
2721 mix
.append([False, points
[i
][int(j
/2)]])
2727 loc
= mathutils
.Vector(bm_mod
.verts
[m
[1]].co
[:])
2730 len_total
+= (loc
- loc_prev
).length
2732 tknots
.append(len_total
)
2734 tpoints
.append(len_total
)
2738 for p
in range(len(points
[i
])):
2739 tpoints
.append((tknots
[p
] + tknots
[p
+1]) / 2)
2740 all_tknots
.append(tknots
)
2741 all_tpoints
.append(tpoints
)
2743 return(all_tknots
, all_tpoints
)
2746 # change the location of the points to their place on the spline
2747 def relax_calculate_verts(bm_mod
, interpolation
, tknots
, knots
, tpoints
,
2751 for i
in range(len(knots
)):
2753 m
= tpoints
[i
][points
[i
].index(p
)]
2755 n
= tknots
[i
].index(m
)
2761 if n
> len(splines
[i
]) - 1:
2762 n
= len(splines
[i
]) - 1
2766 if interpolation
== 'cubic':
2767 ax
, bx
, cx
, dx
, tx
= splines
[i
][n
][0]
2768 x
= ax
+ bx
*(m
-tx
) + cx
*(m
-tx
)**2 + dx
*(m
-tx
)**3
2769 ay
, by
, cy
, dy
, ty
= splines
[i
][n
][1]
2770 y
= ay
+ by
*(m
-ty
) + cy
*(m
-ty
)**2 + dy
*(m
-ty
)**3
2771 az
, bz
, cz
, dz
, tz
= splines
[i
][n
][2]
2772 z
= az
+ bz
*(m
-tz
) + cz
*(m
-tz
)**2 + dz
*(m
-tz
)**3
2773 change
.append([p
, mathutils
.Vector([x
,y
,z
])])
2774 else: # interpolation == 'linear'
2775 a
, d
, t
, u
= splines
[i
][n
]
2778 change
.append([p
, ((m
-t
)/u
)*d
+ a
])
2780 move
.append([c
[0], (bm_mod
.verts
[c
[0]].co
+ c
[1]) / 2])
2785 ##########################################
2786 ####### Space functions ##################
2787 ##########################################
2789 # calculate relative positions compared to first knot
2790 def space_calculate_t(bm_mod
, knots
):
2795 loc
= mathutils
.Vector(bm_mod
.verts
[k
].co
[:])
2798 len_total
+= (loc
- loc_prev
).length
2799 tknots
.append(len_total
)
2802 t_per_segment
= len_total
/ (amount
- 1)
2803 tpoints
= [i
* t_per_segment
for i
in range(amount
)]
2805 return(tknots
, tpoints
)
2808 # change the location of the points to their place on the spline
2809 def space_calculate_verts(bm_mod
, interpolation
, tknots
, tpoints
, points
,
2813 m
= tpoints
[points
.index(p
)]
2821 if n
> len(splines
) - 1:
2822 n
= len(splines
) - 1
2826 if interpolation
== 'cubic':
2827 ax
, bx
, cx
, dx
, tx
= splines
[n
][0]
2828 x
= ax
+ bx
*(m
-tx
) + cx
*(m
-tx
)**2 + dx
*(m
-tx
)**3
2829 ay
, by
, cy
, dy
, ty
= splines
[n
][1]
2830 y
= ay
+ by
*(m
-ty
) + cy
*(m
-ty
)**2 + dy
*(m
-ty
)**3
2831 az
, bz
, cz
, dz
, tz
= splines
[n
][2]
2832 z
= az
+ bz
*(m
-tz
) + cz
*(m
-tz
)**2 + dz
*(m
-tz
)**3
2833 move
.append([p
, mathutils
.Vector([x
,y
,z
])])
2834 else: # interpolation == 'linear'
2835 a
, d
, t
, u
= splines
[n
]
2836 move
.append([p
, ((m
-t
)/u
)*d
+ a
])
2841 ##########################################
2842 ####### Operators ########################
2843 ##########################################
2846 class Bridge(bpy
.types
.Operator
):
2847 bl_idname
= 'mesh.looptools_bridge'
2848 bl_label
= "Bridge / Loft"
2849 bl_description
= "Bridge two, or loft several, loops of vertices"
2850 bl_options
= {'REGISTER', 'UNDO'}
2852 cubic_strength
= bpy
.props
.FloatProperty(name
= "Strength",
2853 description
= "Higher strength results in more fluid curves",
2857 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation mode",
2858 items
= (('cubic', "Cubic", "Gives curved results"),
2859 ('linear', "Linear", "Basic, fast, straight interpolation")),
2860 description
= "Interpolation mode: algorithm used when creating "\
2863 loft
= bpy
.props
.BoolProperty(name
= "Loft",
2864 description
= "Loft multiple loops, instead of considering them as "\
2865 "a multi-input for bridging",
2867 loft_loop
= bpy
.props
.BoolProperty(name
= "Loop",
2868 description
= "Connect the first and the last loop with each other",
2870 min_width
= bpy
.props
.IntProperty(name
= "Minimum width",
2871 description
= "Segments with an edge smaller than this are merged "\
2872 "(compared to base edge)",
2876 subtype
= 'PERCENTAGE')
2877 mode
= bpy
.props
.EnumProperty(name
= "Mode",
2878 items
= (('basic', "Basic", "Fast algorithm"), ('shortest',
2879 "Shortest edge", "Slower algorithm with better vertex matching")),
2880 description
= "Algorithm used for bridging",
2881 default
= 'shortest')
2882 remove_faces
= bpy
.props
.BoolProperty(name
= "Remove faces",
2883 description
= "Remove faces that are internal after bridging",
2885 reverse
= bpy
.props
.BoolProperty(name
= "Reverse",
2886 description
= "Manually override the direction in which the loops "\
2887 "are bridged. Only use if the tool gives the wrong " \
2890 segments
= bpy
.props
.IntProperty(name
= "Segments",
2891 description
= "Number of segments used to bridge the gap "\
2896 twist
= bpy
.props
.IntProperty(name
= "Twist",
2897 description
= "Twist what vertices are connected to each other",
2901 def poll(cls
, context
):
2902 ob
= context
.active_object
2903 return (ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
2905 def draw(self
, context
):
2906 layout
= self
.layout
2907 #layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
2910 col_top
= layout
.column(align
=True)
2911 row
= col_top
.row(align
=True)
2912 col_left
= row
.column(align
=True)
2913 col_right
= row
.column(align
=True)
2914 col_right
.active
= self
.segments
!= 1
2915 col_left
.prop(self
, "segments")
2916 col_right
.prop(self
, "min_width", text
="")
2918 bottom_left
= col_left
.row()
2919 bottom_left
.active
= self
.segments
!= 1
2920 bottom_left
.prop(self
, "interpolation", text
="")
2921 bottom_right
= col_right
.row()
2922 bottom_right
.active
= self
.interpolation
== 'cubic'
2923 bottom_right
.prop(self
, "cubic_strength")
2924 # boolean properties
2925 col_top
.prop(self
, "remove_faces")
2927 col_top
.prop(self
, "loft_loop")
2929 # override properties
2931 row
= layout
.row(align
= True)
2932 row
.prop(self
, "twist")
2933 row
.prop(self
, "reverse")
2935 def invoke(self
, context
, event
):
2936 # load custom settings
2937 context
.window_manager
.looptools
.bridge_loft
= self
.loft
2939 return self
.execute(context
)
2941 def execute(self
, context
):
2943 global_undo
, object, bm
= initialise()
2944 edge_faces
, edgekey_to_edge
, old_selected_faces
, smooth
= \
2945 bridge_initialise(bm
, self
.interpolation
)
2946 settings_write(self
)
2948 # check cache to see if we can save time
2949 input_method
= bridge_input_method(self
.loft
, self
.loft_loop
)
2950 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Bridge",
2951 object, bm
, input_method
, False)
2954 loops
= bridge_get_input(bm
)
2956 # reorder loops if there are more than 2
2959 loops
= bridge_sort_loops(bm
, loops
, self
.loft_loop
)
2961 loops
= bridge_match_loops(bm
, loops
)
2963 # saving cache for faster execution next time
2965 cache_write("Bridge", object, bm
, input_method
, False, False,
2966 loops
, False, False)
2969 # calculate new geometry
2972 max_vert_index
= len(bm
.verts
)-1
2973 for i
in range(1, len(loops
)):
2974 if not self
.loft
and i
%2 == 0:
2976 lines
= bridge_calculate_lines(bm
, loops
[i
-1:i
+1],
2977 self
.mode
, self
.twist
, self
.reverse
)
2978 vertex_normals
= bridge_calculate_virtual_vertex_normals(bm
,
2979 lines
, loops
[i
-1:i
+1], edge_faces
, edgekey_to_edge
)
2980 segments
= bridge_calculate_segments(bm
, lines
,
2981 loops
[i
-1:i
+1], self
.segments
)
2982 new_verts
, new_faces
, max_vert_index
= \
2983 bridge_calculate_geometry(bm
, lines
, vertex_normals
,
2984 segments
, self
.interpolation
, self
.cubic_strength
,
2985 self
.min_width
, max_vert_index
)
2987 vertices
+= new_verts
2990 # make sure faces in loops that aren't used, aren't removed
2991 if self
.remove_faces
and old_selected_faces
:
2992 bridge_save_unused_faces(bm
, old_selected_faces
, loops
)
2995 bridge_create_vertices(bm
, vertices
)
2998 bridge_create_faces(object, bm
, faces
, self
.twist
)
2999 bridge_select_new_faces(bm
, len(faces
), smooth
)
3000 # edge-data could have changed, can't use cache next run
3001 if faces
and not vertices
:
3002 cache_delete("Bridge")
3003 # delete internal faces
3004 if self
.remove_faces
and old_selected_faces
:
3005 bridge_remove_internal_faces(bm
, old_selected_faces
)
3006 # make sure normals are facing outside
3007 bmesh
.update_edit_mesh(object.data
, tessface
=False, destructive
=True)
3008 bpy
.ops
.mesh
.normals_make_consistent()
3011 terminate(global_undo
)
3017 class Circle(bpy
.types
.Operator
):
3018 bl_idname
= "mesh.looptools_circle"
3020 bl_description
= "Move selected vertices into a circle shape"
3021 bl_options
= {'REGISTER', 'UNDO'}
3023 custom_radius
= bpy
.props
.BoolProperty(name
= "Radius",
3024 description
= "Force a custom radius",
3026 fit
= bpy
.props
.EnumProperty(name
= "Method",
3027 items
= (("best", "Best fit", "Non-linear least squares"),
3028 ("inside", "Fit inside","Only move vertices towards the center")),
3029 description
= "Method used for fitting a circle to the vertices",
3031 flatten
= bpy
.props
.BoolProperty(name
= "Flatten",
3032 description
= "Flatten the circle, instead of projecting it on the " \
3035 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3036 description
= "Force of the tool",
3041 subtype
= 'PERCENTAGE')
3042 radius
= bpy
.props
.FloatProperty(name
= "Radius",
3043 description
= "Custom radius for circle",
3047 regular
= bpy
.props
.BoolProperty(name
= "Regular",
3048 description
= "Distribute vertices at constant distances along the " \
3053 def poll(cls
, context
):
3054 ob
= context
.active_object
3055 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3057 def draw(self
, context
):
3058 layout
= self
.layout
3059 col
= layout
.column()
3061 col
.prop(self
, "fit")
3064 col
.prop(self
, "flatten")
3065 row
= col
.row(align
=True)
3066 row
.prop(self
, "custom_radius")
3067 row_right
= row
.row(align
=True)
3068 row_right
.active
= self
.custom_radius
3069 row_right
.prop(self
, "radius", text
="")
3070 col
.prop(self
, "regular")
3073 col
.prop(self
, "influence")
3075 def invoke(self
, context
, event
):
3076 # load custom settings
3078 return self
.execute(context
)
3080 def execute(self
, context
):
3082 global_undo
, object, bm
= initialise()
3083 settings_write(self
)
3084 # check cache to see if we can save time
3085 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Circle",
3086 object, bm
, False, False)
3088 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3091 derived
, bm_mod
, single_vertices
, single_loops
, loops
= \
3092 circle_get_input(object, bm
, context
.scene
)
3093 mapping
= get_mapping(derived
, bm
, bm_mod
, single_vertices
,
3095 single_loops
, loops
= circle_check_loops(single_loops
, loops
,
3098 # saving cache for faster execution next time
3100 cache_write("Circle", object, bm
, False, False, single_loops
,
3101 loops
, derived
, mapping
)
3104 for i
, loop
in enumerate(loops
):
3105 # best fitting flat plane
3106 com
, normal
= calculate_plane(bm_mod
, loop
)
3107 # if circular, shift loop so we get a good starting vertex
3109 loop
= circle_shift_loop(bm_mod
, loop
, com
)
3110 # flatten vertices on plane
3111 locs_2d
, p
, q
= circle_3d_to_2d(bm_mod
, loop
, com
, normal
)
3113 if self
.fit
== 'best':
3114 x0
, y0
, r
= circle_calculate_best_fit(locs_2d
)
3115 else: # self.fit == 'inside'
3116 x0
, y0
, r
= circle_calculate_min_fit(locs_2d
)
3118 if self
.custom_radius
:
3119 r
= self
.radius
/ p
.length
3120 # calculate positions on circle
3122 new_locs_2d
= circle_project_regular(locs_2d
[:], x0
, y0
, r
)
3124 new_locs_2d
= circle_project_non_regular(locs_2d
[:], x0
, y0
, r
)
3125 # take influence into account
3126 locs_2d
= circle_influence_locs(locs_2d
, new_locs_2d
,
3128 # calculate 3d positions of the created 2d input
3129 move
.append(circle_calculate_verts(self
.flatten
, bm_mod
,
3130 locs_2d
, com
, p
, q
, normal
))
3131 # flatten single input vertices on plane defined by loop
3132 if self
.flatten
and single_loops
:
3133 move
.append(circle_flatten_singles(bm_mod
, com
, p
, q
,
3134 normal
, single_loops
[i
]))
3136 # move vertices to new locations
3137 move_verts(object, bm
, mapping
, move
, -1)
3142 terminate(global_undo
)
3148 class Curve(bpy
.types
.Operator
):
3149 bl_idname
= "mesh.looptools_curve"
3151 bl_description
= "Turn a loop into a smooth curve"
3152 bl_options
= {'REGISTER', 'UNDO'}
3154 boundaries
= bpy
.props
.BoolProperty(name
= "Boundaries",
3155 description
= "Limit the tool to work within the boundaries of the "\
3156 "selected vertices",
3158 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3159 description
= "Force of the tool",
3164 subtype
= 'PERCENTAGE')
3165 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
3166 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3167 ("linear", "Linear", "Simple and fast linear algorithm")),
3168 description
= "Algorithm used for interpolation",
3170 regular
= bpy
.props
.BoolProperty(name
= "Regular",
3171 description
= "Distribute vertices at constant distances along the" \
3174 restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
3175 items
= (("none", "None", "No restrictions on vertex movement"),
3176 ("extrude", "Extrude only","Only allow extrusions (no "\
3178 ("indent", "Indent only", "Only allow indentation (no "\
3180 description
= "Restrictions on how the vertices can be moved",
3184 def poll(cls
, context
):
3185 ob
= context
.active_object
3186 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3188 def draw(self
, context
):
3189 layout
= self
.layout
3190 col
= layout
.column()
3192 col
.prop(self
, "interpolation")
3193 col
.prop(self
, "restriction")
3194 col
.prop(self
, "boundaries")
3195 col
.prop(self
, "regular")
3198 col
.prop(self
, "influence")
3200 def invoke(self
, context
, event
):
3201 # load custom settings
3203 return self
.execute(context
)
3205 def execute(self
, context
):
3207 global_undo
, object, bm
= initialise()
3208 settings_write(self
)
3209 # check cache to see if we can save time
3210 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Curve",
3211 object, bm
, False, self
.boundaries
)
3213 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3216 derived
, bm_mod
, loops
= curve_get_input(object, bm
,
3217 self
.boundaries
, context
.scene
)
3218 mapping
= get_mapping(derived
, bm
, bm_mod
, False, True, loops
)
3219 loops
= check_loops(loops
, mapping
, bm_mod
)
3220 verts_selected
= [v
.index
for v
in bm_mod
.verts
if v
.select \
3223 # saving cache for faster execution next time
3225 cache_write("Curve", object, bm
, False, self
.boundaries
, False,
3226 loops
, derived
, mapping
)
3230 knots
, points
= curve_calculate_knots(loop
, verts_selected
)
3231 pknots
= curve_project_knots(bm_mod
, verts_selected
, knots
,
3233 tknots
, tpoints
= curve_calculate_t(bm_mod
, knots
, points
,
3234 pknots
, self
.regular
, loop
[1])
3235 splines
= calculate_splines(self
.interpolation
, bm_mod
,
3237 move
.append(curve_calculate_vertices(bm_mod
, knots
, tknots
,
3238 points
, tpoints
, splines
, self
.interpolation
,
3241 # move vertices to new locations
3242 move_verts(object, bm
, mapping
, move
, self
.influence
)
3247 terminate(global_undo
)
3253 class Flatten(bpy
.types
.Operator
):
3254 bl_idname
= "mesh.looptools_flatten"
3255 bl_label
= "Flatten"
3256 bl_description
= "Flatten vertices on a best-fitting plane"
3257 bl_options
= {'REGISTER', 'UNDO'}
3259 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3260 description
= "Force of the tool",
3265 subtype
= 'PERCENTAGE')
3266 plane
= bpy
.props
.EnumProperty(name
= "Plane",
3267 items
= (("best_fit", "Best fit", "Calculate a best fitting plane"),
3268 ("normal", "Normal", "Derive plane from averaging vertex "\
3270 ("view", "View", "Flatten on a plane perpendicular to the "\
3272 description
= "Plane on which vertices are flattened",
3273 default
= 'best_fit')
3274 restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
3275 items
= (("none", "None", "No restrictions on vertex movement"),
3276 ("bounding_box", "Bounding box", "Vertices are restricted to "\
3277 "movement inside the bounding box of the selection")),
3278 description
= "Restrictions on how the vertices can be moved",
3282 def poll(cls
, context
):
3283 ob
= context
.active_object
3284 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3286 def draw(self
, context
):
3287 layout
= self
.layout
3288 col
= layout
.column()
3290 col
.prop(self
, "plane")
3291 #col.prop(self, "restriction")
3294 col
.prop(self
, "influence")
3296 def invoke(self
, context
, event
):
3297 # load custom settings
3299 return self
.execute(context
)
3301 def execute(self
, context
):
3303 global_undo
, object, bm
= initialise()
3304 settings_write(self
)
3305 # check cache to see if we can save time
3306 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Flatten",
3307 object, bm
, False, False)
3309 # order input into virtual loops
3310 loops
= flatten_get_input(bm
)
3311 loops
= check_loops(loops
, mapping
, bm
)
3313 # saving cache for faster execution next time
3315 cache_write("Flatten", object, bm
, False, False, False, loops
,
3320 # calculate plane and position of vertices on them
3321 com
, normal
= calculate_plane(bm
, loop
, method
=self
.plane
,
3323 to_move
= flatten_project(bm
, loop
, com
, normal
)
3324 if self
.restriction
== 'none':
3325 move
.append(to_move
)
3327 move
.append(to_move
)
3328 move_verts(object, bm
, False, move
, self
.influence
)
3331 terminate(global_undo
)
3337 class GStretch(bpy
.types
.Operator
):
3338 bl_idname
= "mesh.looptools_gstretch"
3339 bl_label
= "Gstretch"
3340 bl_description
= "Stretch selected vertices to Grease Pencil stroke"
3341 bl_options
= {'REGISTER', 'UNDO'}
3343 delete_strokes
= bpy
.props
.BoolProperty(name
="Delete strokes",
3344 description
= "Remove Grease Pencil strokes if they have been used "\
3347 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3348 description
= "Force of the tool",
3353 subtype
= 'PERCENTAGE')
3354 method
= bpy
.props
.EnumProperty(name
= "Method",
3355 items
= (("project", "Project", "Project vertices onto the stroke, "\
3356 "using vertex normals and connected edges"),
3357 ("irregular", "Spread", "Distribute vertices along the full "\
3358 "stroke, retaining relative distances between the vertices"),
3359 ("regular", "Spread evenly", "Distribute vertices at regular "\
3360 "distances along the full stroke")),
3361 description
= "Method of distributing the vertices over the Grease "\
3363 default
= 'regular')
3366 def poll(cls
, context
):
3367 ob
= context
.active_object
3368 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH'
3369 and ob
.grease_pencil
)
3371 def draw(self
, context
):
3372 layout
= self
.layout
3373 col
= layout
.column()
3375 col
.prop(self
, "delete_strokes")
3376 col
.prop(self
, "method")
3378 col
.prop(self
, "influence")
3380 def invoke(self
, context
, event
):
3381 # load custom settings
3383 return self
.execute(context
)
3385 def execute(self
, context
):
3387 global_undo
, object, bm
= initialise()
3388 settings_write(self
)
3390 # check cache to see if we can save time
3391 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Gstretch",
3392 object, bm
, False, False)
3394 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3397 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
3398 context
.scene
, input='selected')
3399 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
3400 loops
= check_loops(loops
, mapping
, bm_mod
)
3401 strokes
= gstretch_get_strokes(object)
3403 # saving cache for faster execution next time
3405 cache_write("Gstretch", object, bm
, False, False, False, loops
,
3408 # pair loops and strokes
3409 ls_pairs
= gstretch_match_loops_strokes(loops
, strokes
, object, bm_mod
)
3410 ls_pairs
= gstretch_align_pairs(ls_pairs
, object, bm_mod
, self
.method
)
3414 for (loop
, stroke
) in ls_pairs
:
3415 move
.append(gstretch_calculate_verts(loop
, stroke
, object,
3416 bm_mod
, self
.method
))
3417 if self
.delete_strokes
:
3418 gstretch_erase_stroke(stroke
, context
)
3420 # move vertices to new locations
3421 move_verts(object, bm
, mapping
, move
, self
.influence
)
3426 terminate(global_undo
)
3432 class Relax(bpy
.types
.Operator
):
3433 bl_idname
= "mesh.looptools_relax"
3435 bl_description
= "Relax the loop, so it is smoother"
3436 bl_options
= {'REGISTER', 'UNDO'}
3438 input = bpy
.props
.EnumProperty(name
= "Input",
3439 items
= (("all", "Parallel (all)", "Also use non-selected "\
3440 "parallel loops as input"),
3441 ("selected", "Selection","Only use selected vertices as input")),
3442 description
= "Loops that are relaxed",
3443 default
= 'selected')
3444 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
3445 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3446 ("linear", "Linear", "Simple and fast linear algorithm")),
3447 description
= "Algorithm used for interpolation",
3449 iterations
= bpy
.props
.EnumProperty(name
= "Iterations",
3450 items
= (("1", "1", "One"),
3451 ("3", "3", "Three"),
3453 ("10", "10", "Ten"),
3454 ("25", "25", "Twenty-five")),
3455 description
= "Number of times the loop is relaxed",
3457 regular
= bpy
.props
.BoolProperty(name
= "Regular",
3458 description
= "Distribute vertices at constant distances along the" \
3463 def poll(cls
, context
):
3464 ob
= context
.active_object
3465 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3467 def draw(self
, context
):
3468 layout
= self
.layout
3469 col
= layout
.column()
3471 col
.prop(self
, "interpolation")
3472 col
.prop(self
, "input")
3473 col
.prop(self
, "iterations")
3474 col
.prop(self
, "regular")
3476 def invoke(self
, context
, event
):
3477 # load custom settings
3479 return self
.execute(context
)
3481 def execute(self
, context
):
3483 global_undo
, object, bm
= initialise()
3484 settings_write(self
)
3485 # check cache to see if we can save time
3486 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Relax",
3487 object, bm
, self
.input, False)
3489 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3492 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
3493 context
.scene
, self
.input)
3494 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
3495 loops
= check_loops(loops
, mapping
, bm_mod
)
3496 knots
, points
= relax_calculate_knots(loops
)
3498 # saving cache for faster execution next time
3500 cache_write("Relax", object, bm
, self
.input, False, False, loops
,
3503 for iteration
in range(int(self
.iterations
)):
3504 # calculate splines and new positions
3505 tknots
, tpoints
= relax_calculate_t(bm_mod
, knots
, points
,
3508 for i
in range(len(knots
)):
3509 splines
.append(calculate_splines(self
.interpolation
, bm_mod
,
3510 tknots
[i
], knots
[i
]))
3511 move
= [relax_calculate_verts(bm_mod
, self
.interpolation
,
3512 tknots
, knots
, tpoints
, points
, splines
)]
3513 move_verts(object, bm
, mapping
, move
, -1)
3518 terminate(global_undo
)
3524 class Space(bpy
.types
.Operator
):
3525 bl_idname
= "mesh.looptools_space"
3527 bl_description
= "Space the vertices in a regular distrubtion on the loop"
3528 bl_options
= {'REGISTER', 'UNDO'}
3530 influence
= bpy
.props
.FloatProperty(name
= "Influence",
3531 description
= "Force of the tool",
3536 subtype
= 'PERCENTAGE')
3537 input = bpy
.props
.EnumProperty(name
= "Input",
3538 items
= (("all", "Parallel (all)", "Also use non-selected "\
3539 "parallel loops as input"),
3540 ("selected", "Selection","Only use selected vertices as input")),
3541 description
= "Loops that are spaced",
3542 default
= 'selected')
3543 interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
3544 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3545 ("linear", "Linear", "Vertices are projected on existing edges")),
3546 description
= "Algorithm used for interpolation",
3550 def poll(cls
, context
):
3551 ob
= context
.active_object
3552 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
3554 def draw(self
, context
):
3555 layout
= self
.layout
3556 col
= layout
.column()
3558 col
.prop(self
, "interpolation")
3559 col
.prop(self
, "input")
3562 col
.prop(self
, "influence")
3564 def invoke(self
, context
, event
):
3565 # load custom settings
3567 return self
.execute(context
)
3569 def execute(self
, context
):
3571 global_undo
, object, bm
= initialise()
3572 settings_write(self
)
3573 # check cache to see if we can save time
3574 cached
, single_loops
, loops
, derived
, mapping
= cache_read("Space",
3575 object, bm
, self
.input, False)
3577 derived
, bm_mod
= get_derived_bmesh(object, bm
, context
.scene
)
3580 derived
, bm_mod
, loops
= get_connected_input(object, bm
,
3581 context
.scene
, self
.input)
3582 mapping
= get_mapping(derived
, bm
, bm_mod
, False, False, loops
)
3583 loops
= check_loops(loops
, mapping
, bm_mod
)
3585 # saving cache for faster execution next time
3587 cache_write("Space", object, bm
, self
.input, False, False, loops
,
3592 # calculate splines and new positions
3593 if loop
[1]: # circular
3594 loop
[0].append(loop
[0][0])
3595 tknots
, tpoints
= space_calculate_t(bm_mod
, loop
[0][:])
3596 splines
= calculate_splines(self
.interpolation
, bm_mod
,
3598 move
.append(space_calculate_verts(bm_mod
, self
.interpolation
,
3599 tknots
, tpoints
, loop
[0][:-1], splines
))
3600 # move vertices to new locations
3601 move_verts(object, bm
, mapping
, move
, self
.influence
)
3606 terminate(global_undo
)
3611 ##########################################
3612 ####### GUI and registration #############
3613 ##########################################
3615 # menu containing all tools
3616 class VIEW3D_MT_edit_mesh_looptools(bpy
.types
.Menu
):
3617 bl_label
= "LoopTools"
3619 def draw(self
, context
):
3620 layout
= self
.layout
3622 layout
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
3623 layout
.operator("mesh.looptools_circle")
3624 layout
.operator("mesh.looptools_curve")
3625 layout
.operator("mesh.looptools_flatten")
3626 layout
.operator("mesh.looptools_gstretch")
3627 layout
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
3628 layout
.operator("mesh.looptools_relax")
3629 layout
.operator("mesh.looptools_space")
3632 # panel containing all tools
3633 class VIEW3D_PT_tools_looptools(bpy
.types
.Panel
):
3634 bl_space_type
= 'VIEW_3D'
3635 bl_region_type
= 'TOOLS'
3636 bl_context
= "mesh_edit"
3637 bl_label
= "LoopTools"
3638 bl_options
= {'DEFAULT_CLOSED'}
3640 def draw(self
, context
):
3641 layout
= self
.layout
3642 col
= layout
.column(align
=True)
3643 lt
= context
.window_manager
.looptools
3645 # bridge - first line
3646 split
= col
.split(percentage
=0.15)
3647 if lt
.display_bridge
:
3648 split
.prop(lt
, "display_bridge", text
="", icon
='DOWNARROW_HLT')
3650 split
.prop(lt
, "display_bridge", text
="", icon
='RIGHTARROW')
3651 split
.operator("mesh.looptools_bridge", text
="Bridge").loft
= False
3653 if lt
.display_bridge
:
3654 box
= col
.column(align
=True).box().column()
3655 #box.prop(self, "mode")
3658 col_top
= box
.column(align
=True)
3659 row
= col_top
.row(align
=True)
3660 col_left
= row
.column(align
=True)
3661 col_right
= row
.column(align
=True)
3662 col_right
.active
= lt
.bridge_segments
!= 1
3663 col_left
.prop(lt
, "bridge_segments")
3664 col_right
.prop(lt
, "bridge_min_width", text
="")
3666 bottom_left
= col_left
.row()
3667 bottom_left
.active
= lt
.bridge_segments
!= 1
3668 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
3669 bottom_right
= col_right
.row()
3670 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
3671 bottom_right
.prop(lt
, "bridge_cubic_strength")
3672 # boolean properties
3673 col_top
.prop(lt
, "bridge_remove_faces")
3675 # override properties
3677 row
= box
.row(align
= True)
3678 row
.prop(lt
, "bridge_twist")
3679 row
.prop(lt
, "bridge_reverse")
3681 # circle - first line
3682 split
= col
.split(percentage
=0.15)
3683 if lt
.display_circle
:
3684 split
.prop(lt
, "display_circle", text
="", icon
='DOWNARROW_HLT')
3686 split
.prop(lt
, "display_circle", text
="", icon
='RIGHTARROW')
3687 split
.operator("mesh.looptools_circle")
3689 if lt
.display_circle
:
3690 box
= col
.column(align
=True).box().column()
3691 box
.prop(lt
, "circle_fit")
3694 box
.prop(lt
, "circle_flatten")
3695 row
= box
.row(align
=True)
3696 row
.prop(lt
, "circle_custom_radius")
3697 row_right
= row
.row(align
=True)
3698 row_right
.active
= lt
.circle_custom_radius
3699 row_right
.prop(lt
, "circle_radius", text
="")
3700 box
.prop(lt
, "circle_regular")
3703 box
.prop(lt
, "circle_influence")
3705 # curve - first line
3706 split
= col
.split(percentage
=0.15)
3707 if lt
.display_curve
:
3708 split
.prop(lt
, "display_curve", text
="", icon
='DOWNARROW_HLT')
3710 split
.prop(lt
, "display_curve", text
="", icon
='RIGHTARROW')
3711 split
.operator("mesh.looptools_curve")
3713 if lt
.display_curve
:
3714 box
= col
.column(align
=True).box().column()
3715 box
.prop(lt
, "curve_interpolation")
3716 box
.prop(lt
, "curve_restriction")
3717 box
.prop(lt
, "curve_boundaries")
3718 box
.prop(lt
, "curve_regular")
3721 box
.prop(lt
, "curve_influence")
3723 # flatten - first line
3724 split
= col
.split(percentage
=0.15)
3725 if lt
.display_flatten
:
3726 split
.prop(lt
, "display_flatten", text
="", icon
='DOWNARROW_HLT')
3728 split
.prop(lt
, "display_flatten", text
="", icon
='RIGHTARROW')
3729 split
.operator("mesh.looptools_flatten")
3730 # flatten - settings
3731 if lt
.display_flatten
:
3732 box
= col
.column(align
=True).box().column()
3733 box
.prop(lt
, "flatten_plane")
3734 #box.prop(lt, "flatten_restriction")
3737 box
.prop(lt
, "flatten_influence")
3739 # gstretch - first line
3740 split
= col
.split(percentage
=0.15)
3741 if lt
.display_gstretch
:
3742 split
.prop(lt
, "display_gstretch", text
="", icon
='DOWNARROW_HLT')
3744 split
.prop(lt
, "display_gstretch", text
="", icon
='RIGHTARROW')
3745 split
.operator("mesh.looptools_gstretch")
3747 if lt
.display_gstretch
:
3748 box
= col
.column(align
=True).box().column()
3749 box
.prop(lt
, "gstretch_delete_strokes")
3750 box
.prop(lt
, "gstretch_method")
3752 box
.prop(lt
, "gstretch_influence")
3755 split
= col
.split(percentage
=0.15)
3757 split
.prop(lt
, "display_loft", text
="", icon
='DOWNARROW_HLT')
3759 split
.prop(lt
, "display_loft", text
="", icon
='RIGHTARROW')
3760 split
.operator("mesh.looptools_bridge", text
="Loft").loft
= True
3763 box
= col
.column(align
=True).box().column()
3764 #box.prop(self, "mode")
3767 col_top
= box
.column(align
=True)
3768 row
= col_top
.row(align
=True)
3769 col_left
= row
.column(align
=True)
3770 col_right
= row
.column(align
=True)
3771 col_right
.active
= lt
.bridge_segments
!= 1
3772 col_left
.prop(lt
, "bridge_segments")
3773 col_right
.prop(lt
, "bridge_min_width", text
="")
3775 bottom_left
= col_left
.row()
3776 bottom_left
.active
= lt
.bridge_segments
!= 1
3777 bottom_left
.prop(lt
, "bridge_interpolation", text
="")
3778 bottom_right
= col_right
.row()
3779 bottom_right
.active
= lt
.bridge_interpolation
== 'cubic'
3780 bottom_right
.prop(lt
, "bridge_cubic_strength")
3781 # boolean properties
3782 col_top
.prop(lt
, "bridge_remove_faces")
3783 col_top
.prop(lt
, "bridge_loft_loop")
3785 # override properties
3787 row
= box
.row(align
= True)
3788 row
.prop(lt
, "bridge_twist")
3789 row
.prop(lt
, "bridge_reverse")
3791 # relax - first line
3792 split
= col
.split(percentage
=0.15)
3793 if lt
.display_relax
:
3794 split
.prop(lt
, "display_relax", text
="", icon
='DOWNARROW_HLT')
3796 split
.prop(lt
, "display_relax", text
="", icon
='RIGHTARROW')
3797 split
.operator("mesh.looptools_relax")
3799 if lt
.display_relax
:
3800 box
= col
.column(align
=True).box().column()
3801 box
.prop(lt
, "relax_interpolation")
3802 box
.prop(lt
, "relax_input")
3803 box
.prop(lt
, "relax_iterations")
3804 box
.prop(lt
, "relax_regular")
3806 # space - first line
3807 split
= col
.split(percentage
=0.15)
3808 if lt
.display_space
:
3809 split
.prop(lt
, "display_space", text
="", icon
='DOWNARROW_HLT')
3811 split
.prop(lt
, "display_space", text
="", icon
='RIGHTARROW')
3812 split
.operator("mesh.looptools_space")
3814 if lt
.display_space
:
3815 box
= col
.column(align
=True).box().column()
3816 box
.prop(lt
, "space_interpolation")
3817 box
.prop(lt
, "space_input")
3820 box
.prop(lt
, "space_influence")
3823 # property group containing all properties for the gui in the panel
3824 class LoopToolsProps(bpy
.types
.PropertyGroup
):
3826 Fake module like class
3827 bpy.context.window_manager.looptools
3830 # general display properties
3831 display_bridge
= bpy
.props
.BoolProperty(name
= "Bridge settings",
3832 description
= "Display settings of the Bridge tool",
3834 display_circle
= bpy
.props
.BoolProperty(name
= "Circle settings",
3835 description
= "Display settings of the Circle tool",
3837 display_curve
= bpy
.props
.BoolProperty(name
= "Curve settings",
3838 description
= "Display settings of the Curve tool",
3840 display_flatten
= bpy
.props
.BoolProperty(name
= "Flatten settings",
3841 description
= "Display settings of the Flatten tool",
3843 display_gstretch
= bpy
.props
.BoolProperty(name
= "Gstretch settings",
3844 description
= "Display settings of the Gstretch tool",
3846 display_loft
= bpy
.props
.BoolProperty(name
= "Loft settings",
3847 description
= "Display settings of the Loft tool",
3849 display_relax
= bpy
.props
.BoolProperty(name
= "Relax settings",
3850 description
= "Display settings of the Relax tool",
3852 display_space
= bpy
.props
.BoolProperty(name
= "Space settings",
3853 description
= "Display settings of the Space tool",
3857 bridge_cubic_strength
= bpy
.props
.FloatProperty(name
= "Strength",
3858 description
= "Higher strength results in more fluid curves",
3862 bridge_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation mode",
3863 items
= (('cubic', "Cubic", "Gives curved results"),
3864 ('linear', "Linear", "Basic, fast, straight interpolation")),
3865 description
= "Interpolation mode: algorithm used when creating "\
3868 bridge_loft
= bpy
.props
.BoolProperty(name
= "Loft",
3869 description
= "Loft multiple loops, instead of considering them as "\
3870 "a multi-input for bridging",
3872 bridge_loft_loop
= bpy
.props
.BoolProperty(name
= "Loop",
3873 description
= "Connect the first and the last loop with each other",
3875 bridge_min_width
= bpy
.props
.IntProperty(name
= "Minimum width",
3876 description
= "Segments with an edge smaller than this are merged "\
3877 "(compared to base edge)",
3881 subtype
= 'PERCENTAGE')
3882 bridge_mode
= bpy
.props
.EnumProperty(name
= "Mode",
3883 items
= (('basic', "Basic", "Fast algorithm"),
3884 ('shortest', "Shortest edge", "Slower algorithm with " \
3885 "better vertex matching")),
3886 description
= "Algorithm used for bridging",
3887 default
= 'shortest')
3888 bridge_remove_faces
= bpy
.props
.BoolProperty(name
= "Remove faces",
3889 description
= "Remove faces that are internal after bridging",
3891 bridge_reverse
= bpy
.props
.BoolProperty(name
= "Reverse",
3892 description
= "Manually override the direction in which the loops "\
3893 "are bridged. Only use if the tool gives the wrong " \
3896 bridge_segments
= bpy
.props
.IntProperty(name
= "Segments",
3897 description
= "Number of segments used to bridge the gap "\
3902 bridge_twist
= bpy
.props
.IntProperty(name
= "Twist",
3903 description
= "Twist what vertices are connected to each other",
3907 circle_custom_radius
= bpy
.props
.BoolProperty(name
= "Radius",
3908 description
= "Force a custom radius",
3910 circle_fit
= bpy
.props
.EnumProperty(name
= "Method",
3911 items
= (("best", "Best fit", "Non-linear least squares"),
3912 ("inside", "Fit inside","Only move vertices towards the center")),
3913 description
= "Method used for fitting a circle to the vertices",
3915 circle_flatten
= bpy
.props
.BoolProperty(name
= "Flatten",
3916 description
= "Flatten the circle, instead of projecting it on the " \
3919 circle_influence
= bpy
.props
.FloatProperty(name
= "Influence",
3920 description
= "Force of the tool",
3925 subtype
= 'PERCENTAGE')
3926 circle_radius
= bpy
.props
.FloatProperty(name
= "Radius",
3927 description
= "Custom radius for circle",
3931 circle_regular
= bpy
.props
.BoolProperty(name
= "Regular",
3932 description
= "Distribute vertices at constant distances along the " \
3937 curve_boundaries
= bpy
.props
.BoolProperty(name
= "Boundaries",
3938 description
= "Limit the tool to work within the boundaries of the "\
3939 "selected vertices",
3941 curve_influence
= bpy
.props
.FloatProperty(name
= "Influence",
3942 description
= "Force of the tool",
3947 subtype
= 'PERCENTAGE')
3948 curve_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
3949 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3950 ("linear", "Linear", "Simple and fast linear algorithm")),
3951 description
= "Algorithm used for interpolation",
3953 curve_regular
= bpy
.props
.BoolProperty(name
= "Regular",
3954 description
= "Distribute vertices at constant distances along the " \
3957 curve_restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
3958 items
= (("none", "None", "No restrictions on vertex movement"),
3959 ("extrude", "Extrude only","Only allow extrusions (no "\
3961 ("indent", "Indent only", "Only allow indentation (no "\
3963 description
= "Restrictions on how the vertices can be moved",
3966 # flatten properties
3967 flatten_influence
= bpy
.props
.FloatProperty(name
= "Influence",
3968 description
= "Force of the tool",
3973 subtype
= 'PERCENTAGE')
3974 flatten_plane
= bpy
.props
.EnumProperty(name
= "Plane",
3975 items
= (("best_fit", "Best fit", "Calculate a best fitting plane"),
3976 ("normal", "Normal", "Derive plane from averaging vertex "\
3978 ("view", "View", "Flatten on a plane perpendicular to the "\
3980 description
= "Plane on which vertices are flattened",
3981 default
= 'best_fit')
3982 flatten_restriction
= bpy
.props
.EnumProperty(name
= "Restriction",
3983 items
= (("none", "None", "No restrictions on vertex movement"),
3984 ("bounding_box", "Bounding box", "Vertices are restricted to "\
3985 "movement inside the bounding box of the selection")),
3986 description
= "Restrictions on how the vertices can be moved",
3989 # gstretch properties
3990 gstretch_delete_strokes
= bpy
.props
.BoolProperty(name
="Delete strokes",
3991 description
= "Remove Grease Pencil strokes if they have been used "\
3994 gstretch_influence
= bpy
.props
.FloatProperty(name
= "Influence",
3995 description
= "Force of the tool",
4000 subtype
= 'PERCENTAGE')
4001 gstretch_method
= bpy
.props
.EnumProperty(name
= "Method",
4002 items
= (("project", "Project", "Project vertices onto the stroke, "\
4003 "using vertex normals and connected edges"),
4004 ("irregular", "Spread", "Distribute vertices along the full "\
4005 "stroke, retaining relative distances between the vertices"),
4006 ("regular", "Spread evenly", "Distribute vertices at regular "\
4007 "distances along the full stroke")),
4008 description
= "Method of distributing the vertices over the Grease "\
4010 default
= 'regular')
4013 relax_input
= bpy
.props
.EnumProperty(name
= "Input",
4014 items
= (("all", "Parallel (all)", "Also use non-selected "\
4015 "parallel loops as input"),
4016 ("selected", "Selection","Only use selected vertices as input")),
4017 description
= "Loops that are relaxed",
4018 default
= 'selected')
4019 relax_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4020 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4021 ("linear", "Linear", "Simple and fast linear algorithm")),
4022 description
= "Algorithm used for interpolation",
4024 relax_iterations
= bpy
.props
.EnumProperty(name
= "Iterations",
4025 items
= (("1", "1", "One"),
4026 ("3", "3", "Three"),
4028 ("10", "10", "Ten"),
4029 ("25", "25", "Twenty-five")),
4030 description
= "Number of times the loop is relaxed",
4032 relax_regular
= bpy
.props
.BoolProperty(name
= "Regular",
4033 description
= "Distribute vertices at constant distances along the" \
4038 space_influence
= bpy
.props
.FloatProperty(name
= "Influence",
4039 description
= "Force of the tool",
4044 subtype
= 'PERCENTAGE')
4045 space_input
= bpy
.props
.EnumProperty(name
= "Input",
4046 items
= (("all", "Parallel (all)", "Also use non-selected "\
4047 "parallel loops as input"),
4048 ("selected", "Selection","Only use selected vertices as input")),
4049 description
= "Loops that are spaced",
4050 default
= 'selected')
4051 space_interpolation
= bpy
.props
.EnumProperty(name
= "Interpolation",
4052 items
= (("cubic", "Cubic", "Natural cubic spline, smooth results"),
4053 ("linear", "Linear", "Vertices are projected on existing edges")),
4054 description
= "Algorithm used for interpolation",
4058 # draw function for integration in menus
4059 def menu_func(self
, context
):
4060 self
.layout
.menu("VIEW3D_MT_edit_mesh_looptools")
4061 self
.layout
.separator()
4064 # define classes for registration
4065 classes
= [VIEW3D_MT_edit_mesh_looptools
,
4066 VIEW3D_PT_tools_looptools
,
4077 # registering and menu integration
4080 bpy
.utils
.register_class(c
)
4081 bpy
.types
.VIEW3D_MT_edit_mesh_specials
.prepend(menu_func
)
4082 bpy
.types
.WindowManager
.looptools
= bpy
.props
.PointerProperty(\
4083 type = LoopToolsProps
)
4086 # unregistering and removing menus
4089 bpy
.utils
.unregister_class(c
)
4090 bpy
.types
.VIEW3D_MT_edit_mesh_specials
.remove(menu_func
)
4092 del bpy
.types
.WindowManager
.looptools
4097 if __name__
== "__main__":