diff --git a/mesh_looptools.py b/mesh_looptools.py new file mode 100644 index 0000000000000000000000000000000000000000..76b471507d839587170a82a338c1a63e69f8907e --- /dev/null +++ b/mesh_looptools.py @@ -0,0 +1,3712 @@ +# ##### 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 ##### + +# <pep8 compliant> + +bl_info = { + 'name': "LoopTools", + 'author': "Bart Crouch", + 'version': (3, 2, 0), + 'blender': (2, 5, 7), + 'api': 35979, + 'location': "View3D > Toolbar and View3D > Specials (W-key)", + 'warning': "", + '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 = [mathutils.Vector(mesh_mod.vertices[v].co[:]) 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[0][1] += (loc[0]-x)*(loc[1]-y) + mat[0][2] += (loc[0]-x)*(loc[2]-z) + mat[1][0] += (loc[1]-y)*(loc[0]-x) + mat[1][1] += (loc[1]-y)**2 + mat[1][2] += (loc[1]-y)*(loc[2]-z) + mat[2][0] += (loc[2]-z)*(loc[0]-x) + mat[2][1] += (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: + itermax = 500 + iter = 0 + vec = mathutils.Vector([1.0, 1.0, 1.0]) + vec2 = (vec*mat)/(vec*mat).length + while vec != vec2 and iter<itermax: + iter += 1 + vec = vec2 + vec2 = (vec*mat)/(vec*mat).length + 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 = mathutils.Vector([0.0, 0.0, 1.0]) * rotation + if object: + normal *= object.matrix_world.inverted().to_euler().to_matrix() + + 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 = [(mathutils.Vector(mesh.vertices[lines[i][0]].co) - \ + mathutils.Vector(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([0,0,0]) + 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[0][1] += (loc[0]-x)*(loc[1]-y) + mat[0][2] += (loc[0]-x)*(loc[2]-z) + mat[1][0] += (loc[1]-y)*(loc[0]-x) + mat[1][1] += (loc[1]-y)**2 + mat[1][2] += (loc[1]-y)*(loc[2]-z) + mat[2][0] += (loc[2]-z)*(loc[0]-x) + mat[2][1] += (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: + itermax = 500 + iter = 0 + vec = mathutils.Vector([1.0, 1.0, 1.0]) + vec2 = (vec*mat)/(vec*mat).length + while vec != vec2 and iter<itermax: + iter+=1 + vec = vec2 + vec2 = (vec*mat)/(vec*mat).length + 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 = [[((mesh.vertices[vertex].co - center1) * \ + rotation_matrix).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 = [((mesh.vertices[loop1[-1]].co - \ + center1) * rotation_matrix).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 = [(mathutils.Vector(mesh.vertices[loop1[i+1]].\ + co) - mathutils.Vector(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 = [(mathutils.Vector(mesh.vertices[loop1[i+1]].\ + co) - mathutils.Vector(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([0, 0, 0]) + 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([0, 0, 0]) + face_center = mathutils.Vector([0, 0, 0]) + 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([0, 0, 0]) + 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([0, 0, 0]) + center = mathutils.Vector([0, 0, 0]) + 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 = [[mathutils.Vector(v.co[:]) - \ + (mathutils.Vector(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[0][1] += jmat[i][0]*jmat[i][1] + jmat2[0][2] += jmat[i][0]*jmat[i][2] + jmat2[1][1] += jmat[i][1]**2 + jmat2[1][2] += jmat[i][1]*jmat[i][2] + jmat2[2][2] += jmat[i][2]**2 + jmat2[1][0] = jmat2[0][1] + jmat2[2][0] = jmat2[0][2] + jmat2[2][1] = jmat2[1][2] + try: + jmat2.invert() + except: + pass + dx0, dy0, dr = k2 * jmat2 + 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()