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
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-03 22:32:31 -05:00
1 parent 7faf7029fb
commit e2c2266087
1 file changed
+79 -1
+79 -1
View File
@@ -108,6 +108,11 @@ fn trace_counts(take: &[Vec<usize>], n: usize, m: usize) -> Option<Vec<usize>> {
(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<usize> {
@@ -121,6 +126,9 @@ fn split_even(advs: &[f64], n: usize, before: usize, after: usize) -> Vec<usize>
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<usize>
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<usize> {
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::<usize>(), 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::<usize>(), 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]);
}
}