From e2c22660878ebfbd6dee034b56e9c94dede9ef6f Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 22:32:31 -0500 Subject: [PATCH] warichu: split long runs in one pass instead of trying every length (#2184) * warichu: split long runs in one pass instead of trying every length The exact split tries every line length for every start, so its work grew with the square of the run's length, and a document can set warichu on a run of any length. Runs longer than 512 glyphs are now split where the running advance reaches each line's share, keeping the break minimums. The glyphs drawn with the run find their anchor by binary search instead of a scan per glyph. * address review: warichu split picks the break closest to the target --- crates/compose/src/warichu.rs | 80 ++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/crates/compose/src/warichu.rs b/crates/compose/src/warichu.rs index 8418faac4..f0bedf183 100644 --- a/crates/compose/src/warichu.rs +++ b/crates/compose/src/warichu.rs @@ -108,6 +108,11 @@ fn trace_counts(take: &[Vec], n: usize, m: usize) -> Option> { (s == m).then_some(counts) } +/// Longest warichu run split exactly. The exact split tries every length for every line start, so +/// its work grows with the square of the run's length; a longer run (from a document) is split by +/// [`split_proportional`] in one pass instead. +const MAX_EXACT_SPLIT_GLYPHS: usize = 512; + /// Glyph counts for `n` lines. Minimises the widest line, and the first and last lines keep their /// break minimums. One line when no split satisfies them. fn split_even(advs: &[f64], n: usize, before: usize, after: usize) -> Vec { @@ -121,6 +126,9 @@ fn split_even(advs: &[f64], n: usize, before: usize, after: usize) -> Vec for (i, a) in advs.iter().enumerate() { pref[i + 1] = pref[i] + *a; } + if m > MAX_EXACT_SPLIT_GLYPHS { + return split_proportional(&pref, n, before, after); + } let mut best = vec![vec![f64::INFINITY; m + 1]; n + 1]; let mut take = vec![vec![0_usize; m + 1]; n + 1]; for s in 0..m { @@ -160,6 +168,34 @@ fn split_even(advs: &[f64], n: usize, before: usize, after: usize) -> Vec trace_counts(&take, n, m).unwrap_or_else(|| vec![m]) } +/// [`split_even`] for a long run: each line ends where the run's advance reaches its share of the +/// total, moved where needed so every line keeps a glyph and the first and last lines their break +/// minimums. `pref` holds the running advance (`pref[i]`: the first `i` glyphs). One line when the +/// minimums can't all be kept. +fn split_proportional(pref: &[f64], n: usize, before: usize, after: usize) -> Vec { + let m = pref.len() - 1; + if before.saturating_add(after).saturating_add(n - 2) > m { + return vec![m]; + } + let total = pref[m]; + let mut counts = Vec::with_capacity(n); + let mut prev = 0; + for k in 1..n { + let target = total * k as f64 / n as f64; + let at = pref.partition_point(|p| *p < target); + let lo = prev + if k == 1 { before } else { 1 }; + // The lines after this one: one glyph each, the last its minimum. + let hi = m - after - (n - 1 - k); + // A wide glyph can straddle the target: take whichever valid break is closer to it. + let (above, below) = (at.clamp(lo, hi), at.saturating_sub(1).clamp(lo, hi)); + let at = if (target - pref[below]).abs() < (pref[above] - target).abs() { below } else { above }; + counts.push(at - prev); + prev = at; + } + counts.push(m - prev); + counts +} + fn restore(line: &mut [PlacedGlyph], members: &[usize], saved: &[(f64, f64, f64, f64)]) { for (i, &k) in members.iter().enumerate() { let Some((y, adv, sx, sy)) = saved.get(i).copied() else { continue }; @@ -283,7 +319,7 @@ fn shift_non_member_glyphs(line: &mut [PlacedGlyph], members: &[usize], old_xs: if g.x >= old_right - 1e-6 { g.x += delta; } else if g.x >= old_x0 - 1e-6 - && let Some(anchor) = placed.iter().rev().find(|p| p.0 <= g.x + 1e-6) + && let Some(anchor) = placed.partition_point(|p| p.0 <= g.x + 1e-6).checked_sub(1).map(|i| placed[i]) { // A mark drawn with the run (a tab leader is not: it sits before old_x0) keeps its // offset from the glyph it was attached to, in the scaled line. @@ -302,4 +338,46 @@ mod tests { assert_eq!(line_count(10, 3, 1, usize::MAX), 1); assert_eq!(line_count(10, 2, 3, 3), 2); } + + /// A run longer than the exact split's limit is split in one pass: the lines cover the run, + /// keep their minimums and come out about even. + #[test] + fn a_long_run_is_split_in_one_pass() { + let m = 200_000; + let advs = vec![1.0; m]; + let t = std::time::Instant::now(); + let counts = split_even(&advs, 16, 3, 5); + assert!(t.elapsed().as_secs() < 5, "{:?}", t.elapsed()); + assert_eq!(counts.len(), 16); + assert_eq!(counts.iter().sum::(), m); + assert!(counts.iter().all(|c| c.abs_diff(m / 16) <= 1), "{counts:?}"); + + // Minimums that the even shares don't meet move the breaks. + let mut advs = vec![0.0; MAX_EXACT_SPLIT_GLYPHS + 10]; + advs[0] = 1000.0; + let counts = split_even(&advs, 3, 4, 7); + assert_eq!(counts.iter().sum::(), advs.len()); + assert!(counts[0] >= 4 && counts[2] >= 7 && counts[1] >= 1, "{counts:?}"); + // Minimums that can't all be kept leave one line. + assert_eq!(split_even(&advs, 3, advs.len(), 1), vec![advs.len()]); + } + + /// Short runs keep the exact split that minimises the widest line. + #[test] + fn a_long_run_breaks_at_the_closer_side_of_a_wide_glyph() { + let mut advs = vec![1.0; 250]; + advs.push(1000.0); + advs.extend([1.0; 262]); + let mut pref = vec![0.0]; + for a in &advs { + pref.push(pref.last().unwrap() + a); + } + assert_eq!(split_proportional(&pref, 3, 1, 1), vec![250, 1, 262]); + } + + #[test] + fn a_short_run_is_split_exactly() { + assert_eq!(split_even(&[1.0, 1.0, 1.0, 1.0, 4.0], 2, 1, 1), vec![4, 1]); + assert_eq!(split_even(&[1.0; 6], 3, 1, 1), vec![2, 2, 2]); + } }