Files
blender-portable-repo/extensions/blender_org/EdgeFlow/edgeloop.py
T
2026-03-17 14:30:01 -06:00

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