From d2a8be197d1e5674f6fcf2193e38f8a97898380a Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 20:05:11 -0500 Subject: [PATCH] geom: measure each follow band once when mapping warped lines (#1758) --- crates/geom/src/warp.rs | 28 +++++++++++++++++++--------- crates/geom/src/warp/tests.rs | 17 +++++++++++++++++ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/crates/geom/src/warp.rs b/crates/geom/src/warp.rs index d3035d512..81376d7d7 100644 --- a/crates/geom/src/warp.rs +++ b/crates/geom/src/warp.rs @@ -928,11 +928,25 @@ impl Warp { k_fill = 1.0; } zoom = zoom.max(k_fill); + // Per band, measured once: its widest line and its first and last lines' baselines. + let mut stats: Vec> = vec![None; nb]; + for (l, &b) in lines.iter().zip(&band_of) { + let w = l.extent.1 - l.extent.0; + if let Some(slot) = stats.get_mut(b) { + match slot { + Some((widest, _, last)) => { + *widest = widest.max(w); + *last = l.ref_y; + } + None => *slot = Some((0.0f64.max(w), l.ref_y, l.ref_y)), + } + } + } let mut maps = Vec::with_capacity(n); let mut offsets = Vec::new(); for (i, l) in lines.iter().enumerate() { let b = band_of.get(i).copied().unwrap_or(0); - let in_band = |j: &usize| band_of.get(*j) == Some(&b); + let band = stats.get(b).copied().flatten(); match self.bands.get(b) { Some(Band::Fill { .. }) | None => { let (y0, y1) = band_y.get(b).copied().flatten().unwrap_or((0.0, 1.0)); @@ -941,16 +955,12 @@ impl Warp { } Some(Band::Follow { path, anchor }) => { let len = path.length(); - let widest = (0..n).filter(in_band).filter_map(|j| lines.get(j)).map(|l| l.extent.1 - l.extent.0).fold(0.0, f64::max); + let widest = band.map_or(0.0, |s| s.0); let k = if widest > len && widest > 1e-9 { len / widest } else { 1.0 }; - let members: Vec<&WarpLine> = (0..n).filter(in_band).filter_map(|j| lines.get(j)).collect(); let ref_y = match anchor { - LineAnchor::Last => members.last().map(|l| l.ref_y), - LineAnchor::First => members.first().map(|l| l.ref_y), - LineAnchor::Middle => match (members.first(), members.last()) { - (Some(a), Some(z)) => Some((a.ref_y + z.ref_y) / 2.0), - _ => None, - }, + LineAnchor::Last => band.map(|s| s.2), + LineAnchor::First => band.map(|s| s.1), + LineAnchor::Middle => band.map(|s| (s.1 + s.2) / 2.0), } .unwrap_or(l.ref_y); // Lines off the guide run on a parallel curve at their natural length. diff --git a/crates/geom/src/warp/tests.rs b/crates/geom/src/warp/tests.rs index c85b2b0ae..c4aa1bbd0 100644 --- a/crates/geom/src/warp/tests.rs +++ b/crates/geom/src/warp/tests.rs @@ -319,3 +319,20 @@ fn arch_and_inflate_handles_follow_the_pointer() { assert!(more > adjust_defaults("textInflate")[0], "{more}"); assert!((more - 20_000.0).abs() < 1.0, "{more}"); } + +#[test] +fn many_lines_on_a_follow_band_are_mapped_from_one_measurement() { + // 50,000 lines on an arch: each line's scale and anchor come from the band measured once. + let w = warp("textArchUp", &[], rect()).unwrap(); + let inner = Rect::new(7.2, 3.6, W - 7.2, H - 3.6); + let lines: Vec = (0..50_000) + .map(|i| { + let y = i as f64 * 0.01; + WarpLine { ink: Some(Rect::new(190.0, y, 230.0, y + 1.0)), extent: (190.0, 230.0), ref_y: y + 1.0 } + }) + .collect(); + let m = w.mapper(&lines, inner); + // The last line sits on the arc, as with two lines. + let last = lines.len() - 1; + assert!(m.map(last, Point::new(210.0, lines[last].ref_y)).y.abs() < 0.05); +}