Automation: a layer or comp id sent as a string resolves as an id (#749)

This commit is contained in:
MarekSpodBiedry committed 2026-10-11 13:54:43 +02:00
1 parent 9a7d3e7699
commit bee798c190
3 files changed
+39 -5

No files matched your search

+21 -4
View File
@@ -442,17 +442,34 @@ pub(crate) fn merge_p(p: &Value) -> Option<&str> {
str_p(p, "merge")
}
/// `comp`: item id or name; default the active comp.
/// `comp`: item id or name (an all-digit string that names no comp is taken as an id, as MCP
/// clients often send numbers as strings); default the active comp.
pub(crate) fn comp_id(s: &Session, p: &Value) -> Result<ItemId> {
let missing = |v: &Value| EngineError::NoSuchComp(v.to_string());
match p.get("comp") {
Some(v @ Value::Number(n)) => n.as_u64().map(ItemId).filter(|id| s.project.comp(*id).is_some()).ok_or_else(|| missing(v)),
Some(v @ Value::String(name)) => s.project.items.values().find(|i| &i.name == name && i.as_comp().is_some()).map(|i| i.id).ok_or_else(|| missing(v)),
Some(v @ Value::String(name)) => s
.project
.items
.values()
.find(|i| &i.name == name && i.as_comp().is_some())
.map(|i| i.id)
.or_else(|| digits_id(name).map(ItemId).filter(|id| s.project.comp(*id).is_some()))
.ok_or_else(|| missing(v)),
_ => s.active_comp_id().ok_or(EngineError::NoComp),
}
}
/// Resolve a layer reference: id (number), `#n` / index (1-based), or name.
/// An all-ASCII-digit string as a number (`"98"` -> 98); anything else (names, `#n`, signs) is `None`.
fn digits_id(s: &str) -> Option<u64> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
s.parse().ok()
}
/// Resolve a layer reference: id (number), `#n` / index (1-based), or name. An all-digit string
/// that names no layer is resolved as the number it spells.
pub(crate) fn resolve_layer(comp: &Comp, v: &Value) -> Option<LayerId> {
match v {
Value::Number(n) => {
@@ -463,7 +480,7 @@ pub(crate) fn resolve_layer(comp: &Comp, v: &Value) -> Option<LayerId> {
if let Some(i) = s.strip_prefix('#').and_then(|i| i.parse::<usize>().ok()) {
return comp.layers.get(i.checked_sub(1)?).map(|l| l.id);
}
comp.layers.iter().find(|l| &l.name == s).map(|l| l.id)
comp.layers.iter().find(|l| &l.name == s).map(|l| l.id).or_else(|| digits_id(s).and_then(|id| resolve_layer(comp, &Value::from(id))))
}
_ => None,
}
+16
View File
@@ -420,3 +420,19 @@ fn camera_focus_commands_undo_and_serde() {
assert!(s.execute("camera.linkFocusToLayer", json!({})).is_err());
assert!(s.execute("camera.linkFocusToPoi", json!({})).is_ok());
}
/// An id sent as a string (`"98"`, as MCP clients often do) resolves as an id when no name matches.
#[test]
fn numeric_string_resolves_as_id() {
let mut s = Session::default();
s.execute("comp.new", json!({"name": "T", "width": 320, "height": 180, "duration": 2.0, "frameRate": 30})).unwrap();
let l = s.execute("layer.newSolid", json!({"color": "#ff0000"})).unwrap()["layer"].as_u64().unwrap();
let cid = s.active_comp_id().unwrap();
assert_eq!(s.resolve_comp(Some(&json!(cid.0.to_string()))).unwrap(), cid);
assert!(s.resolve_comp(Some(&json!("999999"))).is_err());
let comp = s.project.comp(cid).unwrap();
assert_eq!(crate::commands::resolve_layer(comp, &json!(l.to_string())), Some(LayerId(l)));
assert_eq!(crate::commands::resolve_layer(comp, &json!("999999")), None);
let r = s.execute("layer.tree", json!({"layer": l.to_string(), "comp": cid.0.to_string()})).unwrap();
assert_eq!(r["id"].as_u64(), Some(l), "{r}");
}
+2 -1
View File
@@ -123,7 +123,8 @@ See [MCP conventions](mcp.md) for core tools, resources, strict arguments, and r
| `render_preview` | Render a PNG without writing a file (`comp`, `time`, `max_side`, `transparent`). |
Layers are referenced by id (from `get_comp`), `"#n"` (1-based index from the top) or name. Comps are
referenced by id or name, and default to the active comp. Property paths come from `get_layer`, for
referenced by id or name, and default to the active comp. An id may also be sent as a string (`"98"`):
an all-digit string that matches no name is taken as the id. Property paths come from `get_layer`, for
example `transform/position`, `transform/opacity`, `effects/#1/blurriness` or `@57` (by uid). Times
are in seconds. Keyframe times are layer time, which equals comp time unless the layer is offset or
stretched.