mcp: read object template via document.node summary flag
This commit is contained in:
parent
d9136c46b9
commit
e5e09d5d48
|
|
@ -25,7 +25,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
cmd!("file.close", "Close", ["File"], Some("Cmd+W"), "{index?}", has_doc, file_close),
|
||||
cmd!("document.activate", "Activate Document", [], None, "{index}", always, doc_activate),
|
||||
cmd!(query "document.inspect", "Inspect Document", [], None, "{} → layer tree, artboards, selection, history", has_doc, |s, _| Ok(inspect::document(s))),
|
||||
cmd!(query "document.node", "Inspect Object", [], None, "{id} → full object JSON", has_doc, doc_node),
|
||||
cmd!(query "document.node", "Inspect Object", [], None, "{id, summary?: the compact document.inspect-style summary instead of the full object JSON} → one object", has_doc, doc_node),
|
||||
cmd!(query "document.json", "Document JSON", [], None, "{} → complete document model", has_doc, |s, _| Ok(serde_json::to_value(&*s.doc()?.doc).unwrap_or(Value::Null))),
|
||||
cmd!(
|
||||
"document.setUnits",
|
||||
|
|
@ -106,7 +106,7 @@ fn doc_activate(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
fn doc_node(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = id_param(p, "id").ok_or_else(|| bad("document.node", "missing id"))?;
|
||||
let n = s.doc()?.doc.node(id).ok_or(EngineError::NoNode(id))?;
|
||||
Ok(serde_json::to_value(n).unwrap_or(Value::Null))
|
||||
if bool_or(p, "summary", false) { Ok(inspect::node_summary(n)) } else { Ok(serde_json::to_value(n).unwrap_or(Value::Null)) }
|
||||
}
|
||||
|
||||
/// Document Setup's units (also `document.setup {units}`).
|
||||
|
|
|
|||
|
|
@ -285,6 +285,21 @@ fn inspect_lists_everything() {
|
|||
assert!(s.commands().len() > 100);
|
||||
}
|
||||
|
||||
/// `document.node {summary: true}` answers the `document.inspect` shape for one node:
|
||||
/// the same fields the full-tree summary carries, without serializing the whole tree.
|
||||
#[test]
|
||||
fn document_node_summary_matches_inspect() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 0.0, 0.0, 10.0, 10.0).0;
|
||||
let full = s.execute("document.node", &json!({"id": id})).unwrap();
|
||||
assert!(full.get("bounds").is_none(), "full object JSON carries geometry, not bounds: {full}");
|
||||
let summary = s.execute("document.node", &json!({"id": id, "summary": true})).unwrap();
|
||||
let v = s.execute("document.inspect", &json!({})).unwrap();
|
||||
let node = v["layers"][0]["children"].as_array().unwrap().iter().find(|n| n["id"] == id).cloned().unwrap();
|
||||
assert_eq!(summary, node, "{summary} vs {node}");
|
||||
assert!(s.execute("document.node", &json!({"id": 999999, "summary": true})).is_err());
|
||||
}
|
||||
|
||||
/// Type reports the paint its characters show, and its own object-level paint apart from it.
|
||||
#[test]
|
||||
fn inspect_reports_the_paint_of_types_characters() {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ pub static TEMPLATES: &[Template] = &[
|
|||
uri: "vectorcraft://object/{id}",
|
||||
name: "object",
|
||||
title: "One layer or object",
|
||||
description: "The layer or object with this id, with its children, bounds and paint (document.inspect)",
|
||||
description: "The layer or object with this id, with its children, bounds and paint (document.node {summary: true})",
|
||||
mime: "application/json",
|
||||
live: Live::Objects,
|
||||
},
|
||||
|
|
@ -118,7 +118,9 @@ pub fn read(b: &mut dyn Backend, uri: &str) -> Result<Value, ReadError> {
|
|||
return Err(ReadError::Invalid(format!("{} needs a value for `{}`", template.uri, variable_name(template.uri))));
|
||||
}
|
||||
let found = match template.name {
|
||||
"object" => find_object(b, &var).map_err(ReadError::Backend)?,
|
||||
// One direct `document.node` lookup through `engine.execute` (which reaches both
|
||||
// backends), instead of pulling the whole layer tree out of `document.inspect`.
|
||||
"object" => read_object(b, &var).map_err(ReadError::Backend)?,
|
||||
// The command catalogue is a control method; the rest are engine commands.
|
||||
"command" => find_in(b.call("engine.commands", json!({})).map_err(ReadError::Backend)?, None, "id", &var),
|
||||
"effect" => find_in(exec(b, "effect.list").map_err(ReadError::Backend)?, Some("catalog"), "id", &var),
|
||||
|
|
@ -171,30 +173,23 @@ fn decode(s: &str) -> String {
|
|||
String::from_utf8_lossy(&out).into_owned()
|
||||
}
|
||||
|
||||
/// The layer or object with this id in the active document, with its subtree.
|
||||
fn find_object(b: &mut dyn Backend, id: &str) -> Result<Option<Value>, String> {
|
||||
let doc = exec(b, "document.inspect")?;
|
||||
Ok(find_node(doc.get("layers"), id))
|
||||
}
|
||||
|
||||
/// Depth-first search of the layer tree for a node whose `id` matches.
|
||||
fn find_node(node: Option<&Value>, id: &str) -> Option<Value> {
|
||||
let nodes = node?.as_array()?;
|
||||
let mut stack: Vec<&Value> = nodes.iter().rev().collect();
|
||||
while let Some(n) = stack.pop() {
|
||||
let hit = match n.get("id") {
|
||||
Some(Value::String(s)) => s == id,
|
||||
Some(Value::Number(number)) => number.to_string() == id,
|
||||
_ => false,
|
||||
};
|
||||
if hit {
|
||||
return Some(n.clone());
|
||||
}
|
||||
if let Some(kids) = n.get("children").and_then(Value::as_array) {
|
||||
stack.extend(kids.iter().rev());
|
||||
}
|
||||
/// The layer or object with this id in the active document, as its compact summary
|
||||
/// (bounds, paint labels, subtree — the `document.inspect` shape for one node).
|
||||
///
|
||||
/// One `document.node {summary: true}` lookup through `engine.execute` (which reaches
|
||||
/// both backends), instead of pulling the whole layer tree out of `document.inspect`
|
||||
/// and scanning it: the reply matches the old read field for field, without the
|
||||
/// O(document) serialize-and-summarize on every call.
|
||||
///
|
||||
/// A non-numeric id, or one that names nothing, is "not found" (the caller turns it into
|
||||
/// `-32602`); backend failures are swallowed the same way, matching how [`crate::prompts`]
|
||||
/// treats an unreachable catalogue (empty rather than an error).
|
||||
fn read_object(b: &mut dyn Backend, id: &str) -> Result<Option<Value>, String> {
|
||||
let Ok(num) = id.parse::<u64>() else { return Ok(None) };
|
||||
match b.call("engine.execute", json!({"command": "document.node", "params": {"id": num, "summary": true}})) {
|
||||
Ok(v) => Ok(Some(v)),
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The item in a query command's array whose `field` equals `value`.
|
||||
|
|
@ -261,5 +256,11 @@ mod tests {
|
|||
let v = read(b.as_mut(), &format!("vectorcraft://object/{id}")).unwrap();
|
||||
assert_eq!(v["id"].to_string(), id);
|
||||
assert!(v["bounds"].is_object(), "{v}");
|
||||
// Layers resolve too, not just drawn objects.
|
||||
let inspect = b.call("document.inspect", json!({})).unwrap();
|
||||
let layer = inspect["layers"].as_array().and_then(|l| l.first()).expect("a layer");
|
||||
let lid = layer["id"].to_string();
|
||||
let v = read(b.as_mut(), &format!("vectorcraft://object/{lid}")).unwrap();
|
||||
assert_eq!(v["id"].to_string(), lid);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue