mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-15 03:37:07 -04:00
Autoflow: add only the pages breaks ask for (#266)
A frame that a column, frame or page break ends early held only a few characters, and autoflow took that as the rate frames fill at: placing text that starts with a page break added a page per few characters (33 pages for 98 characters, 271 for a one-page Word section). The estimate now comes from full frames only; with none yet, one page is added and the story measured again. Regression test covers a leading break and a break between every paragraph. Fixes #262. Claude-Session: https://claude.ai/code/session_01GLcnoDFjqsh5x3MzVbfZeb Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
4aea57fea7
commit
ecadfcee3e
1 file changed
+42
-9
@@ -105,27 +105,37 @@ fn strip(story: &mut Story) {
|
||||
story.format_chars(0..n, |f| *f = Default::default());
|
||||
}
|
||||
|
||||
/// Is the story overset in its frames, and how much of it do its frames show?
|
||||
fn fit(d: &Document, sid: StoryId) -> (Option<usize>, usize) {
|
||||
/// Is the story overset in its frames, and how much text do its full frames hold (bytes, frames)?
|
||||
/// A frame a column, frame or page break ends early is left out: it says nothing about how much
|
||||
/// a frame holds.
|
||||
fn fit(d: &Document, sid: StoryId) -> (Option<usize>, usize, usize) {
|
||||
let cs = designcraft_compose::compose_story(d, sid, &Default::default());
|
||||
let shown = cs.frames.iter().map(|f| f.range.end).max().unwrap_or(0);
|
||||
(cs.overset_at, shown)
|
||||
let text = d.story(sid).map_or("", |s| s.text.as_str());
|
||||
let is_break = |c: char| matches!(c, designcraft_doc::COLUMN_BREAK | designcraft_doc::FRAME_BREAK | designcraft_doc::PAGE_BREAK);
|
||||
let (mut held, mut full) = (0, 0);
|
||||
for f in &cs.frames {
|
||||
let Some(shown) = text.get(f.range.clone()) else { continue };
|
||||
if !shown.is_empty() && !shown.chars().any(is_break) {
|
||||
held += shown.len();
|
||||
full += 1;
|
||||
}
|
||||
}
|
||||
(cs.overset_at, held, full)
|
||||
}
|
||||
|
||||
/// Thread new frames on new pages (margin rectangles) until the story fits. Returns pages added.
|
||||
pub(crate) fn autoflow(d: &mut Document, sid: StoryId, max_pages: usize) -> Result<usize> {
|
||||
let mut added = 0;
|
||||
loop {
|
||||
let (overset, shown) = fit(d, sid);
|
||||
let (overset, held, full) = fit(d, sid);
|
||||
let Some(at) = overset else { return Ok(added) };
|
||||
if added >= max_pages {
|
||||
return Err(bad("file.place", format!("autoflow stopped after {max_pages} pages")));
|
||||
}
|
||||
let len = d.story(sid).map_or(0, |s| s.len());
|
||||
let frames = d.story(sid).map_or(1, |s| s.frames.len()).max(1);
|
||||
// Frames still needed at the rate the existing ones are filled (at least one).
|
||||
let per_frame = (shown.max(at) / frames).max(1);
|
||||
let want = (len - at).div_ceil(per_frame).clamp(1, max_pages - added);
|
||||
// Frames still needed at the rate the full ones are filled (at least one); with no full
|
||||
// frame to go by yet, one more page.
|
||||
let want = held.checked_div(full).map_or(1, |per_frame| len.saturating_sub(at).div_ceil(per_frame.max(1))).clamp(1, max_pages - added);
|
||||
let last = *d.story(sid).and_then(|s| s.frames.last()).ok_or_else(|| bad("file.place", "the story has no frame"))?;
|
||||
let mut page = d.page_of_item(last).unwrap_or(d.page_count().saturating_sub(1));
|
||||
let parent = d.page_loc(page).and_then(|(si, pi)| d.spreads[si].pages[pi].parent);
|
||||
@@ -258,6 +268,29 @@ mod tests {
|
||||
assert_eq!(st.format_after(6).over.font_style.as_deref(), Some("Bold"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autoflow_adds_only_the_pages_breaks_ask_for() {
|
||||
// Regression (#262): a frame a break ends early made autoflow take it for a frame that
|
||||
// holds a few characters, and add a page per few characters left (33 for this text).
|
||||
let dir = std::env::temp_dir().join(format!("dc-place-breaks-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
for (name, rtf, pages) in [
|
||||
("lead.rtf", r"{\rtf1 \page\par A Short Heading\par First short paragraph.\par Second short paragraph.\par}", 2),
|
||||
("each.rtf", r"{\rtf1 One\par\page\par Two\par\page\par Three\par\page\par Four\par}", 4),
|
||||
] {
|
||||
let path = dir.join(name);
|
||||
std::fs::write(&path, rtf).unwrap();
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"pages": 1})).unwrap();
|
||||
let r = s.execute("file.place", &json!({"path": path.to_string_lossy(), "autoflow": true})).unwrap();
|
||||
assert_eq!(r["overset"], false, "{name}: {r}");
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert_eq!(d.page_count(), pages, "{name}: {r}");
|
||||
assert_eq!(d.story(StoryId(r["story"].as_u64().unwrap())).unwrap().frames.len(), pages, "{name}");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in new issue
Block a user