525 lines
17 KiB
Python
525 lines
17 KiB
Python
import bpy
|
|
import bmesh
|
|
import math
|
|
import mathutils
|
|
|
|
from . import interpolate
|
|
|
|
from .op_set_vertex_curve import map_segment_onto_spline
|
|
|
|
class Loop():
|
|
def __init__(self, bm, edges):
|
|
self.bm = bm
|
|
self.edges = edges
|
|
|
|
#ordered verts of this loop
|
|
self.verts = []
|
|
if len(self.edges) > 1:
|
|
last_vert = None
|
|
for p in self.edges[0].verts:
|
|
if p not in self.edges[1].verts:
|
|
last_vert = p
|
|
|
|
self.verts.append(last_vert)
|
|
for i in range(len(self.edges)):
|
|
vert = self.edges[i].other_vert(last_vert)
|
|
self.verts.append(vert)
|
|
last_vert = vert
|
|
else:
|
|
self.verts = [self.edges[0].verts[0], self.edges[0].verts[1]]
|
|
|
|
# make sure start vert stays 'stable'
|
|
if self.verts[0].co.x + self.verts[0].co.y + self.verts[0].co.z < self.verts[-1].co.x + self.verts[-1].co.y + self.verts[-1].co.z:
|
|
self.verts.reverse()
|
|
self.edges.reverse()
|
|
|
|
#store intial vertex coordinates
|
|
self.initial_vert_positions = []
|
|
for i, v in enumerate(self.verts):
|
|
self.initial_vert_positions.append(v.co.copy())
|
|
|
|
self.is_cyclic = self.verts[0] == self.verts[-1]
|
|
|
|
# print("edgeloop length: %s" % len(self.edges))
|
|
self.valences = []
|
|
|
|
self.ring = {}
|
|
for e in self.edges:
|
|
self.ring[e] = []
|
|
|
|
self.edge_rings = {}
|
|
self.ends = {}
|
|
|
|
def __str__(self):
|
|
str = "\n"
|
|
for index, edge in enumerate(self.edges):
|
|
str += "edge: %s -" % (edge.index)
|
|
str += " valence: %s" % self.valences[index]
|
|
|
|
for r in self.get_ring(edge):
|
|
str += " | %s " % r.index
|
|
|
|
# print(self.edge_ring.values())
|
|
# for k,v in self.edge_ring.items():
|
|
# print("key: ", k.index)
|
|
# print("value: ", v)
|
|
|
|
# for loop in self.edge_ring[edge]:
|
|
# str += " = %s " % loop.edge.index
|
|
str += "\n"
|
|
|
|
ends = self.get_ring_ends(edge)
|
|
for e in ends:
|
|
str += " end: %s" % e.index
|
|
|
|
str += "\n"
|
|
return str
|
|
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
def set_ring(self, edge, ring_edge):
|
|
if edge in self.ring and len(self.ring[edge]) <= 2:
|
|
self.ring[edge].append(ring_edge)
|
|
|
|
def get_ring(self, edge):
|
|
if edge in self.ring:
|
|
return self.ring[edge]
|
|
|
|
raise Exception("edge not in Edgeloop!")
|
|
|
|
def select(self):
|
|
for edge in self.edges:
|
|
edge.select = True
|
|
|
|
def get_ring_ends(self, edge):
|
|
ring = self.edge_rings[edge]
|
|
return (ring[0], ring[len(ring) - 1])
|
|
|
|
def set_curve_flow(self, tension, use_rail, rail_type, rail_start, rail_end):
|
|
count = len(self.edges)
|
|
if count < 2 or self.is_cyclic:
|
|
return
|
|
|
|
self.bm.verts.ensure_lookup_table()
|
|
self.bm.edges.ensure_lookup_table()
|
|
|
|
start_vert, end_vert = None, None
|
|
#get starting points
|
|
for p in self.edges[0].verts:
|
|
if p not in self.edges[1].verts:
|
|
start_vert = p
|
|
|
|
for p in self.edges[-1].verts:
|
|
if p not in self.edges[-2].verts:
|
|
end_vert = p
|
|
|
|
def print_bm_loop(corner):
|
|
'''
|
|
Vert -> head -- Edge -> Tail
|
|
link_loop_prev => where head points to
|
|
'''
|
|
def get_string(corner):
|
|
return f"{corner.index} | vert: {corner.vert.index} edge: {corner.edge.index}"
|
|
|
|
print("----------------------------")
|
|
l = corner
|
|
print("corner: ", get_string(l))
|
|
l = corner.link_loop_next
|
|
print("link_loop_next ", get_string(l))
|
|
l = corner.link_loop_prev
|
|
print("link_loop_prev ", get_string(l))
|
|
l = corner.link_loop_radial_next
|
|
print("link_loop_radial_next", get_string(l))
|
|
l = corner.link_loop_radial_prev
|
|
print("link_loop_radial_prev", get_string(l))
|
|
print("----------------------------")
|
|
|
|
def find_direction(point, edge):
|
|
if len(point.link_edges) == 2:
|
|
# |_ corner case with mesh borders
|
|
a = point.link_edges[0].other_vert(point).co - point.co
|
|
b = point.link_edges[1].other_vert(point).co - point.co
|
|
|
|
# if a is edge
|
|
if point.link_edges[0] == edge:
|
|
c = a.cross(b)
|
|
d = c.cross(b)
|
|
# if b is edge
|
|
else:
|
|
c = b.cross(a)
|
|
d = c.cross(a)
|
|
|
|
return -d.normalized()
|
|
|
|
elif len(point.link_edges) == 3:
|
|
# original_corner = point.link_loops[0]
|
|
# for corner in point.link_loops:
|
|
# if corner.vert == point and corner.edge == edge:
|
|
# original_corner = corner
|
|
|
|
# edge is at an 'end'
|
|
# _|_
|
|
if len(edge.link_faces) == 2:
|
|
a = edge.other_vert(point).co - point.co
|
|
n = edge.link_loops[0].face.normal + edge.link_loops[1].face.normal
|
|
n = n.normalized()
|
|
c = a.cross(n)
|
|
c = c.cross(-n)
|
|
return c.normalized()
|
|
else:
|
|
# |_
|
|
# |
|
|
# search for the edge which is not neighbouring
|
|
# to the face connected to the input edge
|
|
for e in point.link_edges:
|
|
is_connected_to_end_edge = False
|
|
for f in e.link_faces:
|
|
if f in edge.link_faces:
|
|
is_connected_to_end_edge = True
|
|
break
|
|
|
|
if not is_connected_to_end_edge:
|
|
b = e.other_vert(point)
|
|
break
|
|
|
|
a = point
|
|
c = a.co - b.co
|
|
return c.normalized()
|
|
|
|
elif len(point.link_edges) == 4:
|
|
# regular quad case
|
|
# _|_
|
|
# |
|
|
for corner in edge.link_loops:
|
|
if corner.vert == point:
|
|
a = point
|
|
b = corner.link_loop_prev.link_loop_radial_prev.link_loop_prev.vert
|
|
c = a.co - b.co
|
|
return c.normalized()
|
|
else:
|
|
a = edge.other_vert(point).co - point.co
|
|
n = edge.link_loops[0].face.normal + edge.link_loops[1].face.normal
|
|
n = n.normalized()
|
|
c = a.cross(n)
|
|
c = c.cross(n)
|
|
return -c.normalized()
|
|
|
|
# if use_rail:
|
|
# dir1 = self.edges[0].other_vert(start_vert).co - start_vert.co
|
|
# dir1 = dir1.normalized()
|
|
# dir2 = self.edges[-1].other_vert(end_vert).co - end_vert.co
|
|
# dir2 = dir2.normalized()
|
|
# else:
|
|
# dir1 = find_direction(start_vert, self.edges[0])
|
|
# dir2 = find_direction(end_vert, self.edges[-1])
|
|
|
|
dir1_unnormalized = self.edges[0].other_vert(start_vert).co - start_vert.co
|
|
dir1 = dir1_unnormalized.normalized()
|
|
dir2_unnormalized = self.edges[-1].other_vert(end_vert).co - end_vert.co
|
|
dir2 = dir2_unnormalized.normalized()
|
|
|
|
if use_rail:
|
|
if rail_type == 'ABSOLUTE':
|
|
p1 = start_vert.co + (dir1_unnormalized - dir1 * rail_start)
|
|
p4 = end_vert.co + (dir2_unnormalized - dir2 * rail_end)
|
|
else: # == 'FACTOR'
|
|
p1 = start_vert.co + dir1_unnormalized * rail_start
|
|
p4 = end_vert.co + dir2_unnormalized * rail_end
|
|
else:
|
|
p1 = start_vert.co
|
|
p4 = end_vert.co
|
|
|
|
scale = (p1 - p4).length * 0.5
|
|
scale *= tension
|
|
|
|
p2 = p1 + (dir1 * scale)
|
|
p3 = p4 + (dir2 * scale)
|
|
|
|
# add_debug_verts = False
|
|
# if add_debug_verts:
|
|
# bmesh.ops.create_vert(self.bm, co=p1)
|
|
# bmesh.ops.create_vert(self.bm, co=p4)
|
|
# bmesh.ops.create_vert(self.bm, co=p2)
|
|
# bmesh.ops.create_vert(self.bm, co=p3)
|
|
|
|
spline_points = []
|
|
precision = 1000
|
|
spline_points = mathutils.geometry.interpolate_bezier(p1, p2, p3, p4, precision)
|
|
|
|
map_segment_onto_spline(self.verts, spline_points)
|
|
|
|
|
|
|
|
|
|
def straighten(self, distance):
|
|
'''
|
|
this makes takes the end points of an edge and places them even distanced to the 'next' vert in the extension of the edge loop
|
|
|
|
Moves A and B:
|
|
|
|
A' ------ A - B -- B'
|
|
|
|
to:
|
|
|
|
A' --- A --- B --- B'
|
|
'''
|
|
|
|
|
|
edge = self.edges[0]
|
|
|
|
def find_neighbour(p):
|
|
link_edges = set(p.link_edges)
|
|
link_edges.remove(edge)
|
|
|
|
#print("face a:", edge.link_faces[0].index, "face b:", edge.link_faces[1].index)
|
|
|
|
faceA_is_quad = len(edge.link_faces[0].verts) == 4
|
|
|
|
edges = link_edges
|
|
if faceA_is_quad:
|
|
edges -= set(edge.link_faces[0].edges)
|
|
|
|
if not edge.is_boundary:
|
|
faceB_is_quad = len(edge.link_faces[1].verts) == 4
|
|
if faceB_is_quad:
|
|
edges -= set(edge.link_faces[1].edges)
|
|
|
|
v = mathutils.Vector((0, 0, 0))
|
|
count = 0
|
|
|
|
for e in edges:
|
|
for vert in e.verts:
|
|
if vert == p:
|
|
continue
|
|
|
|
v += vert.co
|
|
count += 1
|
|
|
|
if count > 0:
|
|
v /= count
|
|
|
|
return v
|
|
|
|
a1 = edge.verts[0]
|
|
a2 = edge.verts[1]
|
|
|
|
a1_len = len(a1.link_edges)
|
|
a2_len = len(a2.link_edges)
|
|
if a1_len <= 3 or a2_len <= 3:
|
|
return
|
|
|
|
b1 = find_neighbour(a1)
|
|
b2 = find_neighbour(a2)
|
|
|
|
direction = (b2 - b1).normalized()
|
|
max_distance = (b2 - b1).length
|
|
|
|
if distance * 2.0 > max_distance:
|
|
distance = max_distance * 0.5
|
|
|
|
a1.co = b1 + distance * direction
|
|
a2.co = b2 - distance * direction
|
|
|
|
|
|
def set_linear(self, even_spacing):
|
|
count = len(self.edges)
|
|
if count < 2 or self.is_cyclic:
|
|
return
|
|
|
|
for p in self.edges[0].verts:
|
|
if p not in self.edges[1].verts:
|
|
p1 = p
|
|
|
|
for p in self.edges[-1].verts:
|
|
if p not in self.edges[-2].verts:
|
|
p2 = p
|
|
|
|
direction = (p2.co - p1.co)
|
|
direction = direction / (count)
|
|
direction_normalized = direction.normalized()
|
|
|
|
last_vert = p1
|
|
for i in range(count - 1):
|
|
vert = self.edges[i].other_vert(last_vert)
|
|
|
|
if even_spacing:
|
|
vert.co = p1.co + direction * (i + 1)
|
|
else:
|
|
proj = vert.co - p1.co
|
|
scalar = proj.dot(direction_normalized)
|
|
vert.co = p1.co + (direction_normalized * scalar)
|
|
|
|
last_vert = vert
|
|
|
|
|
|
def blend_start_end(self, blend_start, blend_end, blend_type):
|
|
|
|
if self.is_cyclic:
|
|
# print("skip cyclic loop")
|
|
return
|
|
|
|
count = len(self.verts)
|
|
start_count = blend_start
|
|
end_count = blend_end
|
|
|
|
if start_count + end_count >= count:
|
|
if start_count < end_count:
|
|
end_count = max(count - start_count - 1, 0)
|
|
elif end_count < start_count:
|
|
start_count = max(count - end_count - 1, 0)
|
|
else:
|
|
midCount = math.floor(count / 2)
|
|
start_count = count - midCount
|
|
end_count = count - start_count
|
|
|
|
#print(f"start:{blend_start} - end:{blend_end} - vertcount: {count}")
|
|
#print(f"start_count:{start_count} - end_count:{end_count} - count: {count}")
|
|
|
|
def apply_blend(blend_range, reverse):
|
|
indices = list(range(count))
|
|
if reverse:
|
|
indices.reverse()
|
|
|
|
distances = [0]
|
|
total_length = 0
|
|
|
|
for i in range(1, blend_range+1):
|
|
a = self.verts[indices[i]]
|
|
b = self.verts[indices[i-1]]
|
|
length = (a.co - b.co).length
|
|
total_length += length
|
|
distances.append(total_length)
|
|
|
|
# print(f"total length: {total_length} - number of distances: {len(distances)}")
|
|
|
|
if total_length == 0:
|
|
return
|
|
|
|
for i in range(blend_range+1):
|
|
blend_value = distances[i] / total_length
|
|
|
|
if blend_type == 'SMOOTH':
|
|
blend_value = interpolate.smooth_step(0.0, 1.0, blend_value)
|
|
|
|
vert = self.verts[indices[i]]
|
|
intital_position = self.initial_vert_positions[indices[i]]
|
|
vert.co = intital_position.lerp(vert.co, blend_value)
|
|
|
|
if blend_start > 0:
|
|
apply_blend(min(count-1, start_count), reverse=False)
|
|
if blend_end > 0:
|
|
apply_blend(min(count-1, end_count), reverse=True)
|
|
|
|
|
|
def set_flow(self, tension, min_angle):
|
|
|
|
for edge in self.edges:
|
|
target = {}
|
|
|
|
if edge.is_boundary:
|
|
continue
|
|
|
|
for loop in edge.link_loops:
|
|
# todo check triangles/ngons?
|
|
|
|
ring1 = loop.link_loop_next.link_loop_next
|
|
ring2 = loop.link_loop_radial_prev.link_loop_prev.link_loop_prev
|
|
|
|
center = edge.other_vert(loop.vert)
|
|
|
|
p1 = None
|
|
p2 = ring1.vert
|
|
p3 = ring2.link_loop_radial_next.vert
|
|
p4 = None
|
|
|
|
#print("ring1 %s - %s" % (ring1.vert.index, ring1.edge.index))
|
|
#print("ring2 %s - %s" % (ring2.vert.index, ring2.edge.index))
|
|
# print("p2: %s - p3: %s " % (p2.index, p3.index))
|
|
|
|
result = []
|
|
if not ring1.edge.is_boundary:
|
|
|
|
final = ring1.link_loop_radial_next.link_loop_next
|
|
a, b = final.edge.verts
|
|
if p2 == a:
|
|
p1 = b.co
|
|
else:
|
|
p1 = a.co
|
|
|
|
a = (p1 - p2.co).normalized()
|
|
b = (center.co - p2.co).normalized()
|
|
dot = min(1.0, max(-1.0, a.dot(b)))
|
|
angle = math.acos(dot)
|
|
|
|
if angle < min_angle:
|
|
# print("r1: %s" % (math.degrees(angle)))
|
|
p1 = p2.co - (p3.co - p2.co) * 0.5
|
|
# bmesh.ops.create_vert(self.bm, co=p1)
|
|
|
|
else:
|
|
p1 = p2.co - (p3.co - p2.co)
|
|
# bmesh.ops.create_vert(self.bm, co=p1)
|
|
|
|
result.append(p1)
|
|
result.append(p2.co)
|
|
|
|
if not ring2.edge.is_boundary:
|
|
is_quad = len(ring2.face.verts) == 4
|
|
# if is_quad:
|
|
final = ring2.link_loop_radial_prev.link_loop_prev
|
|
# else:
|
|
# final = ring2
|
|
|
|
#print("is_quad:", is_quad, " - ", final.edge.index)
|
|
|
|
a, b = final.edge.verts
|
|
|
|
if p3 == a:
|
|
p4 = b.co
|
|
else:
|
|
p4 = a.co
|
|
|
|
a = (p4 - p3.co).normalized()
|
|
b = (center.co - p3.co).normalized()
|
|
dot = min(1.0, max(-1.0, a.dot(b)))
|
|
angle = math.acos(dot)
|
|
|
|
if angle < min_angle:
|
|
# print("r2: %s" % (math.degrees(angle)))
|
|
p4 = p3.co - (p2.co - p3.co) * 0.5
|
|
|
|
# bmesh.ops.create_vert(self.bm, co=p4)
|
|
|
|
else:
|
|
# radial_next doenst work at boundary
|
|
p3 = ring2.edge.other_vert(p3)
|
|
p4 = p3.co - (p2.co - p3.co)
|
|
# bmesh.ops.create_vert(self.bm, co=p4)
|
|
|
|
result.append(p3.co)
|
|
result.append(p4)
|
|
|
|
target[center] = result
|
|
|
|
for vert, points in target.items():
|
|
p1, p2, p3, p4 = points
|
|
|
|
if p1 == p2 or p3 == p4:
|
|
print("invalid input - two control points are identical!")
|
|
continue
|
|
|
|
# normalize point distances so that long edges dont skew the curve
|
|
d = (p2 - p3).length * 0.5
|
|
|
|
p1 = p2 + (d * (p1 - p2).normalized())
|
|
p4 = p3 + (d * (p4 - p3).normalized())
|
|
|
|
# result = interpolate.catmullrom(p1, p2, p3, p4, 1, 3)[1]
|
|
result = interpolate.hermite_3d(p1, p2, p3, p4, 0.5, -tension, 0)
|
|
result = mathutils.Vector(result)
|
|
vert.co = result
|
|
|
|
|
|
|