Part of #785: run and command_batch steps can use earlier results

A run (vectorcraft-cli run, the MCP command_batch tool) had no way to use
an id an earlier step returned. A parameter string that is exactly
$N.path or $last.path now takes that value from step N's result
(0-based), e.g. {"id":"$1.id"}; $$ starts a literal $, and a
reference to nothing fails the step. Objects given to a command are now
used as given: an ids element or id that isn't an object id (or an
unresolved reference) fails instead of the command acting on the
selection. text.setRangeStyle styles the selected type object when no
id is given, as text.setStyle does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-10 10:28:03 +07:00
co-authored by Claude Opus 5.5
parent c19873e220
commit b767e850ed
16 changed files with 264 additions and 13 deletions
+15 -5
View File
@@ -366,13 +366,23 @@ pub fn color_value(v: &Value) -> Option<Color> {
/// Objects a command targets: explicit `ids` param or the selection.
pub(crate) fn targets(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
if let Some(ids) = ids_param(p, "ids") {
return Ok(ids);
// Objects given are used as given: a value that isn't an object id fails rather than the
// command acting on the selection instead (#785).
let object_id = |v: &Value| v.as_u64().map(NodeId).ok_or_else(|| EngineError::Other(format!("object ids are non-negative integers, not {v}")));
match p.get("ids").filter(|v| !v.is_null()) {
Some(Value::Array(a)) => return a.iter().map(object_id).collect(),
Some(v) => return Err(EngineError::Other(format!("`ids` must be an array of object ids, not {v}"))),
None => {}
}
if let Some(id) = id_param(p, "id") {
return Ok(vec![id]);
match p.get("id").filter(|v| !v.is_null()) {
Some(v @ Value::Number(_)) => Ok(vec![object_id(v)?]),
// A step reference a batch couldn't resolve (other strings are other ids, such as an
// effect's).
Some(Value::String(r)) if r.starts_with('$') => {
Err(EngineError::Other(format!("`id` is the step reference {r:?}, which only `run` and `command_batch` resolve")))
}
_ => Ok(s.doc()?.selection.objects.clone()),
}
Ok(s.doc()?.selection.objects.clone())
}
pub(crate) fn ok() -> Result<Value> {
+9 -2
View File
@@ -27,7 +27,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Character",
[],
None,
"{id, start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …], charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\"} style a character range (runs are split at the range ends) → {id, runs}",
"{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …], charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\"} style a character range (runs are split at the range ends) → {id, runs}",
has_doc,
set_range_style
),
@@ -420,7 +420,14 @@ pub(crate) fn protect_missing_glyphs(before: &[TextRun], runs: &mut Vec<TextRun>
fn set_range_style(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "text.setRangeStyle";
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing `id`"))?;
// The type object given, else the one selected (#785).
let id = match p.get("id").filter(|v| !v.is_null()) {
Some(v) => v.as_u64().map(NodeId).ok_or_else(|| bad(C, format!("`id` must be an object id, not {v}")))?,
None => match super::typecmd::text_targets(s, &json!({}), C)?.as_slice() {
[one] => *one,
_ => return Err(bad(C, "missing `id`: give one, or select one type object")),
},
};
let change = CharChange::parse(p, C, &s.doc()?.doc.setup)?;
if change.is_empty() {
return Err(bad(C, "nothing to change"));