Initial Vibe coded project
This commit is contained in:
commit
0891cf12a5
21 files changed
+2830
No files matched your search
@@ -0,0 +1,158 @@
|
||||
"""Interpolating cubic flight paths and geometric (not mask-based) clipping."""
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
Point2D = tuple[float, float]
|
||||
Rectangle = tuple[float, float, float, float]
|
||||
|
||||
|
||||
def mix(a: Point2D, b: Point2D, fraction: float) -> Point2D:
|
||||
return a[0] + (b[0] - a[0]) * fraction, a[1] + (b[1] - a[1]) * fraction
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Cubic:
|
||||
start: Point2D
|
||||
control1: Point2D
|
||||
control2: Point2D
|
||||
end: Point2D
|
||||
|
||||
@property
|
||||
def points(self) -> tuple[Point2D, Point2D, Point2D, Point2D]:
|
||||
return self.start, self.control1, self.control2, self.end
|
||||
|
||||
def point(self, fraction: float) -> Point2D:
|
||||
a, b, c = (
|
||||
mix(p, q, fraction)
|
||||
for p, q in (
|
||||
(self.start, self.control1),
|
||||
(self.control1, self.control2),
|
||||
(self.control2, self.end),
|
||||
)
|
||||
)
|
||||
return mix(mix(a, b, fraction), mix(b, c, fraction), fraction)
|
||||
|
||||
def split(self, fraction: float) -> tuple["Cubic", "Cubic"]:
|
||||
a, b, c = (
|
||||
mix(p, q, fraction)
|
||||
for p, q in (
|
||||
(self.start, self.control1),
|
||||
(self.control1, self.control2),
|
||||
(self.control2, self.end),
|
||||
)
|
||||
)
|
||||
d, e = mix(a, b, fraction), mix(b, c, fraction)
|
||||
middle = mix(d, e, fraction)
|
||||
return Cubic(self.start, a, d, middle), Cubic(middle, e, c, self.end)
|
||||
|
||||
def portion(self, first: float, last: float) -> "Cubic":
|
||||
if not 0 <= first < last <= 1:
|
||||
raise ValueError("Curve portion must be an increasing interval in [0, 1].")
|
||||
left = self if last == 1 else self.split(last)[0]
|
||||
return left if first == 0 else left.split(first / last)[1]
|
||||
|
||||
def transform(self, matrix: list[float]) -> "Cubic":
|
||||
a, b, d, e, xoff, yoff = matrix
|
||||
points = [(a * x + b * y + xoff, d * x + e * y + yoff) for x, y in self.points]
|
||||
return Cubic(*points)
|
||||
|
||||
def boundary_roots(self, axis: int, boundary: float) -> list[float]:
|
||||
p0, p1, p2, p3 = (point[axis] for point in self.points)
|
||||
a = -p0 + 3 * p1 - 3 * p2 + p3
|
||||
b = 3 * p0 - 6 * p1 + 3 * p2
|
||||
c = -3 * p0 + 3 * p1
|
||||
critical = [0.0, 1.0]
|
||||
if abs(a) < 1e-12:
|
||||
if abs(b) >= 1e-12:
|
||||
critical.append(-c / (2 * b))
|
||||
else:
|
||||
discriminant = 4 * b * b - 12 * a * c
|
||||
if discriminant >= 0:
|
||||
root = math.sqrt(discriminant)
|
||||
critical.extend(((-2 * b - root) / (6 * a), (-2 * b + root) / (6 * a)))
|
||||
critical = sorted(t for t in critical if 0 <= t <= 1)
|
||||
roots = []
|
||||
for first, last in zip(critical, critical[1:], strict=False):
|
||||
low, high = first, last
|
||||
f_low = self.point(low)[axis] - boundary
|
||||
f_high = self.point(high)[axis] - boundary
|
||||
if abs(f_low) < 1e-9:
|
||||
roots.append(low)
|
||||
if abs(f_high) < 1e-9:
|
||||
roots.append(high)
|
||||
if f_low * f_high >= 0:
|
||||
continue
|
||||
for _ in range(55):
|
||||
middle = (low + high) / 2
|
||||
f_middle = self.point(middle)[axis] - boundary
|
||||
if f_low * f_middle <= 0:
|
||||
high = middle
|
||||
else:
|
||||
low, f_low = middle, f_middle
|
||||
roots.append((low + high) / 2)
|
||||
return roots
|
||||
|
||||
def inside_intervals(self, rectangle: Rectangle) -> list[tuple[float, float]]:
|
||||
west, south, east, north = rectangle
|
||||
xs, ys = [p[0] for p in self.points], [p[1] for p in self.points]
|
||||
if min(xs) >= west and max(xs) <= east and min(ys) >= south and max(ys) <= north:
|
||||
return [(0.0, 1.0)]
|
||||
if max(xs) < west or min(xs) > east or max(ys) < south or min(ys) > north:
|
||||
return []
|
||||
parameters = [0.0, 1.0]
|
||||
for axis, boundary in ((0, west), (0, east), (1, south), (1, north)):
|
||||
parameters.extend(self.boundary_roots(axis, boundary))
|
||||
parameters.sort()
|
||||
unique = [parameters[0]]
|
||||
for value in parameters[1:]:
|
||||
if value - unique[-1] > 1e-12:
|
||||
unique.append(value)
|
||||
result: list[tuple[float, float]] = []
|
||||
for first, last in zip(unique, unique[1:], strict=False):
|
||||
x, y = self.point((first + last) / 2)
|
||||
if west - 1e-9 <= x <= east + 1e-9 and south - 1e-9 <= y <= north + 1e-9:
|
||||
if result and abs(result[-1][1] - first) < 1e-12:
|
||||
result[-1] = result[-1][0], last
|
||||
else:
|
||||
result.append((first, last))
|
||||
return result
|
||||
|
||||
def clipped(self, rectangle: Rectangle) -> list["Cubic"]:
|
||||
return [self.portion(first, last) for first, last in self.inside_intervals(rectangle)]
|
||||
|
||||
|
||||
def interpolate(points: list[Point2D]) -> list[Cubic]:
|
||||
"""Centripetal Catmull-Rom converted to cubic Beziers through every sample."""
|
||||
result = []
|
||||
for index in range(len(points) - 1):
|
||||
p1, p2 = points[index], points[index + 1]
|
||||
if p1 == p2:
|
||||
result.append(Cubic(p1, p1, p2, p2))
|
||||
continue
|
||||
p0 = points[index - 1] if index > 0 else p1
|
||||
p3 = points[index + 2] if index + 2 < len(points) else p2
|
||||
if p0 == p1:
|
||||
p0 = (2 * p1[0] - p2[0], 2 * p1[1] - p2[1])
|
||||
if p3 == p2:
|
||||
p3 = (2 * p2[0] - p1[0], 2 * p2[1] - p1[1])
|
||||
d01 = math.sqrt(math.dist(p0, p1))
|
||||
d12 = math.sqrt(math.dist(p1, p2))
|
||||
d23 = math.sqrt(math.dist(p2, p3))
|
||||
control1 = []
|
||||
control2 = []
|
||||
for axis in (0, 1):
|
||||
tangent1 = d12 * (
|
||||
(p1[axis] - p0[axis]) / d01
|
||||
- (p2[axis] - p0[axis]) / (d01 + d12)
|
||||
+ (p2[axis] - p1[axis]) / d12
|
||||
)
|
||||
tangent2 = d12 * (
|
||||
(p2[axis] - p1[axis]) / d12
|
||||
- (p3[axis] - p1[axis]) / (d12 + d23)
|
||||
+ (p3[axis] - p2[axis]) / d23
|
||||
)
|
||||
control1.append(p1[axis] + tangent1 / 3)
|
||||
control2.append(p2[axis] - tangent2 / 3)
|
||||
result.append(Cubic(p1, (control1[0], control1[1]), (control2[0], control2[1]), p2))
|
||||
return result
|
||||
Reference in new issue
Block a user