Merge pull request #1941 from Messhias/feat/g6-graph-variables

G6: graph variables (CSV data merge)
This commit is contained in:
BFlat
2026-10-16 05:13:07 +07:00
committed by GitHub
25 changed files with 183 additions and 48 deletions
+26
View File
@@ -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::<Vec<_>>().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::<f64>().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::<Vec<_>>().join(","));
out.push('\n');
}
out
}
}
impl Default for GraphSpec {
+12 -2
View File
@@ -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<Self> {
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,
}
}
+16 -23
View File
@@ -127,29 +127,22 @@ fn parse_csv(csv: &str) -> (Vec<String>, Vec<String>, Vec<Vec<Option<f64>>>) {
}
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::<Vec<_>>().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::<f64>().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::<Vec<_>>().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 {
+33 -10
View File
@@ -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,7 @@ fn values_param(vars: &Variables, p: &Value, cmd: &str) -> Result<BTreeMap<Strin
fn define(s: &mut Session, p: &Value) -> Result<Value> {
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 +172,7 @@ fn rename(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
// Errors name the command that ran: `variable.make{stem}Dynamic`.
@@ -479,6 +490,7 @@ fn apply_dataset(s: &mut Session, name: &str) -> Result<Value> {
}),
// 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 {
@@ -585,13 +597,15 @@ fn cell_to_value(kind: VariableKind, cell: &str, var: &str, cmd: &str) -> Result
Ok(Some(match kind {
VariableKind::Text => DataValue::Text(t.to_string()),
VariableKind::Image => DataValue::Image(t.to_string()),
VariableKind::Graph if t.len() <= MAX_GRAPH_CSV => DataValue::Graph(t.to_string()),
VariableKind::Graph => return Err(bad(cmd, format!("value for `{var}` is longer than {MAX_GRAPH_CSV} bytes"))),
VariableKind::Visibility => DataValue::Visible(parse_visible_cell(t, var, cmd)?),
}))
}
fn value_to_cell(v: &DataValue) -> String {
match v {
DataValue::Text(t) | DataValue::Image(t) => t.clone(),
DataValue::Text(t) | DataValue::Image(t) | DataValue::Graph(t) => t.clone(),
DataValue::Visible(b) => if *b { "true" } else { "false" }.to_string(),
}
}
@@ -740,7 +754,7 @@ pub fn specs() -> Vec<CommandSpec> {
"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 +804,21 @@ pub fn specs() -> Vec<CommandSpec> {
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
),
+35
View File
@@ -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());
}
+25 -10
View File
@@ -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<St
(Some(DataValue::Text(t)), _) => 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<Value, String> {
let cmd = d.str("__command");
let name = d.str("name");
+1
View File
@@ -4914,3 +4914,4 @@ mark-style Japanese Japonské
Make Image Dynamic Nastavit obraz jako dynamický
Import Data Sets… Importovat sady dat…
Export Data Sets… Exportovat sady dat…
Make Graph Dynamic Nastavit graf jako dynamický
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4450,3 +4450,4 @@ theme Appearance Erscheinungsbild
Make Image Dynamic Bild dynamisch machen
Import Data Sets… Datensätze importieren…
Export Data Sets… Datensätze exportieren…
Make Graph Dynamic Diagramm dynamisch machen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4856,3 +4856,4 @@ mark-style Japanese Japonesas
Make Image Dynamic Hacer imagen dinámica
Import Data Sets… Importar conjuntos de datos…
Export Data Sets… Exportar conjuntos de datos…
Make Graph Dynamic Hacer gráfico dinámico
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4913,3 +4913,4 @@ theme Appearance Apparence
Make Image Dynamic Rendre l'image dynamique
Import Data Sets… Importer des jeux de données…
Export Data Sets… Exporter des jeux de données…
Make Graph Dynamic Rendre le graphe dynamique
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4881,3 +4881,4 @@ imageTrace Outline Körvonal
Make Image Dynamic Kép dinamikussá tétele
Import Data Sets… Adatkészletek importálása…
Export Data Sets… Adatkészletek exportálása…
Make Graph Dynamic Diagram dinamikussá tétele
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4490,3 +4490,4 @@ lighting Light Luce
Make Image Dynamic Rendi immagine dinamica
Import Data Sets… Importa set di dati…
Export Data Sets… Esporta set di dati…
Make Graph Dynamic Rendi grafico dinamico
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4485,3 +4485,4 @@ lighting Light ライト
Make Image Dynamic 画像を動的にする
Import Data Sets… データセットを読み込み…
Export Data Sets… データセットを書き出し…
Make Graph Dynamic グラフを動的にする
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4454,3 +4454,4 @@ lighting Light 광원
Make Image Dynamic 이미지를 동적으로 만들기
Import Data Sets… 데이터 세트 가져오기…
Export Data Sets… 데이터 세트 내보내기…
Make Graph Dynamic 그래프를 동적으로 만들기
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4758,3 +4758,4 @@ mark-style Japanese Japońskie
Make Image Dynamic Uczyń obraz dynamicznym
Import Data Sets… Importuj zestawy danych…
Export Data Sets… Eksportuj zestawy danych…
Make Graph Dynamic Uczyń wykres dynamicznym
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -4486,3 +4486,4 @@ lighting Light Свет
Make Image Dynamic Сделать изображение динамическим
Import Data Sets… Импортировать наборы данных…
Export Data Sets… Экспортировать наборы данных…
Make Graph Dynamic Сделать диаграмму динамической
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4470,3 +4470,4 @@ lighting Light Світло
Make Image Dynamic Зробити зображення динамічним
Import Data Sets… Імпортувати набори даних…
Export Data Sets… Експортувати набори даних…
Make Graph Dynamic Зробити діаграму динамічною
Can't render this file because it contains an unexpected character in line 718 and column 40.
+1
View File
@@ -4470,3 +4470,4 @@ theme Appearance Giao diện
Make Image Dynamic Tạo hình ảnh động
Import Data Sets… Nhập tập dữ liệu…
Export Data Sets… Xuất tập dữ liệu…
Make Graph Dynamic Tạo biểu đồ động
Can't render this file because it contains an unexpected character in line 3726 and column 10.
+1
View File
@@ -4031,3 +4031,4 @@ lighting Light 光源
Make Image Dynamic 使图像动态化
Import Data Sets… 导入数据集…
Export Data Sets… 导出数据集…
Make Graph Dynamic 使图表动态化
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3818,3 +3818,4 @@ lighting Light 光源
Make Image Dynamic 使影像動態化
Import Data Sets… 匯入資料集…
Export Data Sets… 匯出資料集…
Make Graph Dynamic 使圖表動態化
Can't render this file because it contains an unexpected character in line 6 and column 47.
+13 -3
View File
@@ -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<u64> = st.doc.variables.bindings.keys().map(|i| i.0).collect();
let highlighted = highlighted_row(st);
let highlighted_bound: Vec<u64> = 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();
}
+3
View File
@@ -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
+3
View File
@@ -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),
+2
View File
@@ -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 |