Native reopen: type in symbols gets its layout bounds back (#1738)

Type's layout bounds are a cache that isn't saved. On open they were
computed again only for type in the layers, so type in a symbol definition
(or a pattern tile, graph design or opacity mask) kept the rough estimate,
and an envelope warping it was fitted to a different box: warped type in a
symbol changed shape after a save and reopen. The refresh now goes through
every art tree, masks included.
This commit is contained in:
Gert Pellin authored and bflatastic committed 2026-10-14 08:01:50 +07:00
1 parent 904414aa7d
commit e54fe9c31e
2 files changed
+65 -13

No files matched your search

+33 -13
View File
@@ -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<vectorcraft_doc::Node>) {
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)]
+32
View File
@@ -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");
}