diff --git a/mesh_looptools.py b/mesh_looptools.py deleted file mode 100644 index 639b9d7d117ee687cf309708e7c3dc5ebc2e7269..0000000000000000000000000000000000000000 --- a/mesh_looptools.py +++ /dev/null @@ -1,3728 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -bl_info = { - 'name': "LoopTools", - 'author': "Bart Crouch", - 'version': (3, 2, 4), - 'blender': (2, 6, 2), - 'location': "View3D > Toolbar and View3D > Specials (W-key)", - 'warning': "Bridge & Loft functions removed", - 'description': "Mesh modelling toolkit. Several tools to aid modelling", - 'wiki_url': "http://wiki.blender.org/index.php/Extensions:2.5/Py/"\ - "Scripts/Modeling/LoopTools", - 'tracker_url': "http://projects.blender.org/tracker/index.php?"\ - "func=detail&aid=26189", - 'category': 'Mesh'} - - -import bpy -import mathutils -import math - - -########################################## -####### General functions ################ -########################################## - - -# used by all tools to improve speed on reruns -looptools_cache = {} - - -# force a full recalculation next time -def cache_delete(tool): - if tool in looptools_cache: - del looptools_cache[tool] - - -# check cache for stored information -def cache_read(tool, object, mesh, input_method, boundaries): - # current tool not cached yet - if tool not in looptools_cache: - return(False, False, False, False, False) - # check if selected object didn't change - if object.name != looptools_cache[tool]["object"]: - return(False, False, False, False, False) - # check if input didn't change - if input_method != looptools_cache[tool]["input_method"]: - return(False, False, False, False, False) - if boundaries != looptools_cache[tool]["boundaries"]: - return(False, False, False, False, False) - modifiers = [mod.name for mod in object.modifiers if mod.show_viewport \ - and mod.type == 'MIRROR'] - if modifiers != looptools_cache[tool]["modifiers"]: - return(False, False, False, False, False) - input = [v.index for v in mesh.vertices if v.select and not v.hide] - if input != looptools_cache[tool]["input"]: - return(False, False, False, False, False) - # reading values - single_loops = looptools_cache[tool]["single_loops"] - loops = looptools_cache[tool]["loops"] - derived = looptools_cache[tool]["derived"] - mapping = looptools_cache[tool]["mapping"] - - return(True, single_loops, loops, derived, mapping) - - -# store information in the cache -def cache_write(tool, object, mesh, input_method, boundaries, single_loops, -loops, derived, mapping): - # clear cache of current tool - if tool in looptools_cache: - del looptools_cache[tool] - # prepare values to be saved to cache - input = [v.index for v in mesh.vertices if v.select and not v.hide] - modifiers = [mod.name for mod in object.modifiers if mod.show_viewport \ - and mod.type == 'MIRROR'] - # update cache - looptools_cache[tool] = {"input": input, "object": object.name, - "input_method": input_method, "boundaries": boundaries, - "single_loops": single_loops, "loops": loops, - "derived": derived, "mapping": mapping, "modifiers": modifiers} - - -# calculates natural cubic splines through all given knots -def calculate_cubic_splines(mesh_mod, tknots, knots): - # hack for circular loops - if knots[0] == knots[-1] and len(knots) > 1: - circular = True - k_new1 = [] - for k in range(-1, -5, -1): - if k - 1 < -len(knots): - k += len(knots) - k_new1.append(knots[k-1]) - k_new2 = [] - for k in range(4): - if k + 1 > len(knots) - 1: - k -= len(knots) - k_new2.append(knots[k+1]) - for k in k_new1: - knots.insert(0, k) - for k in k_new2: - knots.append(k) - t_new1 = [] - total1 = 0 - for t in range(-1, -5, -1): - if t - 1 < -len(tknots): - t += len(tknots) - total1 += tknots[t] - tknots[t-1] - t_new1.append(tknots[0] - total1) - t_new2 = [] - total2 = 0 - for t in range(4): - if t + 1 > len(tknots) - 1: - t -= len(tknots) - total2 += tknots[t+1] - tknots[t] - t_new2.append(tknots[-1] + total2) - for t in t_new1: - tknots.insert(0, t) - for t in t_new2: - tknots.append(t) - else: - circular = False - # end of hack - - n = len(knots) - if n < 2: - return False - x = tknots[:] - locs = [mesh_mod.vertices[k].co[:] for k in knots] - result = [] - for j in range(3): - a = [] - for i in locs: - a.append(i[j]) - h = [] - for i in range(n-1): - if x[i+1] - x[i] == 0: - h.append(1e-8) - else: - h.append(x[i+1] - x[i]) - q = [False] - for i in range(1, n-1): - q.append(3/h[i]*(a[i+1]-a[i]) - 3/h[i-1]*(a[i]-a[i-1])) - l = [1.0] - u = [0.0] - z = [0.0] - for i in range(1, n-1): - l.append(2*(x[i+1]-x[i-1]) - h[i-1]*u[i-1]) - if l[i] == 0: - l[i] = 1e-8 - u.append(h[i] / l[i]) - z.append((q[i] - h[i-1] * z[i-1]) / l[i]) - l.append(1.0) - z.append(0.0) - b = [False for i in range(n-1)] - c = [False for i in range(n)] - d = [False for i in range(n-1)] - c[n-1] = 0.0 - for i in range(n-2, -1, -1): - c[i] = z[i] - u[i]*c[i+1] - b[i] = (a[i+1]-a[i])/h[i] - h[i]*(c[i+1]+2*c[i])/3 - d[i] = (c[i+1]-c[i]) / (3*h[i]) - for i in range(n-1): - result.append([a[i], b[i], c[i], d[i], x[i]]) - splines = [] - for i in range(len(knots)-1): - splines.append([result[i], result[i+n-1], result[i+(n-1)*2]]) - if circular: # cleaning up after hack - knots = knots[4:-4] - tknots = tknots[4:-4] - - return(splines) - - -# calculates linear splines through all given knots -def calculate_linear_splines(mesh_mod, tknots, knots): - splines = [] - for i in range(len(knots)-1): - a = mesh_mod.vertices[knots[i]].co - b = mesh_mod.vertices[knots[i+1]].co - d = b-a - t = tknots[i] - u = tknots[i+1]-t - splines.append([a, d, t, u]) # [locStart, locDif, tStart, tDif] - - return(splines) - - -# calculate a best-fit plane to the given vertices -def calculate_plane(mesh_mod, loop, method="best_fit", object=False): - # getting the vertex locations - locs = [mesh_mod.vertices[v].co.copy() for v in loop[0]] - - # calculating the center of masss - com = mathutils.Vector() - for loc in locs: - com += loc - com /= len(locs) - x, y, z = com - - if method == 'best_fit': - # creating the covariance matrix - mat = mathutils.Matrix(((0.0, 0.0, 0.0), - (0.0, 0.0, 0.0), - (0.0, 0.0, 0.0), - )) - for loc in locs: - mat[0][0] += (loc[0]-x)**2 - mat[1][0] += (loc[0]-x)*(loc[1]-y) - mat[2][0] += (loc[0]-x)*(loc[2]-z) - mat[0][1] += (loc[1]-y)*(loc[0]-x) - mat[1][1] += (loc[1]-y)**2 - mat[2][1] += (loc[1]-y)*(loc[2]-z) - mat[0][2] += (loc[2]-z)*(loc[0]-x) - mat[1][2] += (loc[2]-z)*(loc[1]-y) - mat[2][2] += (loc[2]-z)**2 - - # calculating the normal to the plane - normal = False - try: - mat.invert() - except: - if sum(mat[0]) == 0.0: - normal = mathutils.Vector((1.0, 0.0, 0.0)) - elif sum(mat[1]) == 0.0: - normal = mathutils.Vector((0.0, 1.0, 0.0)) - elif sum(mat[2]) == 0.0: - normal = mathutils.Vector((0.0, 0.0, 1.0)) - if not normal: - # warning! this is different from .normalize() - itermax = 500 - iter = 0 - vec = mathutils.Vector((1.0, 1.0, 1.0)) - vec2 = (mat * vec)/(mat * vec).length - while vec != vec2 and iter<itermax: - iter+=1 - vec = vec2 - vec2 = mat * vec - if vec2.length != 0: - vec2 /= vec2.length - if vec2.length == 0: - vec2 = mathutils.Vector((1.0, 1.0, 1.0)) - normal = vec2 - - elif method == 'normal': - # averaging the vertex normals - v_normals = [mesh_mod.vertices[v].normal for v in loop[0]] - normal = mathutils.Vector() - for v_normal in v_normals: - normal += v_normal - normal /= len(v_normals) - normal.normalize() - - elif method == 'view': - # calculate view normal - rotation = bpy.context.space_data.region_3d.view_matrix.to_3x3().\ - inverted() - normal = rotation * mathutils.Vector((0.0, 0.0, 1.0)) - if object: - normal = object.matrix_world.inverted().to_euler().to_matrix() * \ - normal - - return(com, normal) - - -# calculate splines based on given interpolation method (controller function) -def calculate_splines(interpolation, mesh_mod, tknots, knots): - if interpolation == 'cubic': - splines = calculate_cubic_splines(mesh_mod, tknots, knots[:]) - else: # interpolations == 'linear' - splines = calculate_linear_splines(mesh_mod, tknots, knots[:]) - - return(splines) - - -# check loops and only return valid ones -def check_loops(loops, mapping, mesh_mod): - valid_loops = [] - for loop, circular in loops: - # loop needs to have at least 3 vertices - if len(loop) < 3: - continue - # loop needs at least 1 vertex in the original, non-mirrored mesh - if mapping: - all_virtual = True - for vert in loop: - if mapping[vert] > -1: - all_virtual = False - break - if all_virtual: - continue - # vertices can not all be at the same location - stacked = True - for i in range(len(loop) - 1): - if (mesh_mod.vertices[loop[i]].co - \ - mesh_mod.vertices[loop[i+1]].co).length > 1e-6: - stacked = False - break - if stacked: - continue - # passed all tests, loop is valid - valid_loops.append([loop, circular]) - - return(valid_loops) - - -# input: mesh, output: dict with the edge-key as key and face-index as value -def dict_edge_faces(mesh): - edge_faces = dict([[edge.key, []] for edge in mesh.edges if not edge.hide]) - for face in mesh.faces: - if face.hide: - continue - for key in face.edge_keys: - edge_faces[key].append(face.index) - - return(edge_faces) - -# input: mesh (edge-faces optional), output: dict with face-face connections -def dict_face_faces(mesh, edge_faces=False): - if not edge_faces: - edge_faces = dict_edge_faces(mesh) - - connected_faces = dict([[face.index, []] for face in mesh.faces if \ - not face.hide]) - for face in mesh.faces: - if face.hide: - continue - for edge_key in face.edge_keys: - for connected_face in edge_faces[edge_key]: - if connected_face == face.index: - continue - connected_faces[face.index].append(connected_face) - - return(connected_faces) - - -# input: mesh, output: dict with the vert index as key and edge-keys as value -def dict_vert_edges(mesh): - vert_edges = dict([[v.index, []] for v in mesh.vertices if not v.hide]) - for edge in mesh.edges: - if edge.hide: - continue - for vert in edge.key: - vert_edges[vert].append(edge.key) - - return(vert_edges) - - -# input: mesh, output: dict with the vert index as key and face index as value -def dict_vert_faces(mesh): - vert_faces = dict([[v.index, []] for v in mesh.vertices if not v.hide]) - for face in mesh.faces: - if not face.hide: - for vert in face.vertices: - vert_faces[vert].append(face.index) - - return(vert_faces) - - -# input: list of edge-keys, output: dictionary with vertex-vertex connections -def dict_vert_verts(edge_keys): - # create connection data - vert_verts = {} - for ek in edge_keys: - for i in range(2): - if ek[i] in vert_verts: - vert_verts[ek[i]].append(ek[1-i]) - else: - vert_verts[ek[i]] = [ek[1-i]] - - return(vert_verts) - - -# calculate input loops -def get_connected_input(object, mesh, scene, input): - # get mesh with modifiers applied - derived, mesh_mod = get_derived_mesh(object, mesh, scene) - - # calculate selected loops - edge_keys = [edge.key for edge in mesh_mod.edges if \ - edge.select and not edge.hide] - loops = get_connected_selections(edge_keys) - - # if only selected loops are needed, we're done - if input == 'selected': - return(derived, mesh_mod, loops) - # elif input == 'all': - loops = get_parallel_loops(mesh_mod, loops) - - return(derived, mesh_mod, loops) - - -# sorts all edge-keys into a list of loops -def get_connected_selections(edge_keys): - # create connection data - vert_verts = dict_vert_verts(edge_keys) - - # find loops consisting of connected selected edges - loops = [] - while len(vert_verts) > 0: - loop = [iter(vert_verts.keys()).__next__()] - growing = True - flipped = False - - # extend loop - while growing: - # no more connection data for current vertex - if loop[-1] not in vert_verts: - if not flipped: - loop.reverse() - flipped = True - else: - growing = False - else: - extended = False - for i, next_vert in enumerate(vert_verts[loop[-1]]): - if next_vert not in loop: - vert_verts[loop[-1]].pop(i) - if len(vert_verts[loop[-1]]) == 0: - del vert_verts[loop[-1]] - # remove connection both ways - if next_vert in vert_verts: - if len(vert_verts[next_vert]) == 1: - del vert_verts[next_vert] - else: - vert_verts[next_vert].remove(loop[-1]) - loop.append(next_vert) - extended = True - break - if not extended: - # found one end of the loop, continue with next - if not flipped: - loop.reverse() - flipped = True - # found both ends of the loop, stop growing - else: - growing = False - - # check if loop is circular - if loop[0] in vert_verts: - if loop[-1] in vert_verts[loop[0]]: - # is circular - if len(vert_verts[loop[0]]) == 1: - del vert_verts[loop[0]] - else: - vert_verts[loop[0]].remove(loop[-1]) - if len(vert_verts[loop[-1]]) == 1: - del vert_verts[loop[-1]] - else: - vert_verts[loop[-1]].remove(loop[0]) - loop = [loop, True] - else: - # not circular - loop = [loop, False] - else: - # not circular - loop = [loop, False] - - loops.append(loop) - - return(loops) - - -# get the derived mesh data, if there is a mirror modifier -def get_derived_mesh(object, mesh, scene): - # check for mirror modifiers - if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]: - derived = True - # disable other modifiers - show_viewport = [mod.name for mod in object.modifiers if \ - mod.show_viewport] - for mod in object.modifiers: - if mod.type != 'MIRROR': - mod.show_viewport = False - # get derived mesh - mesh_mod = object.to_mesh(scene, True, 'PREVIEW') - # re-enable other modifiers - for mod_name in show_viewport: - object.modifiers[mod_name].show_viewport = True - # no mirror modifiers, so no derived mesh necessary - else: - derived = False - mesh_mod = mesh - - return(derived, mesh_mod) - - -# return a mapping of derived indices to indices -def get_mapping(derived, mesh, mesh_mod, single_vertices, full_search, loops): - if not derived: - return(False) - - if full_search: - verts = [v for v in mesh.vertices if not v.hide] - else: - verts = [v for v in mesh.vertices if v.select and not v.hide] - - # non-selected vertices around single vertices also need to be mapped - if single_vertices: - mapping = dict([[vert, -1] for vert in single_vertices]) - verts_mod = [mesh_mod.vertices[vert] for vert in single_vertices] - for v in verts: - for v_mod in verts_mod: - if (v.co - v_mod.co).length < 1e-6: - mapping[v_mod.index] = v.index - break - real_singles = [v_real for v_real in mapping.values() if v_real>-1] - - verts_indices = [vert.index for vert in verts] - for face in [face for face in mesh.faces if not face.select \ - and not face.hide]: - for vert in face.vertices: - if vert in real_singles: - for v in face.vertices: - if not v in verts_indices: - if mesh.vertices[v] not in verts: - verts.append(mesh.vertices[v]) - break - - # create mapping of derived indices to indices - mapping = dict([[vert, -1] for loop in loops for vert in loop[0]]) - if single_vertices: - for single in single_vertices: - mapping[single] = -1 - verts_mod = [mesh_mod.vertices[i] for i in mapping.keys()] - for v in verts: - for v_mod in verts_mod: - if (v.co - v_mod.co).length < 1e-6: - mapping[v_mod.index] = v.index - verts_mod.remove(v_mod) - break - - return(mapping) - - -# returns a list of all loops parallel to the input, input included -def get_parallel_loops(mesh_mod, loops): - # get required dictionaries - edge_faces = dict_edge_faces(mesh_mod) - connected_faces = dict_face_faces(mesh_mod, edge_faces) - # turn vertex loops into edge loops - edgeloops = [] - for loop in loops: - edgeloop = [[sorted([loop[0][i], loop[0][i+1]]) for i in \ - range(len(loop[0])-1)], loop[1]] - if loop[1]: # circular - edgeloop[0].append(sorted([loop[0][-1], loop[0][0]])) - edgeloops.append(edgeloop[:]) - # variables to keep track while iterating - all_edgeloops = [] - has_branches = False - - for loop in edgeloops: - # initialise with original loop - all_edgeloops.append(loop[0]) - newloops = [loop[0]] - verts_used = [] - for edge in loop[0]: - if edge[0] not in verts_used: - verts_used.append(edge[0]) - if edge[1] not in verts_used: - verts_used.append(edge[1]) - - # find parallel loops - while len(newloops) > 0: - side_a = [] - side_b = [] - for i in newloops[-1]: - i = tuple(i) - forbidden_side = False - if not i in edge_faces: - # weird input with branches - has_branches = True - break - for face in edge_faces[i]: - if len(side_a) == 0 and forbidden_side != "a": - side_a.append(face) - if forbidden_side: - break - forbidden_side = "a" - continue - elif side_a[-1] in connected_faces[face] and \ - forbidden_side != "a": - side_a.append(face) - if forbidden_side: - break - forbidden_side = "a" - continue - if len(side_b) == 0 and forbidden_side != "b": - side_b.append(face) - if forbidden_side: - break - forbidden_side = "b" - continue - elif side_b[-1] in connected_faces[face] and \ - forbidden_side != "b": - side_b.append(face) - if forbidden_side: - break - forbidden_side = "b" - continue - - if has_branches: - # weird input with branches - break - - newloops.pop(-1) - sides = [] - if side_a: - sides.append(side_a) - if side_b: - sides.append(side_b) - - for side in sides: - extraloop = [] - for fi in side: - for key in mesh_mod.faces[fi].edge_keys: - if key[0] not in verts_used and key[1] not in \ - verts_used: - extraloop.append(key) - break - if extraloop: - for key in extraloop: - for new_vert in key: - if new_vert not in verts_used: - verts_used.append(new_vert) - newloops.append(extraloop) - all_edgeloops.append(extraloop) - - # input contains branches, only return selected loop - if has_branches: - return(loops) - - # change edgeloops into normal loops - loops = [] - for edgeloop in all_edgeloops: - loop = [] - # grow loop by comparing vertices between consecutive edge-keys - for i in range(len(edgeloop)-1): - for vert in range(2): - if edgeloop[i][vert] in edgeloop[i+1]: - loop.append(edgeloop[i][vert]) - break - if loop: - # add starting vertex - for vert in range(2): - if edgeloop[0][vert] != loop[0]: - loop = [edgeloop[0][vert]] + loop - break - # add ending vertex - for vert in range(2): - if edgeloop[-1][vert] != loop[-1]: - loop.append(edgeloop[-1][vert]) - break - # check if loop is circular - if loop[0] == loop[-1]: - circular = True - loop = loop[:-1] - else: - circular = False - loops.append([loop, circular]) - - return(loops) - - -# gather initial data -def initialise(): - global_undo = bpy.context.user_preferences.edit.use_global_undo - bpy.context.user_preferences.edit.use_global_undo = False - bpy.ops.object.mode_set(mode='OBJECT') - object = bpy.context.active_object - mesh = bpy.context.active_object.data - - return(global_undo, object, mesh) - - -# move the vertices to their new locations -def move_verts(mesh, mapping, move, influence): - for loop in move: - for index, loc in loop: - if mapping: - if mapping[index] == -1: - continue - else: - index = mapping[index] - if influence >= 0: - mesh.vertices[index].co = loc*(influence/100) + \ - mesh.vertices[index].co*((100-influence)/100) - else: - mesh.vertices[index].co = loc - - -# load custom tool settings -def settings_load(self): - lt = bpy.context.window_manager.looptools - tool = self.name.split()[0].lower() - keys = self.as_keywords().keys() - for key in keys: - setattr(self, key, getattr(lt, tool + "_" + key)) - - -# store custom tool settings -def settings_write(self): - lt = bpy.context.window_manager.looptools - tool = self.name.split()[0].lower() - keys = self.as_keywords().keys() - for key in keys: - setattr(lt, tool + "_" + key, getattr(self, key)) - - -# clean up and set settings back to original state -def terminate(global_undo): - bpy.ops.object.mode_set(mode='EDIT') - bpy.context.user_preferences.edit.use_global_undo = global_undo - - -########################################## -####### Bridge functions ################# -########################################## - -# calculate a cubic spline through the middle section of 4 given coordinates -def bridge_calculate_cubic_spline(mesh, coordinates): - result = [] - x = [0, 1, 2, 3] - - for j in range(3): - a = [] - for i in coordinates: - a.append(float(i[j])) - h = [] - for i in range(3): - h.append(x[i+1]-x[i]) - q = [False] - for i in range(1,3): - q.append(3.0/h[i]*(a[i+1]-a[i])-3.0/h[i-1]*(a[i]-a[i-1])) - l = [1.0] - u = [0.0] - z = [0.0] - for i in range(1,3): - l.append(2.0*(x[i+1]-x[i-1])-h[i-1]*u[i-1]) - u.append(h[i]/l[i]) - z.append((q[i]-h[i-1]*z[i-1])/l[i]) - l.append(1.0) - z.append(0.0) - b = [False for i in range(3)] - c = [False for i in range(4)] - d = [False for i in range(3)] - c[3] = 0.0 - for i in range(2,-1,-1): - c[i] = z[i]-u[i]*c[i+1] - b[i] = (a[i+1]-a[i])/h[i]-h[i]*(c[i+1]+2.0*c[i])/3.0 - d[i] = (c[i+1]-c[i])/(3.0*h[i]) - for i in range(3): - result.append([a[i], b[i], c[i], d[i], x[i]]) - spline = [result[1], result[4], result[7]] - - return(spline) - - -# return a list with new vertex location vectors, a list with face vertex -# integers, and the highest vertex integer in the virtual mesh -def bridge_calculate_geometry(mesh, lines, vertex_normals, segments, -interpolation, cubic_strength, min_width, max_vert_index): - new_verts = [] - faces = [] - - # calculate location based on interpolation method - def get_location(line, segment, splines): - v1 = mesh.vertices[lines[line][0]].co - v2 = mesh.vertices[lines[line][1]].co - if interpolation == 'linear': - return v1 + (segment/segments) * (v2-v1) - else: # interpolation == 'cubic' - m = (segment/segments) - ax,bx,cx,dx,tx = splines[line][0] - x = ax+bx*m+cx*m**2+dx*m**3 - ay,by,cy,dy,ty = splines[line][1] - y = ay+by*m+cy*m**2+dy*m**3 - az,bz,cz,dz,tz = splines[line][2] - z = az+bz*m+cz*m**2+dz*m**3 - return mathutils.Vector((x, y, z)) - - # no interpolation needed - if segments == 1: - for i, line in enumerate(lines): - if i < len(lines)-1: - faces.append([line[0], lines[i+1][0], lines[i+1][1], line[1]]) - # more than 1 segment, interpolate - else: - # calculate splines (if necessary) once, so no recalculations needed - if interpolation == 'cubic': - splines = [] - for line in lines: - v1 = mesh.vertices[line[0]].co - v2 = mesh.vertices[line[1]].co - size = (v2-v1).length * cubic_strength - splines.append(bridge_calculate_cubic_spline(mesh, - [v1+size*vertex_normals[line[0]], v1, v2, - v2+size*vertex_normals[line[1]]])) - else: - splines = False - - # create starting situation - virtual_width = [(mesh.vertices[lines[i][0]].co - - mesh.vertices[lines[i+1][0]].co).length for i - in range(len(lines)-1)] - new_verts = [get_location(0, seg, splines) for seg in range(1, - segments)] - first_line_indices = [i for i in range(max_vert_index+1, - max_vert_index+segments)] - - prev_verts = new_verts[:] # vertex locations of verts on previous line - prev_vert_indices = first_line_indices[:] - max_vert_index += segments - 1 # highest vertex index in virtual mesh - next_verts = [] # vertex locations of verts on current line - next_vert_indices = [] - - for i, line in enumerate(lines): - if i < len(lines)-1: - v1 = line[0] - v2 = lines[i+1][0] - end_face = True - for seg in range(1, segments): - loc1 = prev_verts[seg-1] - loc2 = get_location(i+1, seg, splines) - if (loc1-loc2).length < (min_width/100)*virtual_width[i] \ - and line[1]==lines[i+1][1]: - # triangle, no new vertex - faces.append([v1, v2, prev_vert_indices[seg-1], - prev_vert_indices[seg-1]]) - next_verts += prev_verts[seg-1:] - next_vert_indices += prev_vert_indices[seg-1:] - end_face = False - break - else: - if i == len(lines)-2 and lines[0] == lines[-1]: - # quad with first line, no new vertex - faces.append([v1, v2, first_line_indices[seg-1], - prev_vert_indices[seg-1]]) - v2 = first_line_indices[seg-1] - v1 = prev_vert_indices[seg-1] - else: - # quad, add new vertex - max_vert_index += 1 - faces.append([v1, v2, max_vert_index, - prev_vert_indices[seg-1]]) - v2 = max_vert_index - v1 = prev_vert_indices[seg-1] - new_verts.append(loc2) - next_verts.append(loc2) - next_vert_indices.append(max_vert_index) - if end_face: - faces.append([v1, v2, lines[i+1][1], line[1]]) - - prev_verts = next_verts[:] - prev_vert_indices = next_vert_indices[:] - next_verts = [] - next_vert_indices = [] - - return(new_verts, faces, max_vert_index) - - -# calculate lines (list of lists, vertex indices) that are used for bridging -def bridge_calculate_lines(mesh, loops, mode, twist, reverse): - lines = [] - loop1, loop2 = [i[0] for i in loops] - loop1_circular, loop2_circular = [i[1] for i in loops] - circular = loop1_circular or loop2_circular - circle_full = False - - # calculate loop centers - centers = [] - for loop in [loop1, loop2]: - center = mathutils.Vector() - for vertex in loop: - center += mesh.vertices[vertex].co - center /= len(loop) - centers.append(center) - for i, loop in enumerate([loop1, loop2]): - for vertex in loop: - if mesh.vertices[vertex].co == centers[i]: - # prevent zero-length vectors in angle comparisons - centers[i] += mathutils.Vector((0.01, 0, 0)) - break - center1, center2 = centers - - # calculate the normals of the virtual planes that the loops are on - normals = [] - normal_plurity = False - for i, loop in enumerate([loop1, loop2]): - # covariance matrix - mat = mathutils.Matrix(((0.0, 0.0, 0.0), - (0.0, 0.0, 0.0), - (0.0, 0.0, 0.0))) - x, y, z = centers[i] - for loc in [mesh.vertices[vertex].co for vertex in loop]: - mat[0][0] += (loc[0]-x)**2 - mat[1][0] += (loc[0]-x)*(loc[1]-y) - mat[2][0] += (loc[0]-x)*(loc[2]-z) - mat[0][1] += (loc[1]-y)*(loc[0]-x) - mat[1][1] += (loc[1]-y)**2 - mat[2][1] += (loc[1]-y)*(loc[2]-z) - mat[0][2] += (loc[2]-z)*(loc[0]-x) - mat[1][2] += (loc[2]-z)*(loc[1]-y) - mat[2][2] += (loc[2]-z)**2 - # plane normal - normal = False - if sum(mat[0]) < 1e-6 or sum(mat[1]) < 1e-6 or sum(mat[2]) < 1e-6: - normal_plurity = True - try: - mat.invert() - except: - if sum(mat[0]) == 0: - normal = mathutils.Vector((1.0, 0.0, 0.0)) - elif sum(mat[1]) == 0: - normal = mathutils.Vector((0.0, 1.0, 0.0)) - elif sum(mat[2]) == 0: - normal = mathutils.Vector((0.0, 0.0, 1.0)) - if not normal: - # warning! this is different from .normalize() - itermax = 500 - iter = 0 - vec = mathutils.Vector((1.0, 1.0, 1.0)) - vec2 = (mat * vec)/(mat * vec).length - while vec != vec2 and iter<itermax: - iter+=1 - vec = vec2 - vec2 = mat * vec - if vec2.length != 0: - vec2 /= vec2.length - if vec2.length == 0: - vec2 = mathutils.Vector((1.0, 1.0, 1.0)) - normal = vec2 - normals.append(normal) - # have plane normals face in the same direction (maximum angle: 90 degrees) - if ((center1 + normals[0]) - center2).length < \ - ((center1 - normals[0]) - center2).length: - normals[0].negate() - if ((center2 + normals[1]) - center1).length > \ - ((center2 - normals[1]) - center1).length: - normals[1].negate() - - # rotation matrix, representing the difference between the plane normals - axis = normals[0].cross(normals[1]) - axis = mathutils.Vector([loc if abs(loc) > 1e-8 else 0 for loc in axis]) - if axis.angle(mathutils.Vector((0, 0, 1)), 0) > 1.5707964: - axis.negate() - angle = normals[0].dot(normals[1]) - rotation_matrix = mathutils.Matrix.Rotation(angle, 4, axis) - - # if circular, rotate loops so they are aligned - if circular: - # make sure loop1 is the circular one (or both are circular) - if loop2_circular and not loop1_circular: - loop1_circular, loop2_circular = True, False - loop1, loop2 = loop2, loop1 - - # match start vertex of loop1 with loop2 - target_vector = mesh.vertices[loop2[0]].co - center2 - dif_angles = [[(rotation_matrix * (mesh.vertices[vertex].co - center1) - ).angle(target_vector, 0), False, i] for - i, vertex in enumerate(loop1)] - dif_angles.sort() - if len(loop1) != len(loop2): - angle_limit = dif_angles[0][0] * 1.2 # 20% margin - dif_angles = [[(mesh.vertices[loop2[0]].co - \ - mesh.vertices[loop1[index]].co).length, angle, index] for \ - angle, distance, index in dif_angles if angle <= angle_limit] - dif_angles.sort() - loop1 = loop1[dif_angles[0][2]:] + loop1[:dif_angles[0][2]] - - # have both loops face the same way - if normal_plurity and not circular: - second_to_first, second_to_second, second_to_last = \ - [(mesh.vertices[loop1[1]].co - center1).\ - angle(mesh.vertices[loop2[i]].co - center2) for i in [0, 1, -1]] - last_to_first, last_to_second = [(mesh.vertices[loop1[-1]].co - \ - center1).angle(mesh.vertices[loop2[i]].co - center2) for \ - i in [0, 1]] - if (min(last_to_first, last_to_second)*1.1 < min(second_to_first, \ - second_to_second)) or (loop2_circular and second_to_last*1.1 < \ - min(second_to_first, second_to_second)): - loop1.reverse() - if circular: - loop1 = [loop1[-1]] + loop1[:-1] - else: - angle = (mesh.vertices[loop1[0]].co - center1).\ - cross(mesh.vertices[loop1[1]].co - center1).angle(normals[0], 0) - target_angle = (mesh.vertices[loop2[0]].co - center2).\ - cross(mesh.vertices[loop2[1]].co - center2).angle(normals[1], 0) - limit = 1.5707964 # 0.5*pi, 90 degrees - if not ((angle > limit and target_angle > limit) or \ - (angle < limit and target_angle < limit)): - loop1.reverse() - if circular: - loop1 = [loop1[-1]] + loop1[:-1] - elif normals[0].angle(normals[1]) > limit: - loop1.reverse() - if circular: - loop1 = [loop1[-1]] + loop1[:-1] - - # both loops have the same length - if len(loop1) == len(loop2): - # manual override - if twist: - if abs(twist) < len(loop1): - loop1 = loop1[twist:]+loop1[:twist] - if reverse: - loop1.reverse() - - lines.append([loop1[0], loop2[0]]) - for i in range(1, len(loop1)): - lines.append([loop1[i], loop2[i]]) - - # loops of different lengths - else: - # make loop1 longest loop - if len(loop2) > len(loop1): - loop1, loop2 = loop2, loop1 - loop1_circular, loop2_circular = loop2_circular, loop1_circular - - # manual override - if twist: - if abs(twist) < len(loop1): - loop1 = loop1[twist:]+loop1[:twist] - if reverse: - loop1.reverse() - - # shortest angle difference doesn't always give correct start vertex - if loop1_circular and not loop2_circular: - shifting = 1 - while shifting: - if len(loop1) - shifting < len(loop2): - shifting = False - break - to_last, to_first = [(rotation_matrix * - (mesh.vertices[loop1[-1]].co - center1)).angle((mesh.\ - vertices[loop2[i]].co - center2), 0) for i in [-1, 0]] - if to_first < to_last: - loop1 = [loop1[-1]] + loop1[:-1] - shifting += 1 - else: - shifting = False - break - - # basic shortest side first - if mode == 'basic': - lines.append([loop1[0], loop2[0]]) - for i in range(1, len(loop1)): - if i >= len(loop2) - 1: - # triangles - lines.append([loop1[i], loop2[-1]]) - else: - # quads - lines.append([loop1[i], loop2[i]]) - - # shortest edge algorithm - else: # mode == 'shortest' - lines.append([loop1[0], loop2[0]]) - prev_vert2 = 0 - for i in range(len(loop1) -1): - if prev_vert2 == len(loop2) - 1 and not loop2_circular: - # force triangles, reached end of loop2 - tri, quad = 0, 1 - elif prev_vert2 == len(loop2) - 1 and loop2_circular: - # at end of loop2, but circular, so check with first vert - tri, quad = [(mesh.vertices[loop1[i+1]].co - - mesh.vertices[loop2[j]].co).length - for j in [prev_vert2, 0]] - - circle_full = 2 - elif len(loop1) - 1 - i == len(loop2) - 1 - prev_vert2 and \ - not circle_full: - # force quads, otherwise won't make it to end of loop2 - tri, quad = 1, 0 - else: - # calculate if tri or quad gives shortest edge - tri, quad = [(mesh.vertices[loop1[i+1]].co - - mesh.vertices[loop2[j]].co).length - for j in range(prev_vert2, prev_vert2+2)] - - # triangle - if tri < quad: - lines.append([loop1[i+1], loop2[prev_vert2]]) - if circle_full == 2: - circle_full = False - # quad - elif not circle_full: - lines.append([loop1[i+1], loop2[prev_vert2+1]]) - prev_vert2 += 1 - # quad to first vertex of loop2 - else: - lines.append([loop1[i+1], loop2[0]]) - prev_vert2 = 0 - circle_full = True - - # final face for circular loops - if loop1_circular and loop2_circular: - lines.append([loop1[0], loop2[0]]) - - return(lines) - - -# calculate number of segments needed -def bridge_calculate_segments(mesh, lines, loops, segments): - # return if amount of segments is set by user - if segments != 0: - return segments - - # edge lengths - average_edge_length = [(mesh.vertices[vertex].co - \ - mesh.vertices[loop[0][i+1]].co).length for loop in loops for \ - i, vertex in enumerate(loop[0][:-1])] - # closing edges of circular loops - average_edge_length += [(mesh.vertices[loop[0][-1]].co - \ - mesh.vertices[loop[0][0]].co).length for loop in loops if loop[1]] - - # average lengths - average_edge_length = sum(average_edge_length) / len(average_edge_length) - average_bridge_length = sum([(mesh.vertices[v1].co - \ - mesh.vertices[v2].co).length for v1, v2 in lines]) / len(lines) - - segments = max(1, round(average_bridge_length / average_edge_length)) - - return(segments) - - -# return dictionary with vertex index as key, and the normal vector as value -def bridge_calculate_virtual_vertex_normals(mesh, lines, loops, edge_faces, -edgekey_to_edge): - if not edge_faces: # interpolation isn't set to cubic - return False - - # pity reduce() isn't one of the basic functions in python anymore - def average_vector_dictionary(dic): - for key, vectors in dic.items(): - #if type(vectors) == type([]) and len(vectors) > 1: - if len(vectors) > 1: - average = mathutils.Vector() - for vector in vectors: - average += vector - average /= len(vectors) - dic[key] = [average] - return dic - - # get all edges of the loop - edges = [[edgekey_to_edge[tuple(sorted([loops[j][0][i], - loops[j][0][i+1]]))] for i in range(len(loops[j][0])-1)] for \ - j in [0,1]] - edges = edges[0] + edges[1] - for j in [0, 1]: - if loops[j][1]: # circular - edges.append(edgekey_to_edge[tuple(sorted([loops[j][0][0], - loops[j][0][-1]]))]) - - """ - calculation based on face topology (assign edge-normals to vertices) - - edge_normal = face_normal x edge_vector - vertex_normal = average(edge_normals) - """ - vertex_normals = dict([(vertex, []) for vertex in loops[0][0]+loops[1][0]]) - for edge in edges: - faces = edge_faces[edge.key] # valid faces connected to edge - - if faces: - # get edge coordinates - v1, v2 = [mesh.vertices[edge.key[i]].co for i in [0,1]] - edge_vector = v1 - v2 - if edge_vector.length < 1e-4: - # zero-length edge, vertices at same location - continue - edge_center = (v1 + v2) / 2 - - # average face coordinates, if connected to more than 1 valid face - if len(faces) > 1: - face_normal = mathutils.Vector() - face_center = mathutils.Vector() - for face in faces: - face_normal += face.normal - face_center += face.center - face_normal /= len(faces) - face_center /= len(faces) - else: - face_normal = faces[0].normal - face_center = faces[0].center - if face_normal.length < 1e-4: - # faces with a surface of 0 have no face normal - continue - - # calculate virtual edge normal - edge_normal = edge_vector.cross(face_normal) - edge_normal.length = 0.01 - if (face_center - (edge_center + edge_normal)).length > \ - (face_center - (edge_center - edge_normal)).length: - # make normal face the correct way - edge_normal.negate() - edge_normal.normalize() - # add virtual edge normal as entry for both vertices it connects - for vertex in edge.key: - vertex_normals[vertex].append(edge_normal) - - """ - calculation based on connection with other loop (vertex focused method) - - used for vertices that aren't connected to any valid faces - - plane_normal = edge_vector x connection_vector - vertex_normal = plane_normal x edge_vector - """ - vertices = [vertex for vertex, normal in vertex_normals.items() if not \ - normal] - - if vertices: - # edge vectors connected to vertices - edge_vectors = dict([[vertex, []] for vertex in vertices]) - for edge in edges: - for v in edge.key: - if v in edge_vectors: - edge_vector = mesh.vertices[edge.key[0]].co - \ - mesh.vertices[edge.key[1]].co - if edge_vector.length < 1e-4: - # zero-length edge, vertices at same location - continue - edge_vectors[v].append(edge_vector) - - # connection vectors between vertices of both loops - connection_vectors = dict([[vertex, []] for vertex in vertices]) - connections = dict([[vertex, []] for vertex in vertices]) - for v1, v2 in lines: - if v1 in connection_vectors or v2 in connection_vectors: - new_vector = mesh.vertices[v1].co - mesh.vertices[v2].co - if new_vector.length < 1e-4: - # zero-length connection vector, - # vertices in different loops at same location - continue - if v1 in connection_vectors: - connection_vectors[v1].append(new_vector) - connections[v1].append(v2) - if v2 in connection_vectors: - connection_vectors[v2].append(new_vector) - connections[v2].append(v1) - connection_vectors = average_vector_dictionary(connection_vectors) - connection_vectors = dict([[vertex, vector[0]] if vector else \ - [vertex, []] for vertex, vector in connection_vectors.items()]) - - for vertex, values in edge_vectors.items(): - # vertex normal doesn't matter, just assign a random vector to it - if not connection_vectors[vertex]: - vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))] - continue - - # calculate to what location the vertex is connected, - # used to determine what way to flip the normal - connected_center = mathutils.Vector() - for v in connections[vertex]: - connected_center += mesh.vertices[v].co - if len(connections[vertex]) > 1: - connected_center /= len(connections[vertex]) - if len(connections[vertex]) == 0: - # shouldn't be possible, but better safe than sorry - vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))] - continue - - # can't do proper calculations, because of zero-length vector - if not values: - if (connected_center - (mesh.vertices[vertex].co + \ - connection_vectors[vertex])).length < (connected_center - \ - (mesh.vertices[vertex].co - connection_vectors[vertex])).\ - length: - connection_vectors[vertex].negate() - vertex_normals[vertex] = [connection_vectors[vertex].\ - normalized()] - continue - - # calculate vertex normals using edge-vectors, - # connection-vectors and the derived plane normal - for edge_vector in values: - plane_normal = edge_vector.cross(connection_vectors[vertex]) - vertex_normal = edge_vector.cross(plane_normal) - vertex_normal.length = 0.1 - if (connected_center - (mesh.vertices[vertex].co + \ - vertex_normal)).length < (connected_center - \ - (mesh.vertices[vertex].co - vertex_normal)).length: - # make normal face the correct way - vertex_normal.negate() - vertex_normal.normalize() - vertex_normals[vertex].append(vertex_normal) - - # average virtual vertex normals, based on all edges it's connected to - vertex_normals = average_vector_dictionary(vertex_normals) - vertex_normals = dict([[vertex, vector[0]] for vertex, vector in \ - vertex_normals.items()]) - - return(vertex_normals) - - -# add vertices to mesh -def bridge_create_vertices(mesh, vertices): - start_index = len(mesh.vertices) - mesh.vertices.add(len(vertices)) - for i in range(len(vertices)): - mesh.vertices[start_index + i].co = vertices[i] - - -# add faces to mesh -def bridge_create_faces(mesh, faces, twist): - # have the normal point the correct way - if twist < 0: - [face.reverse() for face in faces] - faces = [face[2:]+face[:2] if face[0]==face[1] else face for \ - face in faces] - - # eekadoodle prevention - for i in range(len(faces)): - if not faces[i][-1]: - if faces[i][0] == faces[i][-1]: - faces[i] = [faces[i][1], faces[i][2], faces[i][3], faces[i][1]] - else: - faces[i] = [faces[i][-1]] + faces[i][:-1] - - start_faces = len(mesh.faces) - mesh.faces.add(len(faces)) - for i in range(len(faces)): - mesh.faces[start_faces + i].vertices_raw = faces[i] - mesh.update(calc_edges = True) # calc_edges prevents memory-corruption - - -# calculate input loops -def bridge_get_input(mesh): - # create list of internal edges, which should be skipped - eks_of_selected_faces = [item for sublist in [face.edge_keys for face \ - in mesh.faces if face.select and not face.hide] for item in sublist] - edge_count = {} - for ek in eks_of_selected_faces: - if ek in edge_count: - edge_count[ek] += 1 - else: - edge_count[ek] = 1 - internal_edges = [ek for ek in edge_count if edge_count[ek] > 1] - - # sort correct edges into loops - selected_edges = [edge.key for edge in mesh.edges if edge.select \ - and not edge.hide and edge.key not in internal_edges] - loops = get_connected_selections(selected_edges) - - return(loops) - - -# return values needed by the bridge operator -def bridge_initialise(mesh, interpolation): - if interpolation == 'cubic': - # dict with edge-key as key and list of connected valid faces as value - face_blacklist = [face.index for face in mesh.faces if face.select or \ - face.hide] - edge_faces = dict([[edge.key, []] for edge in mesh.edges if not \ - edge.hide]) - for face in mesh.faces: - if face.index in face_blacklist: - continue - for key in face.edge_keys: - edge_faces[key].append(face) - # dictionary with the edge-key as key and edge as value - edgekey_to_edge = dict([[edge.key, edge] for edge in mesh.edges if \ - edge.select and not edge.hide]) - else: - edge_faces = False - edgekey_to_edge = False - - # selected faces input - old_selected_faces = [face.index for face in mesh.faces if face.select \ - and not face.hide] - - # find out if faces created by bridging should be smoothed - smooth = False - if mesh.faces: - if sum([face.use_smooth for face in mesh.faces])/len(mesh.faces) \ - >= 0.5: - smooth = True - - return(edge_faces, edgekey_to_edge, old_selected_faces, smooth) - - -# return a string with the input method -def bridge_input_method(loft, loft_loop): - method = "" - if loft: - if loft_loop: - method = "Loft loop" - else: - method = "Loft no-loop" - else: - method = "Bridge" - - return(method) - - -# match up loops in pairs, used for multi-input bridging -def bridge_match_loops(mesh, loops): - # calculate average loop normals and centers - normals = [] - centers = [] - for vertices, circular in loops: - normal = mathutils.Vector() - center = mathutils.Vector() - for vertex in vertices: - normal += mesh.vertices[vertex].normal - center += mesh.vertices[vertex].co - normals.append(normal / len(vertices) / 10) - centers.append(center / len(vertices)) - - # possible matches if loop normals are faced towards the center - # of the other loop - matches = dict([[i, []] for i in range(len(loops))]) - matches_amount = 0 - for i in range(len(loops) + 1): - for j in range(i+1, len(loops)): - if (centers[i] - centers[j]).length > (centers[i] - (centers[j] \ - + normals[j])).length and (centers[j] - centers[i]).length > \ - (centers[j] - (centers[i] + normals[i])).length: - matches_amount += 1 - matches[i].append([(centers[i] - centers[j]).length, i, j]) - matches[j].append([(centers[i] - centers[j]).length, j, i]) - # if no loops face each other, just make matches between all the loops - if matches_amount == 0: - for i in range(len(loops) + 1): - for j in range(i+1, len(loops)): - matches[i].append([(centers[i] - centers[j]).length, i, j]) - matches[j].append([(centers[i] - centers[j]).length, j, i]) - for key, value in matches.items(): - value.sort() - - # matches based on distance between centers and number of vertices in loops - new_order = [] - for loop_index in range(len(loops)): - if loop_index in new_order: - continue - loop_matches = matches[loop_index] - if not loop_matches: - continue - shortest_distance = loop_matches[0][0] - shortest_distance *= 1.1 - loop_matches = [[abs(len(loops[loop_index][0]) - \ - len(loops[loop[2]][0])), loop[0], loop[1], loop[2]] for loop in \ - loop_matches if loop[0] < shortest_distance] - loop_matches.sort() - for match in loop_matches: - if match[3] not in new_order: - new_order += [loop_index, match[3]] - break - - # reorder loops based on matches - if len(new_order) >= 2: - loops = [loops[i] for i in new_order] - - return(loops) - - -# have normals of selection face outside -def bridge_recalculate_normals(): - bpy.ops.object.mode_set(mode = 'EDIT') - bpy.ops.mesh.normals_make_consistent() - - -# remove old_selected_faces -def bridge_remove_internal_faces(mesh, old_selected_faces): - select_mode = [i for i in bpy.context.tool_settings.mesh_select_mode] - bpy.context.tool_settings.mesh_select_mode = [False, False, True] - - # hack to keep track of the current selection - for edge in mesh.edges: - if edge.select and not edge.hide: - edge.bevel_weight = (edge.bevel_weight/3) + 0.2 - else: - edge.bevel_weight = (edge.bevel_weight/3) + 0.6 - - # remove faces - bpy.ops.object.mode_set(mode = 'EDIT') - bpy.ops.mesh.select_all(action = 'DESELECT') - bpy.ops.object.mode_set(mode = 'OBJECT') - for face in old_selected_faces: - mesh.faces[face].select = True - bpy.ops.object.mode_set(mode = 'EDIT') - bpy.ops.mesh.delete(type = 'FACE') - - # restore old selection, using hack - bpy.ops.object.mode_set(mode = 'OBJECT') - bpy.context.tool_settings.mesh_select_mode = [False, True, False] - for edge in mesh.edges: - if edge.bevel_weight < 0.6: - edge.bevel_weight = (edge.bevel_weight-0.2) * 3 - edge.select = True - else: - edge.bevel_weight = (edge.bevel_weight-0.6) * 3 - bpy.ops.object.mode_set(mode = 'EDIT') - bpy.ops.object.mode_set(mode = 'OBJECT') - bpy.context.tool_settings.mesh_select_mode = select_mode - - -# update list of internal faces that are flagged for removal -def bridge_save_unused_faces(mesh, old_selected_faces, loops): - # key: vertex index, value: lists of selected faces using it - vertex_to_face = dict([[i, []] for i in range(len(mesh.vertices))]) - [[vertex_to_face[vertex_index].append(face) for vertex_index in \ - mesh.faces[face].vertices] for face in old_selected_faces] - - # group selected faces that are connected - groups = [] - grouped_faces = [] - for face in old_selected_faces: - if face in grouped_faces: - continue - grouped_faces.append(face) - group = [face] - new_faces = [face] - while new_faces: - grow_face = new_faces[0] - for vertex in mesh.faces[grow_face].vertices: - vertex_face_group = [face for face in vertex_to_face[vertex] \ - if face not in grouped_faces] - new_faces += vertex_face_group - grouped_faces += vertex_face_group - group += vertex_face_group - new_faces.pop(0) - groups.append(group) - - # key: vertex index, value: True/False (is it in a loop that is used) - used_vertices = dict([[i, 0] for i in range(len(mesh.vertices))]) - for loop in loops: - for vertex in loop[0]: - used_vertices[vertex] = True - - # check if group is bridged, if not remove faces from internal faces list - for group in groups: - used = False - for face in group: - if used: - break - for vertex in mesh.faces[face].vertices: - if used_vertices[vertex]: - used = True - break - if not used: - for face in group: - old_selected_faces.remove(face) - - -# add the newly created faces to the selection -def bridge_select_new_faces(mesh, amount, smooth): - select_mode = [i for i in bpy.context.tool_settings.mesh_select_mode] - bpy.context.tool_settings.mesh_select_mode = [False, False, True] - for i in range(amount): - mesh.faces[-(i+1)].select = True - mesh.faces[-(i+1)].use_smooth = smooth - bpy.ops.object.mode_set(mode = 'EDIT') - bpy.ops.object.mode_set(mode = 'OBJECT') - bpy.context.tool_settings.mesh_select_mode = select_mode - - -# sort loops, so they are connected in the correct order when lofting -def bridge_sort_loops(mesh, loops, loft_loop): - # simplify loops to single points, and prepare for pathfinding - x, y, z = [[sum([mesh.vertices[i].co[j] for i in loop[0]]) / \ - len(loop[0]) for loop in loops] for j in range(3)] - nodes = [mathutils.Vector((x[i], y[i], z[i])) for i in range(len(loops))] - - active_node = 0 - open = [i for i in range(1, len(loops))] - path = [[0,0]] - # connect node to path, that is shortest to active_node - while len(open) > 0: - distances = [(nodes[active_node] - nodes[i]).length for i in open] - active_node = open[distances.index(min(distances))] - open.remove(active_node) - path.append([active_node, min(distances)]) - # check if we didn't start in the middle of the path - for i in range(2, len(path)): - if (nodes[path[i][0]]-nodes[0]).length < path[i][1]: - temp = path[:i] - path.reverse() - path = path[:-i] + temp - break - - # reorder loops - loops = [loops[i[0]] for i in path] - # if requested, duplicate first loop at last position, so loft can loop - if loft_loop: - loops = loops + [loops[0]] - - return(loops) - - -########################################## -####### Circle functions ################# -########################################## - -# convert 3d coordinates to 2d coordinates on plane -def circle_3d_to_2d(mesh_mod, loop, com, normal): - # project vertices onto the plane - verts = [mesh_mod.vertices[v] for v in loop[0]] - verts_projected = [[v.co - (v.co - com).dot(normal) * normal, v.index] - for v in verts] - - # calculate two vectors (p and q) along the plane - m = mathutils.Vector((normal[0] + 1.0, normal[1], normal[2])) - p = m - (m.dot(normal) * normal) - if p.dot(p) == 0.0: - m = mathutils.Vector((normal[0], normal[1] + 1.0, normal[2])) - p = m - (m.dot(normal) * normal) - q = p.cross(normal) - - # change to 2d coordinates using perpendicular projection - locs_2d = [] - for loc, vert in verts_projected: - vloc = loc - com - x = p.dot(vloc) / p.dot(p) - y = q.dot(vloc) / q.dot(q) - locs_2d.append([x, y, vert]) - - return(locs_2d, p, q) - - -# calculate a best-fit circle to the 2d locations on the plane -def circle_calculate_best_fit(locs_2d): - # initial guess - x0 = 0.0 - y0 = 0.0 - r = 1.0 - - # calculate center and radius (non-linear least squares solution) - for iter in range(500): - jmat = [] - k = [] - for v in locs_2d: - d = (v[0]**2-2.0*x0*v[0]+v[1]**2-2.0*y0*v[1]+x0**2+y0**2)**0.5 - jmat.append([(x0-v[0])/d, (y0-v[1])/d, -1.0]) - k.append(-(((v[0]-x0)**2+(v[1]-y0)**2)**0.5-r)) - jmat2 = mathutils.Matrix(((0.0, 0.0, 0.0), - (0.0, 0.0, 0.0), - (0.0, 0.0, 0.0), - )) - k2 = mathutils.Vector((0.0, 0.0, 0.0)) - for i in range(len(jmat)): - k2 += mathutils.Vector(jmat[i])*k[i] - jmat2[0][0] += jmat[i][0]**2 - jmat2[1][0] += jmat[i][0]*jmat[i][1] - jmat2[2][0] += jmat[i][0]*jmat[i][2] - jmat2[1][1] += jmat[i][1]**2 - jmat2[2][1] += jmat[i][1]*jmat[i][2] - jmat2[2][2] += jmat[i][2]**2 - jmat2[0][1] = jmat2[1][0] - jmat2[0][2] = jmat2[2][0] - jmat2[1][2] = jmat2[2][1] - try: - jmat2.invert() - except: - pass - dx0, dy0, dr = jmat2 * k2 - x0 += dx0 - y0 += dy0 - r += dr - # stop iterating if we're close enough to optimal solution - if abs(dx0)<1e-6 and abs(dy0)<1e-6 and abs(dr)<1e-6: - break - - # return center of circle and radius - return(x0, y0, r) - - -# calculate circle so no vertices have to be moved away from the center -def circle_calculate_min_fit(locs_2d): - # center of circle - x0 = (min([i[0] for i in locs_2d])+max([i[0] for i in locs_2d]))/2.0 - y0 = (min([i[1] for i in locs_2d])+max([i[1] for i in locs_2d]))/2.0 - center = mathutils.Vector([x0, y0]) - # radius of circle - r = min([(mathutils.Vector([i[0], i[1]])-center).length for i in locs_2d]) - - # return center of circle and radius - return(x0, y0, r) - - -# calculate the new locations of the vertices that need to be moved -def circle_calculate_verts(flatten, mesh_mod, locs_2d, com, p, q, normal): - # changing 2d coordinates back to 3d coordinates - locs_3d = [] - for loc in locs_2d: - locs_3d.append([loc[2], loc[0]*p + loc[1]*q + com]) - - if flatten: # flat circle - return(locs_3d) - - else: # project the locations on the existing mesh - vert_edges = dict_vert_edges(mesh_mod) - vert_faces = dict_vert_faces(mesh_mod) - faces = [f for f in mesh_mod.faces if not f.hide] - rays = [normal, -normal] - new_locs = [] - for loc in locs_3d: - projection = False - if mesh_mod.vertices[loc[0]].co == loc[1]: # vertex hasn't moved - projection = loc[1] - else: - dif = normal.angle(loc[1]-mesh_mod.vertices[loc[0]].co) - if -1e-6 < dif < 1e-6 or math.pi-1e-6 < dif < math.pi+1e-6: - # original location is already along projection normal - projection = mesh_mod.vertices[loc[0]].co - else: - # quick search through adjacent faces - for face in vert_faces[loc[0]]: - verts = [mesh_mod.vertices[v].co for v in \ - mesh_mod.faces[face].vertices] - if len(verts) == 3: # triangle - v1, v2, v3 = verts - v4 = False - else: # quad - v1, v2, v3, v4 = verts - for ray in rays: - intersect = mathutils.geometry.\ - intersect_ray_tri(v1, v2, v3, ray, loc[1]) - if intersect: - projection = intersect - break - elif v4: - intersect = mathutils.geometry.\ - intersect_ray_tri(v1, v3, v4, ray, loc[1]) - if intersect: - projection = intersect - break - if projection: - break - if not projection: - # check if projection is on adjacent edges - for edgekey in vert_edges[loc[0]]: - line1 = mesh_mod.vertices[edgekey[0]].co - line2 = mesh_mod.vertices[edgekey[1]].co - intersect, dist = mathutils.geometry.intersect_point_line(\ - loc[1], line1, line2) - if 1e-6 < dist < 1 - 1e-6: - projection = intersect - break - if not projection: - # full search through the entire mesh - hits = [] - for face in faces: - verts = [mesh_mod.vertices[v].co for v in face.vertices] - if len(verts) == 3: # triangle - v1, v2, v3 = verts - v4 = False - else: # quad - v1, v2, v3, v4 = verts - for ray in rays: - intersect = mathutils.geometry.intersect_ray_tri(\ - v1, v2, v3, ray, loc[1]) - if intersect: - hits.append([(loc[1] - intersect).length, - intersect]) - break - elif v4: - intersect = mathutils.geometry.intersect_ray_tri(\ - v1, v3, v4, ray, loc[1]) - if intersect: - hits.append([(loc[1] - intersect).length, - intersect]) - break - if len(hits) >= 1: - # if more than 1 hit with mesh, closest hit is new loc - hits.sort() - projection = hits[0][1] - if not projection: - # nothing to project on, remain at flat location - projection = loc[1] - new_locs.append([loc[0], projection]) - - # return new positions of projected circle - return(new_locs) - - -# check loops and only return valid ones -def circle_check_loops(single_loops, loops, mapping, mesh_mod): - valid_single_loops = {} - valid_loops = [] - for i, [loop, circular] in enumerate(loops): - # loop needs to have at least 3 vertices - if len(loop) < 3: - continue - # loop needs at least 1 vertex in the original, non-mirrored mesh - if mapping: - all_virtual = True - for vert in loop: - if mapping[vert] > -1: - all_virtual = False - break - if all_virtual: - continue - # loop has to be non-collinear - collinear = True - loc0 = mathutils.Vector(mesh_mod.vertices[loop[0]].co[:]) - loc1 = mathutils.Vector(mesh_mod.vertices[loop[1]].co[:]) - for v in loop[2:]: - locn = mathutils.Vector(mesh_mod.vertices[v].co[:]) - if loc0 == loc1 or loc1 == locn: - loc0 = loc1 - loc1 = locn - continue - d1 = loc1-loc0 - d2 = locn-loc1 - if -1e-6 < d1.angle(d2, 0) < 1e-6: - loc0 = loc1 - loc1 = locn - continue - collinear = False - break - if collinear: - continue - # passed all tests, loop is valid - valid_loops.append([loop, circular]) - valid_single_loops[len(valid_loops)-1] = single_loops[i] - - return(valid_single_loops, valid_loops) - - -# calculate the location of single input vertices that need to be flattened -def circle_flatten_singles(mesh_mod, com, p, q, normal, single_loop): - new_locs = [] - for vert in single_loop: - loc = mathutils.Vector(mesh_mod.vertices[vert].co[:]) - new_locs.append([vert, loc - (loc-com).dot(normal)*normal]) - - return(new_locs) - - -# calculate input loops -def circle_get_input(object, mesh, scene): - # get mesh with modifiers applied - derived, mesh_mod = get_derived_mesh(object, mesh, scene) - - # create list of edge-keys based on selection state - faces = False - for face in mesh.faces: - if face.select and not face.hide: - faces = True - break - if faces: - # get selected, non-hidden , non-internal edge-keys - eks_selected = [key for keys in [face.edge_keys for face in \ - mesh_mod.faces if face.select and not face.hide] for key in keys] - edge_count = {} - for ek in eks_selected: - if ek in edge_count: - edge_count[ek] += 1 - else: - edge_count[ek] = 1 - edge_keys = [edge.key for edge in mesh_mod.edges if edge.select \ - and not edge.hide and edge_count.get(edge.key, 1)==1] - else: - # no faces, so no internal edges either - edge_keys = [edge.key for edge in mesh_mod.edges if edge.select \ - and not edge.hide] - - # add edge-keys around single vertices - verts_connected = dict([[vert, 1] for edge in [edge for edge in \ - mesh_mod.edges if edge.select and not edge.hide] for vert in edge.key]) - single_vertices = [vert.index for vert in mesh_mod.vertices if \ - vert.select and not vert.hide and not \ - verts_connected.get(vert.index, False)] - - if single_vertices and len(mesh.faces)>0: - vert_to_single = dict([[v.index, []] for v in mesh_mod.vertices \ - if not v.hide]) - for face in [face for face in mesh_mod.faces if not face.select \ - and not face.hide]: - for vert in face.vertices: - if vert in single_vertices: - for ek in face.edge_keys: - if not vert in ek: - edge_keys.append(ek) - if vert not in vert_to_single[ek[0]]: - vert_to_single[ek[0]].append(vert) - if vert not in vert_to_single[ek[1]]: - vert_to_single[ek[1]].append(vert) - break - - # sort edge-keys into loops - loops = get_connected_selections(edge_keys) - - # find out to which loops the single vertices belong - single_loops = dict([[i, []] for i in range(len(loops))]) - if single_vertices and len(mesh.faces)>0: - for i, [loop, circular] in enumerate(loops): - for vert in loop: - if vert_to_single[vert]: - for single in vert_to_single[vert]: - if single not in single_loops[i]: - single_loops[i].append(single) - - return(derived, mesh_mod, single_vertices, single_loops, loops) - - -# recalculate positions based on the influence of the circle shape -def circle_influence_locs(locs_2d, new_locs_2d, influence): - for i in range(len(locs_2d)): - oldx, oldy, j = locs_2d[i] - newx, newy, k = new_locs_2d[i] - altx = newx*(influence/100)+ oldx*((100-influence)/100) - alty = newy*(influence/100)+ oldy*((100-influence)/100) - locs_2d[i] = [altx, alty, j] - - return(locs_2d) - - -# project 2d locations on circle, respecting distance relations between verts -def circle_project_non_regular(locs_2d, x0, y0, r): - for i in range(len(locs_2d)): - x, y, j = locs_2d[i] - loc = mathutils.Vector([x-x0, y-y0]) - loc.length = r - locs_2d[i] = [loc[0], loc[1], j] - - return(locs_2d) - - -# project 2d locations on circle, with equal distance between all vertices -def circle_project_regular(locs_2d, x0, y0, r): - # find offset angle and circling direction - x, y, i = locs_2d[0] - loc = mathutils.Vector([x-x0, y-y0]) - loc.length = r - offset_angle = loc.angle(mathutils.Vector([1.0, 0.0]), 0.0) - loca = mathutils.Vector([x-x0, y-y0, 0.0]) - if loc[1] < -1e-6: - offset_angle *= -1 - x, y, j = locs_2d[1] - locb = mathutils.Vector([x-x0, y-y0, 0.0]) - if loca.cross(locb)[2] >= 0: - ccw = 1 - else: - ccw = -1 - # distribute vertices along the circle - for i in range(len(locs_2d)): - t = offset_angle + ccw * (i / len(locs_2d) * 2 * math.pi) - x = math.cos(t) * r - y = math.sin(t) * r - locs_2d[i] = [x, y, locs_2d[i][2]] - - return(locs_2d) - - -# shift loop, so the first vertex is closest to the center -def circle_shift_loop(mesh_mod, loop, com): - verts, circular = loop - distances = [[(mesh_mod.vertices[vert].co - com).length, i] \ - for i, vert in enumerate(verts)] - distances.sort() - shift = distances[0][1] - loop = [verts[shift:] + verts[:shift], circular] - - return(loop) - - -########################################## -####### Curve functions ################## -########################################## - -# create lists with knots and points, all correctly sorted -def curve_calculate_knots(loop, verts_selected): - knots = [v for v in loop[0] if v in verts_selected] - points = loop[0][:] - # circular loop, potential for weird splines - if loop[1]: - offset = int(len(loop[0]) / 4) - kpos = [] - for k in knots: - kpos.append(loop[0].index(k)) - kdif = [] - for i in range(len(kpos) - 1): - kdif.append(kpos[i+1] - kpos[i]) - kdif.append(len(loop[0]) - kpos[-1] + kpos[0]) - kadd = [] - for k in kdif: - if k > 2 * offset: - kadd.append([kdif.index(k), True]) - # next 2 lines are optional, they insert - # an extra control point in small gaps - #elif k > offset: - # kadd.append([kdif.index(k), False]) - kins = [] - krot = False - for k in kadd: # extra knots to be added - if k[1]: # big gap (break circular spline) - kpos = loop[0].index(knots[k[0]]) + offset - if kpos > len(loop[0]) - 1: - kpos -= len(loop[0]) - kins.append([knots[k[0]], loop[0][kpos]]) - kpos2 = k[0] + 1 - if kpos2 > len(knots)-1: - kpos2 -= len(knots) - kpos2 = loop[0].index(knots[kpos2]) - offset - if kpos2 < 0: - kpos2 += len(loop[0]) - kins.append([loop[0][kpos], loop[0][kpos2]]) - krot = loop[0][kpos2] - else: # small gap (keep circular spline) - k1 = loop[0].index(knots[k[0]]) - k2 = k[0] + 1 - if k2 > len(knots)-1: - k2 -= len(knots) - k2 = loop[0].index(knots[k2]) - if k2 < k1: - dif = len(loop[0]) - 1 - k1 + k2 - else: - dif = k2 - k1 - kn = k1 + int(dif/2) - if kn > len(loop[0]) - 1: - kn -= len(loop[0]) - kins.append([loop[0][k1], loop[0][kn]]) - for j in kins: # insert new knots - knots.insert(knots.index(j[0]) + 1, j[1]) - if not krot: # circular loop - knots.append(knots[0]) - points = loop[0][loop[0].index(knots[0]):] - points += loop[0][0:loop[0].index(knots[0]) + 1] - else: # non-circular loop (broken by script) - krot = knots.index(krot) - knots = knots[krot:] + knots[0:krot] - if loop[0].index(knots[0]) > loop[0].index(knots[-1]): - points = loop[0][loop[0].index(knots[0]):] - points += loop[0][0:loop[0].index(knots[-1])+1] - else: - points = loop[0][loop[0].index(knots[0]):\ - loop[0].index(knots[-1]) + 1] - # non-circular loop, add first and last point as knots - else: - if loop[0][0] not in knots: - knots.insert(0, loop[0][0]) - if loop[0][-1] not in knots: - knots.append(loop[0][-1]) - - return(knots, points) - - -# calculate relative positions compared to first knot -def curve_calculate_t(mesh_mod, knots, points, pknots, regular, circular): - tpoints = [] - loc_prev = False - len_total = 0 - - for p in points: - if p in knots: - loc = pknots[knots.index(p)] # use projected knot location - else: - loc = mathutils.Vector(mesh_mod.vertices[p].co[:]) - if not loc_prev: - loc_prev = loc - len_total += (loc-loc_prev).length - tpoints.append(len_total) - loc_prev = loc - tknots = [] - for p in points: - if p in knots: - tknots.append(tpoints[points.index(p)]) - if circular: - tknots[-1] = tpoints[-1] - - # regular option - if regular: - tpoints_average = tpoints[-1] / (len(tpoints) - 1) - for i in range(1, len(tpoints) - 1): - tpoints[i] = i * tpoints_average - for i in range(len(knots)): - tknots[i] = tpoints[points.index(knots[i])] - if circular: - tknots[-1] = tpoints[-1] - - - return(tknots, tpoints) - - -# change the location of non-selected points to their place on the spline -def curve_calculate_vertices(mesh_mod, knots, tknots, points, tpoints, splines, -interpolation, restriction): - newlocs = {} - move = [] - - for p in points: - if p in knots: - continue - m = tpoints[points.index(p)] - if m in tknots: - n = tknots.index(m) - else: - t = tknots[:] - t.append(m) - t.sort() - n = t.index(m) - 1 - if n > len(splines) - 1: - n = len(splines) - 1 - elif n < 0: - n = 0 - - if interpolation == 'cubic': - ax, bx, cx, dx, tx = splines[n][0] - x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3 - ay, by, cy, dy, ty = splines[n][1] - y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3 - az, bz, cz, dz, tz = splines[n][2] - z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3 - newloc = mathutils.Vector([x,y,z]) - else: # interpolation == 'linear' - a, d, t, u = splines[n] - newloc = ((m-t)/u)*d + a - - if restriction != 'none': # vertex movement is restricted - newlocs[p] = newloc - else: # set the vertex to its new location - move.append([p, newloc]) - - if restriction != 'none': # vertex movement is restricted - for p in points: - if p in newlocs: - newloc = newlocs[p] - else: - move.append([p, mesh_mod.vertices[p].co]) - continue - oldloc = mesh_mod.vertices[p].co - normal = mesh_mod.vertices[p].normal - dloc = newloc - oldloc - if dloc.length < 1e-6: - move.append([p, newloc]) - elif restriction == 'extrude': # only extrusions - if dloc.angle(normal, 0) < 0.5 * math.pi + 1e-6: - move.append([p, newloc]) - else: # restriction == 'indent' only indentations - if dloc.angle(normal) > 0.5 * math.pi - 1e-6: - move.append([p, newloc]) - - return(move) - - -# trim loops to part between first and last selected vertices (including) -def curve_cut_boundaries(mesh_mod, loops): - cut_loops = [] - for loop, circular in loops: - if circular: - # don't cut - cut_loops.append([loop, circular]) - continue - selected = [mesh_mod.vertices[v].select for v in loop] - first = selected.index(True) - selected.reverse() - last = -selected.index(True) - if last == 0: - cut_loops.append([loop[first:], circular]) - else: - cut_loops.append([loop[first:last], circular]) - - return(cut_loops) - - -# calculate input loops -def curve_get_input(object, mesh, boundaries, scene): - # get mesh with modifiers applied - derived, mesh_mod = get_derived_mesh(object, mesh, scene) - - # vertices that still need a loop to run through it - verts_unsorted = [v.index for v in mesh_mod.vertices if \ - v.select and not v.hide] - # necessary dictionaries - vert_edges = dict_vert_edges(mesh_mod) - edge_faces = dict_edge_faces(mesh_mod) - correct_loops = [] - - # find loops through each selected vertex - while len(verts_unsorted) > 0: - loops = curve_vertex_loops(mesh_mod, verts_unsorted[0], vert_edges, - edge_faces) - verts_unsorted.pop(0) - - # check if loop is fully selected - search_perpendicular = False - i = -1 - for loop, circular in loops: - i += 1 - selected = [v for v in loop if mesh_mod.vertices[v].select] - if len(selected) < 2: - # only one selected vertex on loop, don't use - loops.pop(i) - continue - elif len(selected) == len(loop): - search_perpendicular = loop - break - # entire loop is selected, find perpendicular loops - if search_perpendicular: - for vert in loop: - if vert in verts_unsorted: - verts_unsorted.remove(vert) - perp_loops = curve_perpendicular_loops(mesh_mod, loop, - vert_edges, edge_faces) - for perp_loop in perp_loops: - correct_loops.append(perp_loop) - # normal input - else: - for loop, circular in loops: - correct_loops.append([loop, circular]) - - # boundaries option - if boundaries: - correct_loops = curve_cut_boundaries(mesh_mod, correct_loops) - - return(derived, mesh_mod, correct_loops) - - -# return all loops that are perpendicular to the given one -def curve_perpendicular_loops(mesh_mod, start_loop, vert_edges, edge_faces): - # find perpendicular loops - perp_loops = [] - for start_vert in start_loop: - loops = curve_vertex_loops(mesh_mod, start_vert, vert_edges, - edge_faces) - for loop, circular in loops: - selected = [v for v in loop if mesh_mod.vertices[v].select] - if len(selected) == len(loop): - continue - else: - perp_loops.append([loop, circular, loop.index(start_vert)]) - - # trim loops to same lengths - shortest = [[len(loop[0]), i] for i, loop in enumerate(perp_loops)\ - if not loop[1]] - if not shortest: - # all loops are circular, not trimming - return([[loop[0], loop[1]] for loop in perp_loops]) - else: - shortest = min(shortest) - shortest_start = perp_loops[shortest[1]][2] - before_start = shortest_start - after_start = shortest[0] - shortest_start - 1 - bigger_before = before_start > after_start - trimmed_loops = [] - for loop in perp_loops: - # have the loop face the same direction as the shortest one - if bigger_before: - if loop[2] < len(loop[0]) / 2: - loop[0].reverse() - loop[2] = len(loop[0]) - loop[2] - 1 - else: - if loop[2] > len(loop[0]) / 2: - loop[0].reverse() - loop[2] = len(loop[0]) - loop[2] - 1 - # circular loops can shift, to prevent wrong trimming - if loop[1]: - shift = shortest_start - loop[2] - if loop[2] + shift > 0 and loop[2] + shift < len(loop[0]): - loop[0] = loop[0][-shift:] + loop[0][:-shift] - loop[2] += shift - if loop[2] < 0: - loop[2] += len(loop[0]) - elif loop[2] > len(loop[0]) -1: - loop[2] -= len(loop[0]) - # trim - start = max(0, loop[2] - before_start) - end = min(len(loop[0]), loop[2] + after_start + 1) - trimmed_loops.append([loop[0][start:end], False]) - - return(trimmed_loops) - - -# project knots on non-selected geometry -def curve_project_knots(mesh_mod, verts_selected, knots, points, circular): - # function to project vertex on edge - def project(v1, v2, v3): - # v1 and v2 are part of a line - # v3 is projected onto it - v2 -= v1 - v3 -= v1 - p = v3.project(v2) - return(p + v1) - - if circular: # project all knots - start = 0 - end = len(knots) - pknots = [] - else: # first and last knot shouldn't be projected - start = 1 - end = -1 - pknots = [mathutils.Vector(mesh_mod.vertices[knots[0]].co[:])] - for knot in knots[start:end]: - if knot in verts_selected: - knot_left = knot_right = False - for i in range(points.index(knot)-1, -1*len(points), -1): - if points[i] not in knots: - knot_left = points[i] - break - for i in range(points.index(knot)+1, 2*len(points)): - if i > len(points) - 1: - i -= len(points) - if points[i] not in knots: - knot_right = points[i] - break - if knot_left and knot_right and knot_left != knot_right: - knot_left = mathutils.Vector(\ - mesh_mod.vertices[knot_left].co[:]) - knot_right = mathutils.Vector(\ - mesh_mod.vertices[knot_right].co[:]) - knot = mathutils.Vector(mesh_mod.vertices[knot].co[:]) - pknots.append(project(knot_left, knot_right, knot)) - else: - pknots.append(mathutils.Vector(mesh_mod.vertices[knot].co[:])) - else: # knot isn't selected, so shouldn't be changed - pknots.append(mathutils.Vector(mesh_mod.vertices[knot].co[:])) - if not circular: - pknots.append(mathutils.Vector(mesh_mod.vertices[knots[-1]].co[:])) - - return(pknots) - - -# find all loops through a given vertex -def curve_vertex_loops(mesh_mod, start_vert, vert_edges, edge_faces): - edges_used = [] - loops = [] - - for edge in vert_edges[start_vert]: - if edge in edges_used: - continue - loop = [] - circular = False - for vert in edge: - active_faces = edge_faces[edge] - new_vert = vert - growing = True - while growing: - growing = False - new_edges = vert_edges[new_vert] - loop.append(new_vert) - if len(loop) > 1: - edges_used.append(tuple(sorted([loop[-1], loop[-2]]))) - if len(new_edges) < 3 or len(new_edges) > 4: - # pole - break - else: - # find next edge - for new_edge in new_edges: - if new_edge in edges_used: - continue - eliminate = False - for new_face in edge_faces[new_edge]: - if new_face in active_faces: - eliminate = True - break - if eliminate: - continue - # found correct new edge - active_faces = edge_faces[new_edge] - v1, v2 = new_edge - if v1 != new_vert: - new_vert = v1 - else: - new_vert = v2 - if new_vert == loop[0]: - circular = True - else: - growing = True - break - if circular: - break - loop.reverse() - loops.append([loop, circular]) - - return(loops) - - -########################################## -####### Flatten functions ################ -########################################## - -# sort input into loops -def flatten_get_input(mesh): - vert_verts = dict_vert_verts([edge.key for edge in mesh.edges \ - if edge.select and not edge.hide]) - verts = [v.index for v in mesh.vertices if v.select and not v.hide] - - # no connected verts, consider all selected verts as a single input - if not vert_verts: - return([[verts, False]]) - - loops = [] - while len(verts) > 0: - # start of loop - loop = [verts[0]] - verts.pop(0) - if loop[-1] in vert_verts: - to_grow = vert_verts[loop[-1]] - else: - to_grow = [] - # grow loop - while len(to_grow) > 0: - new_vert = to_grow[0] - to_grow.pop(0) - if new_vert in loop: - continue - loop.append(new_vert) - verts.remove(new_vert) - to_grow += vert_verts[new_vert] - # add loop to loops - loops.append([loop, False]) - - return(loops) - - -# calculate position of vertex projections on plane -def flatten_project(mesh, loop, com, normal): - verts = [mesh.vertices[v] for v in loop[0]] - verts_projected = [[v.index, mathutils.Vector(v.co[:]) - \ - (mathutils.Vector(v.co[:])-com).dot(normal)*normal] for v in verts] - - return(verts_projected) - - -########################################## -####### Relax functions ################## -########################################## - -# create lists with knots and points, all correctly sorted -def relax_calculate_knots(loops): - all_knots = [] - all_points = [] - for loop, circular in loops: - knots = [[], []] - points = [[], []] - if circular: - if len(loop)%2 == 1: # odd - extend = [False, True, 0, 1, 0, 1] - else: # even - extend = [True, False, 0, 1, 1, 2] - else: - if len(loop)%2 == 1: # odd - extend = [False, False, 0, 1, 1, 2] - else: # even - extend = [False, False, 0, 1, 1, 2] - for j in range(2): - if extend[j]: - loop = [loop[-1]] + loop + [loop[0]] - for i in range(extend[2+2*j], len(loop), 2): - knots[j].append(loop[i]) - for i in range(extend[3+2*j], len(loop), 2): - if loop[i] == loop[-1] and not circular: - continue - if len(points[j]) == 0: - points[j].append(loop[i]) - elif loop[i] != points[j][0]: - points[j].append(loop[i]) - if circular: - if knots[j][0] != knots[j][-1]: - knots[j].append(knots[j][0]) - if len(points[1]) == 0: - knots.pop(1) - points.pop(1) - for k in knots: - all_knots.append(k) - for p in points: - all_points.append(p) - - return(all_knots, all_points) - - -# calculate relative positions compared to first knot -def relax_calculate_t(mesh_mod, knots, points, regular): - all_tknots = [] - all_tpoints = [] - for i in range(len(knots)): - amount = len(knots[i]) + len(points[i]) - mix = [] - for j in range(amount): - if j%2 == 0: - mix.append([True, knots[i][round(j/2)]]) - elif j == amount-1: - mix.append([True, knots[i][-1]]) - else: - mix.append([False, points[i][int(j/2)]]) - len_total = 0 - loc_prev = False - tknots = [] - tpoints = [] - for m in mix: - loc = mathutils.Vector(mesh_mod.vertices[m[1]].co[:]) - if not loc_prev: - loc_prev = loc - len_total += (loc - loc_prev).length - if m[0]: - tknots.append(len_total) - else: - tpoints.append(len_total) - loc_prev = loc - if regular: - tpoints = [] - for p in range(len(points[i])): - tpoints.append((tknots[p] + tknots[p+1]) / 2) - all_tknots.append(tknots) - all_tpoints.append(tpoints) - - return(all_tknots, all_tpoints) - - -# change the location of the points to their place on the spline -def relax_calculate_verts(mesh_mod, interpolation, tknots, knots, tpoints, -points, splines): - change = [] - move = [] - for i in range(len(knots)): - for p in points[i]: - m = tpoints[i][points[i].index(p)] - if m in tknots[i]: - n = tknots[i].index(m) - else: - t = tknots[i][:] - t.append(m) - t.sort() - n = t.index(m)-1 - if n > len(splines[i]) - 1: - n = len(splines[i]) - 1 - elif n < 0: - n = 0 - - if interpolation == 'cubic': - ax, bx, cx, dx, tx = splines[i][n][0] - x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3 - ay, by, cy, dy, ty = splines[i][n][1] - y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3 - az, bz, cz, dz, tz = splines[i][n][2] - z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3 - change.append([p, mathutils.Vector([x,y,z])]) - else: # interpolation == 'linear' - a, d, t, u = splines[i][n] - if u == 0: - u = 1e-8 - change.append([p, ((m-t)/u)*d + a]) - for c in change: - move.append([c[0], (mesh_mod.vertices[c[0]].co + c[1]) / 2]) - - return(move) - - -########################################## -####### Space functions ################## -########################################## - -# calculate relative positions compared to first knot -def space_calculate_t(mesh_mod, knots): - tknots = [] - loc_prev = False - len_total = 0 - for k in knots: - loc = mathutils.Vector(mesh_mod.vertices[k].co[:]) - if not loc_prev: - loc_prev = loc - len_total += (loc - loc_prev).length - tknots.append(len_total) - loc_prev = loc - amount = len(knots) - t_per_segment = len_total / (amount - 1) - tpoints = [i * t_per_segment for i in range(amount)] - - return(tknots, tpoints) - - -# change the location of the points to their place on the spline -def space_calculate_verts(mesh_mod, interpolation, tknots, tpoints, points, -splines): - move = [] - for p in points: - m = tpoints[points.index(p)] - if m in tknots: - n = tknots.index(m) - else: - t = tknots[:] - t.append(m) - t.sort() - n = t.index(m) - 1 - if n > len(splines) - 1: - n = len(splines) - 1 - elif n < 0: - n = 0 - - if interpolation == 'cubic': - ax, bx, cx, dx, tx = splines[n][0] - x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3 - ay, by, cy, dy, ty = splines[n][1] - y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3 - az, bz, cz, dz, tz = splines[n][2] - z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3 - move.append([p, mathutils.Vector([x,y,z])]) - else: # interpolation == 'linear' - a, d, t, u = splines[n] - move.append([p, ((m-t)/u)*d + a]) - - return(move) - - -########################################## -####### Operators ######################## -########################################## - -# bridge operator -class Bridge(bpy.types.Operator): - bl_idname = 'mesh.looptools_bridge' - bl_label = "Bridge / Loft" - bl_description = "Bridge two, or loft several, loops of vertices" - bl_options = {'REGISTER', 'UNDO'} - - cubic_strength = bpy.props.FloatProperty(name = "Strength", - description = "Higher strength results in more fluid curves", - default = 1.0, - soft_min = -3.0, - soft_max = 3.0) - interpolation = bpy.props.EnumProperty(name = "Interpolation mode", - items = (('cubic', "Cubic", "Gives curved results"), - ('linear', "Linear", "Basic, fast, straight interpolation")), - description = "Interpolation mode: algorithm used when creating "\ - "segments", - default = 'cubic') - loft = bpy.props.BoolProperty(name = "Loft", - description = "Loft multiple loops, instead of considering them as "\ - "a multi-input for bridging", - default = False) - loft_loop = bpy.props.BoolProperty(name = "Loop", - description = "Connect the first and the last loop with each other", - default = False) - min_width = bpy.props.IntProperty(name = "Minimum width", - description = "Segments with an edge smaller than this are merged "\ - "(compared to base edge)", - default = 0, - min = 0, - max = 100, - subtype = 'PERCENTAGE') - mode = bpy.props.EnumProperty(name = "Mode", - items = (('basic', "Basic", "Fast algorithm"), ('shortest', - "Shortest edge", "Slower algorithm with better vertex matching")), - description = "Algorithm used for bridging", - default = 'shortest') - remove_faces = bpy.props.BoolProperty(name = "Remove faces", - description = "Remove faces that are internal after bridging", - default = True) - reverse = bpy.props.BoolProperty(name = "Reverse", - description = "Manually override the direction in which the loops "\ - "are bridged. Only use if the tool gives the wrong " \ - "result", - default = False) - segments = bpy.props.IntProperty(name = "Segments", - description = "Number of segments used to bridge the gap "\ - "(0 = automatic)", - default = 1, - min = 0, - soft_max = 20) - twist = bpy.props.IntProperty(name = "Twist", - description = "Twist what vertices are connected to each other", - default = 0) - - @classmethod - def poll(cls, context): - ob = context.active_object - return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH') - - def draw(self, context): - layout = self.layout - #layout.prop(self, "mode") # no cases yet where 'basic' mode is needed - - # top row - col_top = layout.column(align=True) - row = col_top.row(align=True) - col_left = row.column(align=True) - col_right = row.column(align=True) - col_right.active = self.segments != 1 - col_left.prop(self, "segments") - col_right.prop(self, "min_width", text="") - # bottom row - bottom_left = col_left.row() - bottom_left.active = self.segments != 1 - bottom_left.prop(self, "interpolation", text="") - bottom_right = col_right.row() - bottom_right.active = self.interpolation == 'cubic' - bottom_right.prop(self, "cubic_strength") - # boolean properties - col_top.prop(self, "remove_faces") - if self.loft: - col_top.prop(self, "loft_loop") - - # override properties - col_top.separator() - row = layout.row(align = True) - row.prop(self, "twist") - row.prop(self, "reverse") - - def invoke(self, context, event): - # load custom settings - context.window_manager.looptools.bridge_loft = self.loft - settings_load(self) - return self.execute(context) - - def execute(self, context): - # initialise - global_undo, object, mesh = initialise() - edge_faces, edgekey_to_edge, old_selected_faces, smooth = \ - bridge_initialise(mesh, self.interpolation) - settings_write(self) - - # check cache to see if we can save time - input_method = bridge_input_method(self.loft, self.loft_loop) - cached, single_loops, loops, derived, mapping = cache_read("Bridge", - object, mesh, input_method, False) - if not cached: - # get loops - loops = bridge_get_input(mesh) - if loops: - # reorder loops if there are more than 2 - if len(loops) > 2: - if self.loft: - loops = bridge_sort_loops(mesh, loops, self.loft_loop) - else: - loops = bridge_match_loops(mesh, loops) - - # saving cache for faster execution next time - if not cached: - cache_write("Bridge", object, mesh, input_method, False, False, - loops, False, False) - - if loops: - # calculate new geometry - vertices = [] - faces = [] - max_vert_index = len(mesh.vertices)-1 - for i in range(1, len(loops)): - if not self.loft and i%2 == 0: - continue - lines = bridge_calculate_lines(mesh, loops[i-1:i+1], - self.mode, self.twist, self.reverse) - vertex_normals = bridge_calculate_virtual_vertex_normals(mesh, - lines, loops[i-1:i+1], edge_faces, edgekey_to_edge) - segments = bridge_calculate_segments(mesh, lines, - loops[i-1:i+1], self.segments) - new_verts, new_faces, max_vert_index = \ - bridge_calculate_geometry(mesh, lines, vertex_normals, - segments, self.interpolation, self.cubic_strength, - self.min_width, max_vert_index) - if new_verts: - vertices += new_verts - if new_faces: - faces += new_faces - # make sure faces in loops that aren't used, aren't removed - if self.remove_faces and old_selected_faces: - bridge_save_unused_faces(mesh, old_selected_faces, loops) - # create vertices - if vertices: - bridge_create_vertices(mesh, vertices) - # create faces - if faces: - bridge_create_faces(mesh, faces, self.twist) - bridge_select_new_faces(mesh, len(faces), smooth) - # edge-data could have changed, can't use cache next run - if faces and not vertices: - cache_delete("Bridge") - # delete internal faces - if self.remove_faces and old_selected_faces: - bridge_remove_internal_faces(mesh, old_selected_faces) - # make sure normals are facing outside - bridge_recalculate_normals() - - terminate(global_undo) - return{'FINISHED'} - - -# circle operator -class Circle(bpy.types.Operator): - bl_idname = "mesh.looptools_circle" - bl_label = "Circle" - bl_description = "Move selected vertices into a circle shape" - bl_options = {'REGISTER', 'UNDO'} - - custom_radius = bpy.props.BoolProperty(name = "Radius", - description = "Force a custom radius", - default = False) - fit = bpy.props.EnumProperty(name = "Method", - items = (("best", "Best fit", "Non-linear least squares"), - ("inside", "Fit inside","Only move vertices towards the center")), - description = "Method used for fitting a circle to the vertices", - default = 'best') - flatten = bpy.props.BoolProperty(name = "Flatten", - description = "Flatten the circle, instead of projecting it on the " \ - "mesh", - default = True) - influence = bpy.props.FloatProperty(name = "Influence", - description = "Force of the tool", - default = 100.0, - min = 0.0, - max = 100.0, - precision = 1, - subtype = 'PERCENTAGE') - radius = bpy.props.FloatProperty(name = "Radius", - description = "Custom radius for circle", - default = 1.0, - min = 0.0, - soft_max = 1000.0) - regular = bpy.props.BoolProperty(name = "Regular", - description = "Distribute vertices at constant distances along the " \ - "circle", - default = True) - - @classmethod - def poll(cls, context): - ob = context.active_object - return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH') - - def draw(self, context): - layout = self.layout - col = layout.column() - - col.prop(self, "fit") - col.separator() - - col.prop(self, "flatten") - row = col.row(align=True) - row.prop(self, "custom_radius") - row_right = row.row(align=True) - row_right.active = self.custom_radius - row_right.prop(self, "radius", text="") - col.prop(self, "regular") - col.separator() - - col.prop(self, "influence") - - def invoke(self, context, event): - # load custom settings - settings_load(self) - return self.execute(context) - - def execute(self, context): - # initialise - global_undo, object, mesh = initialise() - settings_write(self) - # check cache to see if we can save time - cached, single_loops, loops, derived, mapping = cache_read("Circle", - object, mesh, False, False) - if cached: - derived, mesh_mod = get_derived_mesh(object, mesh, context.scene) - else: - # find loops - derived, mesh_mod, single_vertices, single_loops, loops = \ - circle_get_input(object, mesh, context.scene) - mapping = get_mapping(derived, mesh, mesh_mod, single_vertices, - False, loops) - single_loops, loops = circle_check_loops(single_loops, loops, - mapping, mesh_mod) - - # saving cache for faster execution next time - if not cached: - cache_write("Circle", object, mesh, False, False, single_loops, - loops, derived, mapping) - - move = [] - for i, loop in enumerate(loops): - # best fitting flat plane - com, normal = calculate_plane(mesh_mod, loop) - # if circular, shift loop so we get a good starting vertex - if loop[1]: - loop = circle_shift_loop(mesh_mod, loop, com) - # flatten vertices on plane - locs_2d, p, q = circle_3d_to_2d(mesh_mod, loop, com, normal) - # calculate circle - if self.fit == 'best': - x0, y0, r = circle_calculate_best_fit(locs_2d) - else: # self.fit == 'inside' - x0, y0, r = circle_calculate_min_fit(locs_2d) - # radius override - if self.custom_radius: - r = self.radius / p.length - # calculate positions on circle - if self.regular: - new_locs_2d = circle_project_regular(locs_2d[:], x0, y0, r) - else: - new_locs_2d = circle_project_non_regular(locs_2d[:], x0, y0, r) - # take influence into account - locs_2d = circle_influence_locs(locs_2d, new_locs_2d, - self.influence) - # calculate 3d positions of the created 2d input - move.append(circle_calculate_verts(self.flatten, mesh_mod, - locs_2d, com, p, q, normal)) - # flatten single input vertices on plane defined by loop - if self.flatten and single_loops: - move.append(circle_flatten_singles(mesh_mod, com, p, q, - normal, single_loops[i])) - - # move vertices to new locations - move_verts(mesh, mapping, move, -1) - - # cleaning up - if derived: - bpy.context.blend_data.meshes.remove(mesh_mod) - terminate(global_undo) - - return{'FINISHED'} - - -# curve operator -class Curve(bpy.types.Operator): - bl_idname = "mesh.looptools_curve" - bl_label = "Curve" - bl_description = "Turn a loop into a smooth curve" - bl_options = {'REGISTER', 'UNDO'} - - boundaries = bpy.props.BoolProperty(name = "Boundaries", - description = "Limit the tool to work within the boundaries of the "\ - "selected vertices", - default = False) - influence = bpy.props.FloatProperty(name = "Influence", - description = "Force of the tool", - default = 100.0, - min = 0.0, - max = 100.0, - precision = 1, - subtype = 'PERCENTAGE') - interpolation = bpy.props.EnumProperty(name = "Interpolation", - items = (("cubic", "Cubic", "Natural cubic spline, smooth results"), - ("linear", "Linear", "Simple and fast linear algorithm")), - description = "Algorithm used for interpolation", - default = 'cubic') - regular = bpy.props.BoolProperty(name = "Regular", - description = "Distribute vertices at constant distances along the" \ - "curve", - default = True) - restriction = bpy.props.EnumProperty(name = "Restriction", - items = (("none", "None", "No restrictions on vertex movement"), - ("extrude", "Extrude only","Only allow extrusions (no "\ - "indentations)"), - ("indent", "Indent only", "Only allow indentation (no "\ - "extrusions)")), - description = "Restrictions on how the vertices can be moved", - default = 'none') - - @classmethod - def poll(cls, context): - ob = context.active_object - return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH') - - def draw(self, context): - layout = self.layout - col = layout.column() - - col.prop(self, "interpolation") - col.prop(self, "restriction") - col.prop(self, "boundaries") - col.prop(self, "regular") - col.separator() - - col.prop(self, "influence") - - def invoke(self, context, event): - # load custom settings - settings_load(self) - return self.execute(context) - - def execute(self, context): - # initialise - global_undo, object, mesh = initialise() - settings_write(self) - # check cache to see if we can save time - cached, single_loops, loops, derived, mapping = cache_read("Curve", - object, mesh, False, self.boundaries) - if cached: - derived, mesh_mod = get_derived_mesh(object, mesh, context.scene) - else: - # find loops - derived, mesh_mod, loops = curve_get_input(object, mesh, - self.boundaries, context.scene) - mapping = get_mapping(derived, mesh, mesh_mod, False, True, loops) - loops = check_loops(loops, mapping, mesh_mod) - verts_selected = [v.index for v in mesh_mod.vertices if v.select \ - and not v.hide] - - # saving cache for faster execution next time - if not cached: - cache_write("Curve", object, mesh, False, self.boundaries, False, - loops, derived, mapping) - - move = [] - for loop in loops: - knots, points = curve_calculate_knots(loop, verts_selected) - pknots = curve_project_knots(mesh_mod, verts_selected, knots, - points, loop[1]) - tknots, tpoints = curve_calculate_t(mesh_mod, knots, points, - pknots, self.regular, loop[1]) - splines = calculate_splines(self.interpolation, mesh_mod, - tknots, knots) - move.append(curve_calculate_vertices(mesh_mod, knots, tknots, - points, tpoints, splines, self.interpolation, - self.restriction)) - - # move vertices to new locations - move_verts(mesh, mapping, move, self.influence) - - # cleaning up - if derived: - bpy.context.blend_data.meshes.remove(mesh_mod) - - terminate(global_undo) - return{'FINISHED'} - - -# flatten operator -class Flatten(bpy.types.Operator): - bl_idname = "mesh.looptools_flatten" - bl_label = "Flatten" - bl_description = "Flatten vertices on a best-fitting plane" - bl_options = {'REGISTER', 'UNDO'} - - influence = bpy.props.FloatProperty(name = "Influence", - description = "Force of the tool", - default = 100.0, - min = 0.0, - max = 100.0, - precision = 1, - subtype = 'PERCENTAGE') - plane = bpy.props.EnumProperty(name = "Plane", - items = (("best_fit", "Best fit", "Calculate a best fitting plane"), - ("normal", "Normal", "Derive plane from averaging vertex "\ - "normals"), - ("view", "View", "Flatten on a plane perpendicular to the "\ - "viewing angle")), - description = "Plane on which vertices are flattened", - default = 'best_fit') - restriction = bpy.props.EnumProperty(name = "Restriction", - items = (("none", "None", "No restrictions on vertex movement"), - ("bounding_box", "Bounding box", "Vertices are restricted to "\ - "movement inside the bounding box of the selection")), - description = "Restrictions on how the vertices can be moved", - default = 'none') - - @classmethod - def poll(cls, context): - ob = context.active_object - return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH') - - def draw(self, context): - layout = self.layout - col = layout.column() - - col.prop(self, "plane") - #col.prop(self, "restriction") - col.separator() - - col.prop(self, "influence") - - def invoke(self, context, event): - # load custom settings - settings_load(self) - return self.execute(context) - - def execute(self, context): - # initialise - global_undo, object, mesh = initialise() - settings_write(self) - # check cache to see if we can save time - cached, single_loops, loops, derived, mapping = cache_read("Flatten", - object, mesh, False, False) - if not cached: - # order input into virtual loops - loops = flatten_get_input(mesh) - loops = check_loops(loops, mapping, mesh) - - # saving cache for faster execution next time - if not cached: - cache_write("Flatten", object, mesh, False, False, False, loops, - False, False) - - move = [] - for loop in loops: - # calculate plane and position of vertices on them - com, normal = calculate_plane(mesh, loop, method=self.plane, - object=object) - to_move = flatten_project(mesh, loop, com, normal) - if self.restriction == 'none': - move.append(to_move) - else: - move.append(to_move) - move_verts(mesh, False, move, self.influence) - - terminate(global_undo) - return{'FINISHED'} - - -# relax operator -class Relax(bpy.types.Operator): - bl_idname = "mesh.looptools_relax" - bl_label = "Relax" - bl_description = "Relax the loop, so it is smoother" - bl_options = {'REGISTER', 'UNDO'} - - input = bpy.props.EnumProperty(name = "Input", - items = (("all", "Parallel (all)", "Also use non-selected "\ - "parallel loops as input"), - ("selected", "Selection","Only use selected vertices as input")), - description = "Loops that are relaxed", - default = 'selected') - interpolation = bpy.props.EnumProperty(name = "Interpolation", - items = (("cubic", "Cubic", "Natural cubic spline, smooth results"), - ("linear", "Linear", "Simple and fast linear algorithm")), - description = "Algorithm used for interpolation", - default = 'cubic') - iterations = bpy.props.EnumProperty(name = "Iterations", - items = (("1", "1", "One"), - ("3", "3", "Three"), - ("5", "5", "Five"), - ("10", "10", "Ten"), - ("25", "25", "Twenty-five")), - description = "Number of times the loop is relaxed", - default = "1") - regular = bpy.props.BoolProperty(name = "Regular", - description = "Distribute vertices at constant distances along the" \ - "loop", - default = True) - - @classmethod - def poll(cls, context): - ob = context.active_object - return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH') - - def draw(self, context): - layout = self.layout - col = layout.column() - - col.prop(self, "interpolation") - col.prop(self, "input") - col.prop(self, "iterations") - col.prop(self, "regular") - - def invoke(self, context, event): - # load custom settings - settings_load(self) - return self.execute(context) - - def execute(self, context): - # initialise - global_undo, object, mesh = initialise() - settings_write(self) - # check cache to see if we can save time - cached, single_loops, loops, derived, mapping = cache_read("Relax", - object, mesh, self.input, False) - if cached: - derived, mesh_mod = get_derived_mesh(object, mesh, context.scene) - else: - # find loops - derived, mesh_mod, loops = get_connected_input(object, mesh, - context.scene, self.input) - mapping = get_mapping(derived, mesh, mesh_mod, False, False, loops) - loops = check_loops(loops, mapping, mesh_mod) - knots, points = relax_calculate_knots(loops) - - # saving cache for faster execution next time - if not cached: - cache_write("Relax", object, mesh, self.input, False, False, loops, - derived, mapping) - - for iteration in range(int(self.iterations)): - # calculate splines and new positions - tknots, tpoints = relax_calculate_t(mesh_mod, knots, points, - self.regular) - splines = [] - for i in range(len(knots)): - splines.append(calculate_splines(self.interpolation, mesh_mod, - tknots[i], knots[i])) - move = [relax_calculate_verts(mesh_mod, self.interpolation, - tknots, knots, tpoints, points, splines)] - move_verts(mesh, mapping, move, -1) - - # cleaning up - if derived: - bpy.context.blend_data.meshes.remove(mesh_mod) - terminate(global_undo) - - return{'FINISHED'} - - -# space operator -class Space(bpy.types.Operator): - bl_idname = "mesh.looptools_space" - bl_label = "Space" - bl_description = "Space the vertices in a regular distrubtion on the loop" - bl_options = {'REGISTER', 'UNDO'} - - influence = bpy.props.FloatProperty(name = "Influence", - description = "Force of the tool", - default = 100.0, - min = 0.0, - max = 100.0, - precision = 1, - subtype = 'PERCENTAGE') - input = bpy.props.EnumProperty(name = "Input", - items = (("all", "Parallel (all)", "Also use non-selected "\ - "parallel loops as input"), - ("selected", "Selection","Only use selected vertices as input")), - description = "Loops that are spaced", - default = 'selected') - interpolation = bpy.props.EnumProperty(name = "Interpolation", - items = (("cubic", "Cubic", "Natural cubic spline, smooth results"), - ("linear", "Linear", "Vertices are projected on existing edges")), - description = "Algorithm used for interpolation", - default = 'cubic') - - @classmethod - def poll(cls, context): - ob = context.active_object - return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH') - - def draw(self, context): - layout = self.layout - col = layout.column() - - col.prop(self, "interpolation") - col.prop(self, "input") - col.separator() - - col.prop(self, "influence") - - def invoke(self, context, event): - # load custom settings - settings_load(self) - return self.execute(context) - - def execute(self, context): - # initialise - global_undo, object, mesh = initialise() - settings_write(self) - # check cache to see if we can save time - cached, single_loops, loops, derived, mapping = cache_read("Space", - object, mesh, self.input, False) - if cached: - derived, mesh_mod = get_derived_mesh(object, mesh, context.scene) - else: - # find loops - derived, mesh_mod, loops = get_connected_input(object, mesh, - context.scene, self.input) - mapping = get_mapping(derived, mesh, mesh_mod, False, False, loops) - loops = check_loops(loops, mapping, mesh_mod) - - # saving cache for faster execution next time - if not cached: - cache_write("Space", object, mesh, self.input, False, False, loops, - derived, mapping) - - move = [] - for loop in loops: - # calculate splines and new positions - if loop[1]: # circular - loop[0].append(loop[0][0]) - tknots, tpoints = space_calculate_t(mesh_mod, loop[0][:]) - splines = calculate_splines(self.interpolation, mesh_mod, - tknots, loop[0][:]) - move.append(space_calculate_verts(mesh_mod, self.interpolation, - tknots, tpoints, loop[0][:-1], splines)) - - # move vertices to new locations - move_verts(mesh, mapping, move, self.influence) - - # cleaning up - if derived: - bpy.context.blend_data.meshes.remove(mesh_mod) - terminate(global_undo) - - return{'FINISHED'} - - -########################################## -####### GUI and registration ############# -########################################## - -# menu containing all tools -class VIEW3D_MT_edit_mesh_looptools(bpy.types.Menu): - bl_label = "LoopTools" - - def draw(self, context): - layout = self.layout - -# layout.operator("mesh.looptools_bridge", text="Bridge").loft = False - layout.operator("mesh.looptools_circle") - layout.operator("mesh.looptools_curve") - layout.operator("mesh.looptools_flatten") -# layout.operator("mesh.looptools_bridge", text="Loft").loft = True - layout.operator("mesh.looptools_relax") - layout.operator("mesh.looptools_space") - - -# panel containing all tools -class VIEW3D_PT_tools_looptools(bpy.types.Panel): - bl_space_type = 'VIEW_3D' - bl_region_type = 'TOOLS' - bl_context = "mesh_edit" - bl_label = "LoopTools" - - def draw(self, context): - layout = self.layout - col = layout.column(align=True) - lt = context.window_manager.looptools - - # bridge - first line -# split = col.split(percentage=0.15) -# if lt.display_bridge: -# split.prop(lt, "display_bridge", text="", icon='DOWNARROW_HLT') -# else: -# split.prop(lt, "display_bridge", text="", icon='RIGHTARROW') -# split.operator("mesh.looptools_bridge", text="Bridge").loft = False - # bridge - settings -# if lt.display_bridge: -# box = col.column(align=True).box().column() - #box.prop(self, "mode") - - # top row -# col_top = box.column(align=True) -# row = col_top.row(align=True) -# col_left = row.column(align=True) -# col_right = row.column(align=True) -# col_right.active = lt.bridge_segments != 1 -# col_left.prop(lt, "bridge_segments") -# col_right.prop(lt, "bridge_min_width", text="") -# # bottom row -# bottom_left = col_left.row() -# bottom_left.active = lt.bridge_segments != 1 -# bottom_left.prop(lt, "bridge_interpolation", text="") -# bottom_right = col_right.row() -# bottom_right.active = lt.bridge_interpolation == 'cubic' -# bottom_right.prop(lt, "bridge_cubic_strength") - # boolean properties -# col_top.prop(lt, "bridge_remove_faces") - - # override properties -# col_top.separator() -# row = box.row(align = True) -# row.prop(lt, "bridge_twist") -# row.prop(lt, "bridge_reverse") - - # circle - first line - split = col.split(percentage=0.15) - if lt.display_circle: - split.prop(lt, "display_circle", text="", icon='DOWNARROW_HLT') - else: - split.prop(lt, "display_circle", text="", icon='RIGHTARROW') - split.operator("mesh.looptools_circle") - # circle - settings - if lt.display_circle: - box = col.column(align=True).box().column() - box.prop(lt, "circle_fit") - box.separator() - - box.prop(lt, "circle_flatten") - row = box.row(align=True) - row.prop(lt, "circle_custom_radius") - row_right = row.row(align=True) - row_right.active = lt.circle_custom_radius - row_right.prop(lt, "circle_radius", text="") - box.prop(lt, "circle_regular") - box.separator() - - box.prop(lt, "circle_influence") - - # curve - first line - split = col.split(percentage=0.15) - if lt.display_curve: - split.prop(lt, "display_curve", text="", icon='DOWNARROW_HLT') - else: - split.prop(lt, "display_curve", text="", icon='RIGHTARROW') - split.operator("mesh.looptools_curve") - # curve - settings - if lt.display_curve: - box = col.column(align=True).box().column() - box.prop(lt, "curve_interpolation") - box.prop(lt, "curve_restriction") - box.prop(lt, "curve_boundaries") - box.prop(lt, "curve_regular") - box.separator() - - box.prop(lt, "curve_influence") - - # flatten - first line - split = col.split(percentage=0.15) - if lt.display_flatten: - split.prop(lt, "display_flatten", text="", icon='DOWNARROW_HLT') - else: - split.prop(lt, "display_flatten", text="", icon='RIGHTARROW') - split.operator("mesh.looptools_flatten") - # flatten - settings - if lt.display_flatten: - box = col.column(align=True).box().column() - box.prop(lt, "flatten_plane") - #box.prop(lt, "flatten_restriction") - box.separator() - - box.prop(lt, "flatten_influence") - - # loft - first line -# split = col.split(percentage=0.15) -# if lt.display_loft: -# split.prop(lt, "display_loft", text="", icon='DOWNARROW_HLT') -# else: -# split.prop(lt, "display_loft", text="", icon='RIGHTARROW') -# split.operator("mesh.looptools_bridge", text="Loft").loft = True -# # loft - settings -# if lt.display_loft: -# box = col.column(align=True).box().column() -# #box.prop(self, "mode") -# -# # top row -# col_top = box.column(align=True) -# row = col_top.row(align=True) -# col_left = row.column(align=True) -# col_right = row.column(align=True) -# col_right.active = lt.bridge_segments != 1 -# col_left.prop(lt, "bridge_segments") -# col_right.prop(lt, "bridge_min_width", text="") -# # bottom row -# bottom_left = col_left.row() -# bottom_left.active = lt.bridge_segments != 1 -# bottom_left.prop(lt, "bridge_interpolation", text="") -# bottom_right = col_right.row() -# bottom_right.active = lt.bridge_interpolation == 'cubic' -# bottom_right.prop(lt, "bridge_cubic_strength") -# # boolean properties -# col_top.prop(lt, "bridge_remove_faces") -# col_top.prop(lt, "bridge_loft_loop") -# -# # override properties -# col_top.separator() -# row = box.row(align = True) -# row.prop(lt, "bridge_twist") -# row.prop(lt, "bridge_reverse") - - # relax - first line - split = col.split(percentage=0.15) - if lt.display_relax: - split.prop(lt, "display_relax", text="", icon='DOWNARROW_HLT') - else: - split.prop(lt, "display_relax", text="", icon='RIGHTARROW') - split.operator("mesh.looptools_relax") - # relax - settings - if lt.display_relax: - box = col.column(align=True).box().column() - box.prop(lt, "relax_interpolation") - box.prop(lt, "relax_input") - box.prop(lt, "relax_iterations") - box.prop(lt, "relax_regular") - - # space - first line - split = col.split(percentage=0.15) - if lt.display_space: - split.prop(lt, "display_space", text="", icon='DOWNARROW_HLT') - else: - split.prop(lt, "display_space", text="", icon='RIGHTARROW') - split.operator("mesh.looptools_space") - # space - settings - if lt.display_space: - box = col.column(align=True).box().column() - box.prop(lt, "space_interpolation") - box.prop(lt, "space_input") - box.separator() - - box.prop(lt, "space_influence") - - -# property group containing all properties for the gui in the panel -class LoopToolsProps(bpy.types.PropertyGroup): - """ - Fake module like class - bpy.context.window_manager.looptools - """ - - # general display properties -# display_bridge = bpy.props.BoolProperty(name = "Bridge settings", -# description = "Display settings of the Bridge tool", -# default = False) - display_circle = bpy.props.BoolProperty(name = "Circle settings", - description = "Display settings of the Circle tool", - default = False) - display_curve = bpy.props.BoolProperty(name = "Curve settings", - description = "Display settings of the Curve tool", - default = False) - display_flatten = bpy.props.BoolProperty(name = "Flatten settings", - description = "Display settings of the Flatten tool", - default = False) -# display_loft = bpy.props.BoolProperty(name = "Loft settings", -# description = "Display settings of the Loft tool", -# default = False) - display_relax = bpy.props.BoolProperty(name = "Relax settings", - description = "Display settings of the Relax tool", - default = False) - display_space = bpy.props.BoolProperty(name = "Space settings", - description = "Display settings of the Space tool", - default = False) - - # bridge properties - bridge_cubic_strength = bpy.props.FloatProperty(name = "Strength", - description = "Higher strength results in more fluid curves", - default = 1.0, - soft_min = -3.0, - soft_max = 3.0) - bridge_interpolation = bpy.props.EnumProperty(name = "Interpolation mode", - items = (('cubic', "Cubic", "Gives curved results"), - ('linear', "Linear", "Basic, fast, straight interpolation")), - description = "Interpolation mode: algorithm used when creating "\ - "segments", - default = 'cubic') - bridge_loft = bpy.props.BoolProperty(name = "Loft", - description = "Loft multiple loops, instead of considering them as "\ - "a multi-input for bridging", - default = False) - bridge_loft_loop = bpy.props.BoolProperty(name = "Loop", - description = "Connect the first and the last loop with each other", - default = False) - bridge_min_width = bpy.props.IntProperty(name = "Minimum width", - description = "Segments with an edge smaller than this are merged "\ - "(compared to base edge)", - default = 0, - min = 0, - max = 100, - subtype = 'PERCENTAGE') - bridge_mode = bpy.props.EnumProperty(name = "Mode", - items = (('basic', "Basic", "Fast algorithm"), - ('shortest', "Shortest edge", "Slower algorithm with " \ - "better vertex matching")), - description = "Algorithm used for bridging", - default = 'shortest') - bridge_remove_faces = bpy.props.BoolProperty(name = "Remove faces", - description = "Remove faces that are internal after bridging", - default = True) - bridge_reverse = bpy.props.BoolProperty(name = "Reverse", - description = "Manually override the direction in which the loops "\ - "are bridged. Only use if the tool gives the wrong " \ - "result", - default = False) - bridge_segments = bpy.props.IntProperty(name = "Segments", - description = "Number of segments used to bridge the gap "\ - "(0 = automatic)", - default = 1, - min = 0, - soft_max = 20) - bridge_twist = bpy.props.IntProperty(name = "Twist", - description = "Twist what vertices are connected to each other", - default = 0) - - # circle properties - circle_custom_radius = bpy.props.BoolProperty(name = "Radius", - description = "Force a custom radius", - default = False) - circle_fit = bpy.props.EnumProperty(name = "Method", - items = (("best", "Best fit", "Non-linear least squares"), - ("inside", "Fit inside","Only move vertices towards the center")), - description = "Method used for fitting a circle to the vertices", - default = 'best') - circle_flatten = bpy.props.BoolProperty(name = "Flatten", - description = "Flatten the circle, instead of projecting it on the " \ - "mesh", - default = True) - circle_influence = bpy.props.FloatProperty(name = "Influence", - description = "Force of the tool", - default = 100.0, - min = 0.0, - max = 100.0, - precision = 1, - subtype = 'PERCENTAGE') - circle_radius = bpy.props.FloatProperty(name = "Radius", - description = "Custom radius for circle", - default = 1.0, - min = 0.0, - soft_max = 1000.0) - circle_regular = bpy.props.BoolProperty(name = "Regular", - description = "Distribute vertices at constant distances along the " \ - "circle", - default = True) - - # curve properties - curve_boundaries = bpy.props.BoolProperty(name = "Boundaries", - description = "Limit the tool to work within the boundaries of the "\ - "selected vertices", - default = False) - curve_influence = bpy.props.FloatProperty(name = "Influence", - description = "Force of the tool", - default = 100.0, - min = 0.0, - max = 100.0, - precision = 1, - subtype = 'PERCENTAGE') - curve_interpolation = bpy.props.EnumProperty(name = "Interpolation", - items = (("cubic", "Cubic", "Natural cubic spline, smooth results"), - ("linear", "Linear", "Simple and fast linear algorithm")), - description = "Algorithm used for interpolation", - default = 'cubic') - curve_regular = bpy.props.BoolProperty(name = "Regular", - description = "Distribute vertices at constant distances along the" \ - "curve", - default = True) - curve_restriction = bpy.props.EnumProperty(name = "Restriction", - items = (("none", "None", "No restrictions on vertex movement"), - ("extrude", "Extrude only","Only allow extrusions (no "\ - "indentations)"), - ("indent", "Indent only", "Only allow indentation (no "\ - "extrusions)")), - description = "Restrictions on how the vertices can be moved", - default = 'none') - - # flatten properties - flatten_influence = bpy.props.FloatProperty(name = "Influence", - description = "Force of the tool", - default = 100.0, - min = 0.0, - max = 100.0, - precision = 1, - subtype = 'PERCENTAGE') - flatten_plane = bpy.props.EnumProperty(name = "Plane", - items = (("best_fit", "Best fit", "Calculate a best fitting plane"), - ("normal", "Normal", "Derive plane from averaging vertex "\ - "normals"), - ("view", "View", "Flatten on a plane perpendicular to the "\ - "viewing angle")), - description = "Plane on which vertices are flattened", - default = 'best_fit') - flatten_restriction = bpy.props.EnumProperty(name = "Restriction", - items = (("none", "None", "No restrictions on vertex movement"), - ("bounding_box", "Bounding box", "Vertices are restricted to "\ - "movement inside the bounding box of the selection")), - description = "Restrictions on how the vertices can be moved", - default = 'none') - - # relax properties - relax_input = bpy.props.EnumProperty(name = "Input", - items = (("all", "Parallel (all)", "Also use non-selected "\ - "parallel loops as input"), - ("selected", "Selection","Only use selected vertices as input")), - description = "Loops that are relaxed", - default = 'selected') - relax_interpolation = bpy.props.EnumProperty(name = "Interpolation", - items = (("cubic", "Cubic", "Natural cubic spline, smooth results"), - ("linear", "Linear", "Simple and fast linear algorithm")), - description = "Algorithm used for interpolation", - default = 'cubic') - relax_iterations = bpy.props.EnumProperty(name = "Iterations", - items = (("1", "1", "One"), - ("3", "3", "Three"), - ("5", "5", "Five"), - ("10", "10", "Ten"), - ("25", "25", "Twenty-five")), - description = "Number of times the loop is relaxed", - default = "1") - relax_regular = bpy.props.BoolProperty(name = "Regular", - description = "Distribute vertices at constant distances along the" \ - "loop", - default = True) - - # space properties - space_influence = bpy.props.FloatProperty(name = "Influence", - description = "Force of the tool", - default = 100.0, - min = 0.0, - max = 100.0, - precision = 1, - subtype = 'PERCENTAGE') - space_input = bpy.props.EnumProperty(name = "Input", - items = (("all", "Parallel (all)", "Also use non-selected "\ - "parallel loops as input"), - ("selected", "Selection","Only use selected vertices as input")), - description = "Loops that are spaced", - default = 'selected') - space_interpolation = bpy.props.EnumProperty(name = "Interpolation", - items = (("cubic", "Cubic", "Natural cubic spline, smooth results"), - ("linear", "Linear", "Vertices are projected on existing edges")), - description = "Algorithm used for interpolation", - default = 'cubic') - - -# draw function for integration in menus -def menu_func(self, context): - self.layout.menu("VIEW3D_MT_edit_mesh_looptools") - self.layout.separator() - - -# define classes for registration -classes = [VIEW3D_MT_edit_mesh_looptools, - VIEW3D_PT_tools_looptools, - LoopToolsProps, - Bridge, - Circle, - Curve, - Flatten, - Relax, - Space] - - -# registering and menu integration -def register(): - for c in classes: - bpy.utils.register_class(c) - bpy.types.VIEW3D_MT_edit_mesh_specials.prepend(menu_func) - bpy.types.WindowManager.looptools = bpy.props.PointerProperty(\ - type = LoopToolsProps) - - -# unregistering and removing menus -def unregister(): - for c in classes: - bpy.utils.unregister_class(c) - bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func) - try: - del bpy.types.WindowManager.looptools - except: - pass - - -if __name__ == "__main__": - register()