use std::f64::consts::FRAC_PI_2;
use std::ops::Range;
use kurbo::{Affine, CubicBez, ParamCurve, ParamCurveArclen, ParamCurveExtrema, Point, Rect};
const ARCLEN_ACCURACY: f64 = 1e-6;
#[derive(Debug, Clone, PartialEq)]
pub struct SubPath {
pub segments: Vec<CubicBez>,
pub closed: bool,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct VPath {
pub subpaths: Vec<SubPath>,
}
pub fn line_segment(a: Point, b: Point) -> CubicBez {
CubicBez::new(a, a.lerp(b, 1.0 / 3.0), a.lerp(b, 2.0 / 3.0), b)
}
impl SubPath {
pub fn polyline(points: &[Point], closed: bool) -> Self {
let mut segments: Vec<_> = points
.windows(2)
.map(|w| line_segment(w[0], w[1]))
.collect();
if closed && points.len() > 2 {
segments.push(line_segment(points[points.len() - 1], points[0]));
}
Self { segments, closed }
}
pub fn centroid(&self) -> Point {
let n = self.segments.len().max(1) as f64;
let sum = self
.segments
.iter()
.fold(Point::ORIGIN, |acc, s| acc + s.p0.to_vec2());
Point::new(sum.x / n, sum.y / n)
}
fn collapsed(&self) -> Self {
let c = self.centroid();
let seg = CubicBez::new(c, c, c, c);
Self {
segments: vec![seg; self.segments.len()],
closed: self.closed,
}
}
}
impl VPath {
pub fn arc(radius: f64, start: f64, sweep: f64) -> Self {
let n = (sweep.abs() / FRAC_PI_2).ceil().max(1.0) as usize;
let step = sweep / n as f64;
let k = 4.0 / 3.0 * (step / 4.0).tan() * radius;
let at = |th: f64| Point::new(radius * th.cos(), radius * th.sin());
let segments = (0..n)
.map(|i| {
let (a, b) = (start + step * i as f64, start + step * (i + 1) as f64);
let (p0, p3) = (at(a), at(b));
let t0 = kurbo::Vec2::new(-a.sin(), a.cos()) * k;
let t1 = kurbo::Vec2::new(-b.sin(), b.cos()) * k;
CubicBez::new(p0, p0 + t0, p3 - t1, p3)
})
.collect();
let closed = (sweep.abs() - std::f64::consts::TAU).abs() < 1e-9;
Self {
subpaths: vec![SubPath { segments, closed }],
}
}
pub fn polyline(points: &[Point], closed: bool) -> Self {
Self {
subpaths: vec![SubPath::polyline(points, closed)],
}
}
pub fn transform(&self, a: Affine) -> Self {
let subpaths = self
.subpaths
.iter()
.map(|sp| SubPath {
segments: sp.segments.iter().map(|s| a * *s).collect(),
closed: sp.closed,
})
.collect();
Self { subpaths }
}
pub fn bbox(&self) -> Option<Rect> {
self.subpaths
.iter()
.flat_map(|sp| &sp.segments)
.map(|s| s.bounding_box())
.reduce(|a, b| a.union(b))
}
pub fn center(&self) -> Point {
self.bbox().map_or(Point::ORIGIN, |r| r.center())
}
pub fn trim(&self, range: Range<f32>) -> Self {
if range.start <= 0.0 && range.end >= 1.0 {
return self.clone();
}
let lens: Vec<Vec<f64>> = self
.subpaths
.iter()
.map(|sp| {
sp.segments
.iter()
.map(|s| s.arclen(ARCLEN_ACCURACY))
.collect()
})
.collect();
let total: f64 = lens.iter().flatten().sum();
let (from, to) = (f64::from(range.start) * total, f64::from(range.end) * total);
let mut acc = 0.0;
let mut subpaths = Vec::new();
for (sp, lens) in self.subpaths.iter().zip(&lens) {
let mut segments = Vec::new();
for (seg, &len) in sp.segments.iter().zip(lens) {
let (l0, l1) = (acc, acc + len);
acc = l1;
if len == 0.0 || l1 <= from || l0 >= to {
continue;
}
let t0 = if from > l0 {
seg.inv_arclen(from - l0, ARCLEN_ACCURACY)
} else {
0.0
};
let t1 = if to < l1 {
seg.inv_arclen(to - l0, ARCLEN_ACCURACY)
} else {
1.0
};
segments.push(seg.subsegment(t0..t1));
}
if !segments.is_empty() {
subpaths.push(SubPath {
segments,
closed: false,
});
}
}
Self { subpaths }
}
}
pub fn align(a: &VPath, b: &VPath) -> (VPath, VPath) {
let n = a.subpaths.len().max(b.subpaths.len());
let (mut out_a, mut out_b) = (VPath::default(), VPath::default());
for i in 0..n {
let (sa, sb) = match (a.subpaths.get(i), b.subpaths.get(i)) {
(Some(x), Some(y)) => (x.clone(), y.clone()),
(Some(x), None) => (x.clone(), x.collapsed()),
(None, Some(y)) => (y.collapsed(), y.clone()),
(None, None) => unreachable!(),
};
let (sa, sb) = align_subpaths(sa, sb);
out_a.subpaths.push(sa);
out_b.subpaths.push(sb);
}
(out_a, out_b)
}
fn align_subpaths(mut a: SubPath, mut b: SubPath) -> (SubPath, SubPath) {
if a.segments.is_empty() {
a = SubPath {
closed: a.closed,
..b.collapsed()
};
} else if b.segments.is_empty() {
b = SubPath {
closed: b.closed,
..a.collapsed()
};
}
let n = a.segments.len().max(b.segments.len());
subdivide_to(&mut a.segments, n);
subdivide_to(&mut b.segments, n);
if a.closed && b.closed {
let travel = |r: usize| -> f64 {
(0..n)
.map(|i| (a.segments[(i + r) % n].p0 - b.segments[i].p0).hypot2())
.sum()
};
let best = (0..n)
.min_by(|&x, &y| travel(x).total_cmp(&travel(y)))
.unwrap_or(0);
a.segments.rotate_left(best);
}
(a, b)
}
fn subdivide_to(segs: &mut Vec<CubicBez>, n: usize) {
while segs.len() < n {
let lens: Vec<f64> = segs.iter().map(|s| s.arclen(ARCLEN_ACCURACY)).collect();
let i = (0..segs.len())
.max_by(|&x, &y| lens[x].total_cmp(&lens[y]).then(y.cmp(&x)))
.unwrap();
let (l, r) = segs[i].subdivide();
segs[i] = l;
segs.insert(i + 1, r);
}
}