2025-07-01
This commit is contained in:
@@ -0,0 +1,524 @@
|
||||
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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user