From 4f30e523680903f750c83a5b4d5f0f521f6d3e57 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Fri, 16 Oct 2026 19:56:57 +0200 Subject: [PATCH] Type: text.create and text.createInPath return the new type's bounds (#2413) Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/create.rs | 3 ++- crates/engine/src/cmd/textedit.rs | 5 +++-- crates/engine/src/cmd/typecmd.rs | 35 +++++++++++++++++++++++++++++++ crates/engine/src/inspect.rs | 2 +- docs/mcp.md | 3 ++- 5 files changed, 43 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/cmd/create.rs b/crates/engine/src/cmd/create.rs index fd2486c74..23abe9389 100644 --- a/crates/engine/src/cmd/create.rs +++ b/crates/engine/src/cmd/create.rs @@ -74,7 +74,7 @@ pub fn specs() -> Vec { "Create Text", [], None, - "{x, y, text, vertical?: bool = false, size?: pt, font?: family, style?, color?, area?: {width, height, fit?: none|autoHeight|shrinkText, fitMinPercent?}, placeholder?: bool (fill it with placeholder text instead, as the Type tools do with prefs placeholderText), justify?: \"auto\" (default: the start of each paragraph's direction)|\"left\"|\"center\"|\"right\"|\"justifyAll\" (point type: aligned on x, so \"center\" centres each line on the point and \"right\" ends it there; text.setStyle changes it later), leadingModel?: \"romanBaseline\"|\"emBoxTop\", charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (default: emBoxTop and emBoxCenter while the interface is in Japanese, else romanBaseline)} → {id, overflow?: [id] (area type whose text doesn't fit its frame: the lines past it aren't drawn; left out when it fits)}; new area type gets Auto Size (fit autoHeight) when the autoSizeAreaType preference is on", + "{x, y, text, vertical?: bool = false, size?: pt, font?: family, style?, color?, area?: {width, height, fit?: none|autoHeight|shrinkText, fitMinPercent?}, placeholder?: bool (fill it with placeholder text instead, as the Type tools do with prefs placeholderText), justify?: \"auto\" (default: the start of each paragraph's direction)|\"left\"|\"center\"|\"right\"|\"justifyAll\" (point type: aligned on x, so \"center\" centres each line on the point and \"right\" ends it there; text.setStyle changes it later), leadingModel?: \"romanBaseline\"|\"emBoxTop\", charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (default: emBoxTop and emBoxCenter while the interface is in Japanese, else romanBaseline)} → {id, bounds: {x, y, width, height} (the new type's box as document.inspect reports it, after justify), overflow?: [id] (area type whose text doesn't fit its frame: the lines past it aren't drawn; left out when it fits)}; new area type gets Auto Size (fit autoHeight) when the autoSizeAreaType preference is on", has_doc, text_create ), @@ -512,6 +512,7 @@ fn text_create(s: &mut Session, p: &Value) -> Result { } let made = add_node(s, "Type", NodeKind::Text(Box::new(t)), Appearance::default(), None)?; let id = made.get("id").and_then(Value::as_u64).map(NodeId); + let made = super::typecmd::with_bounds(s, id, made); Ok(super::typecmd::with_overflow(s, id.as_slice(), made)) } diff --git a/crates/engine/src/cmd/textedit.rs b/crates/engine/src/cmd/textedit.rs index a18de08ed..7e2cd2ec6 100644 --- a/crates/engine/src/cmd/textedit.rs +++ b/crates/engine/src/cmd/textedit.rs @@ -45,7 +45,7 @@ pub fn specs() -> Vec { "Area / Path Type", [], None, - "{path: id, mode: \"area\"|\"onPath\", text?: \"\", vertical?: bool = false, at?: [x, y] (on-path start: nearest point), size?, font?, style?, color? (as text.create), fit?: none|autoHeight|shrinkText, fitMinPercent? (area: as text.areaOptions; default autoHeight when the autoSizeAreaType preference is on), placeholder?: bool (placeholder text instead, as text.create), justify?, leadingModel?, charAlign? (as text.create)} turn a path into an area-type frame or a type-on-a-path baseline (the path's paint is dropped) → {id, overflow?: [id] (the text doesn't fit; left out when it fits)}", + "{path: id, mode: \"area\"|\"onPath\", text?: \"\", vertical?: bool = false, at?: [x, y] (on-path start: nearest point), size?, font?, style?, color? (as text.create), fit?: none|autoHeight|shrinkText, fitMinPercent? (area: as text.areaOptions; default autoHeight when the autoSizeAreaType preference is on), placeholder?: bool (placeholder text instead, as text.create), justify?, leadingModel?, charAlign? (as text.create)} turn a path into an area-type frame or a type-on-a-path baseline (the path's paint is dropped) → {id, bounds: {x, y, width, height} (as text.create), overflow?: [id] (the text doesn't fit; left out when it fits)}", has_doc, create_in_path ), @@ -609,7 +609,8 @@ fn create_in_path(s: &mut Session, p: &Value) -> Result { sel.set([id]); Ok(id) })?; - Ok(super::typecmd::with_overflow(s, &[id], json!({"id": id.0}))) + let made = super::typecmd::with_bounds(s, Some(id), json!({"id": id.0})); + Ok(super::typecmd::with_overflow(s, &[id], made)) } /// What `type.step` changes. diff --git a/crates/engine/src/cmd/typecmd.rs b/crates/engine/src/cmd/typecmd.rs index 9a1b6bd11..0ebe71c12 100644 --- a/crates/engine/src/cmd/typecmd.rs +++ b/crates/engine/src/cmd/typecmd.rs @@ -269,6 +269,17 @@ pub(crate) fn with_overflow(s: &Session, ids: &[NodeId], mut result: Value) -> V result } +/// Adds `bounds` to the result of a command that made the type object `id`: the same +/// `{x, y, width, height}` box `document.inspect` reports for it (after `justify`, so a script can +/// place the new type without a helper object); `null` when it has none. +pub(crate) fn with_bounds(s: &Session, id: Option, mut result: Value) -> Value { + let Ok(st) = s.doc() else { return result }; + let symbols = st.doc.symbol_boxes(); + let b = id.and_then(|i| st.doc.node(i)).and_then(|n| n.geometric_bounds_with(&symbols)); + result["bounds"] = crate::inspect::rect_json(b); + result +} + /// The warning that Print gives for `ids`, area type and type on a path whose text doesn't fit /// (as [`with_overflow`] finds it): the text past the end of their frames doesn't print. It names /// the first 20 ids and counts the rest; `None` when `ids` is empty. @@ -783,6 +794,30 @@ fn reshape_area(s: &mut Session, p: &Value) -> Result { mod area_tests { use super::*; + /// #2413: `text.create` and `text.createInPath` return the new type's `bounds`, the box + /// `document.inspect` reports for it, so a script can place it without a helper object. + #[test] + fn text_create_returns_the_inspect_bounds() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 600, "height": 400})).unwrap(); + let inspected = |s: &mut Session, id: &Value| { + let doc = s.execute("document.inspect", &json!({})).unwrap(); + fn find(v: &Value, id: &Value) -> Option { + if &v["id"] == id { + return Some(v["bounds"].clone()); + } + v.get("children").or_else(|| v.get("layers"))?.as_array()?.iter().find_map(|c| find(c, id)) + } + find(&doc, id).unwrap() + }; + let made = s.execute("text.create", &json!({"x": 300, "y": 100, "text": "Hello", "size": 24, "justify": "center"})).unwrap(); + assert!(made["bounds"]["width"].as_f64().unwrap() > 10.0, "{made}"); + assert_eq!(made["bounds"], inspected(&mut s, &made["id"])); + let rect = s.execute("shape.rectangle", &json!({"x": 10, "y": 200, "width": 100, "height": 50})).unwrap(); + let area = s.execute("text.createInPath", &json!({"path": rect["id"], "mode": "area", "text": "Boxed"})).unwrap(); + assert_eq!(area["bounds"], inspected(&mut s, &area["id"])); + } + /// #1748: `text.create` takes `justify`; point type then lines up on its point (centred on /// it, or ending there), and `text.setStyle` moves it the same way afterwards. #[test] diff --git a/crates/engine/src/inspect.rs b/crates/engine/src/inspect.rs index 29343bd74..c5fe27670 100644 --- a/crates/engine/src/inspect.rs +++ b/crates/engine/src/inspect.rs @@ -6,7 +6,7 @@ use vectorcraft_doc::{ArrowAlign, Document, LineCap, LineJoin, Node, NodeKind, S use crate::Session; -fn rect_json(r: Option) -> Value { +pub(crate) fn rect_json(r: Option) -> Value { match r { Some(r) => json!({ "x": r.x0, "y": r.y0, "width": r.width(), "height": r.height() }), None => Value::Null, diff --git a/docs/mcp.md b/docs/mcp.md index f77f4b2b7..5f95424d0 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3165,7 +3165,8 @@ Text that doesn't fit is drawn up to the last line that does, so a one-line head draws nothing. The commands that make type or change what it holds say so in their own reply: `text.create`, `text.createInPath`, `text.setText` and `text.setStyle` add `overflow: [id…]` (the area type and type on a path among the objects they made or changed whose text doesn't fit), and the `add_text` tool adds `overflow: true`. The key is -left out when everything fits. The warnings of `file.print` and `print.preview` give the ids of the objects that print +left out when everything fits. `text.create` and `text.createInPath` also return `bounds: {x, y, width, height}`, the new type's box +as `document.inspect` reports it (after `justify`), so a script can place the type without a helper object (#2413). The warnings of `file.print` and `print.preview` give the ids of the objects that print and whose text doesn't fit: the first 20 ids, then how many more. ```json