Merge cache-keys: fix(compose): text recomposes when the baseline grid, a wrap layer's visibility or section numbering changes (#718)

This commit is contained in:
aiongg
2026-10-13 14:29:08 +00:00
3 changed files with 70 additions and 2 deletions
+1
View File
@@ -128,6 +128,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
Newest first.
- **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-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** · The Story Editor stays with the document its story belongs to: it hides while another document is active and closes with its document, so its edits never reach another document's story with the same id (#729). Undo restores the selection from before the command and Redo the one after it (#716). No readiness percentage change.
- **2026-10-13** · Saving never reports success without a file: a document that can't be written is an error from Save, Save As, Save a Copy, Package, snippet export and the web serializer, leaving the existing file, the unsaved state and the recovery checkpoint as they were. Placed files over 4 GiB are written with zip64 sizes. A web Save whose browser download can't start reports the error and keeps the document unsaved (#741, #708). No readiness percentage change.
+24 -2
View File
@@ -69,7 +69,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()
}
@@ -329,8 +340,17 @@ impl Cache {
fn signature(doc: &Document, story: &Arc<Story>) -> (Vec<usize>, Vec<Arc<Item>>) {
// 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 {
@@ -358,6 +378,8 @@ fn signature(doc: &Document, story: &Arc<Story>) -> (Vec<usize>, Vec<Arc<Item>>)
it.walk(&mut |i| wraps |= i.has_wrap());
if wraps {
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());
}
}
+45
View File
@@ -7008,3 +7008,48 @@ fn degenerate_frame_outlines_compose_without_panicking() {
let _ = compose_story(&d, sid, &ComposeOptions::default());
}
}
#[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);
}