From dc390aba37f15a15e4a76741a7c8f48ef9bd9b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabio=20William=20Concei=C3=A7=C3=A3o?= Date: Wed, 14 Oct 2026 12:55:36 +0100 Subject: [PATCH] G6: graph variables (CSV data merge) VariableKind::Graph binds a graph root; dataset rows supply CSV as graph.setData. Make Graph Dynamic, Variables dialog, capture/apply, and pt-br for the new strings. Co-authored-by: Cursor --- crates/doc/src/graph.rs | 26 ++++++++++++++++ crates/doc/src/variables.rs | 14 +++++++-- crates/engine/src/cmd/graph.rs | 39 ++++++++++-------------- crates/engine/src/cmd/variablecmds.rs | 40 +++++++++++++++++++------ crates/engine/src/tests_variables.rs | 35 ++++++++++++++++++++++ crates/ui-egui/src/dialogs/variables.rs | 35 +++++++++++++++------- crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/panels/variables.rs | 16 ++++++++-- docs/gaps.md | 3 ++ docs/mcp.md | 3 ++ docs/target-app-parity.md | 2 ++ 11 files changed, 167 insertions(+), 47 deletions(-) diff --git a/crates/doc/src/graph.rs b/crates/doc/src/graph.rs index 5dc018401..37329ec62 100644 --- a/crates/doc/src/graph.rs +++ b/crates/doc/src/graph.rs @@ -422,6 +422,32 @@ impl GraphSpec { cells.iter().enumerate().flat_map(|(c, r)| r.iter().enumerate().filter(|(_, v)| v.is_none()).map(move |(s, _)| [c, s])).collect(); self.rows = cells.into_iter().map(|r| r.into_iter().map(|v| v.unwrap_or(0.0)).collect()).collect(); } + + /// CSV of this graph's data (header row with series names, then category + values), as Graph + /// Data and `graph.setData` use. Always writes a header so a blank first category isn't + /// misread as one. + pub fn to_csv(&self) -> String { + let cells = self.cells(); + let width = cells.iter().map(Vec::len).max().unwrap_or(0).max(self.series.len()); + let mut out = String::from(","); + out.push_str(&(0..width).map(|s| self.series.get(s).cloned().unwrap_or_default()).collect::>().join(",")); + out.push('\n'); + let labelled = !self.categories.is_empty(); + for (i, r) in cells.iter().enumerate() { + if labelled { + let c = self.categories.get(i).map(String::as_str).unwrap_or_default(); + if c.trim().is_empty() || c.trim().parse::().is_ok() || c != c.trim() { + out.push_str(&format!("\"{c}\"")); + } else { + out.push_str(c); + } + out.push(','); + } + out.push_str(&r.iter().map(|v| v.map(|v| v.to_string()).unwrap_or_default()).collect::>().join(",")); + out.push('\n'); + } + out + } } impl Default for GraphSpec { diff --git a/crates/doc/src/variables.rs b/crates/doc/src/variables.rs index 3a5f3d908..1147268cd 100644 --- a/crates/doc/src/variables.rs +++ b/crates/doc/src/variables.rs @@ -19,16 +19,19 @@ pub enum VariableKind { Visibility, /// A linked image's file path (the pixels swap; the object's bounds stay). Image, + /// A graph object's data (CSV of categories and series, as Graph Data uses). + Graph, } impl VariableKind { /// Parse the `kind` of `variable.define`: `text`, `visibility` (`visible` is accepted - /// as the same thing, as the panel and the docs call it both), or `image`. + /// as the same thing, as the panel and the docs call it both), `image` or `graph`. pub fn parse(s: &str) -> Option { match s.to_ascii_lowercase().as_str() { "text" => Some(Self::Text), "visibility" | "visible" => Some(Self::Visibility), "image" => Some(Self::Image), + "graph" => Some(Self::Graph), _ => None, } } @@ -39,6 +42,7 @@ impl VariableKind { Self::Text => "text", Self::Visibility => "visibility", Self::Image => "image", + Self::Graph => "graph", } } } @@ -58,6 +62,8 @@ pub enum DataValue { Visible(bool), /// Absolute path of the image file an image variable should show. Image(String), + /// CSV of a graph's categories and series (same shape as Graph Data / `graph.setData`). + Graph(String), } impl DataValue { @@ -66,7 +72,10 @@ impl DataValue { pub fn fits(&self, kind: VariableKind) -> bool { matches!( (self, kind), - (Self::Text(_), VariableKind::Text) | (Self::Visible(_), VariableKind::Visibility) | (Self::Image(_), VariableKind::Image) + (Self::Text(_), VariableKind::Text) + | (Self::Visible(_), VariableKind::Visibility) + | (Self::Image(_), VariableKind::Image) + | (Self::Graph(_), VariableKind::Graph) ) } } @@ -196,6 +205,7 @@ impl Document { (VariableKind::Text, NodeKind::Text(t)) => Some(DataValue::Text(t.plain_text())), (VariableKind::Visibility, _) => Some(DataValue::Visible(node.visible)), (VariableKind::Image, NodeKind::Image(im)) => im.link.as_ref().map(|l| DataValue::Image(l.path.clone())), + (VariableKind::Graph, _) => node.extras().graph.as_ref().map(|g| DataValue::Graph(g.to_csv())), (VariableKind::Text | VariableKind::Image, _) => None, } } diff --git a/crates/engine/src/cmd/graph.rs b/crates/engine/src/cmd/graph.rs index 94c950807..4e8086393 100644 --- a/crates/engine/src/cmd/graph.rs +++ b/crates/engine/src/cmd/graph.rs @@ -127,29 +127,22 @@ fn parse_csv(csv: &str) -> (Vec, Vec, Vec>>) { } fn to_csv(g: &GraphSpec) -> String { - // Always a header row (empty first cell), so a first data row starting with a blank isn't read as one. - let cells = g.cells(); - let width = cells.iter().map(Vec::len).max().unwrap_or(0).max(g.series.len()); - let mut out = String::from(","); - out.push_str(&(0..width).map(|s| g.series.get(s).cloned().unwrap_or_default()).collect::>().join(",")); - out.push('\n'); - let labelled = !g.categories.is_empty(); - for (i, r) in cells.iter().enumerate() { - if labelled { - // An empty label, one that reads as a number or one with spaces around it goes in quotes, so it reads back - // as the same label. - let c = g.categories.get(i).map(String::as_str).unwrap_or_default(); - if c.trim().is_empty() || c.trim().parse::().is_ok() || c != c.trim() { - out.push_str(&format!("\"{c}\"")); - } else { - out.push_str(c); - } - out.push(','); - } - out.push_str(&r.iter().map(|v| v.map(|v| v.to_string()).unwrap_or_default()).collect::>().join(",")); - out.push('\n'); - } - out + g.to_csv() +} + +/// Apply CSV graph data to the graph root `id` and rebuild its art (Variables data merge). +pub(crate) fn apply_csv(d: &mut Document, id: NodeId, csv: &str) -> Result<()> { + let Some(node) = d.node(id) else { + return Err(EngineError::NoNode(id)); + }; + let Some(mut spec) = node.extras().graph.as_deref().cloned() else { + return Err(bad("graph.applyCsv", format!("node {} is not a graph", id.0))); + }; + let (series, categories, rows) = parse_csv(csv); + spec.series = series; + spec.categories = categories; + spec.set_cells(rows); + regenerate(d, id, spec) } fn apply_data(g: &mut GraphSpec, p: &Value) -> bool { diff --git a/crates/engine/src/cmd/variablecmds.rs b/crates/engine/src/cmd/variablecmds.rs index df578ed79..31cfff228 100644 --- a/crates/engine/src/cmd/variablecmds.rs +++ b/crates/engine/src/cmd/variablecmds.rs @@ -1,11 +1,12 @@ //! Variables (data merge): named values bound to objects, applied per dataset. //! //! One template document produces many variants: bind a type object to a `text` -//! variable, anything to a `visibility` variable, and a linked image to an `image` -//! variable, put a row of values in each dataset, and `dataset.select` swaps the -//! whole document to that row in one undo step. `dataset.import` / `dataset.export` -//! move those rows as CSV (header: `name` then variable names). Agents reach every -//! command through `run_command`, like all engine commands. +//! variable, anything to a `visibility` variable, a linked image to an `image` +//! variable, or a graph root to a `graph` variable (CSV data), put a row of values in +//! each dataset, and `dataset.select` swaps the whole document to that row in one undo +//! step. `dataset.import` / `dataset.export` move those rows as CSV (header: `name` +//! then variable names). Agents reach every command through `run_command`, like all +//! engine commands. use std::collections::{BTreeMap, BTreeSet}; @@ -19,6 +20,8 @@ use super::*; /// The longest variable or dataset name, in characters (names arrive from MCP and files). const MAX_NAME: usize = 255; +/// Cap on a graph variable's CSV (MCP and files are untrusted). +const MAX_GRAPH_CSV: usize = 256 * 1024; /// Caps on a datasets CSV (paths and MCP payloads are untrusted). const MAX_CSV_BYTES: usize = 1_048_576; @@ -71,6 +74,9 @@ fn check_bindable(doc: &vectorcraft_doc::Document, ids: &[NodeId], kind: Variabl Some(n) if kind == VariableKind::Image && !matches!(n.kind, NodeKind::Image(_)) => { return Err(bad(cmd, format!("node {} is not an image", id.0))); } + Some(n) if kind == VariableKind::Graph && n.extras().graph.is_none() => { + return Err(bad(cmd, format!("node {} is not a graph", id.0))); + } Some(_) => {} } } @@ -97,6 +103,11 @@ fn dataset_value(cmd: &str, var: &str, kind: VariableKind, v: &Value) -> Result< VariableKind::Visibility => v.as_bool().map(DataValue::Visible), // An image value is a path string; empty is not a file to open. VariableKind::Image => v.as_str().map(str::trim).filter(|t| !t.is_empty()).map(|t| DataValue::Image(t.to_string())), + VariableKind::Graph => v + .as_str() + .map(str::trim) + .filter(|t| !t.is_empty()) + .and_then(|t| if t.len() > MAX_GRAPH_CSV { None } else { Some(DataValue::Graph(t.to_string())) }), } .ok_or_else(|| bad(cmd, format!("value for `{var}` must match its variable kind"))) } @@ -118,7 +129,8 @@ fn values_param(vars: &Variables, p: &Value, cmd: &str) -> Result Result { const C: &str = "variable.define"; let name = var_name(p, C)?; - let kind = str_param(p, "kind").and_then(VariableKind::parse).ok_or_else(|| bad(C, "`kind` must be `text`, `visibility` or `image`"))?; + let kind = + str_param(p, "kind").and_then(VariableKind::parse).ok_or_else(|| bad(C, "`kind` must be `text`, `visibility`, `image` or `graph`"))?; if s.doc()?.doc.variables.variable(&name).is_some() { return Err(bad(C, format!("variable `{name}` is already defined"))); } @@ -161,7 +173,7 @@ fn rename(s: &mut Session, p: &Value) -> Result { Ok(json!({"from": from, "name": to})) } -/// Make Text / Visibility / Image Dynamic: define a variable of that kind and bind the +/// Make Text / Visibility / Image / Graph Dynamic: define a variable of that kind and bind the /// selection to it in one step, naming it after what it drives — the panel's buttons. fn make_dynamic(s: &mut Session, kind: VariableKind, stem: &str) -> Result { // Errors name the command that ran: `variable.make{stem}Dynamic`. @@ -479,6 +491,7 @@ fn apply_dataset(s: &mut Session, name: &str) -> Result { }), // Swap the linked file; keep the object's bounds. A missing path skips. (VariableKind::Image, DataValue::Image(path)) => links::set_image_from_path(d, id, path).is_ok(), + (VariableKind::Graph, DataValue::Graph(csv)) => super::graph::apply_csv(d, id, csv).is_ok(), _ => false, }; if done { @@ -740,7 +753,7 @@ pub fn specs() -> Vec { "New Variable…", ["Window", "Variables"], None, - "{name, kind: text|visibility|image} define a variable", + "{name, kind: text|visibility|image|graph} define a variable (graph: CSV data on a graph root)", has_doc, define ), @@ -790,12 +803,21 @@ pub fn specs() -> Vec { has_selection, |s, _| make_dynamic(s, VariableKind::Image, "Image") ), + cmd!( + "variable.makeGraphDynamic", + "Make Graph Dynamic", + ["Window", "Variables"], + None, + "{} define a graph variable over the selected graph root(s), named after it and bound to it; each dataset row supplies CSV as graph.setData", + has_selection, + |s, _| make_dynamic(s, VariableKind::Graph, "Graph") + ), cmd!( "variable.bind", "Bind Variable", ["Window", "Variables"], None, - "{variable, ids?} bind objects (default: the selection) to a variable; text needs type, image needs an image object, locked art is refused. An object may hold several variables, and binding it to one it already has changes nothing → {bound, already}", + "{variable, ids?} bind objects (default: the selection) to a variable; text needs type, image needs an image object, graph needs a graph root, locked art is refused. An object may hold several variables, and binding it to one it already has changes nothing → {bound, already}", has_doc, bind ), diff --git a/crates/engine/src/tests_variables.rs b/crates/engine/src/tests_variables.rs index 90e48b3f5..05b3cbc19 100644 --- a/crates/engine/src/tests_variables.rs +++ b/crates/engine/src/tests_variables.rs @@ -947,3 +947,38 @@ fn datasets_csv_round_trips_and_rejects_bad_input() { // One undo undoes the last import's replacements. s.execute("edit.undo", &json!({})).unwrap(); } + +#[test] +fn graph_variable_applies_csv_and_captures() { + let mut s = session(); + let id = s + .execute( + "graph.create", + &json!({ + "type": "column", + "x": 10, + "y": 10, + "width": 120, + "height": 80, + "csv": ",Sales\nA,1\nB,2" + }), + ) + .unwrap()["id"] + .as_u64() + .unwrap(); + s.execute("select.set", &json!({"ids": [id]})).unwrap(); + s.execute("variable.makeGraphDynamic", &json!({})).unwrap(); + let name = s.execute("variable.list", &json!({})).unwrap()["variables"][0]["name"].as_str().unwrap().to_string(); + assert_eq!(s.execute("variable.list", &json!({})).unwrap()["variables"][0]["kind"], "graph"); + s.execute("dataset.new", &json!({"name": "Alt", "values": {name.clone(): ",Sales\nA,10\nB,20"}})).unwrap(); + let v = s.execute("dataset.select", &json!({"name": "Alt"})).unwrap(); + assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(1), Some(0))); + let csv = s.doc().unwrap().doc.variable_value(NodeId(id), vectorcraft_doc::VariableKind::Graph).unwrap(); + match csv { + vectorcraft_doc::DataValue::Graph(c) => assert!(c.contains("10") && c.contains("20"), "{c}"), + other => panic!("expected graph csv, got {other:?}"), + } + // Binding a non-graph is refused. + let r = rect(&mut s); + assert!(s.execute("variable.bind", &json!({"variable": name, "ids": [r.0]})).is_err()); +} diff --git a/crates/ui-egui/src/dialogs/variables.rs b/crates/ui-egui/src/dialogs/variables.rs index 918bad157..ee30d81a8 100644 --- a/crates/ui-egui/src/dialogs/variables.rs +++ b/crates/ui-egui/src/dialogs/variables.rs @@ -3,7 +3,7 @@ //! //! One dialog covers all of them: what it shows is what the command takes. A variable is a //! name and a kind; a dataset is a name and one field per variable — a text field for a -//! `text` or `image` variable (path for image), a checkbox for a `visibility` one. +//! `text`, `image` (a path) or `graph` (CSV) variable, a checkbox for a `visibility` one. //! `dataset.set` opens with the row's current values, so editing a row is editing what it //! already says. @@ -20,11 +20,11 @@ use crate::widgets; pub const KIND: &str = "variables"; /// The kinds a variable can have, as `variable.define` names them. -const KINDS: [&str; 3] = ["text", "visibility", "image"]; +const KINDS: [&str; 4] = ["text", "visibility", "image", "graph"]; /// What the dialog shows for each of [`KINDS`]. -fn kind_labels() -> [&'static str; 3] { - [tl!("Text"), tl!("Visibility"), tl!("Image")] +fn kind_labels() -> [&'static str; 4] { + [tl!("Text"), tl!("Visibility"), tl!("Image"), tl!("Graph")] } pub(super) const SPEC: DialogSpec = @@ -125,9 +125,11 @@ fn value_fields(vars: &Variables, current: Option<&std::collections::BTreeMap json!(t), (Some(DataValue::Visible(b)), _) => json!(b), (Some(DataValue::Image(p)), _) => json!(p), - // Without a value in the row a text/image field starts empty (no value in this row) - // and a visibility one starts shown: its false is a value, so it is never "unset". - (None, VariableKind::Text | VariableKind::Image) => json!(""), + (Some(DataValue::Graph(c)), _) => json!(c), + // Without a value in the row a text, image or graph field starts empty (no value in + // this row) and a visibility one starts shown: its false is a value, so it is never + // "unset". + (None, VariableKind::Text | VariableKind::Image | VariableKind::Graph) => json!(""), (None, VariableKind::Visibility) => json!(true), }; out.insert(value_key(&v.name), value); @@ -207,6 +209,9 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { VariableKind::Text | VariableKind::Image => { text(ui, d, &key, 220.0); } + VariableKind::Graph => { + text_area(ui, d, &key, 220.0); + } VariableKind::Visibility => checkbox(ui, d, &key), } ui.end_row(); @@ -250,14 +255,15 @@ fn dropdown(ui: &mut egui::Ui, d: &mut Dialog, key: &str, options: &[&str], widt } } -/// The `values` object the dialog's fields mean: a text or image variable whose field is empty -/// carries no value in this row, and a visibility checkbox is always a value — its false, too. +/// The `values` object the dialog's fields mean: a text, image or graph variable whose field is +/// empty carries no value in this row, and a visibility checkbox is always a value — its false, +/// too. fn values_of(d: &Dialog, vars: &Variables) -> Value { let mut out = serde_json::Map::new(); for v in &vars.variables { let key = value_key(&v.name); match v.kind { - VariableKind::Text | VariableKind::Image => { + VariableKind::Text | VariableKind::Image | VariableKind::Graph => { let text = d.str(&key); if !text.is_empty() { out.insert(v.name.clone(), json!(text)); @@ -271,6 +277,15 @@ fn values_of(d: &Dialog, vars: &Variables) -> Value { Value::Object(out) } +/// A multiline field for graph CSV. +fn text_area(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f32) { + let mut s = d.str(key); + let r = ui.add(egui::TextEdit::multiline(&mut s).desired_width(width).desired_rows(4)); + if r.changed() { + d.fields.insert(key.into(), json!(s)); + } +} + fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { let cmd = d.str("__command"); let name = d.str("name"); diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 125a9f836..f33beaf0c 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3802,6 +3802,7 @@ axis Both Ambos Softness Suavidade Ambient Light Luz ambiente Light behind the object Luz atrás do objeto + Make Graph Dynamic Tornar gráfico dinâmico Registration: Registro: # Dimension tool (linked dimension annotations) Dimension Tool Ferramenta Cota diff --git a/crates/ui-egui/src/panels/variables.rs b/crates/ui-egui/src/panels/variables.rs index 84990d9a5..48d0c24ba 100644 --- a/crates/ui-egui/src/panels/variables.rs +++ b/crates/ui-egui/src/panels/variables.rs @@ -74,6 +74,8 @@ struct Picked { single_type: bool, /// Make Image Dynamic needs exactly one image object. single_image: bool, + /// Make Graph Dynamic needs a selection of graph roots only. + selection_graph: bool, } fn picked(st: &vectorcraft_engine::DocState) -> Picked { @@ -81,7 +83,8 @@ fn picked(st: &vectorcraft_engine::DocState) -> Picked { let selection_bound = selected.iter().any(|id| st.doc.variables.bindings.contains_key(&NodeId(*id))); let single_type = matches!(selected.as_slice(), [one] if st.doc.node(NodeId(*one)).is_some_and(|n| matches!(n.kind, NodeKind::Text(_)))); let single_image = matches!(selected.as_slice(), [one] if st.doc.node(NodeId(*one)).is_some_and(|n| matches!(n.kind, NodeKind::Image(_)))); - Picked { has_selection: !selected.is_empty(), selected, selection_bound, single_type, single_image } + let selection_graph = !selected.is_empty() && selected.iter().all(|id| st.doc.node(NodeId(*id)).is_some_and(|n| n.extras().graph.is_some())); + Picked { has_selection: !selected.is_empty(), selected, selection_bound, single_type, single_image, selection_graph } } /// Delete Variable: the highlighted variable only (the command's default); the rest stay. @@ -131,7 +134,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }) .collect(); let active = sets.iter().find(|s| s.active); - let Picked { selected, has_selection, selection_bound, single_type, single_image } = picked(st); + let Picked { selected, has_selection, selection_bound, single_type, single_image, selection_graph } = picked(st); let highlighted = highlighted_row(st); // The current data set across the top: its name, and the arrows that step through them. @@ -167,6 +170,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { VariableKind::Text => "type", VariableKind::Visibility => "eye", VariableKind::Image => "image", + VariableKind::Graph => "chart-column", }; ui.add(egui::Image::new(crate::icons::source(icon)).fit_to_exact_size(egui::vec2(14.0, 14.0))); // The object a variable drives is named beside it, as the reference panel does. @@ -212,6 +216,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { if widgets::icon_button_enabled(ui, "image", tl!("Make Image Dynamic"), false, single_image, 24.0).clicked() { app.run("variable.makeImageDynamic", json!({})).ok(); } + if widgets::icon_button_enabled(ui, "chart-column", tl!("Make Graph Dynamic"), false, selection_graph, 24.0).clicked() { + app.run("variable.makeGraphDynamic", json!({})).ok(); + } if widgets::icon_button(ui, "plus", tl!("New Variable…"), false, 24.0).clicked() { crate::menus::invoke(app, "variable.define", json!({})); } @@ -246,7 +253,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let bound: Vec = st.doc.variables.bindings.keys().map(|i| i.0).collect(); let highlighted = highlighted_row(st); let highlighted_bound: Vec = highlighted.as_ref().map(|h| bound_objects(st, h)).unwrap_or_default(); - let Picked { selected, has_selection, selection_bound, single_type, single_image } = picked(st); + let Picked { selected, has_selection, selection_bound, single_type, single_image, selection_graph } = picked(st); if widgets::menu_item(ui, tl!("New Variable…"), true, false) { crate::menus::invoke(app, "variable.define", json!({})); @@ -265,6 +272,9 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { if widgets::menu_item(ui, tl!("Make Image Dynamic"), single_image, false) { app.run("variable.makeImageDynamic", json!({})).ok(); } + if widgets::menu_item(ui, tl!("Make Graph Dynamic"), selection_graph, false) { + app.run("variable.makeGraphDynamic", json!({})).ok(); + } if widgets::menu_item(ui, tl!("Unbind Variable"), selection_bound, false) { app.run("variable.unbind", json!({"ids": selected})).ok(); } diff --git a/docs/gaps.md b/docs/gaps.md index 87f1007bb..d5dcb2e96 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -265,6 +265,9 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall ## G6 Automation and scripting +- **Missing:** a scripting surface over the command registry (Illustrator: JavaScript), Batch, image variable kind and dataset import/export (graph variables land separately). Actions, Variables (text, + visibility, graph) and full agent control over MCP are done. +- **Estimate:** 8–14 h. - **Missing:** a scripting surface over the command registry (Illustrator: JavaScript), Batch, graph variable kinds. Actions, Variables (text, visibility and image), dataset CSV import/export and full agent control over kinds, dataset import/export. Actions, Variables (text, visibility and image) and full agent control over diff --git a/docs/mcp.md b/docs/mcp.md index a98423c52..83f431f1d 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -986,6 +986,9 @@ already walks Image Trace. Short agent entry points: `symbol.replace`, `symbol.edit`, `symbol.duplicate`, `symbol.selectInstances {name}`. - Brushes: `brush.list`, `brush.get {name}`, `brush.apply {name, ids?}`, `brush.new {type, name?, params?, ids?}`, `brush.setCurrent {name}`, `brush.options {name, params?}`, `brush.freehand`, `object.expandBrush {ids?}`. +- Variables: `variable.list`, `variable.define {name, kind: text|visibility|graph}`, `variable.bind {variable, ids?}`, + `variable.makeTextDynamic`, `variable.makeVisibilityDynamic`, `variable.makeGraphDynamic`, `variable.unbind`, + `dataset.new` / `dataset.set` / `dataset.select` (graph values are CSV as `graph.setData`). - Variables: `variable.list`, `variable.define {name, kind: text|visibility|image}`, `variable.bind {variable, ids?}`, `variable.makeTextDynamic`, `variable.makeVisibilityDynamic`, `variable.makeImageDynamic`, `variable.unbind`, `dataset.new` / `dataset.set` / `dataset.select` (image values are file path strings; apply keeps object bounds), diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 773e4be6c..e7abe1783 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -89,6 +89,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | Guides, grids, smart guides, snapping, rulers | 3 | 80% | 30.0's tangent and perpendicular snapping and its snapping panel, "intersect" smart guides for a moved selection's anchors (drawn points and dragged anchors snap to intersections, #1658), global/artboard ruler mode switch and video rulers, distance readouts while drawing | 4–7 | estimated (lowered from 85%: target moved); new ruler-guide Alt orientation implemented | | Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | anti-aliasing ghost borders (#983); TIFF choices and settings are implemented | 2–4 | estimated | | Print, colour management, separations, flattener | 3 | 80% | print preview fidelity | 2–4 | estimated | +| Automation (Actions, Variables, scripting, batch) | 3 | 68% | a scripting surface (Illustrator: JavaScript/ExtendScript); Batch, image variables and dataset CSV open as PRs; graph variables done | 5–10 | estimated | | Automation (Actions, Variables, scripting, batch) | 3 | 65% | a scripting surface (Illustrator: JavaScript/ExtendScript), Batch, graph variables | 4–8 | estimated | | Automation (Actions, Variables, scripting, batch) | 3 | 62% | a scripting surface (Illustrator: JavaScript/ExtendScript), Batch, graph variables, dataset import/export | 6–12 | estimated | | Automation (Actions, Variables, scripting, batch) | 3 | 60% | a scripting surface (Illustrator: JavaScript/ExtendScript), Batch, image and graph variables, dataset import/export | 8–14 | estimated; MCP `add_text` exposes existing vertical type; score and hours unchanged | @@ -337,6 +338,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale) | File formats | 6 | 85% | Affinity export, and the rest of Affinity import (effects, adjustments, brushes, master pages, live symbol instances), DWG (no open spec: DXF instead), PSD placement as layers (Photoshop Import Options; the merged image is done), EPS checked against more real files users send (Inkscape files open as vectors; other generators' constructs are tested with programs written from the PostScript Language Reference only; CID-keyed CJK type and `CCITTFaxDecode` images not read), legacy `.ai` files saved without the prolog that defines their operators (their own operator set isn't read yet), the rest of Illustrator's editing data (appearances and live effects, symbols, pattern fills, brushes, placed files: #637), the remaining fidelity polish; EPS, DXF, EMF/WMF, TIFF, BMP, Targa, PSD export, SVGZ, PDF security, PDF/X, PDF layers, presets and Affinity import are done | 6–10 (existing scope; Affinity export unestimated) | | Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | polish only (all four are done) | 2–4 | | Print, colour management, separations, flattener | 3 | 80% | print preview fidelity (flattener presets in PDF and print, overprint in PDF and composite print, Print dialog, presets, PostScript, marks, separations, Print Tiling, print overprint/flattener/bitmap options and printer profile are done) | 1–2 | +| Automation | 3 | 70% | scripting surface, batch, image variables and dataset CSV (open as PRs); Variables: text, visibility and graph (CSV) bound to objects, dataset rows applied in one undo step | 4–8 | | Automation | 3 | 72% | scripting surface, batch (Variables: text/visibility/image variables bound to objects, an object may carry several, dataset rows applied in one undo step, CSV import/export, panel and Window › Variables with dialogs; graph kinds open) | 4–7 | | Automation | 3 | 68% | scripting surface, batch (Variables: text/visibility/image variables bound to objects, an object may carry several, dataset rows applied in one undo step, panel and Window › Variables with dialogs; graph kinds and dataset import/export open) | 5–9 | | Automation | 3 | 65% | scripting surface, batch (MCP drops malformed app replies without replay; Variables: text/visibility variables bound to objects, an object may carry several, dataset rows applied in one undo step, panel and Window › Variables with dialogs; image/graph kinds and dataset import/export open) | 6–10 |