From ea40d9fbc1aee2776b6e1f7618fa7a0f289e8cdd Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 3 Nov 2026 13:45:25 +0100 Subject: [PATCH] :bug: Stop pushing a long forbidden run out of every vertical column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an item overflowed a column, column_break moved the characters that may not start a column into the next one, and only checked that they fit there with the overflowing item. The characters after it may not start a column either, so a run longer than a column was pushed out again at every column: "ださい。ー、ャュョッ。" in a five-character box left "ださ", "い", "。" and "ー" alone in four columns. Measure the whole unit that moves, up to the next allowed break, and keep the natural break when that unit is longer than a column. AI-assisted-by: claude-opus-5-5 --- render-wasm/src/shapes/text_vertical/flow.rs | 31 ++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/render-wasm/src/shapes/text_vertical/flow.rs b/render-wasm/src/shapes/text_vertical/flow.rs index 2981b52130..162b1c5257 100644 --- a/render-wasm/src/shapes/text_vertical/flow.rs +++ b/render-wasm/src/shapes/text_vertical/flow.rs @@ -235,11 +235,19 @@ fn column_break( } } // Drop the shift if it would empty this column or overflow the new one: - // a kinsoku violation is better than overflowing the wrap budget. - let shifted_extent: f32 = items[break_at..i].iter().map(|it| it.extent).sum(); - if break_at == column_start - || (break_at < i && shifted_extent + items[i].extent > max_height + FIT_TOLERANCE) - { + // a kinsoku violation is better than overflowing the wrap budget. The + // moved items stay with what follows up to the next allowed break, so + // measure that whole unit: a forbidden run longer than a column would + // otherwise be pushed out again at every column. + let unit_end = (i + 1..items.len()) + .find(|&next| break_block(items, next).is_none()) + .unwrap_or(items.len()); + let unit_extent: f32 = items[break_at..unit_end] + .iter() + .filter(|it| !it.ch.is_some_and(can_hang)) + .map(|it| it.extent) + .sum(); + if break_at == column_start || (break_at < i && unit_extent > max_height + FIT_TOLERANCE) { break_at = natural; } Some(break_at) @@ -1280,6 +1288,19 @@ mod tests { } } + #[test] + fn plan_columns_does_not_cascade_a_forbidden_run_longer_than_a_column() { + // ー、ャュョッ。 cannot start a column, and the run is longer than a + // column. Pushing its head out cannot satisfy kinsoku: each new column + // would push it out again and leave one or two characters behind. + let items: Vec = "ださい。ー、ャュョッ。" + .chars() + .map(|c| item(10.0, c)) + .collect(); + let placements = plan_columns(&items, 50.0); + assert_eq!(columns(&placements), vec![0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1]); + } + #[test] fn capped_adjustment_redistributes_after_a_boundary_saturates() { let allocations = capped_equal_allocations(&[(0, 1.0), (1, 3.0)], 3.0);