From 6521789c2c83529ff7bcd88ec58c16bf0d8c3dbe Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 12 Oct 2026 17:20:26 -0400 Subject: [PATCH] Text warps: bands with fewer lines keep the fuller band's text height Multi-band fill warps (Deflate-Inflate, Button Pour) map a box as tall as the tallest band's ink onto every band, centred on the band's own ink, as PowerPoint does: a band holding one line next to a band of two keeps the line at the same size instead of stretching it over the whole band. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/geom/src/warp.rs | 8 +++++++- crates/geom/src/warp/tests.rs | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/geom/src/warp.rs b/crates/geom/src/warp.rs index 1ac3681..acdee99 100644 --- a/crates/geom/src/warp.rs +++ b/crates/geom/src/warp.rs @@ -4,7 +4,8 @@ //! Text lines are shared out over the bands in order (the first ⌈n/k⌉ lines to the first band //! and so on). A band either //! - stretches its lines between a top and a bottom guide ([`Band::Fill`]): the ink box of the -//! band's lines fills the band vertically, and horizontally the text is scaled so that the +//! band's lines fills the band vertically (a band whose ink is shorter than the tallest band's +//! keeps the same scale, centred), and horizontally the text is scaled so that the //! widest band is filled along the mean length of its guides (one scale for the whole text), or //! - sets its lines at their natural size along one guide ([`Band::Follow`], arches and circles): //! glyphs keep their size, are placed by arc length (aligned as in the paragraph, the whole @@ -658,6 +659,11 @@ impl Warp { }); } } + // Every fill band maps a box as tall as the tallest band's ink, centred on its own ink, so + // a band with fewer lines keeps them at the same size, centred between its guides. + let tallest = band_y.iter().flatten().map(|(y0, y1)| y1 - y0).fold(0.0, f64::max); + let band_y: Vec> = + band_y.iter().map(|r| r.map(|(y0, y1)| ((y0 + y1 - tallest) / 2.0, (y0 + y1 + tallest) / 2.0))).collect(); for (b, band) in self.bands.iter().enumerate() { if let Band::Fill { top, bottom } = band { let half = lines diff --git a/crates/geom/src/warp/tests.rs b/crates/geom/src/warp/tests.rs index 2ffdc3f..2d18b13 100644 --- a/crates/geom/src/warp/tests.rs +++ b/crates/geom/src/warp/tests.rs @@ -228,6 +228,15 @@ fn deflate_inflate_maps_each_band_separately() { assert!((m.map(0, Point::new(100.0, 40.0)).y / H - 0.45).abs() < 0.004); assert!((m.map(1, Point::new(100.0, 50.0)).y / H - 0.55).abs() < 0.004); assert!((m.map(1, Point::new(300.0, 80.0)).y / H - 1.0).abs() < 0.004); + // A band with fewer lines keeps them at the size of the fuller band, centred in its band: + // two lines (ink 10..60) in band 1, one line (ink 70..80) in band 2 of three. + let a = WarpLine { ink: Some(Rect::new(100.0, 10.0, 300.0, 30.0)), extent: (100.0, 300.0), ref_y: 28.0 }; + let b = WarpLine { ink: Some(Rect::new(100.0, 40.0, 300.0, 60.0)), extent: (100.0, 300.0), ref_y: 58.0 }; + let c = WarpLine { ink: Some(Rect::new(100.0, 70.0, 300.0, 80.0)), extent: (100.0, 300.0), ref_y: 78.0 }; + let m = w.mapper(&[a, b, c], rect()); + let (top, bot) = (m.map(2, Point::new(200.0, 70.0)).y / H, m.map(2, Point::new(200.0, 80.0)).y / H); + // Band 2 runs from 0.4 to 1 at the centre; the line takes the middle fifth of it. + assert!((top - 0.64).abs() < 0.005 && (bot - 0.76).abs() < 0.005, "{top} {bot}"); // A shorter first line keeps its share of the width. let short = WarpLine { ink: Some(Rect::new(150.0, 10.0, 250.0, 40.0)), extent: (150.0, 250.0), ref_y: 34.0 }; let m = w.mapper(&[short, l2], rect());