5245 lines
186 KiB
Python
5245 lines
186 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
#
|
|
# Contributed to Germano Cavalcante (mano-wii), Florian Meyer (testscreenings),
|
|
# Brendon Murphy (meta-androcto),
|
|
# Maintainer: Vladimir Spivak (cwolf3d)
|
|
# Originally an addon by Bart Crouch
|
|
|
|
bl_info = {
|
|
"name": "LoopTools",
|
|
"author": "Bart Crouch, Vladimir Spivak (cwolf3d)",
|
|
"version": (4, 7, 7),
|
|
"blender": (2, 80, 0),
|
|
"location": "View3D > Sidebar > Edit Tab / Edit Mode Context Menu",
|
|
"warning": "",
|
|
"description": "Mesh modelling toolkit. Several tools to aid modelling",
|
|
"doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/looptools.html",
|
|
"category": "Mesh",
|
|
}
|
|
|
|
|
|
import bmesh
|
|
import bpy
|
|
import collections
|
|
import mathutils
|
|
import math
|
|
from bpy_extras import view3d_utils
|
|
from bpy.types import (
|
|
Operator,
|
|
Menu,
|
|
Panel,
|
|
PropertyGroup,
|
|
AddonPreferences,
|
|
)
|
|
from bpy.props import (
|
|
BoolProperty,
|
|
EnumProperty,
|
|
FloatProperty,
|
|
IntProperty,
|
|
PointerProperty,
|
|
StringProperty,
|
|
)
|
|
|
|
# ########################################
|
|
# ##### General functions ################
|
|
# ########################################
|
|
|
|
# used by all tools to improve speed on reruns Unlink
|
|
looptools_cache = {}
|
|
|
|
|
|
def get_strokes(self, context):
|
|
looptools = context.window_manager.looptools
|
|
if looptools.gstretch_use_guide == "Annotation":
|
|
try:
|
|
strokes = bpy.data.grease_pencils[0].layers.active.active_frame.strokes
|
|
return True
|
|
except:
|
|
self.report({'WARNING'}, "active Annotation strokes not found")
|
|
return False
|
|
if looptools.gstretch_use_guide == "GPencil" and not looptools.gstretch_guide == None:
|
|
try:
|
|
strokes = looptools.gstretch_guide.data.layers.active.active_frame.strokes
|
|
return True
|
|
except:
|
|
self.report({'WARNING'}, "active GPencil strokes not found")
|
|
return False
|
|
else:
|
|
return False
|
|
|
|
# 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, bm, 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 bm.verts 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, bm, 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 bm.verts 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(bm_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 = [bm_mod.verts[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(bm_mod, tknots, knots):
|
|
splines = []
|
|
for i in range(len(knots) - 1):
|
|
a = bm_mod.verts[knots[i]].co
|
|
b = bm_mod.verts[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(bm_mod, loop, method="best_fit", object=False):
|
|
# getting the vertex locations
|
|
locs = [bm_mod.verts[v].co.copy() for v in loop[0]]
|
|
|
|
# calculating the center of masss
|
|
com = mathutils.Vector()
|
|
for loc in locs:
|
|
com += loc
|
|
com /= len(locs)
|
|
x, y, z = com
|
|
|
|
if method == 'best_fit':
|
|
# creating the covariance matrix
|
|
mat = mathutils.Matrix(((0.0, 0.0, 0.0),
|
|
(0.0, 0.0, 0.0),
|
|
(0.0, 0.0, 0.0),
|
|
))
|
|
for loc in locs:
|
|
mat[0][0] += (loc[0] - x) ** 2
|
|
mat[1][0] += (loc[0] - x) * (loc[1] - y)
|
|
mat[2][0] += (loc[0] - x) * (loc[2] - z)
|
|
mat[0][1] += (loc[1] - y) * (loc[0] - x)
|
|
mat[1][1] += (loc[1] - y) ** 2
|
|
mat[2][1] += (loc[1] - y) * (loc[2] - z)
|
|
mat[0][2] += (loc[2] - z) * (loc[0] - x)
|
|
mat[1][2] += (loc[2] - z) * (loc[1] - y)
|
|
mat[2][2] += (loc[2] - z) ** 2
|
|
|
|
# calculating the normal to the plane
|
|
normal = False
|
|
try:
|
|
mat = matrix_invert(mat)
|
|
except:
|
|
ax = 2
|
|
if math.fabs(sum(mat[0])) < math.fabs(sum(mat[1])):
|
|
if math.fabs(sum(mat[0])) < math.fabs(sum(mat[2])):
|
|
ax = 0
|
|
elif math.fabs(sum(mat[1])) < math.fabs(sum(mat[2])):
|
|
ax = 1
|
|
if ax == 0:
|
|
normal = mathutils.Vector((1.0, 0.0, 0.0))
|
|
elif ax == 1:
|
|
normal = mathutils.Vector((0.0, 1.0, 0.0))
|
|
else:
|
|
normal = mathutils.Vector((0.0, 0.0, 1.0))
|
|
if not normal:
|
|
# warning! this is different from .normalize()
|
|
itermax = 500
|
|
vec2 = mathutils.Vector((1.0, 1.0, 1.0))
|
|
for i in range(itermax):
|
|
vec = vec2
|
|
vec2 = mat @ vec
|
|
# Calculate length with double precision to avoid problems with `inf`
|
|
vec2_length = math.sqrt(vec2[0] ** 2 + vec2[1] ** 2 + vec2[2] ** 2)
|
|
if vec2_length != 0:
|
|
vec2 /= vec2_length
|
|
if vec2 == vec:
|
|
break
|
|
if vec2.length == 0:
|
|
vec2 = mathutils.Vector((1.0, 1.0, 1.0))
|
|
normal = vec2
|
|
|
|
elif method == 'normal':
|
|
# averaging the vertex normals
|
|
v_normals = [bm_mod.verts[v].normal for v in loop[0]]
|
|
normal = mathutils.Vector()
|
|
for v_normal in v_normals:
|
|
normal += v_normal
|
|
normal /= len(v_normals)
|
|
normal.normalize()
|
|
|
|
elif method == 'view':
|
|
# calculate view normal
|
|
rotation = bpy.context.space_data.region_3d.view_matrix.to_3x3().\
|
|
inverted()
|
|
normal = rotation @ mathutils.Vector((0.0, 0.0, 1.0))
|
|
if object:
|
|
normal = object.matrix_world.inverted().to_euler().to_matrix() @ \
|
|
normal
|
|
|
|
return(com, normal)
|
|
|
|
|
|
# calculate splines based on given interpolation method (controller function)
|
|
def calculate_splines(interpolation, bm_mod, tknots, knots):
|
|
if interpolation == 'cubic':
|
|
splines = calculate_cubic_splines(bm_mod, tknots, knots[:])
|
|
else: # interpolations == 'linear'
|
|
splines = calculate_linear_splines(bm_mod, tknots, knots[:])
|
|
|
|
return(splines)
|
|
|
|
|
|
# check loops and only return valid ones
|
|
def check_loops(loops, mapping, bm_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 (bm_mod.verts[loop[i]].co - bm_mod.verts[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: bmesh, output: dict with the edge-key as key and face-index as value
|
|
def dict_edge_faces(bm):
|
|
edge_faces = dict([[edgekey(edge), []] for edge in bm.edges if not edge.hide])
|
|
for face in bm.faces:
|
|
if face.hide:
|
|
continue
|
|
for key in face_edgekeys(face):
|
|
edge_faces[key].append(face.index)
|
|
|
|
return(edge_faces)
|
|
|
|
|
|
# input: bmesh (edge-faces optional), output: dict with face-face connections
|
|
def dict_face_faces(bm, edge_faces=False):
|
|
if not edge_faces:
|
|
edge_faces = dict_edge_faces(bm)
|
|
|
|
connected_faces = dict([[face.index, []] for face in bm.faces if not face.hide])
|
|
for face in bm.faces:
|
|
if face.hide:
|
|
continue
|
|
for edge_key in face_edgekeys(face):
|
|
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: bmesh, output: dict with the vert index as key and edge-keys as value
|
|
def dict_vert_edges(bm):
|
|
vert_edges = dict([[v.index, []] for v in bm.verts if not v.hide])
|
|
for edge in bm.edges:
|
|
if edge.hide:
|
|
continue
|
|
ek = edgekey(edge)
|
|
for vert in ek:
|
|
vert_edges[vert].append(ek)
|
|
|
|
return(vert_edges)
|
|
|
|
|
|
# input: bmesh, output: dict with the vert index as key and face index as value
|
|
def dict_vert_faces(bm):
|
|
vert_faces = dict([[v.index, []] for v in bm.verts if not v.hide])
|
|
for face in bm.faces:
|
|
if not face.hide:
|
|
for vert in face.verts:
|
|
vert_faces[vert.index].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)
|
|
|
|
|
|
# return the edgekey ([v1.index, v2.index]) of a bmesh edge
|
|
def edgekey(edge):
|
|
return(tuple(sorted([edge.verts[0].index, edge.verts[1].index])))
|
|
|
|
|
|
# returns the edgekeys of a bmesh face
|
|
def face_edgekeys(face):
|
|
return([tuple(sorted([edge.verts[0].index, edge.verts[1].index])) for edge in face.edges])
|
|
|
|
|
|
# calculate input loops
|
|
def get_connected_input(object, bm, not_use_mirror, input):
|
|
# get mesh with modifiers applied
|
|
derived, bm_mod = get_derived_bmesh(object, bm, not_use_mirror)
|
|
|
|
# calculate selected loops
|
|
edge_keys = [edgekey(edge) for edge in bm_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, bm_mod, loops)
|
|
# elif input == 'all':
|
|
loops = get_parallel_loops(bm_mod, loops)
|
|
|
|
return(derived, bm_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_bmesh(object, bm, not_use_mirror):
|
|
# 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]
|
|
merge = []
|
|
for mod in object.modifiers:
|
|
if mod.type != 'MIRROR':
|
|
mod.show_viewport = False
|
|
#leave the merge points untouched
|
|
if mod.type == 'MIRROR':
|
|
merge.append(mod.use_mirror_merge)
|
|
if not_use_mirror:
|
|
mod.use_mirror_merge = False
|
|
# get derived mesh
|
|
bm_mod = bmesh.new()
|
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
|
object_eval = object.evaluated_get(depsgraph)
|
|
mesh_mod = object_eval.to_mesh()
|
|
bm_mod.from_mesh(mesh_mod)
|
|
object_eval.to_mesh_clear()
|
|
# re-enable other modifiers
|
|
for mod_name in show_viewport:
|
|
object.modifiers[mod_name].show_viewport = True
|
|
merge.reverse()
|
|
for mod in object.modifiers:
|
|
if mod.type == 'MIRROR':
|
|
mod.use_mirror_merge = merge.pop()
|
|
# no mirror modifiers, so no derived mesh necessary
|
|
else:
|
|
derived = False
|
|
bm_mod = bm
|
|
|
|
bm_mod.verts.ensure_lookup_table()
|
|
bm_mod.edges.ensure_lookup_table()
|
|
bm_mod.faces.ensure_lookup_table()
|
|
|
|
return(derived, bm_mod)
|
|
|
|
|
|
# return a mapping of derived indices to indices
|
|
def get_mapping(derived, bm, bm_mod, single_vertices, full_search, loops):
|
|
if not derived:
|
|
return(False)
|
|
|
|
if full_search:
|
|
verts = [v for v in bm.verts if not v.hide]
|
|
else:
|
|
verts = [v for v in bm.verts 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 = [bm_mod.verts[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 bm.faces if not face.select and not face.hide]:
|
|
for vert in face.verts:
|
|
if vert.index in real_singles:
|
|
for v in face.verts:
|
|
if v.index not in verts_indices:
|
|
if v not in verts:
|
|
verts.append(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 = [bm_mod.verts[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)
|
|
|
|
|
|
# calculate the determinant of a matrix
|
|
def matrix_determinant(m):
|
|
determinant = m[0][0] * m[1][1] * m[2][2] + m[0][1] * m[1][2] * m[2][0] \
|
|
+ m[0][2] * m[1][0] * m[2][1] - m[0][2] * m[1][1] * m[2][0] \
|
|
- m[0][1] * m[1][0] * m[2][2] - m[0][0] * m[1][2] * m[2][1]
|
|
|
|
return(determinant)
|
|
|
|
|
|
# custom matrix inversion, to provide higher precision than the built-in one
|
|
def matrix_invert(m):
|
|
r = mathutils.Matrix((
|
|
(m[1][1] * m[2][2] - m[1][2] * m[2][1], m[0][2] * m[2][1] - m[0][1] * m[2][2],
|
|
m[0][1] * m[1][2] - m[0][2] * m[1][1]),
|
|
(m[1][2] * m[2][0] - m[1][0] * m[2][2], m[0][0] * m[2][2] - m[0][2] * m[2][0],
|
|
m[0][2] * m[1][0] - m[0][0] * m[1][2]),
|
|
(m[1][0] * m[2][1] - m[1][1] * m[2][0], m[0][1] * m[2][0] - m[0][0] * m[2][1],
|
|
m[0][0] * m[1][1] - m[0][1] * m[1][0])))
|
|
|
|
return (r * (1 / matrix_determinant(m)))
|
|
|
|
|
|
# returns a list of all loops parallel to the input, input included
|
|
def get_parallel_loops(bm_mod, loops):
|
|
# get required dictionaries
|
|
edge_faces = dict_edge_faces(bm_mod)
|
|
connected_faces = dict_face_faces(bm_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 i not 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 face_edgekeys(bm_mod.faces[fi]):
|
|
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():
|
|
object = bpy.context.active_object
|
|
if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
|
|
# ensure that selection is synced for the derived mesh
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
bm = bmesh.from_edit_mesh(object.data)
|
|
|
|
bm.verts.ensure_lookup_table()
|
|
bm.edges.ensure_lookup_table()
|
|
bm.faces.ensure_lookup_table()
|
|
|
|
return(object, bm)
|
|
|
|
|
|
# move the vertices to their new locations
|
|
def move_verts(object, bm, mapping, move, lock, influence):
|
|
if lock:
|
|
lock_x, lock_y, lock_z = lock
|
|
orient_slot = bpy.context.scene.transform_orientation_slots[0]
|
|
custom = orient_slot.custom_orientation
|
|
if custom:
|
|
mat = custom.matrix.to_4x4().inverted() @ object.matrix_world.copy()
|
|
elif orient_slot.type == 'LOCAL':
|
|
mat = mathutils.Matrix.Identity(4)
|
|
elif orient_slot.type == 'VIEW':
|
|
mat = bpy.context.region_data.view_matrix.copy() @ \
|
|
object.matrix_world.copy()
|
|
else: # orientation == 'GLOBAL'
|
|
mat = object.matrix_world.copy()
|
|
mat_inv = mat.inverted()
|
|
|
|
# get all mirror vectors
|
|
mirror_Vectors = []
|
|
if object.data.use_mirror_x:
|
|
mirror_Vectors.append(mathutils.Vector((-1, 1, 1)))
|
|
if object.data.use_mirror_y:
|
|
mirror_Vectors.append(mathutils.Vector((1, -1, 1)))
|
|
if object.data.use_mirror_x and object.data.use_mirror_y:
|
|
mirror_Vectors.append(mathutils.Vector((-1, -1, 1)))
|
|
z_mirror_Vectors = []
|
|
if object.data.use_mirror_z:
|
|
for v in mirror_Vectors:
|
|
z_mirror_Vectors.append(mathutils.Vector((1, 1, -1)) * v)
|
|
mirror_Vectors.extend(z_mirror_Vectors)
|
|
mirror_Vectors.append(mathutils.Vector((1, 1, -1)))
|
|
|
|
for loop in move:
|
|
for index, loc in loop:
|
|
if mapping:
|
|
if mapping[index] == -1:
|
|
continue
|
|
else:
|
|
index = mapping[index]
|
|
if lock:
|
|
delta = (loc - bm.verts[index].co) @ mat_inv
|
|
if lock_x:
|
|
delta[0] = 0
|
|
if lock_y:
|
|
delta[1] = 0
|
|
if lock_z:
|
|
delta[2] = 0
|
|
delta = delta @ mat
|
|
loc = bm.verts[index].co + delta
|
|
if influence < 0:
|
|
new_loc = loc
|
|
else:
|
|
new_loc = loc * (influence / 100) + \
|
|
bm.verts[index].co * ((100 - influence) / 100)
|
|
|
|
for mirror_Vector in mirror_Vectors:
|
|
for vert in bm.verts:
|
|
if vert.co == mirror_Vector * bm.verts[index].co:
|
|
vert.co = mirror_Vector * new_loc
|
|
|
|
bm.verts[index].co = new_loc
|
|
|
|
bm.normal_update()
|
|
object.data.update()
|
|
|
|
bm.verts.ensure_lookup_table()
|
|
bm.edges.ensure_lookup_table()
|
|
bm.faces.ensure_lookup_table()
|
|
|
|
|
|
# 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():
|
|
# update editmesh cached data
|
|
obj = bpy.context.active_object
|
|
if obj.mode == 'EDIT':
|
|
bmesh.update_edit_mesh(obj.data, loop_triangles=True, destructive=True)
|
|
|
|
|
|
# ########################################
|
|
# ##### Bridge functions #################
|
|
# ########################################
|
|
|
|
# calculate a cubic spline through the middle section of 4 given coordinates
|
|
def bridge_calculate_cubic_spline(bm, 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(bm, 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 = bm.verts[lines[line][0]].co
|
|
v2 = bm.verts[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 = bm.verts[line[0]].co
|
|
v2 = bm.verts[line[1]].co
|
|
size = (v2 - v1).length * cubic_strength
|
|
splines.append(bridge_calculate_cubic_spline(bm,
|
|
[v1 + size * vertex_normals[line[0]], v1, v2,
|
|
v2 + size * vertex_normals[line[1]]]))
|
|
else:
|
|
splines = False
|
|
|
|
# create starting situation
|
|
virtual_width = [(bm.verts[lines[i][0]].co -
|
|
bm.verts[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(bm, loops, mode, twist, reverse):
|
|
lines = []
|
|
loop1, loop2 = [i[0] for i in loops]
|
|
loop1_circular, loop2_circular = [i[1] for i in loops]
|
|
circular = loop1_circular or loop2_circular
|
|
circle_full = False
|
|
|
|
# calculate loop centers
|
|
centers = []
|
|
for loop in [loop1, loop2]:
|
|
center = mathutils.Vector()
|
|
for vertex in loop:
|
|
center += bm.verts[vertex].co
|
|
center /= len(loop)
|
|
centers.append(center)
|
|
for i, loop in enumerate([loop1, loop2]):
|
|
for vertex in loop:
|
|
if bm.verts[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 [bm.verts[vertex].co for vertex in loop]:
|
|
mat[0][0] += (loc[0] - x) ** 2
|
|
mat[1][0] += (loc[0] - x) * (loc[1] - y)
|
|
mat[2][0] += (loc[0] - x) * (loc[2] - z)
|
|
mat[0][1] += (loc[1] - y) * (loc[0] - x)
|
|
mat[1][1] += (loc[1] - y) ** 2
|
|
mat[2][1] += (loc[1] - y) * (loc[2] - z)
|
|
mat[0][2] += (loc[2] - z) * (loc[0] - x)
|
|
mat[1][2] += (loc[2] - z) * (loc[1] - y)
|
|
mat[2][2] += (loc[2] - z) ** 2
|
|
# plane normal
|
|
normal = False
|
|
if sum(mat[0]) < 1e-6 or sum(mat[1]) < 1e-6 or sum(mat[2]) < 1e-6:
|
|
normal_plurity = True
|
|
try:
|
|
mat.invert()
|
|
except:
|
|
if sum(mat[0]) == 0:
|
|
normal = mathutils.Vector((1.0, 0.0, 0.0))
|
|
elif sum(mat[1]) == 0:
|
|
normal = mathutils.Vector((0.0, 1.0, 0.0))
|
|
elif sum(mat[2]) == 0:
|
|
normal = mathutils.Vector((0.0, 0.0, 1.0))
|
|
if not normal:
|
|
# warning! this is different from .normalize()
|
|
itermax = 500
|
|
iter = 0
|
|
vec = mathutils.Vector((1.0, 1.0, 1.0))
|
|
vec2 = (mat @ vec) / (mat @ vec).length
|
|
while vec != vec2 and iter < itermax:
|
|
iter += 1
|
|
vec = vec2
|
|
vec2 = mat @ vec
|
|
if vec2.length != 0:
|
|
vec2 /= vec2.length
|
|
if vec2.length == 0:
|
|
vec2 = mathutils.Vector((1.0, 1.0, 1.0))
|
|
normal = vec2
|
|
normals.append(normal)
|
|
# have plane normals face in the same direction (maximum angle: 90 degrees)
|
|
if ((center1 + normals[0]) - center2).length < \
|
|
((center1 - normals[0]) - center2).length:
|
|
normals[0].negate()
|
|
if ((center2 + normals[1]) - center1).length > \
|
|
((center2 - normals[1]) - center1).length:
|
|
normals[1].negate()
|
|
|
|
# rotation matrix, representing the difference between the plane normals
|
|
axis = normals[0].cross(normals[1])
|
|
axis = mathutils.Vector([loc if abs(loc) > 1e-8 else 0 for loc in axis])
|
|
if axis.angle(mathutils.Vector((0, 0, 1)), 0) > 1.5707964:
|
|
axis.negate()
|
|
angle = normals[0].dot(normals[1])
|
|
rotation_matrix = mathutils.Matrix.Rotation(angle, 4, axis)
|
|
|
|
# if circular, rotate loops so they are aligned
|
|
if circular:
|
|
# make sure loop1 is the circular one (or both are circular)
|
|
if loop2_circular and not loop1_circular:
|
|
loop1_circular, loop2_circular = True, False
|
|
loop1, loop2 = loop2, loop1
|
|
|
|
# match start vertex of loop1 with loop2
|
|
target_vector = bm.verts[loop2[0]].co - center2
|
|
dif_angles = [[(rotation_matrix @ (bm.verts[vertex].co - center1)
|
|
).angle(target_vector, 0), False, i] for
|
|
i, vertex in enumerate(loop1)]
|
|
dif_angles.sort()
|
|
if len(loop1) != len(loop2):
|
|
angle_limit = dif_angles[0][0] * 1.2 # 20% margin
|
|
dif_angles = [
|
|
[(bm.verts[loop2[0]].co -
|
|
bm.verts[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 = [
|
|
(bm.verts[loop1[1]].co - center1).angle(
|
|
bm.verts[loop2[i]].co - center2) for i in [0, 1, -1]
|
|
]
|
|
last_to_first, last_to_second = [
|
|
(bm.verts[loop1[-1]].co -
|
|
center1).angle(bm.verts[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 = (bm.verts[loop1[0]].co - center1).\
|
|
cross(bm.verts[loop1[1]].co - center1).angle(normals[0], 0)
|
|
target_angle = (bm.verts[loop2[0]].co - center2).\
|
|
cross(bm.verts[loop2[1]].co - center2).angle(normals[1], 0)
|
|
limit = 1.5707964 # 0.5*pi, 90 degrees
|
|
if not ((angle > limit and target_angle > limit) or
|
|
(angle < limit and target_angle < limit)):
|
|
loop1.reverse()
|
|
if circular:
|
|
loop1 = [loop1[-1]] + loop1[:-1]
|
|
elif normals[0].angle(normals[1]) > limit:
|
|
loop1.reverse()
|
|
if circular:
|
|
loop1 = [loop1[-1]] + loop1[:-1]
|
|
|
|
# both loops have the same length
|
|
if len(loop1) == len(loop2):
|
|
# manual override
|
|
if twist:
|
|
if abs(twist) < len(loop1):
|
|
loop1 = loop1[twist:] + loop1[:twist]
|
|
if reverse:
|
|
loop1.reverse()
|
|
|
|
lines.append([loop1[0], loop2[0]])
|
|
for i in range(1, len(loop1)):
|
|
lines.append([loop1[i], loop2[i]])
|
|
|
|
# loops of different lengths
|
|
else:
|
|
# make loop1 longest loop
|
|
if len(loop2) > len(loop1):
|
|
loop1, loop2 = loop2, loop1
|
|
loop1_circular, loop2_circular = loop2_circular, loop1_circular
|
|
|
|
# manual override
|
|
if twist:
|
|
if abs(twist) < len(loop1):
|
|
loop1 = loop1[twist:] + loop1[:twist]
|
|
if reverse:
|
|
loop1.reverse()
|
|
|
|
# shortest angle difference doesn't always give correct start vertex
|
|
if loop1_circular and not loop2_circular:
|
|
shifting = 1
|
|
while shifting:
|
|
if len(loop1) - shifting < len(loop2):
|
|
shifting = False
|
|
break
|
|
to_last, to_first = [
|
|
(rotation_matrix @ (bm.verts[loop1[-1]].co - center1)).angle(
|
|
(bm.verts[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 = [(bm.verts[loop1[i + 1]].co -
|
|
bm.verts[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 = [(bm.verts[loop1[i + 1]].co -
|
|
bm.verts[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(bm, lines, loops, segments):
|
|
# return if amount of segments is set by user
|
|
if segments != 0:
|
|
return segments
|
|
|
|
# edge lengths
|
|
average_edge_length = [
|
|
(bm.verts[vertex].co -
|
|
bm.verts[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 += [
|
|
(bm.verts[loop[0][-1]].co -
|
|
bm.verts[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(
|
|
[(bm.verts[v1].co -
|
|
bm.verts[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(bm, lines, loops, edge_faces,
|
|
edgekey_to_edge):
|
|
if not edge_faces: # interpolation isn't set to cubic
|
|
return False
|
|
|
|
# pity reduce() isn't one of the basic functions in python anymore
|
|
def average_vector_dictionary(dic):
|
|
for key, vectors in dic.items():
|
|
# if type(vectors) == type([]) and len(vectors) > 1:
|
|
if len(vectors) > 1:
|
|
average = mathutils.Vector()
|
|
for vector in vectors:
|
|
average += vector
|
|
average /= len(vectors)
|
|
dic[key] = [average]
|
|
return dic
|
|
|
|
# get all edges of the loop
|
|
edges = [
|
|
[edgekey_to_edge[tuple(sorted([loops[j][0][i],
|
|
loops[j][0][i + 1]]))] for i in range(len(loops[j][0]) - 1)] for
|
|
j in [0, 1]
|
|
]
|
|
edges = edges[0] + edges[1]
|
|
for j in [0, 1]:
|
|
if loops[j][1]: # circular
|
|
edges.append(edgekey_to_edge[tuple(sorted([loops[j][0][0],
|
|
loops[j][0][-1]]))])
|
|
|
|
"""
|
|
calculation based on face topology (assign edge-normals to vertices)
|
|
|
|
edge_normal = face_normal x edge_vector
|
|
vertex_normal = average(edge_normals)
|
|
"""
|
|
vertex_normals = dict([(vertex, []) for vertex in loops[0][0] + loops[1][0]])
|
|
for edge in edges:
|
|
faces = edge_faces[edgekey(edge)] # valid faces connected to edge
|
|
|
|
if faces:
|
|
# get edge coordinates
|
|
v1, v2 = [bm.verts[edgekey(edge)[i]].co for i in [0, 1]]
|
|
edge_vector = v1 - v2
|
|
if edge_vector.length < 1e-4:
|
|
# zero-length edge, vertices at same location
|
|
continue
|
|
edge_center = (v1 + v2) / 2
|
|
|
|
# average face coordinates, if connected to more than 1 valid face
|
|
if len(faces) > 1:
|
|
face_normal = mathutils.Vector()
|
|
face_center = mathutils.Vector()
|
|
for face in faces:
|
|
face_normal += face.normal
|
|
face_center += face.calc_center_median()
|
|
face_normal /= len(faces)
|
|
face_center /= len(faces)
|
|
else:
|
|
face_normal = faces[0].normal
|
|
face_center = faces[0].calc_center_median()
|
|
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 edgekey(edge):
|
|
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 edgekey(edge):
|
|
if v in edge_vectors:
|
|
edge_vector = bm.verts[edgekey(edge)[0]].co - \
|
|
bm.verts[edgekey(edge)[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 = bm.verts[v1].co - bm.verts[v2].co
|
|
if new_vector.length < 1e-4:
|
|
# zero-length connection vector,
|
|
# vertices in different loops at same location
|
|
continue
|
|
if v1 in connection_vectors:
|
|
connection_vectors[v1].append(new_vector)
|
|
connections[v1].append(v2)
|
|
if v2 in connection_vectors:
|
|
connection_vectors[v2].append(new_vector)
|
|
connections[v2].append(v1)
|
|
connection_vectors = average_vector_dictionary(connection_vectors)
|
|
connection_vectors = dict(
|
|
[[vertex, vector[0]] if vector else
|
|
[vertex, []] for vertex, vector in connection_vectors.items()]
|
|
)
|
|
|
|
for vertex, values in edge_vectors.items():
|
|
# vertex normal doesn't matter, just assign a random vector to it
|
|
if not connection_vectors[vertex]:
|
|
vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
|
|
continue
|
|
|
|
# calculate to what location the vertex is connected,
|
|
# used to determine what way to flip the normal
|
|
connected_center = mathutils.Vector()
|
|
for v in connections[vertex]:
|
|
connected_center += bm.verts[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 - (bm.verts[vertex].co +
|
|
connection_vectors[vertex])).length < (connected_center -
|
|
(bm.verts[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 - (bm.verts[vertex].co +
|
|
vertex_normal)).length < (connected_center -
|
|
(bm.verts[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(bm, vertices):
|
|
for i in range(len(vertices)):
|
|
bm.verts.new(vertices[i])
|
|
bm.verts.ensure_lookup_table()
|
|
|
|
|
|
# add faces to mesh
|
|
def bridge_create_faces(object, bm, 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]
|
|
# result of converting from pre-bmesh period
|
|
if faces[i][-1] == faces[i][-2]:
|
|
faces[i] = faces[i][:-1]
|
|
|
|
new_faces = []
|
|
for i in range(len(faces)):
|
|
try:
|
|
new_faces.append(bm.faces.new([bm.verts[v] for v in faces[i]]))
|
|
except:
|
|
# face already exists
|
|
pass
|
|
bm.normal_update()
|
|
object.data.update(calc_edges=True) # calc_edges prevents memory-corruption
|
|
|
|
bm.verts.ensure_lookup_table()
|
|
bm.edges.ensure_lookup_table()
|
|
bm.faces.ensure_lookup_table()
|
|
|
|
return(new_faces)
|
|
|
|
|
|
# calculate input loops
|
|
def bridge_get_input(bm):
|
|
# create list of internal edges, which should be skipped
|
|
eks_of_selected_faces = [
|
|
item for sublist in [face_edgekeys(face) for
|
|
face in bm.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 = [
|
|
edgekey(edge) for edge in bm.edges if edge.select and
|
|
not edge.hide and edgekey(edge) not in internal_edges
|
|
]
|
|
loops = get_connected_selections(selected_edges)
|
|
|
|
return(loops)
|
|
|
|
|
|
# return values needed by the bridge operator
|
|
def bridge_initialise(bm, 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 bm.faces if face.select or
|
|
face.hide
|
|
]
|
|
edge_faces = dict(
|
|
[[edgekey(edge), []] for edge in bm.edges if not edge.hide]
|
|
)
|
|
for face in bm.faces:
|
|
if face.index in face_blacklist:
|
|
continue
|
|
for key in face_edgekeys(face):
|
|
edge_faces[key].append(face)
|
|
# dictionary with the edge-key as key and edge as value
|
|
edgekey_to_edge = dict(
|
|
[[edgekey(edge), edge] for edge in bm.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 bm.faces if face.select and not face.hide
|
|
]
|
|
|
|
# find out if faces created by bridging should be smoothed
|
|
smooth = False
|
|
if bm.faces:
|
|
if sum([face.smooth for face in bm.faces]) / len(bm.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(bm, loops):
|
|
# calculate average loop normals and centers
|
|
normals = []
|
|
centers = []
|
|
for vertices, circular in loops:
|
|
normal = mathutils.Vector()
|
|
center = mathutils.Vector()
|
|
for vertex in vertices:
|
|
normal += bm.verts[vertex].normal
|
|
center += bm.verts[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)
|
|
|
|
|
|
# remove old_selected_faces
|
|
def bridge_remove_internal_faces(bm, old_selected_faces):
|
|
# collect bmesh faces and internal bmesh edges
|
|
remove_faces = [bm.faces[face] for face in old_selected_faces]
|
|
edges = collections.Counter(
|
|
[edge.index for face in remove_faces for edge in face.edges]
|
|
)
|
|
remove_edges = [bm.edges[edge] for edge in edges if edges[edge] > 1]
|
|
|
|
# remove internal faces and edges
|
|
for face in remove_faces:
|
|
bm.faces.remove(face)
|
|
for edge in remove_edges:
|
|
bm.edges.remove(edge)
|
|
|
|
bm.faces.ensure_lookup_table()
|
|
bm.edges.ensure_lookup_table()
|
|
bm.verts.ensure_lookup_table()
|
|
|
|
|
|
# update list of internal faces that are flagged for removal
|
|
def bridge_save_unused_faces(bm, old_selected_faces, loops):
|
|
# key: vertex index, value: lists of selected faces using it
|
|
|
|
vertex_to_face = dict([[i, []] for i in range(len(bm.verts))])
|
|
[[vertex_to_face[vertex.index].append(face) for vertex in
|
|
bm.faces[face].verts] 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 bm.faces[grow_face].verts:
|
|
vertex_face_group = [
|
|
face for face in vertex_to_face[vertex.index] 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(bm.verts))])
|
|
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 bm.faces[face].verts:
|
|
if used_vertices[vertex.index]:
|
|
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(new_faces, smooth):
|
|
for face in new_faces:
|
|
face.select_set(True)
|
|
face.smooth = smooth
|
|
|
|
|
|
# sort loops, so they are connected in the correct order when lofting
|
|
def bridge_sort_loops(bm, loops, loft_loop):
|
|
# simplify loops to single points, and prepare for pathfinding
|
|
x, y, z = [
|
|
[sum([bm.verts[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)
|
|
|
|
|
|
# remapping old indices to new position in list
|
|
def bridge_update_old_selection(bm, old_selected_faces):
|
|
"""
|
|
old_indices = old_selected_faces[:]
|
|
old_selected_faces = []
|
|
for i, face in enumerate(bm.faces):
|
|
if face.index in old_indices:
|
|
old_selected_faces.append(i)
|
|
"""
|
|
old_selected_faces = [
|
|
i for i, face in enumerate(bm.faces) if face.index in old_selected_faces
|
|
]
|
|
|
|
return(old_selected_faces)
|
|
|
|
|
|
# ########################################
|
|
# ##### Circle functions #################
|
|
# ########################################
|
|
|
|
# convert 3d coordinates to 2d coordinates on plane
|
|
def circle_3d_to_2d(bm_mod, loop, com, normal):
|
|
# project vertices onto the plane
|
|
verts = [bm_mod.verts[v] for v in loop[0]]
|
|
verts_projected = [[v.co - (v.co - com).dot(normal) * normal, v.index]
|
|
for v in verts]
|
|
|
|
# calculate two vectors (p and q) along the plane
|
|
m = mathutils.Vector((normal[0] + 1.0, normal[1], normal[2]))
|
|
p = m - (m.dot(normal) * normal)
|
|
if p.dot(p) < 1e-6:
|
|
m = mathutils.Vector((normal[0], normal[1] + 1.0, normal[2]))
|
|
p = m - (m.dot(normal) * normal)
|
|
q = p.cross(normal)
|
|
|
|
# change to 2d coordinates using perpendicular projection
|
|
locs_2d = []
|
|
for loc, vert in verts_projected:
|
|
vloc = loc - com
|
|
x = p.dot(vloc) / p.dot(p)
|
|
y = q.dot(vloc) / q.dot(q)
|
|
locs_2d.append([x, y, vert])
|
|
|
|
return(locs_2d, p, q)
|
|
|
|
|
|
# calculate a best-fit circle to the 2d locations on the plane
|
|
def circle_calculate_best_fit(locs_2d):
|
|
# initial guess
|
|
x0 = 0.0
|
|
y0 = 0.0
|
|
r = 1.0
|
|
|
|
# calculate center and radius (non-linear least squares solution)
|
|
for iter in range(500):
|
|
jmat = []
|
|
k = []
|
|
for v in locs_2d:
|
|
d = (v[0] ** 2 - 2.0 * x0 * v[0] + v[1] ** 2 - 2.0 * y0 * v[1] + x0 ** 2 + y0 ** 2) ** 0.5
|
|
jmat.append([(x0 - v[0]) / d, (y0 - v[1]) / d, -1.0])
|
|
k.append(-(((v[0] - x0) ** 2 + (v[1] - y0) ** 2) ** 0.5 - r))
|
|
jmat2 = mathutils.Matrix(((0.0, 0.0, 0.0),
|
|
(0.0, 0.0, 0.0),
|
|
(0.0, 0.0, 0.0),
|
|
))
|
|
k2 = mathutils.Vector((0.0, 0.0, 0.0))
|
|
for i in range(len(jmat)):
|
|
k2 += mathutils.Vector(jmat[i]) * k[i]
|
|
jmat2[0][0] += jmat[i][0] ** 2
|
|
jmat2[1][0] += jmat[i][0] * jmat[i][1]
|
|
jmat2[2][0] += jmat[i][0] * jmat[i][2]
|
|
jmat2[1][1] += jmat[i][1] ** 2
|
|
jmat2[2][1] += jmat[i][1] * jmat[i][2]
|
|
jmat2[2][2] += jmat[i][2] ** 2
|
|
jmat2[0][1] = jmat2[1][0]
|
|
jmat2[0][2] = jmat2[2][0]
|
|
jmat2[1][2] = jmat2[2][1]
|
|
try:
|
|
jmat2.invert()
|
|
except:
|
|
pass
|
|
dx0, dy0, dr = jmat2 @ k2
|
|
x0 += dx0
|
|
y0 += dy0
|
|
r += dr
|
|
# stop iterating if we're close enough to optimal solution
|
|
if abs(dx0) < 1e-6 and abs(dy0) < 1e-6 and abs(dr) < 1e-6:
|
|
break
|
|
|
|
# return center of circle and radius
|
|
return(x0, y0, r)
|
|
|
|
|
|
# calculate circle so no vertices have to be moved away from the center
|
|
def circle_calculate_min_fit(locs_2d):
|
|
# center of circle
|
|
x0 = (min([i[0] for i in locs_2d]) + max([i[0] for i in locs_2d])) / 2.0
|
|
y0 = (min([i[1] for i in locs_2d]) + max([i[1] for i in locs_2d])) / 2.0
|
|
center = mathutils.Vector([x0, y0])
|
|
# radius of circle
|
|
r = min([(mathutils.Vector([i[0], i[1]]) - center).length for i in locs_2d])
|
|
|
|
# return center of circle and radius
|
|
return(x0, y0, r)
|
|
|
|
|
|
# calculate the new locations of the vertices that need to be moved
|
|
def circle_calculate_verts(flatten, bm_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(bm_mod)
|
|
vert_faces = dict_vert_faces(bm_mod)
|
|
faces = [f for f in bm_mod.faces if not f.hide]
|
|
rays = [normal, -normal]
|
|
new_locs = []
|
|
for loc in locs_3d:
|
|
projection = False
|
|
if bm_mod.verts[loc[0]].co == loc[1]: # vertex hasn't moved
|
|
projection = loc[1]
|
|
else:
|
|
dif = normal.angle(loc[1] - bm_mod.verts[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 = bm_mod.verts[loc[0]].co
|
|
else:
|
|
# quick search through adjacent faces
|
|
for face in vert_faces[loc[0]]:
|
|
verts = [v.co for v in bm_mod.faces[face].verts]
|
|
if len(verts) == 3: # triangle
|
|
v1, v2, v3 = verts
|
|
v4 = False
|
|
else: # assume quad
|
|
v1, v2, v3, v4 = verts[:4]
|
|
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 = bm_mod.verts[edgekey[0]].co
|
|
line2 = bm_mod.verts[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 = [v.co for v in face.verts]
|
|
if len(verts) == 3: # triangle
|
|
v1, v2, v3 = verts
|
|
v4 = False
|
|
else: # assume quad
|
|
v1, v2, v3, v4 = verts[:4]
|
|
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, bm_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(bm_mod.verts[loop[0]].co[:])
|
|
loc1 = mathutils.Vector(bm_mod.verts[loop[1]].co[:])
|
|
for v in loop[2:]:
|
|
locn = mathutils.Vector(bm_mod.verts[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(bm_mod, com, p, q, normal, single_loop):
|
|
new_locs = []
|
|
for vert in single_loop:
|
|
loc = mathutils.Vector(bm_mod.verts[vert].co[:])
|
|
new_locs.append([vert, loc - (loc - com).dot(normal) * normal])
|
|
|
|
return(new_locs)
|
|
|
|
|
|
# calculate input loops
|
|
def circle_get_input(object, bm):
|
|
# get mesh with modifiers applied
|
|
derived, bm_mod = get_derived_bmesh(object, bm, False)
|
|
|
|
# create list of edge-keys based on selection state
|
|
faces = False
|
|
for face in bm.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_edgekeys(face) for face in
|
|
bm_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 = [
|
|
edgekey(edge) for edge in bm_mod.edges if edge.select and
|
|
not edge.hide and edge_count.get(edgekey(edge), 1) == 1
|
|
]
|
|
else:
|
|
# no faces, so no internal edges either
|
|
edge_keys = [
|
|
edgekey(edge) for edge in bm_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
|
|
bm_mod.edges if edge.select and not edge.hide] for vert in
|
|
edgekey(edge)]
|
|
)
|
|
single_vertices = [
|
|
vert.index for vert in bm_mod.verts if
|
|
vert.select and not vert.hide and
|
|
not verts_connected.get(vert.index, False)
|
|
]
|
|
|
|
if single_vertices and len(bm.faces) > 0:
|
|
vert_to_single = dict(
|
|
[[v.index, []] for v in bm_mod.verts if not v.hide]
|
|
)
|
|
for face in [face for face in bm_mod.faces if not face.select and not face.hide]:
|
|
for vert in face.verts:
|
|
vert = vert.index
|
|
if vert in single_vertices:
|
|
for ek in face_edgekeys(face):
|
|
if vert not 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(bm.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, bm_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, angle):
|
|
for i in range(len(locs_2d)):
|
|
x, y, j = locs_2d[i]
|
|
loc = mathutils.Vector([x - x0, y - y0])
|
|
mat_rot = mathutils.Matrix.Rotation(angle, 2, 'X')
|
|
loc.rotate(mat_rot)
|
|
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, angle):
|
|
# 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 + angle) * r
|
|
y = math.sin(t + angle) * 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(bm_mod, loop, com):
|
|
verts, circular = loop
|
|
distances = [
|
|
[(bm_mod.verts[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(bm_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(bm_mod.verts[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(bm_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, bm_mod.verts[p].co])
|
|
continue
|
|
oldloc = bm_mod.verts[p].co
|
|
normal = bm_mod.verts[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(bm_mod, loops):
|
|
cut_loops = []
|
|
for loop, circular in loops:
|
|
if circular:
|
|
selected = [bm_mod.verts[v].select for v in loop]
|
|
first = selected.index(True)
|
|
selected.reverse()
|
|
last = -selected.index(True)
|
|
if last == 0:
|
|
if len(loop[first:]) < len(loop)/2:
|
|
cut_loops.append([loop[first:], False])
|
|
else:
|
|
if len(loop[first:last]) < len(loop)/2:
|
|
cut_loops.append([loop[first:last], False])
|
|
continue
|
|
selected = [bm_mod.verts[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, bm, boundaries):
|
|
# get mesh with modifiers applied
|
|
derived, bm_mod = get_derived_bmesh(object, bm, False)
|
|
|
|
# vertices that still need a loop to run through it
|
|
verts_unsorted = [
|
|
v.index for v in bm_mod.verts if v.select and not v.hide
|
|
]
|
|
# necessary dictionaries
|
|
vert_edges = dict_vert_edges(bm_mod)
|
|
edge_faces = dict_edge_faces(bm_mod)
|
|
correct_loops = []
|
|
# find loops through each selected vertex
|
|
while len(verts_unsorted) > 0:
|
|
loops = curve_vertex_loops(bm_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 bm_mod.verts[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(bm_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(bm_mod, correct_loops)
|
|
|
|
return(derived, bm_mod, correct_loops)
|
|
|
|
|
|
# return all loops that are perpendicular to the given one
|
|
def curve_perpendicular_loops(bm_mod, start_loop, vert_edges, edge_faces):
|
|
# find perpendicular loops
|
|
perp_loops = []
|
|
for start_vert in start_loop:
|
|
loops = curve_vertex_loops(bm_mod, start_vert, vert_edges,
|
|
edge_faces)
|
|
for loop, circular in loops:
|
|
selected = [v for v in loop if bm_mod.verts[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(bm_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(bm_mod.verts[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(bm_mod.verts[knot_left].co[:])
|
|
knot_right = mathutils.Vector(bm_mod.verts[knot_right].co[:])
|
|
knot = mathutils.Vector(bm_mod.verts[knot].co[:])
|
|
pknots.append(project(knot_left, knot_right, knot))
|
|
else:
|
|
pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
|
|
else: # knot isn't selected, so shouldn't be changed
|
|
pknots.append(mathutils.Vector(bm_mod.verts[knot].co[:]))
|
|
if not circular:
|
|
pknots.append(mathutils.Vector(bm_mod.verts[knots[-1]].co[:]))
|
|
|
|
return(pknots)
|
|
|
|
|
|
# find all loops through a given vertex
|
|
def curve_vertex_loops(bm_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(bm):
|
|
vert_verts = dict_vert_verts(
|
|
[edgekey(edge) for edge in bm.edges if edge.select and not edge.hide]
|
|
)
|
|
verts = [v.index for v in bm.verts 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(bm, loop, com, normal):
|
|
verts = [bm.verts[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)
|
|
|
|
|
|
# ########################################
|
|
# ##### Gstretch functions ###############
|
|
# ########################################
|
|
|
|
# fake stroke class, used to create custom strokes if no GP data is found
|
|
class gstretch_fake_stroke():
|
|
def __init__(self, points):
|
|
self.points = [gstretch_fake_stroke_point(p) for p in points]
|
|
|
|
|
|
# fake stroke point class, used in fake strokes
|
|
class gstretch_fake_stroke_point():
|
|
def __init__(self, loc):
|
|
self.co = loc
|
|
|
|
|
|
# flips loops, if necessary, to obtain maximum alignment to stroke
|
|
def gstretch_align_pairs(ls_pairs, object, bm_mod, method):
|
|
# returns total distance between all verts in loop and corresponding stroke
|
|
def distance_loop_stroke(loop, stroke, object, bm_mod, method):
|
|
stroke_lengths_cache = False
|
|
loop_length = len(loop[0])
|
|
total_distance = 0
|
|
|
|
if method != 'regular':
|
|
relative_lengths = gstretch_relative_lengths(loop, bm_mod)
|
|
|
|
for i, v_index in enumerate(loop[0]):
|
|
if method == 'regular':
|
|
relative_distance = i / (loop_length - 1)
|
|
else:
|
|
relative_distance = relative_lengths[i]
|
|
|
|
loc1 = object.matrix_world @ bm_mod.verts[v_index].co
|
|
loc2, stroke_lengths_cache = gstretch_eval_stroke(stroke,
|
|
relative_distance, stroke_lengths_cache)
|
|
total_distance += (loc2 - loc1).length
|
|
|
|
return(total_distance)
|
|
|
|
if ls_pairs:
|
|
for (loop, stroke) in ls_pairs:
|
|
total_dist = distance_loop_stroke(loop, stroke, object, bm_mod,
|
|
method)
|
|
loop[0].reverse()
|
|
total_dist_rev = distance_loop_stroke(loop, stroke, object, bm_mod,
|
|
method)
|
|
if total_dist_rev > total_dist:
|
|
loop[0].reverse()
|
|
|
|
return(ls_pairs)
|
|
|
|
|
|
# calculate vertex positions on stroke
|
|
def gstretch_calculate_verts(loop, stroke, object, bm_mod, method):
|
|
move = []
|
|
stroke_lengths_cache = False
|
|
loop_length = len(loop[0])
|
|
matrix_inverse = object.matrix_world.inverted()
|
|
|
|
# return intersection of line with stroke, or None
|
|
def intersect_line_stroke(vec1, vec2, stroke):
|
|
for i, p in enumerate(stroke.points[1:]):
|
|
intersections = mathutils.geometry.intersect_line_line(vec1, vec2,
|
|
p.co, stroke.points[i].co)
|
|
if intersections and \
|
|
(intersections[0] - intersections[1]).length < 1e-2:
|
|
x, dist = mathutils.geometry.intersect_point_line(
|
|
intersections[0], p.co, stroke.points[i].co)
|
|
if -1 < dist < 1:
|
|
return(intersections[0])
|
|
return(None)
|
|
|
|
if method == 'project':
|
|
vert_edges = dict_vert_edges(bm_mod)
|
|
|
|
for v_index in loop[0]:
|
|
intersection = None
|
|
for ek in vert_edges[v_index]:
|
|
v1, v2 = ek
|
|
v1 = bm_mod.verts[v1]
|
|
v2 = bm_mod.verts[v2]
|
|
if v1.select + v2.select == 1 and not v1.hide and not v2.hide:
|
|
vec1 = object.matrix_world @ v1.co
|
|
vec2 = object.matrix_world @ v2.co
|
|
intersection = intersect_line_stroke(vec1, vec2, stroke)
|
|
if intersection:
|
|
break
|
|
if not intersection:
|
|
v = bm_mod.verts[v_index]
|
|
intersection = intersect_line_stroke(v.co, v.co + v.normal,
|
|
stroke)
|
|
if intersection:
|
|
move.append([v_index, matrix_inverse @ intersection])
|
|
|
|
else:
|
|
if method == 'irregular':
|
|
relative_lengths = gstretch_relative_lengths(loop, bm_mod)
|
|
|
|
for i, v_index in enumerate(loop[0]):
|
|
if method == 'regular':
|
|
relative_distance = i / (loop_length - 1)
|
|
else: # method == 'irregular'
|
|
relative_distance = relative_lengths[i]
|
|
loc, stroke_lengths_cache = gstretch_eval_stroke(stroke,
|
|
relative_distance, stroke_lengths_cache)
|
|
loc = matrix_inverse @ loc
|
|
move.append([v_index, loc])
|
|
|
|
return(move)
|
|
|
|
|
|
# create new vertices, based on GP strokes
|
|
def gstretch_create_verts(object, bm_mod, strokes, method, conversion,
|
|
conversion_distance, conversion_max, conversion_min, conversion_vertices):
|
|
move = []
|
|
stroke_verts = []
|
|
mat_world = object.matrix_world.inverted()
|
|
singles = gstretch_match_single_verts(bm_mod, strokes, mat_world)
|
|
|
|
for stroke in strokes:
|
|
stroke_verts.append([stroke, []])
|
|
min_end_point = 0
|
|
if conversion == 'vertices':
|
|
min_end_point = conversion_vertices
|
|
end_point = conversion_vertices
|
|
elif conversion == 'limit_vertices':
|
|
min_end_point = conversion_min
|
|
end_point = conversion_max
|
|
else:
|
|
end_point = len(stroke.points)
|
|
# creation of new vertices at fixed user-defined distances
|
|
if conversion == 'distance':
|
|
method = 'project'
|
|
prev_point = stroke.points[0]
|
|
stroke_verts[-1][1].append(bm_mod.verts.new(mat_world @ prev_point.co))
|
|
distance = 0
|
|
limit = conversion_distance
|
|
for point in stroke.points:
|
|
new_distance = distance + (point.co - prev_point.co).length
|
|
iteration = 0
|
|
while new_distance > limit:
|
|
to_cover = limit - distance + (limit * iteration)
|
|
new_loc = prev_point.co + to_cover * \
|
|
(point.co - prev_point.co).normalized()
|
|
stroke_verts[-1][1].append(bm_mod.verts.new(mat_world * new_loc))
|
|
new_distance -= limit
|
|
iteration += 1
|
|
distance = new_distance
|
|
prev_point = point
|
|
# creation of new vertices for other methods
|
|
else:
|
|
# add vertices at stroke points
|
|
for point in stroke.points[:end_point]:
|
|
stroke_verts[-1][1].append(bm_mod.verts.new(mat_world @ point.co))
|
|
# add more vertices, beyond the points that are available
|
|
if min_end_point > min(len(stroke.points), end_point):
|
|
for i in range(min_end_point -
|
|
(min(len(stroke.points), end_point))):
|
|
stroke_verts[-1][1].append(bm_mod.verts.new(mat_world @ point.co))
|
|
# force even spreading of points, so they are placed on stroke
|
|
method = 'regular'
|
|
bm_mod.verts.ensure_lookup_table()
|
|
bm_mod.verts.index_update()
|
|
for stroke, verts_seq in stroke_verts:
|
|
if len(verts_seq) < 2:
|
|
continue
|
|
# spread vertices evenly over the stroke
|
|
if method == 'regular':
|
|
loop = [[vert.index for vert in verts_seq], False]
|
|
move += gstretch_calculate_verts(loop, stroke, object, bm_mod,
|
|
method)
|
|
# create edges
|
|
for i, vert in enumerate(verts_seq):
|
|
if i > 0:
|
|
bm_mod.edges.new((verts_seq[i - 1], verts_seq[i]))
|
|
vert.select = True
|
|
# connect single vertices to the closest stroke
|
|
if singles:
|
|
for vert, m_stroke, point in singles:
|
|
if m_stroke != stroke:
|
|
continue
|
|
bm_mod.edges.new((vert, verts_seq[point]))
|
|
bm_mod.edges.ensure_lookup_table()
|
|
bmesh.update_edit_mesh(object.data)
|
|
|
|
return(move)
|
|
|
|
|
|
# erases the grease pencil stroke
|
|
def gstretch_erase_stroke(stroke, context):
|
|
# change 3d coordinate into a stroke-point
|
|
def sp(loc, context):
|
|
lib = {'name': "",
|
|
'pen_flip': False,
|
|
'is_start': False,
|
|
'location': (0, 0, 0),
|
|
'mouse': (
|
|
view3d_utils.location_3d_to_region_2d(
|
|
context.region, context.space_data.region_3d, loc)
|
|
),
|
|
'pressure': 1,
|
|
'size': 0,
|
|
'time': 0}
|
|
return(lib)
|
|
|
|
if type(stroke) != bpy.types.GPencilStroke:
|
|
# fake stroke, there is nothing to delete
|
|
return
|
|
|
|
erase_stroke = [sp(p.co, context) for p in stroke.points]
|
|
if erase_stroke:
|
|
erase_stroke[0]['is_start'] = True
|
|
#bpy.ops.gpencil.draw(mode='ERASER', stroke=erase_stroke)
|
|
bpy.ops.gpencil.data_unlink()
|
|
|
|
|
|
|
|
# get point on stroke, given by relative distance (0.0 - 1.0)
|
|
def gstretch_eval_stroke(stroke, distance, stroke_lengths_cache=False):
|
|
# use cache if available
|
|
if not stroke_lengths_cache:
|
|
lengths = [0]
|
|
for i, p in enumerate(stroke.points[1:]):
|
|
lengths.append((p.co - stroke.points[i].co).length + lengths[-1])
|
|
total_length = max(lengths[-1], 1e-7)
|
|
stroke_lengths_cache = [length / total_length for length in
|
|
lengths]
|
|
stroke_lengths = stroke_lengths_cache[:]
|
|
|
|
if distance in stroke_lengths:
|
|
loc = stroke.points[stroke_lengths.index(distance)].co
|
|
elif distance > stroke_lengths[-1]:
|
|
# should be impossible, but better safe than sorry
|
|
loc = stroke.points[-1].co
|
|
else:
|
|
stroke_lengths.append(distance)
|
|
stroke_lengths.sort()
|
|
stroke_index = stroke_lengths.index(distance)
|
|
interval_length = stroke_lengths[
|
|
stroke_index + 1] - stroke_lengths[stroke_index - 1
|
|
]
|
|
distance_relative = (distance - stroke_lengths[stroke_index - 1]) / interval_length
|
|
interval_vector = stroke.points[stroke_index].co - stroke.points[stroke_index - 1].co
|
|
loc = stroke.points[stroke_index - 1].co + distance_relative * interval_vector
|
|
|
|
return(loc, stroke_lengths_cache)
|
|
|
|
|
|
# create fake grease pencil strokes for the active object
|
|
def gstretch_get_fake_strokes(object, bm_mod, loops):
|
|
strokes = []
|
|
for loop in loops:
|
|
p1 = object.matrix_world @ bm_mod.verts[loop[0][0]].co
|
|
p2 = object.matrix_world @ bm_mod.verts[loop[0][-1]].co
|
|
strokes.append(gstretch_fake_stroke([p1, p2]))
|
|
|
|
return(strokes)
|
|
|
|
# get strokes
|
|
def gstretch_get_strokes(self, context):
|
|
looptools = context.window_manager.looptools
|
|
gp = get_strokes(self, context)
|
|
if not gp:
|
|
return(None)
|
|
if looptools.gstretch_use_guide == "Annotation":
|
|
layer = bpy.data.grease_pencils[0].layers.active
|
|
if looptools.gstretch_use_guide == "GPencil" and not looptools.gstretch_guide == None:
|
|
layer = looptools.gstretch_guide.data.layers.active
|
|
if not layer:
|
|
return(None)
|
|
frame = layer.active_frame
|
|
if not frame:
|
|
return(None)
|
|
strokes = frame.strokes
|
|
if len(strokes) < 1:
|
|
return(None)
|
|
|
|
return(strokes)
|
|
|
|
# returns a list with loop-stroke pairs
|
|
def gstretch_match_loops_strokes(loops, strokes, object, bm_mod):
|
|
if not loops or not strokes:
|
|
return(None)
|
|
|
|
# calculate loop centers
|
|
loop_centers = []
|
|
bm_mod.verts.ensure_lookup_table()
|
|
for loop in loops:
|
|
center = mathutils.Vector()
|
|
for v_index in loop[0]:
|
|
center += bm_mod.verts[v_index].co
|
|
center /= len(loop[0])
|
|
center = object.matrix_world @ center
|
|
loop_centers.append([center, loop])
|
|
|
|
# calculate stroke centers
|
|
stroke_centers = []
|
|
for stroke in strokes:
|
|
center = mathutils.Vector()
|
|
for p in stroke.points:
|
|
center += p.co
|
|
center /= len(stroke.points)
|
|
stroke_centers.append([center, stroke, 0])
|
|
|
|
# match, first by stroke use count, then by distance
|
|
ls_pairs = []
|
|
for lc in loop_centers:
|
|
distances = []
|
|
for i, sc in enumerate(stroke_centers):
|
|
distances.append([sc[2], (lc[0] - sc[0]).length, i])
|
|
distances.sort()
|
|
best_stroke = distances[0][2]
|
|
ls_pairs.append([lc[1], stroke_centers[best_stroke][1]])
|
|
stroke_centers[best_stroke][2] += 1 # increase stroke use count
|
|
|
|
return(ls_pairs)
|
|
|
|
|
|
# match single selected vertices to the closest stroke endpoint
|
|
# returns a list of tuples, constructed as: (vertex, stroke, stroke point index)
|
|
def gstretch_match_single_verts(bm_mod, strokes, mat_world):
|
|
# calculate stroke endpoints in object space
|
|
endpoints = []
|
|
for stroke in strokes:
|
|
endpoints.append((mat_world @ stroke.points[0].co, stroke, 0))
|
|
endpoints.append((mat_world @ stroke.points[-1].co, stroke, -1))
|
|
|
|
distances = []
|
|
# find single vertices (not connected to other selected verts)
|
|
for vert in bm_mod.verts:
|
|
if not vert.select:
|
|
continue
|
|
single = True
|
|
for edge in vert.link_edges:
|
|
if edge.other_vert(vert).select:
|
|
single = False
|
|
break
|
|
if not single:
|
|
continue
|
|
# calculate distances from vertex to endpoints
|
|
distance = [((vert.co - loc).length, vert, stroke, stroke_point,
|
|
endpoint_index) for endpoint_index, (loc, stroke, stroke_point) in
|
|
enumerate(endpoints)]
|
|
distance.sort()
|
|
distances.append(distance[0])
|
|
|
|
# create matches, based on shortest distance first
|
|
singles = []
|
|
while distances:
|
|
distances.sort()
|
|
singles.append((distances[0][1], distances[0][2], distances[0][3]))
|
|
endpoints.pop(distances[0][4])
|
|
distances.pop(0)
|
|
distances_new = []
|
|
for (i, vert, j, k, l) in distances:
|
|
distance_new = [((vert.co - loc).length, vert, stroke, stroke_point,
|
|
endpoint_index) for endpoint_index, (loc, stroke,
|
|
stroke_point) in enumerate(endpoints)]
|
|
distance_new.sort()
|
|
distances_new.append(distance_new[0])
|
|
distances = distances_new
|
|
|
|
return(singles)
|
|
|
|
|
|
# returns list with a relative distance (0.0 - 1.0) of each vertex on the loop
|
|
def gstretch_relative_lengths(loop, bm_mod):
|
|
lengths = [0]
|
|
for i, v_index in enumerate(loop[0][1:]):
|
|
lengths.append(
|
|
(bm_mod.verts[v_index].co -
|
|
bm_mod.verts[loop[0][i]].co).length + lengths[-1]
|
|
)
|
|
total_length = max(lengths[-1], 1e-7)
|
|
relative_lengths = [length / total_length for length in
|
|
lengths]
|
|
|
|
return(relative_lengths)
|
|
|
|
|
|
# convert cache-stored strokes into usable (fake) GP strokes
|
|
def gstretch_safe_to_true_strokes(safe_strokes):
|
|
strokes = []
|
|
for safe_stroke in safe_strokes:
|
|
strokes.append(gstretch_fake_stroke(safe_stroke))
|
|
|
|
return(strokes)
|
|
|
|
|
|
# convert a GP stroke into a list of points which can be stored in cache
|
|
def gstretch_true_to_safe_strokes(strokes):
|
|
safe_strokes = []
|
|
for stroke in strokes:
|
|
safe_strokes.append([p.co.copy() for p in stroke.points])
|
|
|
|
return(safe_strokes)
|
|
|
|
|
|
# force consistency in GUI, max value can never be lower than min value
|
|
def gstretch_update_max(self, context):
|
|
# called from operator settings (after execution)
|
|
if 'conversion_min' in self.keys():
|
|
if self.conversion_min > self.conversion_max:
|
|
self.conversion_max = self.conversion_min
|
|
# called from toolbar
|
|
else:
|
|
lt = context.window_manager.looptools
|
|
if lt.gstretch_conversion_min > lt.gstretch_conversion_max:
|
|
lt.gstretch_conversion_max = lt.gstretch_conversion_min
|
|
|
|
|
|
# force consistency in GUI, min value can never be higher than max value
|
|
def gstretch_update_min(self, context):
|
|
# called from operator settings (after execution)
|
|
if 'conversion_max' in self.keys():
|
|
if self.conversion_max < self.conversion_min:
|
|
self.conversion_min = self.conversion_max
|
|
# called from toolbar
|
|
else:
|
|
lt = context.window_manager.looptools
|
|
if lt.gstretch_conversion_max < lt.gstretch_conversion_min:
|
|
lt.gstretch_conversion_min = lt.gstretch_conversion_max
|
|
|
|
|
|
# ########################################
|
|
# ##### 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(bm_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(bm_mod.verts[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(bm_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], (bm_mod.verts[c[0]].co + c[1]) / 2])
|
|
|
|
return(move)
|
|
|
|
|
|
# ########################################
|
|
# ##### Space functions ##################
|
|
# ########################################
|
|
|
|
# calculate relative positions compared to first knot
|
|
def space_calculate_t(bm_mod, knots):
|
|
tknots = []
|
|
loc_prev = False
|
|
len_total = 0
|
|
for k in knots:
|
|
loc = mathutils.Vector(bm_mod.verts[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(bm_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(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: FloatProperty(
|
|
name="Strength",
|
|
description="Higher strength results in more fluid curves",
|
|
default=1.0,
|
|
soft_min=-3.0,
|
|
soft_max=3.0
|
|
)
|
|
interpolation: 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: BoolProperty(
|
|
name="Loft",
|
|
description="Loft multiple loops, instead of considering them as "
|
|
"a multi-input for bridging",
|
|
default=False
|
|
)
|
|
loft_loop: BoolProperty(
|
|
name="Loop",
|
|
description="Connect the first and the last loop with each other",
|
|
default=False
|
|
)
|
|
min_width: 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: 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: BoolProperty(
|
|
name="Remove faces",
|
|
description="Remove faces that are internal after bridging",
|
|
default=True
|
|
)
|
|
reverse: 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: IntProperty(
|
|
name="Segments",
|
|
description="Number of segments used to bridge the gap (0=automatic)",
|
|
default=1,
|
|
min=0,
|
|
soft_max=20
|
|
)
|
|
twist: 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
|
|
object, bm = initialise()
|
|
edge_faces, edgekey_to_edge, old_selected_faces, smooth = \
|
|
bridge_initialise(bm, 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, bm, input_method, False)
|
|
if not cached:
|
|
# get loops
|
|
loops = bridge_get_input(bm)
|
|
if loops:
|
|
# reorder loops if there are more than 2
|
|
if len(loops) > 2:
|
|
if self.loft:
|
|
loops = bridge_sort_loops(bm, loops, self.loft_loop)
|
|
else:
|
|
loops = bridge_match_loops(bm, loops)
|
|
|
|
# saving cache for faster execution next time
|
|
if not cached:
|
|
cache_write("Bridge", object, bm, input_method, False, False,
|
|
loops, False, False)
|
|
|
|
if loops:
|
|
# calculate new geometry
|
|
vertices = []
|
|
faces = []
|
|
max_vert_index = len(bm.verts) - 1
|
|
for i in range(1, len(loops)):
|
|
if not self.loft and i % 2 == 0:
|
|
continue
|
|
lines = bridge_calculate_lines(bm, loops[i - 1:i + 1],
|
|
self.mode, self.twist, self.reverse)
|
|
vertex_normals = bridge_calculate_virtual_vertex_normals(bm,
|
|
lines, loops[i - 1:i + 1], edge_faces, edgekey_to_edge)
|
|
segments = bridge_calculate_segments(bm, lines,
|
|
loops[i - 1:i + 1], self.segments)
|
|
new_verts, new_faces, max_vert_index = \
|
|
bridge_calculate_geometry(
|
|
bm, 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(bm, old_selected_faces, loops)
|
|
# create vertices
|
|
if vertices:
|
|
bridge_create_vertices(bm, vertices)
|
|
# delete internal faces
|
|
if self.remove_faces and old_selected_faces:
|
|
bridge_remove_internal_faces(bm, old_selected_faces)
|
|
# create faces
|
|
if faces:
|
|
new_faces = bridge_create_faces(object, bm, faces, self.twist)
|
|
bridge_select_new_faces(new_faces, smooth)
|
|
# edge-data could have changed, can't use cache next run
|
|
if faces and not vertices:
|
|
cache_delete("Bridge")
|
|
# make sure normals are facing outside
|
|
bmesh.update_edit_mesh(object.data, loop_triangles=False, destructive=True)
|
|
bpy.ops.mesh.normals_make_consistent()
|
|
|
|
# cleaning up
|
|
terminate()
|
|
|
|
return{'FINISHED'}
|
|
|
|
|
|
# circle operator
|
|
class Circle(Operator):
|
|
bl_idname = "mesh.looptools_circle"
|
|
bl_label = "Circle"
|
|
bl_description = "Move selected vertices into a circle shape"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
custom_radius: BoolProperty(
|
|
name="Radius",
|
|
description="Force a custom radius",
|
|
default=False
|
|
)
|
|
fit: 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: BoolProperty(
|
|
name="Flatten",
|
|
description="Flatten the circle, instead of projecting it on the mesh",
|
|
default=True
|
|
)
|
|
influence: FloatProperty(
|
|
name="Influence",
|
|
description="Force of the tool",
|
|
default=100.0,
|
|
min=0.0,
|
|
max=100.0,
|
|
precision=1,
|
|
subtype='PERCENTAGE'
|
|
)
|
|
lock_x: BoolProperty(
|
|
name="Lock X",
|
|
description="Lock editing of the x-coordinate",
|
|
default=False
|
|
)
|
|
lock_y: BoolProperty(
|
|
name="Lock Y",
|
|
description="Lock editing of the y-coordinate",
|
|
default=False
|
|
)
|
|
lock_z: BoolProperty(name="Lock Z",
|
|
description="Lock editing of the z-coordinate",
|
|
default=False
|
|
)
|
|
radius: FloatProperty(
|
|
name="Radius",
|
|
description="Custom radius for circle",
|
|
default=1.0,
|
|
min=0.0,
|
|
soft_max=1000.0
|
|
)
|
|
angle: FloatProperty(
|
|
name="Angle",
|
|
description="Rotate a circle by an angle",
|
|
unit='ROTATION',
|
|
default=math.radians(0.0),
|
|
soft_min=math.radians(-360.0),
|
|
soft_max=math.radians(360.0)
|
|
)
|
|
regular: 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.prop(self, "angle")
|
|
col.separator()
|
|
|
|
col_move = col.column(align=True)
|
|
row = col_move.row(align=True)
|
|
if self.lock_x:
|
|
row.prop(self, "lock_x", text="X", icon='LOCKED')
|
|
else:
|
|
row.prop(self, "lock_x", text="X", icon='UNLOCKED')
|
|
if self.lock_y:
|
|
row.prop(self, "lock_y", text="Y", icon='LOCKED')
|
|
else:
|
|
row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
|
|
if self.lock_z:
|
|
row.prop(self, "lock_z", text="Z", icon='LOCKED')
|
|
else:
|
|
row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
|
|
col_move.prop(self, "influence")
|
|
|
|
def invoke(self, context, event):
|
|
# load custom settings
|
|
settings_load(self)
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
# initialise
|
|
object, bm = initialise()
|
|
settings_write(self)
|
|
# check cache to see if we can save time
|
|
cached, single_loops, loops, derived, mapping = cache_read("Circle",
|
|
object, bm, False, False)
|
|
if cached:
|
|
derived, bm_mod = get_derived_bmesh(object, bm, False)
|
|
else:
|
|
# find loops
|
|
derived, bm_mod, single_vertices, single_loops, loops = \
|
|
circle_get_input(object, bm)
|
|
mapping = get_mapping(derived, bm, bm_mod, single_vertices,
|
|
False, loops)
|
|
single_loops, loops = circle_check_loops(single_loops, loops,
|
|
mapping, bm_mod)
|
|
|
|
# saving cache for faster execution next time
|
|
if not cached:
|
|
cache_write("Circle", object, bm, False, False, single_loops,
|
|
loops, derived, mapping)
|
|
|
|
move = []
|
|
for i, loop in enumerate(loops):
|
|
# best fitting flat plane
|
|
com, normal = calculate_plane(bm_mod, loop)
|
|
# if circular, shift loop so we get a good starting vertex
|
|
if loop[1]:
|
|
loop = circle_shift_loop(bm_mod, loop, com)
|
|
# flatten vertices on plane
|
|
locs_2d, p, q = circle_3d_to_2d(bm_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, self.angle)
|
|
else:
|
|
new_locs_2d = circle_project_non_regular(locs_2d[:], x0, y0, r, self.angle)
|
|
# 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, bm_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(bm_mod, com, p, q,
|
|
normal, single_loops[i]))
|
|
|
|
# move vertices to new locations
|
|
if self.lock_x or self.lock_y or self.lock_z:
|
|
lock = [self.lock_x, self.lock_y, self.lock_z]
|
|
else:
|
|
lock = False
|
|
move_verts(object, bm, mapping, move, lock, -1)
|
|
|
|
# cleaning up
|
|
if derived:
|
|
bm_mod.free()
|
|
terminate()
|
|
|
|
return{'FINISHED'}
|
|
|
|
|
|
# curve operator
|
|
class Curve(Operator):
|
|
bl_idname = "mesh.looptools_curve"
|
|
bl_label = "Curve"
|
|
bl_description = "Turn a loop into a smooth curve"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
boundaries: BoolProperty(
|
|
name="Boundaries",
|
|
description="Limit the tool to work within the boundaries of the selected vertices",
|
|
default=False
|
|
)
|
|
influence: FloatProperty(
|
|
name="Influence",
|
|
description="Force of the tool",
|
|
default=100.0,
|
|
min=0.0,
|
|
max=100.0,
|
|
precision=1,
|
|
subtype='PERCENTAGE'
|
|
)
|
|
interpolation: 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'
|
|
)
|
|
lock_x: BoolProperty(
|
|
name="Lock X",
|
|
description="Lock editing of the x-coordinate",
|
|
default=False
|
|
)
|
|
lock_y: BoolProperty(
|
|
name="Lock Y",
|
|
description="Lock editing of the y-coordinate",
|
|
default=False
|
|
)
|
|
lock_z: BoolProperty(
|
|
name="Lock Z",
|
|
description="Lock editing of the z-coordinate",
|
|
default=False
|
|
)
|
|
regular: BoolProperty(
|
|
name="Regular",
|
|
description="Distribute vertices at constant distances along the curve",
|
|
default=True
|
|
)
|
|
restriction: 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_move = col.column(align=True)
|
|
row = col_move.row(align=True)
|
|
if self.lock_x:
|
|
row.prop(self, "lock_x", text="X", icon='LOCKED')
|
|
else:
|
|
row.prop(self, "lock_x", text="X", icon='UNLOCKED')
|
|
if self.lock_y:
|
|
row.prop(self, "lock_y", text="Y", icon='LOCKED')
|
|
else:
|
|
row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
|
|
if self.lock_z:
|
|
row.prop(self, "lock_z", text="Z", icon='LOCKED')
|
|
else:
|
|
row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
|
|
col_move.prop(self, "influence")
|
|
|
|
def invoke(self, context, event):
|
|
# load custom settings
|
|
settings_load(self)
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
# initialise
|
|
object, bm = initialise()
|
|
settings_write(self)
|
|
# check cache to see if we can save time
|
|
cached, single_loops, loops, derived, mapping = cache_read("Curve",
|
|
object, bm, False, self.boundaries)
|
|
if cached:
|
|
derived, bm_mod = get_derived_bmesh(object, bm, False)
|
|
else:
|
|
# find loops
|
|
derived, bm_mod, loops = curve_get_input(object, bm, self.boundaries)
|
|
mapping = get_mapping(derived, bm, bm_mod, False, True, loops)
|
|
loops = check_loops(loops, mapping, bm_mod)
|
|
verts_selected = [
|
|
v.index for v in bm_mod.verts if v.select and not v.hide
|
|
]
|
|
|
|
# saving cache for faster execution next time
|
|
if not cached:
|
|
cache_write("Curve", object, bm, False, self.boundaries, False,
|
|
loops, derived, mapping)
|
|
|
|
move = []
|
|
for loop in loops:
|
|
knots, points = curve_calculate_knots(loop, verts_selected)
|
|
pknots = curve_project_knots(bm_mod, verts_selected, knots,
|
|
points, loop[1])
|
|
tknots, tpoints = curve_calculate_t(bm_mod, knots, points,
|
|
pknots, self.regular, loop[1])
|
|
splines = calculate_splines(self.interpolation, bm_mod,
|
|
tknots, knots)
|
|
move.append(curve_calculate_vertices(bm_mod, knots, tknots,
|
|
points, tpoints, splines, self.interpolation,
|
|
self.restriction))
|
|
|
|
# move vertices to new locations
|
|
if self.lock_x or self.lock_y or self.lock_z:
|
|
lock = [self.lock_x, self.lock_y, self.lock_z]
|
|
else:
|
|
lock = False
|
|
move_verts(object, bm, mapping, move, lock, self.influence)
|
|
|
|
# cleaning up
|
|
if derived:
|
|
bm_mod.free()
|
|
terminate()
|
|
|
|
return{'FINISHED'}
|
|
|
|
|
|
# flatten operator
|
|
class Flatten(Operator):
|
|
bl_idname = "mesh.looptools_flatten"
|
|
bl_label = "Flatten"
|
|
bl_description = "Flatten vertices on a best-fitting plane"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
influence: FloatProperty(
|
|
name="Influence",
|
|
description="Force of the tool",
|
|
default=100.0,
|
|
min=0.0,
|
|
max=100.0,
|
|
precision=1,
|
|
subtype='PERCENTAGE'
|
|
)
|
|
lock_x: BoolProperty(
|
|
name="Lock X",
|
|
description="Lock editing of the x-coordinate",
|
|
default=False
|
|
)
|
|
lock_y: BoolProperty(
|
|
name="Lock Y",
|
|
description="Lock editing of the y-coordinate",
|
|
default=False
|
|
)
|
|
lock_z: BoolProperty(name="Lock Z",
|
|
description="Lock editing of the z-coordinate",
|
|
default=False
|
|
)
|
|
plane: 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: 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_move = col.column(align=True)
|
|
row = col_move.row(align=True)
|
|
if self.lock_x:
|
|
row.prop(self, "lock_x", text="X", icon='LOCKED')
|
|
else:
|
|
row.prop(self, "lock_x", text="X", icon='UNLOCKED')
|
|
if self.lock_y:
|
|
row.prop(self, "lock_y", text="Y", icon='LOCKED')
|
|
else:
|
|
row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
|
|
if self.lock_z:
|
|
row.prop(self, "lock_z", text="Z", icon='LOCKED')
|
|
else:
|
|
row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
|
|
col_move.prop(self, "influence")
|
|
|
|
def invoke(self, context, event):
|
|
# load custom settings
|
|
settings_load(self)
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
# initialise
|
|
object, bm = initialise()
|
|
settings_write(self)
|
|
# check cache to see if we can save time
|
|
cached, single_loops, loops, derived, mapping = cache_read("Flatten",
|
|
object, bm, False, False)
|
|
if not cached:
|
|
# order input into virtual loops
|
|
loops = flatten_get_input(bm)
|
|
loops = check_loops(loops, mapping, bm)
|
|
|
|
# saving cache for faster execution next time
|
|
if not cached:
|
|
cache_write("Flatten", object, bm, False, False, False, loops,
|
|
False, False)
|
|
|
|
move = []
|
|
for loop in loops:
|
|
# calculate plane and position of vertices on them
|
|
com, normal = calculate_plane(bm, loop, method=self.plane,
|
|
object=object)
|
|
to_move = flatten_project(bm, loop, com, normal)
|
|
if self.restriction == 'none':
|
|
move.append(to_move)
|
|
else:
|
|
move.append(to_move)
|
|
|
|
# move vertices to new locations
|
|
if self.lock_x or self.lock_y or self.lock_z:
|
|
lock = [self.lock_x, self.lock_y, self.lock_z]
|
|
else:
|
|
lock = False
|
|
move_verts(object, bm, False, move, lock, self.influence)
|
|
|
|
# cleaning up
|
|
terminate()
|
|
|
|
return{'FINISHED'}
|
|
|
|
|
|
# Annotation operator
|
|
class RemoveAnnotation(Operator):
|
|
bl_idname = "remove.annotation"
|
|
bl_label = "Remove Annotation"
|
|
bl_description = "Remove all Annotation Strokes"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
|
|
try:
|
|
bpy.data.grease_pencils[0].layers.active.clear()
|
|
except:
|
|
self.report({'INFO'}, "No Annotation data to Unlink")
|
|
return {'CANCELLED'}
|
|
|
|
return{'FINISHED'}
|
|
|
|
# GPencil operator
|
|
class RemoveGPencil(Operator):
|
|
bl_idname = "remove.gp"
|
|
bl_label = "Remove GPencil"
|
|
bl_description = "Remove all GPencil Strokes"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
|
|
try:
|
|
looptools = context.window_manager.looptools
|
|
looptools.gstretch_guide.data.layers.data.clear()
|
|
looptools.gstretch_guide.data.update_tag()
|
|
except:
|
|
self.report({'INFO'}, "No GPencil data to Unlink")
|
|
return {'CANCELLED'}
|
|
|
|
return{'FINISHED'}
|
|
|
|
|
|
class GStretch(Operator):
|
|
bl_idname = "mesh.looptools_gstretch"
|
|
bl_label = "Gstretch"
|
|
bl_description = "Stretch selected vertices to active stroke"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
conversion: EnumProperty(
|
|
name="Conversion",
|
|
items=(("distance", "Distance", "Set the distance between vertices "
|
|
"of the converted stroke"),
|
|
("limit_vertices", "Limit vertices", "Set the minimum and maximum "
|
|
"number of vertices that converted strokes will have"),
|
|
("vertices", "Exact vertices", "Set the exact number of vertices "
|
|
"that converted strokes will have. Short strokes "
|
|
"with few points may contain less vertices than this number."),
|
|
("none", "No simplification", "Convert each point "
|
|
"to a vertex")),
|
|
description="If strokes are converted to geometry, "
|
|
"use this simplification method",
|
|
default='limit_vertices'
|
|
)
|
|
conversion_distance: FloatProperty(
|
|
name="Distance",
|
|
description="Absolute distance between vertices along the converted "
|
|
" stroke",
|
|
default=0.1,
|
|
min=0.000001,
|
|
soft_min=0.01,
|
|
soft_max=100
|
|
)
|
|
conversion_max: IntProperty(
|
|
name="Max Vertices",
|
|
description="Maximum number of vertices strokes will "
|
|
"have, when they are converted to geomtery",
|
|
default=32,
|
|
min=3,
|
|
soft_max=500,
|
|
update=gstretch_update_min
|
|
)
|
|
conversion_min: IntProperty(
|
|
name="Min Vertices",
|
|
description="Minimum number of vertices strokes will "
|
|
"have, when they are converted to geomtery",
|
|
default=8,
|
|
min=3,
|
|
soft_max=500,
|
|
update=gstretch_update_max
|
|
)
|
|
conversion_vertices: IntProperty(
|
|
name="Vertices",
|
|
description="Number of vertices strokes will "
|
|
"have, when they are converted to geometry. If strokes have less "
|
|
"points than required, the 'Spread evenly' method is used",
|
|
default=32,
|
|
min=3,
|
|
soft_max=500
|
|
)
|
|
delete_strokes: BoolProperty(
|
|
name="Delete strokes",
|
|
description="Remove strokes if they have been used."
|
|
"WARNING: DOES NOT SUPPORT UNDO",
|
|
default=False
|
|
)
|
|
influence: FloatProperty(
|
|
name="Influence",
|
|
description="Force of the tool",
|
|
default=100.0,
|
|
min=0.0,
|
|
max=100.0,
|
|
precision=1,
|
|
subtype='PERCENTAGE'
|
|
)
|
|
lock_x: BoolProperty(
|
|
name="Lock X",
|
|
description="Lock editing of the x-coordinate",
|
|
default=False
|
|
)
|
|
lock_y: BoolProperty(
|
|
name="Lock Y",
|
|
description="Lock editing of the y-coordinate",
|
|
default=False
|
|
)
|
|
lock_z: BoolProperty(
|
|
name="Lock Z",
|
|
description="Lock editing of the z-coordinate",
|
|
default=False
|
|
)
|
|
method: EnumProperty(
|
|
name="Method",
|
|
items=(("project", "Project", "Project vertices onto the stroke, "
|
|
"using vertex normals and connected edges"),
|
|
("irregular", "Spread", "Distribute vertices along the full "
|
|
"stroke, retaining relative distances between the vertices"),
|
|
("regular", "Spread evenly", "Distribute vertices at regular "
|
|
"distances along the full stroke")),
|
|
description="Method of distributing the vertices over the "
|
|
"stroke",
|
|
default='regular'
|
|
)
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
ob = context.active_object
|
|
return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
|
|
|
|
def draw(self, context):
|
|
looptools = context.window_manager.looptools
|
|
layout = self.layout
|
|
col = layout.column()
|
|
|
|
col.prop(self, "method")
|
|
col.separator()
|
|
|
|
col_conv = col.column(align=True)
|
|
col_conv.prop(self, "conversion", text="")
|
|
if self.conversion == 'distance':
|
|
col_conv.prop(self, "conversion_distance")
|
|
elif self.conversion == 'limit_vertices':
|
|
row = col_conv.row(align=True)
|
|
row.prop(self, "conversion_min", text="Min")
|
|
row.prop(self, "conversion_max", text="Max")
|
|
elif self.conversion == 'vertices':
|
|
col_conv.prop(self, "conversion_vertices")
|
|
col.separator()
|
|
|
|
col_move = col.column(align=True)
|
|
row = col_move.row(align=True)
|
|
if self.lock_x:
|
|
row.prop(self, "lock_x", text="X", icon='LOCKED')
|
|
else:
|
|
row.prop(self, "lock_x", text="X", icon='UNLOCKED')
|
|
if self.lock_y:
|
|
row.prop(self, "lock_y", text="Y", icon='LOCKED')
|
|
else:
|
|
row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
|
|
if self.lock_z:
|
|
row.prop(self, "lock_z", text="Z", icon='LOCKED')
|
|
else:
|
|
row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
|
|
col_move.prop(self, "influence")
|
|
col.separator()
|
|
if looptools.gstretch_use_guide == "Annotation":
|
|
col.operator("remove.annotation", text="Delete annotation strokes")
|
|
if looptools.gstretch_use_guide == "GPencil":
|
|
col.operator("remove.gp", text="Delete GPencil strokes")
|
|
|
|
def invoke(self, context, event):
|
|
# flush cached strokes
|
|
if 'Gstretch' in looptools_cache:
|
|
looptools_cache['Gstretch']['single_loops'] = []
|
|
# load custom settings
|
|
settings_load(self)
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
# initialise
|
|
object, bm = initialise()
|
|
settings_write(self)
|
|
|
|
# check cache to see if we can save time
|
|
cached, safe_strokes, loops, derived, mapping = cache_read("Gstretch",
|
|
object, bm, False, False)
|
|
if cached:
|
|
straightening = False
|
|
if safe_strokes:
|
|
strokes = gstretch_safe_to_true_strokes(safe_strokes)
|
|
# cached strokes were flushed (see operator's invoke function)
|
|
elif get_strokes(self, context):
|
|
strokes = gstretch_get_strokes(self, context)
|
|
else:
|
|
# straightening function (no GP) -> loops ignore modifiers
|
|
straightening = True
|
|
derived = False
|
|
bm_mod = bm.copy()
|
|
bm_mod.verts.ensure_lookup_table()
|
|
bm_mod.edges.ensure_lookup_table()
|
|
bm_mod.faces.ensure_lookup_table()
|
|
strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
|
|
if not straightening:
|
|
derived, bm_mod = get_derived_bmesh(object, bm, False)
|
|
else:
|
|
# get loops and strokes
|
|
if get_strokes(self, context):
|
|
# find loops
|
|
derived, bm_mod, loops = get_connected_input(object, bm, False, input='selected')
|
|
mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
|
|
loops = check_loops(loops, mapping, bm_mod)
|
|
# get strokes
|
|
strokes = gstretch_get_strokes(self, context)
|
|
else:
|
|
# straightening function (no GP) -> loops ignore modifiers
|
|
derived = False
|
|
mapping = False
|
|
bm_mod = bm.copy()
|
|
bm_mod.verts.ensure_lookup_table()
|
|
bm_mod.edges.ensure_lookup_table()
|
|
bm_mod.faces.ensure_lookup_table()
|
|
edge_keys = [
|
|
edgekey(edge) for edge in bm_mod.edges if
|
|
edge.select and not edge.hide
|
|
]
|
|
loops = get_connected_selections(edge_keys)
|
|
loops = check_loops(loops, mapping, bm_mod)
|
|
# create fake strokes
|
|
strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
|
|
|
|
# saving cache for faster execution next time
|
|
if not cached:
|
|
if strokes:
|
|
safe_strokes = gstretch_true_to_safe_strokes(strokes)
|
|
else:
|
|
safe_strokes = []
|
|
cache_write("Gstretch", object, bm, False, False,
|
|
safe_strokes, loops, derived, mapping)
|
|
|
|
# pair loops and strokes
|
|
ls_pairs = gstretch_match_loops_strokes(loops, strokes, object, bm_mod)
|
|
ls_pairs = gstretch_align_pairs(ls_pairs, object, bm_mod, self.method)
|
|
|
|
move = []
|
|
if not loops:
|
|
# no selected geometry, convert GP to verts
|
|
if strokes:
|
|
move.append(gstretch_create_verts(object, bm, strokes,
|
|
self.method, self.conversion, self.conversion_distance,
|
|
self.conversion_max, self.conversion_min,
|
|
self.conversion_vertices))
|
|
for stroke in strokes:
|
|
gstretch_erase_stroke(stroke, context)
|
|
elif ls_pairs:
|
|
for (loop, stroke) in ls_pairs:
|
|
move.append(gstretch_calculate_verts(loop, stroke, object,
|
|
bm_mod, self.method))
|
|
if self.delete_strokes:
|
|
if type(stroke) != bpy.types.GPencilStroke:
|
|
# in case of cached fake stroke, get the real one
|
|
if get_strokes(self, context):
|
|
strokes = gstretch_get_strokes(self, context)
|
|
if loops and strokes:
|
|
ls_pairs = gstretch_match_loops_strokes(loops,
|
|
strokes, object, bm_mod)
|
|
ls_pairs = gstretch_align_pairs(ls_pairs,
|
|
object, bm_mod, self.method)
|
|
for (l, s) in ls_pairs:
|
|
if l == loop:
|
|
stroke = s
|
|
break
|
|
gstretch_erase_stroke(stroke, context)
|
|
|
|
# move vertices to new locations
|
|
if self.lock_x or self.lock_y or self.lock_z:
|
|
lock = [self.lock_x, self.lock_y, self.lock_z]
|
|
else:
|
|
lock = False
|
|
bmesh.update_edit_mesh(object.data, loop_triangles=True, destructive=True)
|
|
move_verts(object, bm, mapping, move, lock, self.influence)
|
|
|
|
# cleaning up
|
|
if derived:
|
|
bm_mod.free()
|
|
terminate()
|
|
|
|
return{'FINISHED'}
|
|
|
|
|
|
# relax operator
|
|
class Relax(Operator):
|
|
bl_idname = "mesh.looptools_relax"
|
|
bl_label = "Relax"
|
|
bl_description = "Relax the loop, so it is smoother"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
input: 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: 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: 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: 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
|
|
object, bm = initialise()
|
|
settings_write(self)
|
|
# check cache to see if we can save time
|
|
cached, single_loops, loops, derived, mapping = cache_read("Relax",
|
|
object, bm, self.input, False)
|
|
if cached:
|
|
derived, bm_mod = get_derived_bmesh(object, bm, False)
|
|
else:
|
|
# find loops
|
|
derived, bm_mod, loops = get_connected_input(object, bm, False, self.input)
|
|
mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
|
|
loops = check_loops(loops, mapping, bm_mod)
|
|
knots, points = relax_calculate_knots(loops)
|
|
|
|
# saving cache for faster execution next time
|
|
if not cached:
|
|
cache_write("Relax", object, bm, self.input, False, False, loops,
|
|
derived, mapping)
|
|
|
|
for iteration in range(int(self.iterations)):
|
|
# calculate splines and new positions
|
|
tknots, tpoints = relax_calculate_t(bm_mod, knots, points,
|
|
self.regular)
|
|
splines = []
|
|
for i in range(len(knots)):
|
|
splines.append(calculate_splines(self.interpolation, bm_mod,
|
|
tknots[i], knots[i]))
|
|
move = [relax_calculate_verts(bm_mod, self.interpolation,
|
|
tknots, knots, tpoints, points, splines)]
|
|
move_verts(object, bm, mapping, move, False, -1)
|
|
|
|
# cleaning up
|
|
if derived:
|
|
bm_mod.free()
|
|
terminate()
|
|
|
|
return{'FINISHED'}
|
|
|
|
|
|
# space operator
|
|
class Space(Operator):
|
|
bl_idname = "mesh.looptools_space"
|
|
bl_label = "Space"
|
|
bl_description = "Space the vertices in a regular distribution on the loop"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
influence: FloatProperty(
|
|
name="Influence",
|
|
description="Force of the tool",
|
|
default=100.0,
|
|
min=0.0,
|
|
max=100.0,
|
|
precision=1,
|
|
subtype='PERCENTAGE'
|
|
)
|
|
input: 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: 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'
|
|
)
|
|
lock_x: BoolProperty(
|
|
name="Lock X",
|
|
description="Lock editing of the x-coordinate",
|
|
default=False
|
|
)
|
|
lock_y: BoolProperty(
|
|
name="Lock Y",
|
|
description="Lock editing of the y-coordinate",
|
|
default=False
|
|
)
|
|
lock_z: BoolProperty(
|
|
name="Lock Z",
|
|
description="Lock editing of the z-coordinate",
|
|
default=False
|
|
)
|
|
|
|
@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_move = col.column(align=True)
|
|
row = col_move.row(align=True)
|
|
if self.lock_x:
|
|
row.prop(self, "lock_x", text="X", icon='LOCKED')
|
|
else:
|
|
row.prop(self, "lock_x", text="X", icon='UNLOCKED')
|
|
if self.lock_y:
|
|
row.prop(self, "lock_y", text="Y", icon='LOCKED')
|
|
else:
|
|
row.prop(self, "lock_y", text="Y", icon='UNLOCKED')
|
|
if self.lock_z:
|
|
row.prop(self, "lock_z", text="Z", icon='LOCKED')
|
|
else:
|
|
row.prop(self, "lock_z", text="Z", icon='UNLOCKED')
|
|
col_move.prop(self, "influence")
|
|
|
|
def invoke(self, context, event):
|
|
# load custom settings
|
|
settings_load(self)
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
# initialise
|
|
object, bm = initialise()
|
|
settings_write(self)
|
|
# check cache to see if we can save time
|
|
cached, single_loops, loops, derived, mapping = cache_read("Space",
|
|
object, bm, self.input, False)
|
|
if cached:
|
|
derived, bm_mod = get_derived_bmesh(object, bm, True)
|
|
else:
|
|
# find loops
|
|
derived, bm_mod, loops = get_connected_input(object, bm, True, self.input)
|
|
mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
|
|
loops = check_loops(loops, mapping, bm_mod)
|
|
|
|
# saving cache for faster execution next time
|
|
if not cached:
|
|
cache_write("Space", object, bm, 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(bm_mod, loop[0][:])
|
|
splines = calculate_splines(self.interpolation, bm_mod,
|
|
tknots, loop[0][:])
|
|
move.append(space_calculate_verts(bm_mod, self.interpolation,
|
|
tknots, tpoints, loop[0][:-1], splines))
|
|
# move vertices to new locations
|
|
if self.lock_x or self.lock_y or self.lock_z:
|
|
lock = [self.lock_x, self.lock_y, self.lock_z]
|
|
else:
|
|
lock = False
|
|
move_verts(object, bm, mapping, move, lock, self.influence)
|
|
|
|
# cleaning up
|
|
if derived:
|
|
bm_mod.free()
|
|
terminate()
|
|
|
|
cache_delete("Space")
|
|
|
|
return{'FINISHED'}
|
|
|
|
|
|
# ########################################
|
|
# ##### GUI and registration #############
|
|
# ########################################
|
|
|
|
# menu containing all tools
|
|
class VIEW3D_MT_edit_mesh_looptools(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_gstretch")
|
|
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(Panel):
|
|
bl_space_type = 'VIEW_3D'
|
|
bl_region_type = 'UI'
|
|
bl_category = 'Edit'
|
|
bl_context = "mesh_edit"
|
|
bl_label = "LoopTools"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
col = layout.column(align=True)
|
|
lt = context.window_manager.looptools
|
|
|
|
# bridge - first line
|
|
split = col.split(factor=0.15, align=True)
|
|
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(factor=0.15, align=True)
|
|
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()
|
|
|
|
col_move = box.column(align=True)
|
|
row = col_move.row(align=True)
|
|
if lt.circle_lock_x:
|
|
row.prop(lt, "circle_lock_x", text="X", icon='LOCKED')
|
|
else:
|
|
row.prop(lt, "circle_lock_x", text="X", icon='UNLOCKED')
|
|
if lt.circle_lock_y:
|
|
row.prop(lt, "circle_lock_y", text="Y", icon='LOCKED')
|
|
else:
|
|
row.prop(lt, "circle_lock_y", text="Y", icon='UNLOCKED')
|
|
if lt.circle_lock_z:
|
|
row.prop(lt, "circle_lock_z", text="Z", icon='LOCKED')
|
|
else:
|
|
row.prop(lt, "circle_lock_z", text="Z", icon='UNLOCKED')
|
|
col_move.prop(lt, "circle_influence")
|
|
|
|
# curve - first line
|
|
split = col.split(factor=0.15, align=True)
|
|
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()
|
|
|
|
col_move = box.column(align=True)
|
|
row = col_move.row(align=True)
|
|
if lt.curve_lock_x:
|
|
row.prop(lt, "curve_lock_x", text="X", icon='LOCKED')
|
|
else:
|
|
row.prop(lt, "curve_lock_x", text="X", icon='UNLOCKED')
|
|
if lt.curve_lock_y:
|
|
row.prop(lt, "curve_lock_y", text="Y", icon='LOCKED')
|
|
else:
|
|
row.prop(lt, "curve_lock_y", text="Y", icon='UNLOCKED')
|
|
if lt.curve_lock_z:
|
|
row.prop(lt, "curve_lock_z", text="Z", icon='LOCKED')
|
|
else:
|
|
row.prop(lt, "curve_lock_z", text="Z", icon='UNLOCKED')
|
|
col_move.prop(lt, "curve_influence")
|
|
|
|
# flatten - first line
|
|
split = col.split(factor=0.15, align=True)
|
|
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()
|
|
|
|
col_move = box.column(align=True)
|
|
row = col_move.row(align=True)
|
|
if lt.flatten_lock_x:
|
|
row.prop(lt, "flatten_lock_x", text="X", icon='LOCKED')
|
|
else:
|
|
row.prop(lt, "flatten_lock_x", text="X", icon='UNLOCKED')
|
|
if lt.flatten_lock_y:
|
|
row.prop(lt, "flatten_lock_y", text="Y", icon='LOCKED')
|
|
else:
|
|
row.prop(lt, "flatten_lock_y", text="Y", icon='UNLOCKED')
|
|
if lt.flatten_lock_z:
|
|
row.prop(lt, "flatten_lock_z", text="Z", icon='LOCKED')
|
|
else:
|
|
row.prop(lt, "flatten_lock_z", text="Z", icon='UNLOCKED')
|
|
col_move.prop(lt, "flatten_influence")
|
|
|
|
# gstretch - first line
|
|
split = col.split(factor=0.15, align=True)
|
|
if lt.display_gstretch:
|
|
split.prop(lt, "display_gstretch", text="", icon='DOWNARROW_HLT')
|
|
else:
|
|
split.prop(lt, "display_gstretch", text="", icon='RIGHTARROW')
|
|
split.operator("mesh.looptools_gstretch")
|
|
# gstretch settings
|
|
if lt.display_gstretch:
|
|
box = col.column(align=True).box().column()
|
|
box.prop(lt, "gstretch_use_guide")
|
|
if lt.gstretch_use_guide == "GPencil":
|
|
box.prop(lt, "gstretch_guide")
|
|
box.prop(lt, "gstretch_method")
|
|
|
|
col_conv = box.column(align=True)
|
|
col_conv.prop(lt, "gstretch_conversion", text="")
|
|
if lt.gstretch_conversion == 'distance':
|
|
col_conv.prop(lt, "gstretch_conversion_distance")
|
|
elif lt.gstretch_conversion == 'limit_vertices':
|
|
row = col_conv.row(align=True)
|
|
row.prop(lt, "gstretch_conversion_min", text="Min")
|
|
row.prop(lt, "gstretch_conversion_max", text="Max")
|
|
elif lt.gstretch_conversion == 'vertices':
|
|
col_conv.prop(lt, "gstretch_conversion_vertices")
|
|
box.separator()
|
|
|
|
col_move = box.column(align=True)
|
|
row = col_move.row(align=True)
|
|
if lt.gstretch_lock_x:
|
|
row.prop(lt, "gstretch_lock_x", text="X", icon='LOCKED')
|
|
else:
|
|
row.prop(lt, "gstretch_lock_x", text="X", icon='UNLOCKED')
|
|
if lt.gstretch_lock_y:
|
|
row.prop(lt, "gstretch_lock_y", text="Y", icon='LOCKED')
|
|
else:
|
|
row.prop(lt, "gstretch_lock_y", text="Y", icon='UNLOCKED')
|
|
if lt.gstretch_lock_z:
|
|
row.prop(lt, "gstretch_lock_z", text="Z", icon='LOCKED')
|
|
else:
|
|
row.prop(lt, "gstretch_lock_z", text="Z", icon='UNLOCKED')
|
|
col_move.prop(lt, "gstretch_influence")
|
|
if lt.gstretch_use_guide == "Annotation":
|
|
box.operator("remove.annotation", text="Delete Annotation Strokes")
|
|
if lt.gstretch_use_guide == "GPencil":
|
|
box.operator("remove.gp", text="Delete GPencil Strokes")
|
|
|
|
# loft - first line
|
|
split = col.split(factor=0.15, align=True)
|
|
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(factor=0.15, align=True)
|
|
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(factor=0.15, align=True)
|
|
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()
|
|
|
|
col_move = box.column(align=True)
|
|
row = col_move.row(align=True)
|
|
if lt.space_lock_x:
|
|
row.prop(lt, "space_lock_x", text="X", icon='LOCKED')
|
|
else:
|
|
row.prop(lt, "space_lock_x", text="X", icon='UNLOCKED')
|
|
if lt.space_lock_y:
|
|
row.prop(lt, "space_lock_y", text="Y", icon='LOCKED')
|
|
else:
|
|
row.prop(lt, "space_lock_y", text="Y", icon='UNLOCKED')
|
|
if lt.space_lock_z:
|
|
row.prop(lt, "space_lock_z", text="Z", icon='LOCKED')
|
|
else:
|
|
row.prop(lt, "space_lock_z", text="Z", icon='UNLOCKED')
|
|
col_move.prop(lt, "space_influence")
|
|
|
|
|
|
# property group containing all properties for the gui in the panel
|
|
class LoopToolsProps(PropertyGroup):
|
|
"""
|
|
Fake module like class
|
|
bpy.context.window_manager.looptools
|
|
"""
|
|
# general display properties
|
|
display_bridge: BoolProperty(
|
|
name="Bridge settings",
|
|
description="Display settings of the Bridge tool",
|
|
default=False
|
|
)
|
|
display_circle: BoolProperty(
|
|
name="Circle settings",
|
|
description="Display settings of the Circle tool",
|
|
default=False
|
|
)
|
|
display_curve: BoolProperty(
|
|
name="Curve settings",
|
|
description="Display settings of the Curve tool",
|
|
default=False
|
|
)
|
|
display_flatten: BoolProperty(
|
|
name="Flatten settings",
|
|
description="Display settings of the Flatten tool",
|
|
default=False
|
|
)
|
|
display_gstretch: BoolProperty(
|
|
name="Gstretch settings",
|
|
description="Display settings of the Gstretch tool",
|
|
default=False
|
|
)
|
|
display_loft: BoolProperty(
|
|
name="Loft settings",
|
|
description="Display settings of the Loft tool",
|
|
default=False
|
|
)
|
|
display_relax: BoolProperty(
|
|
name="Relax settings",
|
|
description="Display settings of the Relax tool",
|
|
default=False
|
|
)
|
|
display_space: BoolProperty(
|
|
name="Space settings",
|
|
description="Display settings of the Space tool",
|
|
default=False
|
|
)
|
|
|
|
# bridge properties
|
|
bridge_cubic_strength: FloatProperty(
|
|
name="Strength",
|
|
description="Higher strength results in more fluid curves",
|
|
default=1.0,
|
|
soft_min=-3.0,
|
|
soft_max=3.0
|
|
)
|
|
bridge_interpolation: 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: BoolProperty(
|
|
name="Loft",
|
|
description="Loft multiple loops, instead of considering them as "
|
|
"a multi-input for bridging",
|
|
default=False
|
|
)
|
|
bridge_loft_loop: BoolProperty(
|
|
name="Loop",
|
|
description="Connect the first and the last loop with each other",
|
|
default=False
|
|
)
|
|
bridge_min_width: 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: 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: BoolProperty(
|
|
name="Remove faces",
|
|
description="Remove faces that are internal after bridging",
|
|
default=True
|
|
)
|
|
bridge_reverse: 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: IntProperty(
|
|
name="Segments",
|
|
description="Number of segments used to bridge the gap (0=automatic)",
|
|
default=1,
|
|
min=0,
|
|
soft_max=20
|
|
)
|
|
bridge_twist: IntProperty(
|
|
name="Twist",
|
|
description="Twist what vertices are connected to each other",
|
|
default=0
|
|
)
|
|
|
|
# circle properties
|
|
circle_custom_radius: BoolProperty(
|
|
name="Radius",
|
|
description="Force a custom radius",
|
|
default=False
|
|
)
|
|
circle_fit: 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: BoolProperty(
|
|
name="Flatten",
|
|
description="Flatten the circle, instead of projecting it on the mesh",
|
|
default=True
|
|
)
|
|
circle_influence: FloatProperty(
|
|
name="Influence",
|
|
description="Force of the tool",
|
|
default=100.0,
|
|
min=0.0,
|
|
max=100.0,
|
|
precision=1,
|
|
subtype='PERCENTAGE'
|
|
)
|
|
circle_lock_x: BoolProperty(
|
|
name="Lock X",
|
|
description="Lock editing of the x-coordinate",
|
|
default=False
|
|
)
|
|
circle_lock_y: BoolProperty(
|
|
name="Lock Y",
|
|
description="Lock editing of the y-coordinate",
|
|
default=False
|
|
)
|
|
circle_lock_z: BoolProperty(
|
|
name="Lock Z",
|
|
description="Lock editing of the z-coordinate",
|
|
default=False
|
|
)
|
|
circle_radius: FloatProperty(
|
|
name="Radius",
|
|
description="Custom radius for circle",
|
|
default=1.0,
|
|
min=0.0,
|
|
soft_max=1000.0
|
|
)
|
|
circle_regular: BoolProperty(
|
|
name="Regular",
|
|
description="Distribute vertices at constant distances along the circle",
|
|
default=True
|
|
)
|
|
circle_angle: FloatProperty(
|
|
name="Angle",
|
|
description="Rotate a circle by an angle",
|
|
unit='ROTATION',
|
|
default=math.radians(0.0),
|
|
soft_min=math.radians(-360.0),
|
|
soft_max=math.radians(360.0)
|
|
)
|
|
# curve properties
|
|
curve_boundaries: BoolProperty(
|
|
name="Boundaries",
|
|
description="Limit the tool to work within the boundaries of the "
|
|
"selected vertices",
|
|
default=False
|
|
)
|
|
curve_influence: FloatProperty(
|
|
name="Influence",
|
|
description="Force of the tool",
|
|
default=100.0,
|
|
min=0.0,
|
|
max=100.0,
|
|
precision=1,
|
|
subtype='PERCENTAGE'
|
|
)
|
|
curve_interpolation: 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_lock_x: BoolProperty(
|
|
name="Lock X",
|
|
description="Lock editing of the x-coordinate",
|
|
default=False
|
|
)
|
|
curve_lock_y: BoolProperty(
|
|
name="Lock Y",
|
|
description="Lock editing of the y-coordinate",
|
|
default=False
|
|
)
|
|
curve_lock_z: BoolProperty(
|
|
name="Lock Z",
|
|
description="Lock editing of the z-coordinate",
|
|
default=False
|
|
)
|
|
curve_regular: BoolProperty(
|
|
name="Regular",
|
|
description="Distribute vertices at constant distances along the curve",
|
|
default=True
|
|
)
|
|
curve_restriction: 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: FloatProperty(
|
|
name="Influence",
|
|
description="Force of the tool",
|
|
default=100.0,
|
|
min=0.0,
|
|
max=100.0,
|
|
precision=1,
|
|
subtype='PERCENTAGE'
|
|
)
|
|
flatten_lock_x: BoolProperty(
|
|
name="Lock X",
|
|
description="Lock editing of the x-coordinate",
|
|
default=False)
|
|
flatten_lock_y: BoolProperty(name="Lock Y",
|
|
description="Lock editing of the y-coordinate",
|
|
default=False
|
|
)
|
|
flatten_lock_z: BoolProperty(
|
|
name="Lock Z",
|
|
description="Lock editing of the z-coordinate",
|
|
default=False
|
|
)
|
|
flatten_plane: 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: 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'
|
|
)
|
|
|
|
# gstretch properties
|
|
gstretch_conversion: EnumProperty(
|
|
name="Conversion",
|
|
items=(("distance", "Distance", "Set the distance between vertices "
|
|
"of the converted stroke"),
|
|
("limit_vertices", "Limit vertices", "Set the minimum and maximum "
|
|
"number of vertices that converted GP strokes will have"),
|
|
("vertices", "Exact vertices", "Set the exact number of vertices "
|
|
"that converted strokes will have. Short strokes "
|
|
"with few points may contain less vertices than this number."),
|
|
("none", "No simplification", "Convert each point "
|
|
"to a vertex")),
|
|
description="If strokes are converted to geometry, "
|
|
"use this simplification method",
|
|
default='limit_vertices'
|
|
)
|
|
gstretch_conversion_distance: FloatProperty(
|
|
name="Distance",
|
|
description="Absolute distance between vertices along the converted "
|
|
"stroke",
|
|
default=0.1,
|
|
min=0.000001,
|
|
soft_min=0.01,
|
|
soft_max=100
|
|
)
|
|
gstretch_conversion_max: IntProperty(
|
|
name="Max Vertices",
|
|
description="Maximum number of vertices strokes will "
|
|
"have, when they are converted to geomtery",
|
|
default=32,
|
|
min=3,
|
|
soft_max=500,
|
|
update=gstretch_update_min
|
|
)
|
|
gstretch_conversion_min: IntProperty(
|
|
name="Min Vertices",
|
|
description="Minimum number of vertices strokes will "
|
|
"have, when they are converted to geomtery",
|
|
default=8,
|
|
min=3,
|
|
soft_max=500,
|
|
update=gstretch_update_max
|
|
)
|
|
gstretch_conversion_vertices: IntProperty(
|
|
name="Vertices",
|
|
description="Number of vertices strokes will "
|
|
"have, when they are converted to geometry. If strokes have less "
|
|
"points than required, the 'Spread evenly' method is used",
|
|
default=32,
|
|
min=3,
|
|
soft_max=500
|
|
)
|
|
gstretch_delete_strokes: BoolProperty(
|
|
name="Delete strokes",
|
|
description="Remove Grease Pencil strokes if they have been used "
|
|
"for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
|
|
default=False
|
|
)
|
|
gstretch_influence: FloatProperty(
|
|
name="Influence",
|
|
description="Force of the tool",
|
|
default=100.0,
|
|
min=0.0,
|
|
max=100.0,
|
|
precision=1,
|
|
subtype='PERCENTAGE'
|
|
)
|
|
gstretch_lock_x: BoolProperty(
|
|
name="Lock X",
|
|
description="Lock editing of the x-coordinate",
|
|
default=False
|
|
)
|
|
gstretch_lock_y: BoolProperty(
|
|
name="Lock Y",
|
|
description="Lock editing of the y-coordinate",
|
|
default=False
|
|
)
|
|
gstretch_lock_z: BoolProperty(
|
|
name="Lock Z",
|
|
description="Lock editing of the z-coordinate",
|
|
default=False
|
|
)
|
|
gstretch_method: EnumProperty(
|
|
name="Method",
|
|
items=(("project", "Project", "Project vertices onto the stroke, "
|
|
"using vertex normals and connected edges"),
|
|
("irregular", "Spread", "Distribute vertices along the full "
|
|
"stroke, retaining relative distances between the vertices"),
|
|
("regular", "Spread evenly", "Distribute vertices at regular "
|
|
"distances along the full stroke")),
|
|
description="Method of distributing the vertices over the Grease "
|
|
"Pencil stroke",
|
|
default='regular'
|
|
)
|
|
gstretch_use_guide: EnumProperty(
|
|
name="Use guides",
|
|
items=(("None", "None", "None"),
|
|
("Annotation", "Annotation", "Annotation"),
|
|
("GPencil", "GPencil", "GPencil")),
|
|
default="None"
|
|
)
|
|
gstretch_guide: PointerProperty(
|
|
name="GPencil object",
|
|
description="Set GPencil object",
|
|
type=bpy.types.Object
|
|
)
|
|
|
|
# relax properties
|
|
relax_input: 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: 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: 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: BoolProperty(
|
|
name="Regular",
|
|
description="Distribute vertices at constant distances along the loop",
|
|
default=True
|
|
)
|
|
|
|
# space properties
|
|
space_influence: FloatProperty(
|
|
name="Influence",
|
|
description="Force of the tool",
|
|
default=100.0,
|
|
min=0.0,
|
|
max=100.0,
|
|
precision=1,
|
|
subtype='PERCENTAGE'
|
|
)
|
|
space_input: 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: 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'
|
|
)
|
|
space_lock_x: BoolProperty(
|
|
name="Lock X",
|
|
description="Lock editing of the x-coordinate",
|
|
default=False
|
|
)
|
|
space_lock_y: BoolProperty(
|
|
name="Lock Y",
|
|
description="Lock editing of the y-coordinate",
|
|
default=False
|
|
)
|
|
space_lock_z: BoolProperty(
|
|
name="Lock Z",
|
|
description="Lock editing of the z-coordinate",
|
|
default=False
|
|
)
|
|
|
|
# draw function for integration in menus
|
|
def menu_func(self, context):
|
|
self.layout.menu("VIEW3D_MT_edit_mesh_looptools")
|
|
self.layout.separator()
|
|
|
|
|
|
# Add-ons Preferences Update Panel
|
|
|
|
# Define Panel classes for updating
|
|
panels = (
|
|
VIEW3D_PT_tools_looptools,
|
|
)
|
|
|
|
|
|
def update_panel(self, context):
|
|
message = "LoopTools: Updating Panel locations has failed"
|
|
try:
|
|
for panel in panels:
|
|
if "bl_rna" in panel.__dict__:
|
|
bpy.utils.unregister_class(panel)
|
|
|
|
for panel in panels:
|
|
panel.bl_category = context.preferences.addons[__name__].preferences.category
|
|
bpy.utils.register_class(panel)
|
|
|
|
except Exception as e:
|
|
print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
|
|
pass
|
|
|
|
|
|
class LoopPreferences(AddonPreferences):
|
|
# this must match the addon name, use '__package__'
|
|
# when defining this in a submodule of a python package.
|
|
bl_idname = __name__
|
|
|
|
category: StringProperty(
|
|
name="Tab Category",
|
|
description="Choose a name for the category of the panel",
|
|
default="Edit",
|
|
update=update_panel
|
|
)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
row = layout.row()
|
|
col = row.column()
|
|
col.label(text="Tab Category:")
|
|
col.prop(self, "category", text="")
|
|
|
|
|
|
# define classes for registration
|
|
classes = (
|
|
VIEW3D_MT_edit_mesh_looptools,
|
|
VIEW3D_PT_tools_looptools,
|
|
LoopToolsProps,
|
|
Bridge,
|
|
Circle,
|
|
Curve,
|
|
Flatten,
|
|
GStretch,
|
|
Relax,
|
|
Space,
|
|
LoopPreferences,
|
|
RemoveAnnotation,
|
|
RemoveGPencil,
|
|
)
|
|
|
|
|
|
# registering and menu integration
|
|
def register():
|
|
for cls in classes:
|
|
bpy.utils.register_class(cls)
|
|
bpy.types.VIEW3D_MT_edit_mesh_context_menu.prepend(menu_func)
|
|
bpy.types.WindowManager.looptools = PointerProperty(type=LoopToolsProps)
|
|
update_panel(None, bpy.context)
|
|
|
|
|
|
# unregistering and removing menus
|
|
def unregister():
|
|
for cls in reversed(classes):
|
|
bpy.utils.unregister_class(cls)
|
|
bpy.types.VIEW3D_MT_edit_mesh_context_menu.remove(menu_func)
|
|
try:
|
|
del bpy.types.WindowManager.looptools
|
|
except Exception as e:
|
|
print('unregister fail:\n', e)
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register()
|