use std::cell::RefCell;
use std::hash::{Hash, Hasher};
use kurbo::{BezPath, CubicBez, ParamCurve, PathEl, Point, Vec2};
use crate::Lerp;
#[derive(Debug, Clone, PartialEq)]
struct Run {
segs: Vec<CubicBez>,
closed: bool,
}
impl Run {
fn start(&self) -> Point {
self.segs.first().map_or(Point::ZERO, |s| s.p0)
}
fn centroid(&self) -> Point {
if self.segs.is_empty() {
return Point::ZERO;
}
let sum = self
.segs
.iter()
.fold(Vec2::ZERO, |acc, s| acc + s.p0.to_vec2());
(sum / self.segs.len() as f64).to_point()
}
fn signed_area(&self) -> f64 {
self.segs
.iter()
.map(|s| {
let pts = [s.p0, s.p1, s.p2, s.p3];
pts.windows(2)
.map(|w| w[0].x * w[1].y - w[1].x * w[0].y)
.sum::<f64>()
})
.sum::<f64>()
/ 2.0
}
fn reversed(&self) -> Run {
Run {
segs: self
.segs
.iter()
.rev()
.map(|s| CubicBez::new(s.p3, s.p2, s.p1, s.p0))
.collect(),
closed: self.closed,
}
}
fn rotated(&self, k: usize) -> Run {
let mut segs = self.segs.clone();
segs.rotate_left(k);
Run {
segs,
closed: self.closed,
}
}
fn point(p: Point, n: usize, closed: bool) -> Run {
Run {
segs: vec![CubicBez::new(p, p, p, p); n.max(1)],
closed,
}
}
fn split_to(&mut self, n: usize) {
while self.segs.len() < n {
let (i, _) = self
.segs
.iter()
.enumerate()
.map(|(i, s)| (i, s.p0.distance(s.p3) + 1e-9 * i as f64))
.fold(
(0, f64::MIN),
|best, cur| if cur.1 > best.1 { cur } else { best },
);
let s = self.segs[i];
let (a, b) = (s.subsegment(0.0..0.5), s.subsegment(0.5..1.0));
self.segs.splice(i..=i, [a, b]);
}
}
fn to_path(&self, out: &mut BezPath) {
let Some(first) = self.segs.first() else {
return;
};
out.move_to(first.p0);
for s in &self.segs {
out.curve_to(s.p1, s.p2, s.p3);
}
if self.closed {
out.close_path();
}
}
}
fn line(a: Point, b: Point) -> CubicBez {
CubicBez::new(a, a.lerp(b, 1.0 / 3.0), a.lerp(b, 2.0 / 3.0), b)
}
fn runs(path: &BezPath) -> Vec<Run> {
let mut out: Vec<Run> = Vec::new();
let mut cur: Option<Run> = None;
let (mut start, mut last) = (Point::ZERO, Point::ZERO);
let finish = |cur: &mut Option<Run>, out: &mut Vec<Run>| {
if let Some(r) = cur.take()
&& !r.segs.is_empty()
{
out.push(r);
}
};
for el in path.elements() {
match *el {
PathEl::MoveTo(p) => {
finish(&mut cur, &mut out);
cur = Some(Run {
segs: Vec::new(),
closed: false,
});
(start, last) = (p, p);
}
PathEl::LineTo(p) => {
cur.get_or_insert_with(|| Run {
segs: Vec::new(),
closed: false,
})
.segs
.push(line(last, p));
last = p;
}
PathEl::QuadTo(c, p) => {
let q = kurbo::QuadBez::new(last, c, p).raise();
cur.get_or_insert_with(|| Run {
segs: Vec::new(),
closed: false,
})
.segs
.push(q);
last = p;
}
PathEl::CurveTo(a, b, p) => {
cur.get_or_insert_with(|| Run {
segs: Vec::new(),
closed: false,
})
.segs
.push(CubicBez::new(last, a, b, p));
last = p;
}
PathEl::ClosePath => {
if let Some(r) = cur.as_mut() {
if last.distance(start) > 1e-9 {
r.segs.push(line(last, start));
}
r.closed = true;
}
finish(&mut cur, &mut out);
last = start;
}
}
}
finish(&mut cur, &mut out);
out
}
fn align_run(a: &mut Run, b: &mut Run) {
let n = a.segs.len().max(b.segs.len());
a.split_to(n);
b.split_to(n);
if !(a.closed && b.closed) {
return;
}
if a.signed_area() * b.signed_area() < 0.0 {
*b = b.reversed();
}
let cost = |k: usize| -> f64 {
(0..n)
.map(|i| a.segs[i].p0.distance_squared(b.segs[(i + k) % n].p0))
.sum()
};
let best = (0..n)
.map(|k| (k, cost(k)))
.fold(
(0, f64::MAX),
|best, cur| if cur.1 < best.1 { cur } else { best },
)
.0;
*b = b.rotated(best);
}
fn align(from: &BezPath, to: &BezPath) -> (Vec<Run>, Vec<Run>) {
let mut a = runs(from);
let mut b = runs(to);
let n = a.len().max(b.len());
for i in 0..n {
match (a.get(i), b.get(i)) {
(Some(r), None) => b.push(Run::point(r.centroid(), r.segs.len(), r.closed)),
(None, Some(r)) => a.push(Run::point(r.centroid(), r.segs.len(), r.closed)),
_ => {}
}
}
for (ra, rb) in a.iter_mut().zip(b.iter_mut()) {
if ra.closed != rb.closed {
ra.closed = true;
rb.closed = true;
for r in [&mut *ra, &mut *rb] {
let (s, e) = (r.start(), r.segs.last().map_or(r.start(), |c| c.p3));
if s.distance(e) > 1e-9 {
r.segs.push(line(e, s));
}
}
}
align_run(ra, rb);
}
(a, b)
}
fn path_hash(p: &BezPath, h: &mut impl Hasher) {
for el in p.elements() {
let pts: &[Point] = match el {
PathEl::MoveTo(p) | PathEl::LineTo(p) => std::slice::from_ref(p),
PathEl::QuadTo(a, b) => &[*a, *b],
PathEl::CurveTo(a, b, c) => &[*a, *b, *c],
PathEl::ClosePath => &[],
};
std::mem::discriminant(el).hash(h);
for p in pts {
p.x.to_bits().hash(h);
p.y.to_bits().hash(h);
}
}
}
type Aligned = (u64, std::rc::Rc<(Vec<Run>, Vec<Run>)>);
thread_local! {
static CACHE: RefCell<Vec<Aligned>> = const { RefCell::new(Vec::new()) };
}
const CACHE_SIZE: usize = 16;
fn aligned(from: &BezPath, to: &BezPath) -> std::rc::Rc<(Vec<Run>, Vec<Run>)> {
let mut h = std::hash::DefaultHasher::new();
path_hash(from, &mut h);
0xffu8.hash(&mut h);
path_hash(to, &mut h);
let key = h.finish();
CACHE.with(|c| {
let mut c = c.borrow_mut();
if let Some(pos) = c.iter().position(|(k, _)| *k == key) {
let entry = c.remove(pos);
c.push(entry.clone());
return entry.1;
}
let value = std::rc::Rc::new(align(from, to));
if c.len() >= CACHE_SIZE {
c.remove(0);
}
c.push((key, value.clone()));
value
})
}
impl Lerp for BezPath {
fn lerp(&self, to: &Self, t: f64) -> Self {
if t <= 0.0 {
return self.clone();
}
if t >= 1.0 {
return to.clone();
}
let pair = aligned(self, to);
let mut out = BezPath::new();
for (ra, rb) in pair.0.iter().zip(&pair.1) {
let run = Run {
segs: ra
.segs
.iter()
.zip(&rb.segs)
.map(|(x, y)| {
CubicBez::new(
x.p0.lerp(y.p0, t),
x.p1.lerp(y.p1, t),
x.p2.lerp(y.p2, t),
x.p3.lerp(y.p3, t),
)
})
.collect(),
closed: ra.closed,
};
run.to_path(&mut out);
}
out
}
}
#[cfg(test)]
mod tests {
use kurbo::{Circle, Rect, Shape};
use super::*;
fn sample(p: &BezPath, n: usize) -> Vec<Point> {
let segs: Vec<_> = p.segments().collect();
(0..n)
.map(|i| {
let u = i as f64 / n as f64 * segs.len() as f64;
segs[(u as usize).min(segs.len() - 1)].eval(u.fract())
})
.collect()
}
#[test]
fn endpoints_are_exact() {
let a = Rect::new(0.0, 0.0, 10.0, 10.0).to_path(0.1);
let b = Circle::new((5.0, 5.0), 5.0).to_path(0.1);
assert_eq!(a.lerp(&b, 0.0), a);
assert_eq!(a.lerp(&b, 1.0), b);
}
#[test]
fn midpoint_of_identical_shapes_is_the_shape() {
let a = Circle::new((0.0, 0.0), 10.0).to_path(0.1);
let mid = a.lerp(&a, 0.5);
for (p, q) in sample(&mid, 32).iter().zip(sample(&a, 32)) {
assert!(p.distance(q) < 1e-9);
}
}
#[test]
fn rect_to_circle_stays_between_them() {
let a = Rect::new(-10.0, -10.0, 10.0, 10.0).to_path(0.1);
let b = Circle::new((0.0, 0.0), 10.0).to_path(0.1);
let mid = a.lerp(&b, 0.5);
for p in sample(&mid, 64) {
let r = p.to_vec2().hypot();
assert!((9.0..=14.2).contains(&r), "{p:?} r={r}");
}
}
#[test]
fn rotation_and_direction_are_aligned() {
let a = Rect::new(-10.0, -10.0, 10.0, 10.0).to_path(0.1);
let mut b = BezPath::new();
b.move_to((10.0, 10.0));
b.line_to((10.0, -10.0));
b.line_to((-10.0, -10.0));
b.line_to((-10.0, 10.0));
b.close_path();
let mid = a.lerp(&b, 0.5);
for p in sample(&mid, 16) {
assert!(p.x.abs().max(p.y.abs()) > 9.99, "{p:?}");
}
}
#[test]
fn different_run_counts_grow_from_points() {
let a = Circle::new((0.0, 0.0), 10.0).to_path(0.1);
let mut b = Circle::new((-20.0, 0.0), 5.0).to_path(0.1);
b.extend(Circle::new((20.0, 0.0), 5.0).to_path(0.1));
let mid = a.lerp(&b, 0.5);
assert_eq!(
mid.elements()
.iter()
.filter(|e| matches!(e, PathEl::MoveTo(_)))
.count(),
2
);
}
}