mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-28 12:27:11 -06:00
#3692: a linked text-box chain lays its story out at most 16 widths
Audit finding 7: flow() laid the head box's whole story out again at every distinct box line length in the chain and kept each layout while splitting, so a chain of many one-line boxes of different widths multiplied the story's layout work and memory by the chain length. Past MAX_FLOW_WIDTHS (16) distinct lengths the text stops: the box that would need another layout and the rest of the chain show nothing, as overflow past the last box is hidden. Work is now at most 16x the story. Test: tests_budget::a_linked_chain_lays_its_story_out_at_a_bounded_number_of_widths. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4963e30eed
commit
8efba59b35
@@ -2,14 +2,56 @@
|
||||
//! is bounded by a cap, and real documents stay well under it.
|
||||
|
||||
use super::*;
|
||||
use wordcraft_doc::Block;
|
||||
use wordcraft_doc::table::{MAX_COLS, Table};
|
||||
use wordcraft_doc::{Block, PartKind, Pos, TextBody, para_block};
|
||||
|
||||
fn lay(doc: &Document) -> DocLayout {
|
||||
let mut c = LayoutCache::new();
|
||||
layout(doc, &mut c, &LayoutOptions::default())
|
||||
}
|
||||
|
||||
/// A `w` × `h` text box showing `text`, inline at the end of body paragraph `para`; its story id.
|
||||
fn text_box(d: &mut Document, para: usize, text: &str, w: f32, h: f32) -> u32 {
|
||||
let id = d.add_part(PartKind::TextBox, vec![para_block(Paragraph::with_text(text, Default::default()))]);
|
||||
if let Some(p) = d.parts.get_mut(&id) {
|
||||
p.body = TextBody::default();
|
||||
}
|
||||
let shape = InlineObject::Shape {
|
||||
kind: wordcraft_doc::para::ShapeKind::TextBox,
|
||||
w,
|
||||
h,
|
||||
fill: None,
|
||||
stroke: None,
|
||||
stroke_width: 1.0,
|
||||
float: Default::default(),
|
||||
story: Some(id),
|
||||
effects: Default::default(),
|
||||
freeform: None,
|
||||
extra: Default::default(),
|
||||
};
|
||||
let off = d.para(StoryRef::Body, &Path::top(para)).map_or(0, |p| p.len());
|
||||
d.insert_object(&Pos::body(para, off), shape, &Default::default()).unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_linked_chain_lays_its_story_out_at_a_bounded_number_of_widths() {
|
||||
// Forty linked boxes, each a line deep and each a different width, over a long story: every
|
||||
// width would lay the whole story out again. Past the cap, the chain's later boxes show nothing.
|
||||
let n = 40;
|
||||
let mut d = Document::from_text(&vec!["p"; n].join("\n"));
|
||||
let story = "Words that run on and on through every box of the chain. ".repeat(200);
|
||||
let ids: Vec<u32> = (0..n).map(|i| text_box(&mut d, i, if i == 0 { &story } else { "" }, 100.0 + 3.0 * i as f32, 28.0)).collect();
|
||||
for w in ids.windows(2) {
|
||||
d.parts.get_mut(&w[0]).unwrap().body.next = Some(w[1]);
|
||||
}
|
||||
let l = lay(&d);
|
||||
let shown =
|
||||
l.pages.iter().flat_map(|p| &p.items).filter(|it| matches!(it, Placed::Lines { story: StoryRef::Part(s), .. } if *s == ids[0])).count();
|
||||
assert!(shown > 1, "the first boxes still show the story: {shown}");
|
||||
assert!(shown <= textbox::MAX_FLOW_WIDTHS, "{shown} boxes laid the story out");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_huge_grid_after_is_bounded_like_the_docx_readers() {
|
||||
// A native document can say a row leaves four billion columns empty after its cells.
|
||||
|
||||
@@ -208,6 +208,12 @@ struct Cut {
|
||||
byte: usize,
|
||||
}
|
||||
|
||||
/// Most line lengths (and text directions) a chain's story is laid out at. Each lays the whole
|
||||
/// story out again and is kept while the chain is split, so a chain of boxes that each differ in
|
||||
/// width would multiply its story's layout work and memory; boxes needing another one show no
|
||||
/// text. Far above a real chain's few box sizes.
|
||||
pub(crate) const MAX_FLOW_WIDTHS: usize = 16;
|
||||
|
||||
/// Split the text of `chain`'s first box over its boxes.
|
||||
fn flow(ctx: &mut Ctx, chain: &[u32], depth: usize) -> Vec<Vec<Placed>> {
|
||||
let Some(&head) = chain.first() else { return Vec::new() };
|
||||
@@ -236,6 +242,11 @@ fn flow(ctx: &mut Ctx, chain: &[u32], depth: usize) -> Vec<Vec<Placed>> {
|
||||
let key = (len.to_bits(), body.vert);
|
||||
let items = match layouts.iter().find(|(k, _)| *k == key) {
|
||||
Some((_, items)) => items.clone(),
|
||||
// Past the cap the text stops here: this box and the rest show none.
|
||||
None if layouts.len() >= MAX_FLOW_WIDTHS => {
|
||||
out.push(Vec::new());
|
||||
continue;
|
||||
}
|
||||
None => {
|
||||
(ctx.counters, ctx.eq_count) = (snap.0.clone(), snap.1);
|
||||
let items = lay(ctx, head, &blocks, len, body.vert, depth);
|
||||
|
||||
Reference in New Issue
Block a user