1use std::cell::RefCell;
10use std::hash::{Hash, Hasher};
11
12use kurbo::{BezPath, CubicBez, ParamCurve, PathEl, Point, Vec2};
13
14use crate::Lerp;
15
16#[derive(Debug, Clone, PartialEq)]
17struct Run {
18 segs: Vec<CubicBez>,
19 closed: bool,
20}
21
22impl Run {
23 fn start(&self) -> Point {
24 self.segs.first().map_or(Point::ZERO, |s| s.p0)
25 }
26
27 fn centroid(&self) -> Point {
28 if self.segs.is_empty() {
29 return Point::ZERO;
30 }
31 let sum = self
32 .segs
33 .iter()
34 .fold(Vec2::ZERO, |acc, s| acc + s.p0.to_vec2());
35 (sum / self.segs.len() as f64).to_point()
36 }
37
38 fn signed_area(&self) -> f64 {
40 self.segs
41 .iter()
42 .map(|s| {
43 let pts = [s.p0, s.p1, s.p2, s.p3];
44 pts.windows(2)
45 .map(|w| w[0].x * w[1].y - w[1].x * w[0].y)
46 .sum::<f64>()
47 })
48 .sum::<f64>()
49 / 2.0
50 }
51
52 fn reversed(&self) -> Run {
53 Run {
54 segs: self
55 .segs
56 .iter()
57 .rev()
58 .map(|s| CubicBez::new(s.p3, s.p2, s.p1, s.p0))
59 .collect(),
60 closed: self.closed,
61 }
62 }
63
64 fn rotated(&self, k: usize) -> Run {
65 let mut segs = self.segs.clone();
66 segs.rotate_left(k);
67 Run {
68 segs,
69 closed: self.closed,
70 }
71 }
72
73 fn point(p: Point, n: usize, closed: bool) -> Run {
75 Run {
76 segs: vec![CubicBez::new(p, p, p, p); n.max(1)],
77 closed,
78 }
79 }
80
81 fn split_to(&mut self, n: usize) {
83 while self.segs.len() < n {
84 let (i, _) = self
85 .segs
86 .iter()
87 .enumerate()
88 .map(|(i, s)| (i, s.p0.distance(s.p3) + 1e-9 * i as f64))
89 .fold(
90 (0, f64::MIN),
91 |best, cur| if cur.1 > best.1 { cur } else { best },
92 );
93 let s = self.segs[i];
94 let (a, b) = (s.subsegment(0.0..0.5), s.subsegment(0.5..1.0));
95 self.segs.splice(i..=i, [a, b]);
96 }
97 }
98
99 fn to_path(&self, out: &mut BezPath) {
100 let Some(first) = self.segs.first() else {
101 return;
102 };
103 out.move_to(first.p0);
104 for s in &self.segs {
105 out.curve_to(s.p1, s.p2, s.p3);
106 }
107 if self.closed {
108 out.close_path();
109 }
110 }
111}
112
113fn line(a: Point, b: Point) -> CubicBez {
114 CubicBez::new(a, a.lerp(b, 1.0 / 3.0), a.lerp(b, 2.0 / 3.0), b)
115}
116
117fn runs(path: &BezPath) -> Vec<Run> {
118 let mut out: Vec<Run> = Vec::new();
119 let mut cur: Option<Run> = None;
120 let (mut start, mut last) = (Point::ZERO, Point::ZERO);
121 let finish = |cur: &mut Option<Run>, out: &mut Vec<Run>| {
122 if let Some(r) = cur.take()
123 && !r.segs.is_empty()
124 {
125 out.push(r);
126 }
127 };
128 for el in path.elements() {
129 match *el {
130 PathEl::MoveTo(p) => {
131 finish(&mut cur, &mut out);
132 cur = Some(Run {
133 segs: Vec::new(),
134 closed: false,
135 });
136 (start, last) = (p, p);
137 }
138 PathEl::LineTo(p) => {
139 cur.get_or_insert_with(|| Run {
140 segs: Vec::new(),
141 closed: false,
142 })
143 .segs
144 .push(line(last, p));
145 last = p;
146 }
147 PathEl::QuadTo(c, p) => {
148 let q = kurbo::QuadBez::new(last, c, p).raise();
149 cur.get_or_insert_with(|| Run {
150 segs: Vec::new(),
151 closed: false,
152 })
153 .segs
154 .push(q);
155 last = p;
156 }
157 PathEl::CurveTo(a, b, p) => {
158 cur.get_or_insert_with(|| Run {
159 segs: Vec::new(),
160 closed: false,
161 })
162 .segs
163 .push(CubicBez::new(last, a, b, p));
164 last = p;
165 }
166 PathEl::ClosePath => {
167 if let Some(r) = cur.as_mut() {
168 if last.distance(start) > 1e-9 {
169 r.segs.push(line(last, start));
170 }
171 r.closed = true;
172 }
173 finish(&mut cur, &mut out);
174 last = start;
175 }
176 }
177 }
178 finish(&mut cur, &mut out);
179 out
180}
181
182fn align_run(a: &mut Run, b: &mut Run) {
184 let n = a.segs.len().max(b.segs.len());
185 a.split_to(n);
186 b.split_to(n);
187 if !(a.closed && b.closed) {
188 return;
189 }
190 if a.signed_area() * b.signed_area() < 0.0 {
191 *b = b.reversed();
192 }
193 let cost = |k: usize| -> f64 {
194 (0..n)
195 .map(|i| a.segs[i].p0.distance_squared(b.segs[(i + k) % n].p0))
196 .sum()
197 };
198 let best = (0..n)
199 .map(|k| (k, cost(k)))
200 .fold(
201 (0, f64::MAX),
202 |best, cur| if cur.1 < best.1 { cur } else { best },
203 )
204 .0;
205 *b = b.rotated(best);
206}
207
208fn align(from: &BezPath, to: &BezPath) -> (Vec<Run>, Vec<Run>) {
209 let mut a = runs(from);
210 let mut b = runs(to);
211 let n = a.len().max(b.len());
214 for i in 0..n {
215 match (a.get(i), b.get(i)) {
216 (Some(r), None) => b.push(Run::point(r.centroid(), r.segs.len(), r.closed)),
217 (None, Some(r)) => a.push(Run::point(r.centroid(), r.segs.len(), r.closed)),
218 _ => {}
219 }
220 }
221 for (ra, rb) in a.iter_mut().zip(b.iter_mut()) {
222 if ra.closed != rb.closed {
223 ra.closed = true;
224 rb.closed = true;
225 for r in [&mut *ra, &mut *rb] {
227 let (s, e) = (r.start(), r.segs.last().map_or(r.start(), |c| c.p3));
228 if s.distance(e) > 1e-9 {
229 r.segs.push(line(e, s));
230 }
231 }
232 }
233 align_run(ra, rb);
234 }
235 (a, b)
236}
237
238fn path_hash(p: &BezPath, h: &mut impl Hasher) {
239 for el in p.elements() {
240 let pts: &[Point] = match el {
241 PathEl::MoveTo(p) | PathEl::LineTo(p) => std::slice::from_ref(p),
242 PathEl::QuadTo(a, b) => &[*a, *b],
243 PathEl::CurveTo(a, b, c) => &[*a, *b, *c],
244 PathEl::ClosePath => &[],
245 };
246 std::mem::discriminant(el).hash(h);
247 for p in pts {
248 p.x.to_bits().hash(h);
249 p.y.to_bits().hash(h);
250 }
251 }
252}
253
254type Aligned = (u64, std::rc::Rc<(Vec<Run>, Vec<Run>)>);
255
256thread_local! {
257 static CACHE: RefCell<Vec<Aligned>> = const { RefCell::new(Vec::new()) };
258}
259
260const CACHE_SIZE: usize = 16;
261
262fn aligned(from: &BezPath, to: &BezPath) -> std::rc::Rc<(Vec<Run>, Vec<Run>)> {
263 let mut h = std::hash::DefaultHasher::new();
266 path_hash(from, &mut h);
267 0xffu8.hash(&mut h);
268 path_hash(to, &mut h);
269 let key = h.finish();
270 CACHE.with(|c| {
271 let mut c = c.borrow_mut();
272 if let Some(pos) = c.iter().position(|(k, _)| *k == key) {
273 let entry = c.remove(pos);
274 c.push(entry.clone());
275 return entry.1;
276 }
277 let value = std::rc::Rc::new(align(from, to));
278 if c.len() >= CACHE_SIZE {
279 c.remove(0);
280 }
281 c.push((key, value.clone()));
282 value
283 })
284}
285
286impl Lerp for BezPath {
287 fn lerp(&self, to: &Self, t: f64) -> Self {
288 if t <= 0.0 {
289 return self.clone();
290 }
291 if t >= 1.0 {
292 return to.clone();
293 }
294 let pair = aligned(self, to);
295 let mut out = BezPath::new();
296 for (ra, rb) in pair.0.iter().zip(&pair.1) {
297 let run = Run {
298 segs: ra
299 .segs
300 .iter()
301 .zip(&rb.segs)
302 .map(|(x, y)| {
303 CubicBez::new(
304 x.p0.lerp(y.p0, t),
305 x.p1.lerp(y.p1, t),
306 x.p2.lerp(y.p2, t),
307 x.p3.lerp(y.p3, t),
308 )
309 })
310 .collect(),
311 closed: ra.closed,
312 };
313 run.to_path(&mut out);
314 }
315 out
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use kurbo::{Circle, Rect, Shape};
322
323 use super::*;
324
325 fn sample(p: &BezPath, n: usize) -> Vec<Point> {
326 let segs: Vec<_> = p.segments().collect();
327 (0..n)
328 .map(|i| {
329 let u = i as f64 / n as f64 * segs.len() as f64;
330 segs[(u as usize).min(segs.len() - 1)].eval(u.fract())
331 })
332 .collect()
333 }
334
335 #[test]
336 fn endpoints_are_exact() {
337 let a = Rect::new(0.0, 0.0, 10.0, 10.0).to_path(0.1);
338 let b = Circle::new((5.0, 5.0), 5.0).to_path(0.1);
339 assert_eq!(a.lerp(&b, 0.0), a);
340 assert_eq!(a.lerp(&b, 1.0), b);
341 }
342
343 #[test]
344 fn midpoint_of_identical_shapes_is_the_shape() {
345 let a = Circle::new((0.0, 0.0), 10.0).to_path(0.1);
346 let mid = a.lerp(&a, 0.5);
347 for (p, q) in sample(&mid, 32).iter().zip(sample(&a, 32)) {
348 assert!(p.distance(q) < 1e-9);
349 }
350 }
351
352 #[test]
353 fn rect_to_circle_stays_between_them() {
354 let a = Rect::new(-10.0, -10.0, 10.0, 10.0).to_path(0.1);
355 let b = Circle::new((0.0, 0.0), 10.0).to_path(0.1);
356 let mid = a.lerp(&b, 0.5);
357 for p in sample(&mid, 64) {
360 let r = p.to_vec2().hypot();
361 assert!((9.0..=14.2).contains(&r), "{p:?} r={r}");
362 }
363 }
364
365 #[test]
366 fn rotation_and_direction_are_aligned() {
367 let a = Rect::new(-10.0, -10.0, 10.0, 10.0).to_path(0.1);
370 let mut b = BezPath::new();
371 b.move_to((10.0, 10.0));
372 b.line_to((10.0, -10.0));
373 b.line_to((-10.0, -10.0));
374 b.line_to((-10.0, 10.0));
375 b.close_path();
376 let mid = a.lerp(&b, 0.5);
377 for p in sample(&mid, 16) {
378 assert!(p.x.abs().max(p.y.abs()) > 9.99, "{p:?}");
379 }
380 }
381
382 #[test]
383 fn different_run_counts_grow_from_points() {
384 let a = Circle::new((0.0, 0.0), 10.0).to_path(0.1);
385 let mut b = Circle::new((-20.0, 0.0), 5.0).to_path(0.1);
386 b.extend(Circle::new((20.0, 0.0), 5.0).to_path(0.1));
387 let mid = a.lerp(&b, 0.5);
388 assert_eq!(
389 mid.elements()
390 .iter()
391 .filter(|e| matches!(e, PathEl::MoveTo(_)))
392 .count(),
393 2
394 );
395 }
396}