diff --git a/ROADMAP.md b/ROADMAP.md index bb6896b2e..aec4e71da 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -183,6 +183,7 @@ Newest first. - **2026-10-13** · Path edits (`path.setAnchor`, `path.moveAnchors`, `path.appendAnchor`, `path.reverse`, `path.close`, anchor add/delete/convert, Erase, Smooth and Scissors) refuse a path that is locked, sits in a locked group or on a locked layer (#735). Properties links to the Stroke, Character, Paragraph, Effects, Text Wrap and Info panels open them in a customized panel layout too (#736). No readiness percentage change. - **2026-10-13** · IDML export keeps parent item overrides: each page lists its overridden parent items and the local copy keeps its link to the parent item, so a reopened IDML file shows only the local copy (#725). No readiness percentage change. - **2026-10-13** · macOS: a command chosen with the mouse in the menu bar runs at once. It used to wait for the next mouse move or key press over the window, so File › Open, Place and Save As seemed to take 10–20 seconds to open their dialogs. No readiness percentage change. +- **2026-10-13** · Text recomposes when the document baseline grid changes, when a layer holding a text-wrap object is shown or hidden, or when a section's start number, numbering style, prefix or marker changes; the composition cache keyed only on the section count and item identities and kept the old layout. No readiness percentage change. - **2026-10-12** · macOS Dock Quit uses the same unsaved-document prompts as window and menu Quit. Repeated requests keep the current answers; Cancel retains every tab and unsaved recovery checkpoint, and completed Save/Don't Save answers exit through the ordinary event loop. No readiness percentage change. - **2026-10-12** · Text strokes can sit behind the fill: the stroke lies wholly outside the letters at its full weight on screen, in PNG and in PDF, set from the Properties panel or Character Style Options. IDML `StrokeAlignment` on text imports and exports as this setting, so imported text with outside-aligned strokes keeps its letter shapes (#619). No readiness percentage change. - **2026-10-12** · Placed Word documents keep their bullets and numbering (#626): `numbering.xml` levels become bullet or numbered paragraphs with Word's number formats, label text, start numbers, restarts and list indents, also when a paragraph style carries the list. No readiness percentage change. diff --git a/crates/compose/src/cache.rs b/crates/compose/src/cache.rs index 4ede03594..8f436a31f 100644 --- a/crates/compose/src/cache.rs +++ b/crates/compose/src/cache.rs @@ -47,7 +47,18 @@ fn doc_signature(doc: &Document) -> u64 { } (Arc::as_ptr(&doc.styles) as usize).hash(&mut h); format!("{:?}", doc.text_variables).hash(&mut h); + sections_hash(doc).hash(&mut h); + h.finish() +} + +/// Everything page names and section markers are made from. +fn sections_hash(doc: &Document) -> u64 { + use std::hash::{Hash, Hasher}; + let mut h = std::collections::hash_map::DefaultHasher::new(); doc.sections.len().hash(&mut h); + for s in &doc.sections { + (s.start, s.start_number, s.style as u8, &s.prefix, &s.marker, s.include_prefix).hash(&mut h); + } h.finish() } @@ -261,8 +272,17 @@ impl Cache { fn signature(doc: &Document, story: &Arc) -> (Vec, Vec>) { // The document's font scope: another document's fonts of the same name set it differently. - let mut sig = - vec![Arc::as_ptr(story) as usize, story.rev as usize, Arc::as_ptr(&doc.styles) as usize, doc.sections.len(), doc.font_scope as usize]; + // Sections name the pages under the frames (page-number and section markers). + let g = &doc.settings.baseline_grid; + let mut sig = vec![ + Arc::as_ptr(story) as usize, + story.rev as usize, + Arc::as_ptr(&doc.styles) as usize, + sections_hash(doc) as usize, + doc.font_scope as usize, + g.start.to_bits() as usize, + g.increment.to_bits() as usize, + ]; let mut keep = Vec::new(); let mut spreads = Vec::new(); for f in &story.frames { @@ -281,6 +301,8 @@ fn signature(doc: &Document, story: &Arc) -> (Vec, Vec>) for it in &sp.items { if it.wrap.mode != WrapMode::None { sig.push(Arc::as_ptr(it) as usize); + // Wrap objects on hidden layers don't wrap text. + sig.push(usize::from(doc.layer(it.layer).is_some_and(|l| l.visible))); keep.push(it.clone()); } } diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index ece34fc59..945f5cfc6 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -4182,3 +4182,47 @@ fn empty_paragraphs_get_no_bullet_or_number() { assert_eq!(list_labels(&cs, LABEL_CHARS), ["1.", "", "2.", ""], "list `{name}`"); } } + +#[test] +fn cache_recomposes_when_the_baseline_grid_changes() { + let para = ParaAttrs { grid_align: Some(GridAlign::AllLines), ..Default::default() }; + let (mut d, sid, _) = doc_with(LOREM, Rect::new(0.0, 0.0, 200.0, 600.0), para); + let cache = Cache::new(); + let a = cache.get(&d, sid, None); + d.settings.baseline_grid.increment = 30.0; + let b = cache.get(&d, sid, None); + assert!(!Arc::ptr_eq(&a, &b)); + let (la, lb) = (&a.frames[0].lines[1], &b.frames[0].lines[1]); + assert!((la.baseline - lb.baseline).abs() > 1.0, "{} {}", la.baseline, lb.baseline); +} + +#[test] +fn cache_recomposes_when_a_wrap_layer_is_hidden() { + let (mut d, sid, _) = doc_with(&[LOREM; 2].join(" "), Rect::new(0.0, 0.0, 300.0, 800.0), ParaAttrs::default()); + let lid = d.default_layer(); + let id = ItemId(d.alloc()); + let mut it = + designcraft_doc::Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 120.0, 60.0))); + it.wrap.mode = WrapMode::BoundingBox; + d.insert_item(SpreadRef::Doc(0), it, None).unwrap(); + let cache = Cache::new(); + let a = cache.get(&d, sid, None); + assert!(a.frames[0].lines[0].x0 >= 120.0 - 1e-6); + d.layer_mut(lid).unwrap().visible = false; + let b = cache.get(&d, sid, None); + assert!(!Arc::ptr_eq(&a, &b)); + assert!(b.frames[0].lines[0].x0 < 1.0, "{}", b.frames[0].lines[0].x0); +} + +#[test] +fn cache_recomposes_page_numbers_when_a_section_is_renumbered() { + let text = story::PAGE_NUMBER.to_string(); + let (mut d, sid, _) = doc_with(&text, Rect::new(0.0, 0.0, 300.0, 100.0), ParaAttrs::default()); + let cache = Cache::new(); + let a = cache.get(&d, sid, None); + assert_eq!(a.frames[0].lines[0].glyphs.len(), 1); + d.sections[0].start_number = Some(1000); + let b = cache.get(&d, sid, None); + assert!(!Arc::ptr_eq(&a, &b)); + assert_eq!(b.frames[0].lines[0].glyphs.len(), 4); +}