use super::rounding::{Point, RoundedPath, Seg, round};
use super::values::num;
use crate::ledger::consts::{WAVY_AMPLITUDE, WAVY_WAVELENGTH};
use std::f64::consts::TAU;
use std::fmt::Write;
const FLATTEN_STEP: f64 = 1.5;
pub fn wavy_d(pts: &[Point], targets: &[f64]) -> Option<String> {
let line = Centerline::flatten(&round(pts, targets));
let total = line.total();
if total < WAVY_WAVELENGTH {
return None;
}
let taper = (WAVY_WAVELENGTH / 2.0).min(total / 2.0);
let k = TAU / WAVY_WAVELENGTH;
let steps = ((total / (WAVY_WAVELENGTH / 4.0)).round() as usize).max(2);
let sample = |s: f64| -> (Point, Point) {
let (p, t) = line.at(s);
let (env, denv) = envelope(s, total, taper);
let phase = k * s;
let offset = env * WAVY_AMPLITUDE * phase.sin();
let d_offset = denv * WAVY_AMPLITUDE * phase.sin() + env * WAVY_AMPLITUDE * k * phase.cos();
let normal = (-t.1, t.0);
let w = (p.0 + normal.0 * offset, p.1 + normal.1 * offset);
let wt = unit((t.0 + normal.0 * d_offset, t.1 + normal.1 * d_offset));
(w, wt)
};
let (mut from, mut from_t) = sample(0.0);
let mut d = format!("M {} {}", num(from.0), num(from.1));
for i in 1..=steps {
let s = (total * i as f64 / steps as f64).min(total);
let (to, to_t) = sample(s);
let h = dist(from, to) / 3.0;
write!(
d,
" C {} {} {} {} {} {}",
num(from.0 + from_t.0 * h),
num(from.1 + from_t.1 * h),
num(to.0 - to_t.0 * h),
num(to.1 - to_t.1 * h),
num(to.0),
num(to.1),
)
.unwrap();
(from, from_t) = (to, to_t);
}
Some(d)
}
fn envelope(s: f64, total: f64, taper: f64) -> (f64, f64) {
let (u, du) = if s <= total - s {
(s / taper, 1.0 / taper)
} else {
((total - s) / taper, -1.0 / taper)
};
if u >= 1.0 {
return (1.0, 0.0);
}
(3.0 * u * u - 2.0 * u * u * u, (6.0 * u - 6.0 * u * u) * du)
}
struct Centerline {
pts: Vec<Point>,
cum: Vec<f64>,
}
impl Centerline {
fn flatten(rp: &RoundedPath) -> Self {
let mut pts = vec![rp.start];
for seg in &rp.segs {
match seg {
Seg::Line { to } => pts.push(*to),
Seg::Arc {
to, center, radius, ..
} => flatten_arc(*pts.last().unwrap(), *to, *center, *radius, &mut pts),
}
}
pts.dedup_by(|a, b| dist(*a, *b) < 1e-9);
let mut cum = vec![0.0];
for w in pts.windows(2) {
cum.push(cum.last().unwrap() + dist(w[0], w[1]));
}
Centerline { pts, cum }
}
fn total(&self) -> f64 {
*self.cum.last().unwrap_or(&0.0)
}
fn at(&self, s: f64) -> (Point, Point) {
let s = s.clamp(0.0, self.total());
let j = self
.cum
.partition_point(|&c| c <= s)
.clamp(1, self.pts.len() - 1)
- 1;
let (a, b) = (self.pts[j], self.pts[j + 1]);
let span = self.cum[j + 1] - self.cum[j];
let t = if span > 1e-9 {
(s - self.cum[j]) / span
} else {
0.0
};
let p = (a.0 + (b.0 - a.0) * t, a.1 + (b.1 - a.1) * t);
(p, unit((b.0 - a.0, b.1 - a.1)))
}
}
fn flatten_arc(from: Point, to: Point, center: Point, radius: f64, out: &mut Vec<Point>) {
let v0 = (from.0 - center.0, from.1 - center.1);
let v1 = (to.0 - center.0, to.1 - center.1);
let angle = (v0.0 * v1.1 - v0.1 * v1.0).atan2(v0.0 * v1.0 + v0.1 * v1.1);
let steps = ((radius * angle.abs() / FLATTEN_STEP).ceil() as usize).max(1);
for i in 1..steps {
let a = angle * i as f64 / steps as f64;
let (cos, sin) = (a.cos(), a.sin());
out.push((
center.0 + v0.0 * cos - v0.1 * sin,
center.1 + v0.0 * sin + v0.1 * cos,
));
}
out.push(to);
}
fn unit(v: Point) -> Point {
let len = (v.0 * v.0 + v.1 * v.1).sqrt();
if len < 1e-12 {
(1.0, 0.0)
} else {
(v.0 / len, v.1 / len)
}
}
fn dist(a: Point, b: Point) -> f64 {
((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
fn anchors(d: &str) -> Vec<Point> {
let nums: Vec<f64> = d
.split([' ', ','])
.filter_map(|t| t.parse::<f64>().ok())
.collect();
let mut pts = vec![(nums[0], nums[1])];
let mut i = 2;
while i + 6 <= nums.len() {
pts.push((nums[i + 4], nums[i + 5]));
i += 6;
}
pts
}
#[test]
fn short_route_declines_to_wave() {
assert!(wavy_d(&[(0.0, 0.0), (8.0, 0.0)], &[]).is_none());
}
#[test]
fn wave_stays_on_the_line_at_both_ends() {
let d = wavy_d(&[(0.0, 0.0), (120.0, 0.0)], &[]).expect("waves");
let pts = anchors(&d);
assert!(pts.len() > 4, "expected several Bézier hops: {d}");
assert!(pts.first().unwrap().1.abs() < 1e-9);
assert!(pts.last().unwrap().1.abs() < 1e-9);
assert!((pts.first().unwrap().0 - 0.0).abs() < 1e-9);
assert!((pts.last().unwrap().0 - 120.0).abs() < 1e-9);
}
#[test]
fn amplitude_is_bounded_and_actually_oscillates() {
let d = wavy_d(&[(0.0, 0.0), (160.0, 0.0)], &[]).expect("waves");
let pts = anchors(&d);
let peak = pts.iter().map(|p| p.1.abs()).fold(0.0_f64, f64::max);
assert!(peak > WAVY_AMPLITUDE * 0.8, "wave too shallow: {peak}");
assert!(peak <= WAVY_AMPLITUDE + 1e-9, "wave overshoots: {peak}");
}
#[test]
fn turns_a_corner_without_blowing_up() {
let d = wavy_d(&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0)], &[16.0]).expect("waves");
assert!(!d.contains("NaN") && !d.contains("inf"), "{d}");
let pts = anchors(&d);
assert!(pts.len() > 8, "expected a wave around the corner: {d}");
}
}