diff --git a/ROADMAP.md b/ROADMAP.md index 20525e4a6..deda0dc4f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -380,7 +380,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. -- **2026-10-28** · Autoflow no longer sizes the rest of a story by a frame that a Start Paragraph option (next column, frame, page, or odd or even page) ends early (#1209). With a title before a chapter that starts on the next odd page, it estimated a page per few characters, added up to 2000 pages (the autoflow limit) and removed them again: placing 2000 words took 36 s in the app, now 0.4 s, with the same pages. No readiness percentage change. +- **2026-10-28** · Autoflow no longer sizes the rest of a story by a frame that a Start Paragraph option (next column, frame, page, or odd or even page) ends early (#1209). With a title before a chapter that starts on the next odd page, it estimated a page per few characters, added up to 2000 pages (the autoflow limit) and removed them again: placing 2000 words took 36 s in the app, now 0.4 s, with the same pages. It looks past tracked-deleted paragraphs for the option, as composing does, and asks for every frame that start options and breaks force in one round: 200 one-line chapters on their own pages take 9 ms (58 ms before). No readiness percentage change. - **2026-10-27** · An iconic panel's flyout stays open while you work in the canvas or a text field: Auto-Collapse Iconic Panels is off by default, and preferences saved with it on start with it off. Preferences › Interface › Panels holds the setting next to the column bar's menu item. No readiness percentage change. - **2026-10-24** · Column rules run from the top line's ascent to the bottom line's descent, as InDesign draws them, instead of from the top to the bottom of the columns, on screen, in PDF and in PowerPoint export. The top and bottom insets shorten the rule from those lines. Existing documents draw their column rules at the new length. No readiness percentage change. - **2026-10-20** · Edit › Keyboard Shortcuts is craft-keymap's shared dialog: commands grouped by menu with Show and Filter, recording that asks before taking a key from another command, undo and redo, named shortcut sets (edits of Default become Custom; an imported set becomes a set of its own), an on-screen US keyboard, InDesign Show Set files reviewed before they apply, JSON export and an HTML summary. ui.json keeps the `shortcuts` map earlier versions read, for the active set. No readiness percentage change. diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index 88becd85f..131e5273c 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -3483,8 +3483,8 @@ fn rewind( } /// Paragraphs that tracked changes delete: all their text is deleted, and so is their return or -/// the return before them (then they join a paragraph and add nothing to it). -fn deleted_paras(story: &Story, ranges: &[Range]) -> Vec { +/// the return before them (then they join a paragraph and add nothing to it). Composing skips them. +pub fn deleted_paras(story: &Story, ranges: &[Range]) -> Vec { let mut spans: Vec> = Vec::new(); for (r, f) in story.runs() { if f.over.change != Some(designcraft_doc::ChangeMark::Deleted) { diff --git a/crates/engine/src/cmd/place_text.rs b/crates/engine/src/cmd/place_text.rs index 7932a185c..961f6f630 100644 --- a/crates/engine/src/cmd/place_text.rs +++ b/crates/engine/src/cmd/place_text.rs @@ -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) { +fn fit(d: &Document, sid: StoryId) -> (Option, 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) { 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(); diff --git a/crates/engine/src/cmd/place_text/tests.rs b/crates/engine/src/cmd/place_text/tests.rs index bdce2a975..183a46074 100644 --- a/crates/engine/src/cmd/place_text/tests.rs +++ b/crates/engine/src/cmd/place_text/tests.rs @@ -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::().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); +}