use super::frame::Bx; use super::{Ctx, MAX_LINE_PTS, MAX_MARKERS, Prim, Rgba, WHITE}; use std::collections::HashSet; // ---------------------------------------------------------------- helpers /// Reduces a long polyline to first/min/max/last per pixel column. pub(super) fn decimate(pts: Vec<[f32; 2]>, max: usize) -> Vec<[f32; 2]> { if pts.len() <= max { return pts; } let mut out = Vec::with_capacity(max * 2); let mut i = 0; while let Some(p) = pts.get(i) { let col = p[0].floor(); let (start, mut mn, mut mx) = (i, i, i); let mut k = i + 1; while let Some(q) = pts.get(k) { if q[0].floor() != col { break; } if pts.get(mn).is_some_and(|m| q[1] < m[1]) { mn = k; } if pts.get(mx).is_some_and(|m| q[1] > m[1]) { mx = k; } k += 1; } let mut idx = [start, mn, mx, k - 1]; idx.sort_unstable(); let mut last = usize::MAX; for ix in idx { if ix != last && let Some(q) = pts.get(ix) { out.push(*q); last = ix; } } i = k; } if out.len() > max * 4 { let step = out.len() / (max * 2) + 1; let last = out.last().copied(); let mut s: Vec<[f32; 2]> = out.into_iter().step_by(step).collect(); if let Some(l) = last && s.last() != Some(&l) { s.push(l); } return s; } out } /// Draws a polyline split at gaps (`None`), decimated when long. With `smooth` (the plot box), /// each run is interpolated with a Catmull-Rom spline so the line curves through the data points. pub(super) fn draw_runs(ctx: &mut Ctx, pts: &[Option<[f32; 2]>], color: Rgba, width: f32, smooth: Option) { let mut run: Vec<[f32; 2]> = Vec::new(); for p in pts.iter().chain(std::iter::once(&None)) { match p { Some(p) => run.push(*p), None => { if run.len() >= 2 { let r = decimate(std::mem::take(&mut run), MAX_LINE_PTS); let r = match smooth { Some(plot) => smooth_run(r, plot), None => r, }; ctx.line(r, color, width); } else { run.clear(); } } } } } /// Catmull-Rom spline through `pts`: every segment gets the same number of sub-segments (up to /// 100), chosen so the whole curve stays within `MAX_LINE_PTS * 2` points. Endpoints are /// duplicated so the curve passes through the first and last points, and the curve is clamped /// to the `plot` box (a spline overshoots its data near sharp turns). fn smooth_run(pts: Vec<[f32; 2]>, plot: Bx) -> Vec<[f32; 2]> { let n = pts.len(); if n < 3 { return pts; } let cap = MAX_LINE_PTS * 2; let steps = (cap / n).clamp(1, 100); let mut out: Vec<[f32; 2]> = Vec::with_capacity(n.saturating_mul(steps).min(cap) + 1); let at = |i: isize| -> [f32; 2] { let j = i.clamp(0, n as isize - 1) as usize; pts.get(j).copied().unwrap_or([0.0, 0.0]) }; let (x0, x1) = (plot.x.min(plot.right()), plot.x.max(plot.right())); let (y0, y1) = (plot.y.min(plot.bottom()), plot.y.max(plot.bottom())); let clip = |p: [f32; 2]| [p[0].clamp(x0, x1), p[1].clamp(y0, y1)]; for i in 0..n.saturating_sub(1) { let (p0, p1, p2, p3) = (at(i as isize - 1), at(i as isize), at(i as isize + 1), at(i as isize + 2)); for s in 0..steps { let t = s as f32 / steps as f32; out.push(clip(catmull(p0, p1, p2, p3, t))); } } out.push(clip(at(n as isize - 1))); out } /// One Catmull-Rom point between `p1` and `p2` (uniform parameterisation), tangent-scaled by 0.5. fn catmull(p0: [f32; 2], p1: [f32; 2], p2: [f32; 2], p3: [f32; 2], t: f32) -> [f32; 2] { let t2 = t * t; let t3 = t2 * t; let mut o = [0.0f32; 2]; for k in 0..2 { let (a0, a1, a2, a3) = (p0[k], p1[k], p2[k], p3[k]); o[k] = 0.5 * ((2.0 * a1) + (-a0 + a2) * t + (2.0 * a0 - 5.0 * a1 + 4.0 * a2 - a3) * t2 + (-a0 + 3.0 * a1 - 3.0 * a2 + a3) * t3); } o } pub(super) fn draw_markers(ctx: &mut Ctx, pts: impl Iterator, r: f32, color: Rgba) { let mut seen = HashSet::new(); for p in pts { if seen.len() >= MAX_MARKERS { break; } if seen.insert(((p[0] * 0.5) as i32, (p[1] * 0.5) as i32)) { ctx.out.push(Prim::Circle { cx: p[0], cy: p[1], r, fill: Some(color), stroke: Some((WHITE, 0.75)) }); } } } #[cfg(test)] mod smooth_tests { use super::*; #[test] fn long_runs_are_smoothed_uniformly_within_the_cap() { let plot = Bx { x: 0.0, y: 0.0, w: 3000.0, h: 100.0 }; let pts: Vec<[f32; 2]> = (0..1500).map(|i| [i as f32 * 2.0, if i % 2 == 0 { 20.0 } else { 80.0 }]).collect(); let out = smooth_run(pts, plot); assert!(out.len() <= MAX_LINE_PTS * 2 + 1, "{}", out.len()); // Two sub-segments per segment all the way to the end, not 100 each and then a straight tail. assert_eq!(out.len(), 1499 * 2 + 1); assert_eq!(out.last().copied(), Some([2998.0, 80.0])); } #[test] fn spline_overshoot_is_clamped_to_the_plot() { let plot = Bx { x: 0.0, y: 10.0, w: 300.0, h: 100.0 }; // A step right at the plot edges: Catmull-Rom overshoots past both. let pts = vec![[0.0, 110.0], [100.0, 110.0], [110.0, 10.0], [200.0, 10.0], [300.0, 10.0]]; let out = smooth_run(pts, plot); assert!(out.len() > 5); assert!(out.iter().all(|p| (0.0..=300.0).contains(&p[0]) && (10.0..=110.0).contains(&p[1]))); } }