pub type Point = (f64, f64);
pub enum Seg {
Line {
to: Point,
},
Arc {
to: Point,
center: Point,
radius: f64,
sweep: u8,
},
}
pub struct RoundedPath {
pub start: Point,
pub segs: Vec<Seg>,
}
pub fn path_d(pts: &[Point], targets: &[f64]) -> String {
use super::values::num;
use std::fmt::Write;
let rounded = round(pts, targets);
let mut d = format!("M {} {}", num(rounded.start.0), num(rounded.start.1));
for seg in &rounded.segs {
match seg {
Seg::Line { to } => write!(d, " L {} {}", num(to.0), num(to.1)).unwrap(),
Seg::Arc {
to, radius, sweep, ..
} => write!(
d,
" A {r} {r} 0 0 {sweep} {} {}",
num(to.0),
num(to.1),
r = num(*radius),
)
.unwrap(),
}
}
d
}
pub fn round(pts: &[Point], targets: &[f64]) -> RoundedPath {
let corners = pts.len().saturating_sub(2);
let target = |k: usize| targets.get(k).copied().unwrap_or(0.0);
let leg = |i: usize| (pts[i + 1].0 - pts[i].0).abs() + (pts[i + 1].1 - pts[i].1).abs();
let radius = |k: usize| {
let t = target(k);
if t <= 0.0 {
return 0.0;
}
let before = if k == 0 { 0.0 } else { target(k - 1) };
let after = if k + 1 < corners { target(k + 1) } else { 0.0 };
let f = (leg(k) / (before + t)).min(leg(k + 1) / (t + after));
t * f.min(1.0)
};
let mut segs = Vec::new();
for i in 1..pts.len() - 1 {
let (a, b, c) = (pts[i - 1], pts[i], pts[i + 1]);
let (in_dx, in_dy) = (b.0 - a.0, b.1 - a.1);
let (out_dx, out_dy) = (c.0 - b.0, c.1 - b.1);
let in_len = in_dx.abs() + in_dy.abs();
let out_len = out_dx.abs() + out_dy.abs();
let r = radius(i - 1);
let cross = in_dx * out_dy - in_dy * out_dx;
if r < 0.5 || cross == 0.0 {
segs.push(Seg::Line { to: b });
continue;
}
let (ux, uy) = (in_dx / in_len, in_dy / in_len);
let (vx, vy) = (out_dx / out_len, out_dy / out_len);
let enter = (b.0 - ux * r, b.1 - uy * r);
segs.push(Seg::Line { to: enter });
segs.push(Seg::Arc {
to: (b.0 + vx * r, b.1 + vy * r),
center: (enter.0 + vx * r, enter.1 + vy * r),
radius: r,
sweep: if cross > 0.0 { 1 } else { 0 },
});
}
segs.push(Seg::Line {
to: *pts.last().expect("a link polyline has at least two points"),
});
RoundedPath {
start: pts[0],
segs,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn radii(pts: &[Point], targets: &[f64]) -> Vec<f64> {
round(pts, targets)
.segs
.iter()
.filter_map(|s| match s {
Seg::Arc { radius, .. } => Some(*radius),
Seg::Line { .. } => None,
})
.collect()
}
#[test]
fn corners_share_a_legs_length_in_proportion() {
let pts = [(0.0, 0.0), (40.0, 0.0), (40.0, 40.0), (80.0, 40.0)];
assert_eq!(radii(&pts, &[30.0, 30.0]), vec![20.0, 20.0]);
let pts = [(0.0, 0.0), (60.0, 0.0), (60.0, 40.0), (120.0, 40.0)];
assert_eq!(radii(&pts, &[10.0, 30.0]), vec![10.0, 30.0]);
let pts = [(0.0, 0.0), (60.0, 0.0), (60.0, 20.0), (120.0, 20.0)];
assert_eq!(radii(&pts, &[10.0, 30.0]), vec![5.0, 15.0]);
}
#[test]
fn a_lone_corner_may_take_a_whole_terminal_leg() {
let pts = [(0.0, 0.0), (64.0, 0.0), (64.0, 56.0), (100.0, 56.0)];
assert_eq!(radii(&pts, &[40.0, 10.0]), vec![40.0, 10.0]);
}
}