diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 3ff92a9f..162aa608 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1229,22 +1229,17 @@ impl Session { format!("Untitled-{}", self.untitled_counter) } /// Text layout bounds are a cache (not saved): compute them for a document just read, or - /// selection boxes and hit testing would use the rough estimate until each text is edited. + /// selection boxes and hit testing would use the rough estimate until each text is edited, and + /// what is fitted to type's bounds (an envelope warping it) would come out differently. That + /// holds for the type in symbol definitions, pattern tiles, graph designs and opacity masks too, + /// not only in the layers (#1738). pub(crate) fn refresh_text_bounds(doc: &mut Document) { // Inline graphics in text take their size from their symbols' art (not saved either). doc.resolve_inline_art(); - let mut texts = vec![]; - doc.walk(|n| { - if matches!(n.kind, NodeKind::Text(_)) { - texts.push(n.id); - } - }); - for id in texts { - if let Some(NodeKind::Text(t)) = doc.node_mut(id).map(|n| &mut n.kind) - && t.cached_bounds.is_none() - { - cmd::typecmd::refresh_bounds(t); - } + let trees = doc.layers.iter_mut().chain(doc.symbols.iter_mut().map(|s| &mut s.art)); + let trees = trees.chain(doc.graph_designs.iter_mut().map(|g| &mut g.art)); + for tree in trees.chain(doc.patterns.iter_mut().flat_map(|p| p.art.iter_mut())) { + refresh_tree_text_bounds(tree); } } /// Add a document and make it active. @@ -1704,6 +1699,31 @@ impl Session { } } +/// Lay out the type in tree `n` (opacity mask art included) whose bounds aren't cached. Only the +/// nodes on the way to such type are copied. +fn refresh_tree_text_bounds(n: &mut Arc) { + fn stale(n: &vectorcraft_doc::Node) -> bool { + matches!(&n.kind, NodeKind::Text(t) if t.cached_bounds.is_none()) + || n.extras().mask.as_deref().is_some_and(|m| stale(&m.art)) + || n.children().is_some_and(|ch| ch.iter().any(|c| stale(c))) + } + if !stale(n) { + return; + } + let n = Arc::make_mut(n); + if let NodeKind::Text(t) = &mut n.kind + && t.cached_bounds.is_none() + { + cmd::typecmd::refresh_bounds(t); + } + if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_deref_mut()) { + refresh_tree_text_bounds(&mut m.art); + } + for c in n.children_mut().into_iter().flatten() { + refresh_tree_text_bounds(c); + } +} + #[cfg(test)] mod tests; #[cfg(test)] diff --git a/crates/engine/src/tests_envelope.rs b/crates/engine/src/tests_envelope.rs index 43819cf8..a6e917d0 100644 --- a/crates/engine/src/tests_envelope.rs +++ b/crates/engine/src/tests_envelope.rs @@ -288,3 +288,35 @@ fn enveloped_type_keeps_each_runs_colour_on_the_canvas() { } assert!(red > 100 && black > 100, "{red} red, {black} black"); } + +/// #1738: type warped by an envelope inside a symbol looks the same after a native save and +/// reopen: the type in symbol definitions gets its layout bounds back (they aren't saved), so the +/// envelope is fitted to the same box as before. +#[test] +fn warped_type_in_a_symbol_reopens_unchanged() { + let mut s = session(); + s.execute("text.create", &json!({"x": 30, "y": 160, "text": "STUDIO", "size": 72, "color": "#E3AB49"})).unwrap(); + s.execute("object.envelope.makeWithWarp", &json!({"style": "wave", "bend": 18})).unwrap(); + s.execute("symbol.new", &json!({"name": "Master"})).unwrap(); + let render = |s: &Session| vectorcraft_render::Renderer::new().render(&s.doc().unwrap().doc, 400, 300, Affine::IDENTITY, &Default::default()); + let before = render(&s); + let dir = std::env::temp_dir().join(format!("vectorcraft-1738-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("warped.vectorcraft"); + s.execute("document.save", &json!({"path": path})).unwrap(); + let mut o = Session::new(); + o.execute("document.open", &json!({"path": path})).unwrap(); + let _ = std::fs::remove_dir_all(&dir); + let d = &o.doc().unwrap().doc; + let mut texts = 0; + d.symbols[0].art.walk(&mut |n| { + if let NodeKind::Text(t) = &n.kind { + texts += 1; + assert!(t.cached_bounds.is_some(), "the symbol's type has no layout bounds after reopening"); + } + }); + assert_eq!(texts, 1); + let after = render(&o); + let differ = (0..300).flat_map(|y| (0..400).map(move |x| (x, y))).filter(|&(x, y)| before.pixel(x, y) != after.pixel(x, y)).count(); + assert_eq!(differ, 0, "{differ} pixels changed on reopening"); +}