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#""#;
+ 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![];