diff --git a/crates/engine/src/cmd/clipboard.rs b/crates/engine/src/cmd/clipboard.rs index aaf8d5af..512d6035 100644 --- a/crates/engine/src/cmd/clipboard.rs +++ b/crates/engine/src/cmd/clipboard.rs @@ -259,6 +259,26 @@ mod tests { assert!((b.width() - 30.0).abs() < 1e-6 && (b.height() - 40.0).abs() < 1e-6); } + #[test] + fn pasted_type_gets_its_layout_bounds_cache() { + // Reported from Illustrator: pasted type kept no layout bounds, so its box (and alignment) + // fell back to the rough estimate until the file was saved and reopened. + let mut s = session(); + let svg = r#"WWWWWWWW"#; + s.execute("clipboard.importSvg", &json!({"svg": svg})).unwrap(); + s.execute("edit.paste", &json!({})).unwrap(); + let mut checked = 0; + s.doc().unwrap().doc.walk(|n| { + let vectorcraft_doc::NodeKind::Text(t) = &n.kind else { return }; + let cached = t.cached_bounds.expect("pasted type computed its bounds"); + let real = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t).bounds; + assert!((cached.width() - real.width()).abs() < 1e-6, "{cached:?} {real:?}"); + assert!(cached.width() > t.estimate_bounds().width() + 1.0, "{cached:?} is just the estimate"); + checked += 1; + }); + assert_eq!(checked, 1, "one text object pasted"); + } + #[test] fn paste_never_lands_in_an_object_that_reused_an_undone_layers_id() { // Found by the model-based test: undo restores the id counter, so a compound path made diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index d52008e2..8ebe35dc 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -381,6 +381,9 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho } } sel.set(new_ids.iter().copied()); + // Pasted type arrives without the layout bounds cache: fill it so boxes and alignment are + // right away (opening the file later would, but paste must not depend on a reopen). + super::typecmd::refresh_bounds_of(d, &new_ids); Ok((new_ids, imported, artboard)) })?; // The Artboard tool takes the pasted artboard. diff --git a/crates/engine/src/cmd/typecmd.rs b/crates/engine/src/cmd/typecmd.rs index e083021d..97dd421b 100644 --- a/crates/engine/src/cmd/typecmd.rs +++ b/crates/engine/src/cmd/typecmd.rs @@ -187,6 +187,20 @@ fn auto_height(t: &mut TextObject) -> bool { true } +/// Recompute the layout bounds cache for the text among `ids` (and their descendants) that lacks +/// it. Pasted or imported type arrives without the cache (not serialized), so selection boxes, the +/// Transform panel and hit testing would fall back to [`TextObject::estimate_bounds`] (a rough +/// 0.55 em per character) until the text is edited — the box comes up short and alignment looks off. +pub(crate) fn refresh_bounds_of(d: &mut Document, ids: &[NodeId]) { + for id in text_ids(d, ids) { + if let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) + && t.cached_bounds.is_none() + { + refresh_bounds(t); + } + } +} + /// Text objects among `ids` and their descendants. fn text_ids(d: &Document, ids: &[NodeId]) -> Vec { let mut out = vec![];