use crate::ledger::consts::NATURAL_PULL;
pub(crate) type Pt = (f64, f64);
pub(crate) const SAMPLES: usize = 24;
fn sub(a: Pt, b: Pt) -> Pt {
(a.0 - b.0, a.1 - b.1)
}
fn add(a: Pt, b: Pt) -> Pt {
(a.0 + b.0, a.1 + b.1)
}
fn mul(a: Pt, k: f64) -> Pt {
(a.0 * k, a.1 * k)
}
fn dot(a: Pt, b: Pt) -> f64 {
a.0 * b.0 + a.1 * b.1
}
fn len(a: Pt) -> f64 {
a.0.hypot(a.1)
}
fn unit(a: Pt) -> Pt {
let l = len(a);
if l <= 0.0 {
(0.0, 0.0)
} else {
mul(a, 1.0 / l)
}
}
pub(crate) fn bezier(c: &[Pt; 4], t: f64) -> Pt {
let u = 1.0 - t;
let (b0, b1, b2, b3) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t);
(
b0 * c[0].0 + b1 * c[1].0 + b2 * c[2].0 + b3 * c[3].0,
b0 * c[0].1 + b1 * c[1].1 + b2 * c[2].1 + b3 * c[3].1,
)
}
fn span(p0: Pt, t0: Pt, p1: Pt, t1: Pt) -> [Pt; 4] {
let d = sub(p1, p0);
let pull = NATURAL_PULL * len(d);
let clamp = |t: Pt| {
let travel = dot(d, t);
if travel > 0.0 { pull.min(travel) } else { pull }
};
[
p0,
add(p0, mul(t0, clamp(t0))),
sub(p1, mul(t1, clamp(t1))),
p1,
]
}
pub(crate) struct Stubs {
pub a: Pt,
pub b: Pt,
pub da: Pt,
pub db: Pt,
}
pub(crate) fn stubs(poly: &[Pt], stub_a: f64, stub_b: f64) -> Stubs {
let last = poly.len() - 1;
let da = unit(sub(poly[1], poly[0]));
let db = unit(sub(poly[last - 1], poly[last]));
let (la, lb) = (
len(sub(poly[1], poly[0])),
len(sub(poly[last], poly[last - 1])),
);
let (sa_len, sb_len) = if last == 1 {
(stub_a.min(la / 2.0), stub_b.min(la / 2.0))
} else {
(stub_a.min(la), stub_b.min(lb))
};
Stubs {
a: add(poly[0], mul(da, sa_len)),
b: add(poly[last], mul(db, sb_len)),
da,
db,
}
}
pub(crate) fn spans(knots: &[Pt], da: Pt, db: Pt) -> Vec<[Pt; 4]> {
let tangent = |i: usize| {
if i == 0 {
da
} else if i == knots.len() - 1 {
mul(db, -1.0)
} else {
unit(sub(knots[i + 1], knots[i - 1]))
}
};
(0..knots.len() - 1)
.filter(|&i| knots[i] != knots[i + 1])
.map(|i| span(knots[i], tangent(i), knots[i + 1], tangent(i + 1)))
.collect()
}
pub(crate) fn sample_span(c: &[Pt; 4]) -> Vec<Pt> {
std::iter::once(c[0])
.chain((1..=SAMPLES).map(|j| bezier(c, j as f64 / SAMPLES as f64)))
.collect()
}
pub(crate) fn sample(pa: Pt, sa: Pt, pb: Pt, curve: &[[Pt; 4]]) -> Vec<Pt> {
let mut path = vec![pa];
if sa != pa {
path.push(sa);
}
for c in curve {
for j in 1..=SAMPLES {
path.push(bezier(c, j as f64 / SAMPLES as f64));
}
}
if *path.last().expect("non-empty") != pb {
path.push(pb);
}
path
}
pub(crate) fn fit(poly: &[Pt], stub_a: f64, stub_b: f64) -> (Vec<Pt>, Vec<[Pt; 4]>) {
let last = poly.len() - 1;
let s = stubs(poly, stub_a, stub_b);
let mut knots: Vec<Pt> = vec![s.a];
for i in 1..last.saturating_sub(1) {
knots.push(mul(add(poly[i], poly[i + 1]), 0.5));
}
knots.push(s.b);
let d = sub(s.b, s.a);
if knots.len() == 3 && dot(d, s.da) > 0.0 && dot(d, mul(s.db, -1.0)) > 0.0 {
knots.remove(1);
}
let curve = spans(&knots, s.da, s.db);
(sample(poly[0], s.a, poly[last], &curve), curve)
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-9;
#[test]
fn an_aligned_pair_draws_dead_straight() {
let (path, curve) = fit(&[(40.0, 50.0), (160.0, 50.0)], 16.0, 16.0);
assert_eq!(curve.len(), 1);
assert_eq!(*path.first().unwrap(), (40.0, 50.0));
assert_eq!(*path.last().unwrap(), (160.0, 50.0));
for p in &path {
assert!((p.1 - 50.0).abs() < EPS, "off the line: {p:?}");
}
for w in path.windows(2) {
assert!(w[1].0 >= w[0].0, "doubled back: {w:?}");
}
}
#[test]
fn the_dogleg_draws_the_classic_s() {
let poly = [(40.0, 30.0), (100.0, 30.0), (100.0, 90.0), (160.0, 90.0)];
let (path, curve) = fit(&poly, 16.0, 16.0);
assert_eq!(curve.len(), 1, "one cubic absorbs the whole offset");
assert_eq!(path[0], (40.0, 30.0));
assert_eq!(path[1], (56.0, 30.0));
assert_eq!(*path.last().unwrap(), (160.0, 90.0));
assert_eq!(path[path.len() - 2], (144.0, 90.0));
assert!((curve[0][1].1 - curve[0][0].1).abs() < EPS);
assert!((curve[0][2].1 - curve[0][3].1).abs() < EPS);
for w in path.windows(2) {
assert!(w[1].0 >= w[0].0 - EPS, "x doubled back: {w:?}");
assert!(w[1].1 >= w[0].1 - EPS, "y doubled back: {w:?}");
}
let mid = bezier(&curve[0], 0.5);
assert!((mid.0 - 100.0).abs() < EPS && (mid.1 - 60.0).abs() < EPS);
}
#[test]
fn samples_start_and_end_on_the_ports() {
let poly = [(0.0, 0.0), (50.0, 0.0), (50.0, 40.0)];
let (path, _) = fit(&poly, 10.0, 10.0);
assert_eq!(*path.first().unwrap(), (0.0, 0.0));
assert_eq!(*path.last().unwrap(), (50.0, 40.0));
}
}