Autoflow: skip deleted paragraphs and ask for every forced frame at once (#1209)

The start option lookup now reads the next paragraph composing places,
past tracked-deleted ones. Frames that start options and breaks force
are counted and asked for in one round, so a story with a paragraph per
page no longer gets one frame per round.
This commit is contained in:
VoiceLessQ
2026-10-28 22:32:07 -02:00
parent e248b63165
commit ca85be89d2
4 changed files with 60 additions and 13 deletions
+24 -10
View File
@@ -197,18 +197,24 @@ pub(crate) fn reimport(
Ok(imp.warnings)
}
/// Is the story overset in its frames, and how much text do its full frames hold (bytes, frames)?
/// Is the story overset in its frames, how much text do its full frames hold (bytes, frames), and
/// how many frames does the overset text start on its own (breaks, Start Paragraph options)?
/// A frame a column, frame or page break ends early is left out: it says nothing about how much
/// a frame holds. So is one a Start Paragraph option ends early (#1209).
fn fit(d: &Document, sid: StoryId) -> (Option<usize>, usize, usize) {
fn fit(d: &Document, sid: StoryId) -> (Option<usize>, usize, usize, usize) {
let cs = designcraft_compose::compose_story(d, sid, &Default::default());
let Some(story) = d.story(sid) else { return (cs.overset_at, 0, 0) };
let Some(story) = d.story(sid) else { return (cs.overset_at, 0, 0, 0) };
let text = story.text.as_str();
// A frame's range stops before the paragraph's return.
let deleted = designcraft_compose::deleted_paras(story, &story.para_ranges());
let starts = |p: usize| {
!deleted.get(p).copied().unwrap_or(false)
&& story.paras.get(p).is_some_and(|pf| d.styles.resolve_para(pf).0.start_paragraph != StartParagraph::Anywhere)
};
// A frame's range stops before the paragraph's return; composing skips deleted paragraphs.
let moved_on = |end: usize| {
let next = if text.as_bytes().get(end) == Some(&b'\n') { end.saturating_add(1) } else { end };
next.checked_sub(1).is_some_and(|i| text.as_bytes().get(i) == Some(&b'\n'))
&& story.paras.get(story.para_at(next)).is_some_and(|p| d.styles.resolve_para(p).0.start_paragraph != StartParagraph::Anywhere)
&& (story.para_at(next)..story.paras.len()).find(|&p| !deleted.get(p).copied().unwrap_or(false)).is_some_and(starts)
};
let (mut held, mut full) = (0, 0);
for f in &cs.frames {
@@ -218,7 +224,14 @@ fn fit(d: &Document, sid: StoryId) -> (Option<usize>, usize, usize) {
full += 1;
}
}
(cs.overset_at, held, full)
let forced = cs.overset_at.map_or(0, |at| {
let first = story.para_at(at);
// The paragraph the overset text starts with counts when it starts there.
let from = if at.checked_sub(1).is_none_or(|i| text.as_bytes().get(i) == Some(&b'\n')) { first } else { first.saturating_add(1) };
let breaks = text.get(at..).map_or(0, |t| t.chars().filter(|&c| designcraft_doc::is_break_char(c)).count());
(from..story.paras.len()).filter(|&p| starts(p)).count().saturating_add(breaks)
});
(cs.overset_at, held, full, forced)
}
/// The frames autoflow sets on a page: one per column inside the margins, in the order a story
@@ -263,12 +276,13 @@ pub(crate) fn autoflow(d: &mut Document, sid: StoryId, max_pages: usize, cmd: &s
// The next page of the document autoflow may set frames on; it only moves forward.
let mut following = 0;
loop {
let (overset, held, full) = fit(d, sid);
let (overset, held, full, forced) = fit(d, sid);
let Some(at) = overset else { break };
let len = d.story(sid).map_or(0, |s| s.len());
// Frames still needed at the rate the full ones are filled (at least one); with no full
// frame to go by yet, one more.
let want = held.checked_div(full).map_or(1, |per_frame| len.saturating_sub(at).div_ceil(per_frame.max(1))).max(1);
// Frames still needed at the rate the full ones are filled, plus one per frame the text
// starts on its own (at least one); with neither to go by yet, one more.
let rate = held.checked_div(full).map_or(0, |per_frame| len.saturating_sub(at).div_ceil(per_frame.max(1)));
let want = rate.saturating_add(forced).max(1);
let last = *d.story(sid).and_then(|s| s.frames.last()).ok_or_else(|| bad(cmd, "the story has no frame"))?;
let page = d.page_of_item(last).unwrap_or(d.page_count().saturating_sub(1));
let layer = d.default_layer();
+33
View File
@@ -344,3 +344,36 @@ fn autoflow_sizes_no_frame_by_one_a_start_option_ends_early() {
assert!(d.next_id - ids < 100, "{start}: {} ids", d.next_id - ids);
}
}
#[test]
fn autoflow_looks_past_a_deleted_paragraph_for_the_start_option() {
// The title frame ends before a tracked deletion; the chapter after it starts on an odd page.
use designcraft_doc::{ChangeMark, build::NewDocument};
let mut d = Document::new(&NewDocument { pages: 1, facing_pages: false, primary_text_frame: true, ..Default::default() });
let sid = d.settings.primary_story.unwrap();
let st = d.story_mut(sid).unwrap();
st.insert(0, &format!("Front\nGone\nChapter one\n{}", "lorem ipsum dolor sit amet ".repeat(400).trim()));
st.paras[2].para.start_paragraph = Some(StartParagraph::NextOddPage);
st.format_chars(6..11, |f| f.over.change = Some(ChangeMark::Deleted));
let ids = d.next_id;
assert_eq!(autoflow(&mut d, sid, 2000, "file.place", false).unwrap(), 4);
assert_eq!(designcraft_compose::compose_story(&d, sid, &Default::default()).overset_at, None);
assert!(d.next_id - ids < 100, "{} ids", d.next_id - ids);
}
#[test]
fn autoflow_asks_for_every_frame_the_start_options_ask_for_at_once() {
// A paragraph per page: no frame sets a fill rate, yet one round shouldn't add one frame.
use designcraft_doc::build::NewDocument;
let mut d = Document::new(&NewDocument { pages: 1, facing_pages: false, primary_text_frame: true, ..Default::default() });
let sid = d.settings.primary_story.unwrap();
let st = d.story_mut(sid).unwrap();
st.insert(0, (0..200).map(|i| format!("Chapter {i}\n")).collect::<String>().trim_end());
for p in &mut st.paras {
p.para.start_paragraph = Some(StartParagraph::NextPage);
}
let ids = d.next_id;
assert_eq!(autoflow(&mut d, sid, 2000, "file.place", false).unwrap(), 199);
assert_eq!(designcraft_compose::compose_story(&d, sid, &Default::default()).overset_at, None);
assert!(d.next_id - ids < 2000, "{} ids", d.next_id - ids);
}