mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-02 22:50:27 -06:00
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:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user