Variables: Update Data Set writes only the variables the active data set holds

A data set can hold values for only some variables: `dataset.new` and
`dataset.set` store only the values they are given, and selecting the
data set leaves the other variables alone. Update Data Set replaced the
active data set's values with the values the art reports for every
variable. After one Update, a data set that set only a visibility
variable also held the current text of the bound type, and selecting it
again wrote that text back. Update also dropped a variable's value when
its objects were locked or inside a hidden or locked layer or group,
because such objects report no value.

`dataset.update` now writes the art's current value for each variable
the active data set holds, and keeps the data set's value when the art
reports none. A variable that the data set does not hold stays out of
it, including one defined after the data set was captured;
`dataset.update {all: true}` also adds the other variables that the art
reports a value for. The reply adds `left`, the variables it did not
write. The Variables panel's Update Data Set button and menu item run
it without `all`.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-16 02:26:08 +07:00
committed by bflatastic
parent ff33e0556f
commit 00b02c1bdb
5 changed files with 106 additions and 11 deletions
+3 -2
View File
@@ -190,8 +190,9 @@ impl Document {
/// The values a dataset captured right now: what each bound object holds, in the order the
/// variables are defined. `dataset.capture` writes these into a new row and
/// `dataset.update` into the active one; the Variables panel compares the active row with
/// them to tell whether the art still matches what it says.
/// `dataset.update` into the active one, for the variables that row holds (`all: true` adds
/// the rest); the Variables panel compares the active row with them to tell whether the art
/// still matches what it says.
pub fn captured_values(&self) -> BTreeMap<String, DataValue> {
let mut out = BTreeMap::new();
for v in &self.variables.variables {
+20 -9
View File
@@ -5,7 +5,7 @@
//! dataset, and `dataset.select` swaps the whole document to that row in one undo
//! step. Agents reach every command through `run_command`, like all engine commands.
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use serde_json::{Value, json};
use vectorcraft_doc::{DataSet, DataValue, NodeId, NodeKind, Variable, VariableKind, Variables};
@@ -327,10 +327,14 @@ fn dataset_capture(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"name": name, "values": values.len()}))
}
/// Update Data Set: write what the bound objects hold now into the row that is active. Without
/// an active row there is nothing to update, so say so rather than picking one.
fn dataset_update(s: &mut Session, _p: &Value) -> Result<Value> {
/// Update Data Set: write what the bound objects hold now into the row that is active, for each
/// variable the row holds. A variable whose objects report no value (locked, inside a hidden or
/// locked layer or group, or gone) keeps the row's value. With `all`, every other variable the
/// objects report a value for is added to the row. Without an active row there is nothing to
/// update, so say so rather than picking one.
fn dataset_update(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "dataset.update";
let all = bool_or(p, "all", false);
let values = s.doc()?.doc.captured_values();
let Some(name) = s.doc()?.doc.variables.active_dataset.clone() else {
return Err(bad(C, "no active data set"));
@@ -338,12 +342,19 @@ fn dataset_update(s: &mut Session, _p: &Value) -> Result<Value> {
if values.is_empty() {
return Err(bad(C, "bind something to a variable first"));
}
s.edit("Update Data Set", |d, _| {
let (written, left) = s.edit("Update Data Set", |d, _| {
let ds = d.variables.datasets.iter_mut().find(|d| d.name == name).ok_or_else(|| bad(C, format!("no dataset named `{name}`")))?;
ds.values = values.clone();
Ok(())
let mut written = BTreeSet::new();
for (var, value) in &values {
if all || ds.values.contains_key(var) {
ds.values.insert(var.clone(), value.clone());
written.insert(var.as_str());
}
}
let left: Vec<String> = d.variables.variables.iter().filter(|v| !written.contains(v.name.as_str())).map(|v| v.name.clone()).collect();
Ok((written.len(), left))
})?;
Ok(json!({"name": name, "values": values.len()}))
Ok(json!({"name": name, "values": written, "left": left}))
}
fn dataset_delete(s: &mut Session, p: &Value) -> Result<Value> {
@@ -544,7 +555,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Update Data Set",
["Window", "Variables"],
None,
"{} write what the bound objects hold now into the active dataset",
"{all?: bool} write what the bound objects hold now into the active dataset, for each variable the dataset holds; a variable whose objects report no value (locked, inside a hidden or locked layer or group, or gone) keeps the dataset's value. `all: true` also adds every other variable the objects report a value for; `left` names the variables it didn't write → {values, left}",
has_doc,
dataset_update
),
+76
View File
@@ -344,6 +344,82 @@ fn updating_writes_the_art_into_the_active_row() {
assert_eq!(plain(&s, t), "Caz");
}
/// Update Data Set writes only the variables the active row holds, so a row that sets some of
/// them still leaves the others alone when it is selected again. `all: true` adds the rest.
#[test]
fn updating_writes_only_the_variables_the_row_holds() {
let mut s = session();
let t = text(&mut s, "Alice");
let r = rect(&mut s);
s.execute("variable.define", &json!({"name": "Client", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Client", "ids": [t.0]})).unwrap();
s.execute("variable.define", &json!({"name": "Badge", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Badge", "ids": [r.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "Badge off", "values": {"Badge": false}})).unwrap();
s.execute("dataset.select", &json!({"name": "Badge off"})).unwrap();
s.execute("text.editRange", &json!({"id": t.0, "insert": "Typed by hand"})).unwrap();
s.execute("object.setProps", &json!({"ids": [r.0], "visible": true})).unwrap();
let v = s.execute("dataset.update", &json!({})).unwrap();
assert_eq!((v["values"].clone(), v["left"].clone()), (json!(1), json!(["Client"])), "{v}");
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][0]["values"], json!({"Badge": {"visible": true}}), "{sets}");
// `all: true` adds every variable the art reports a value for.
let v = s.execute("dataset.update", &json!({"all": true})).unwrap();
assert_eq!((v["values"].clone(), v["left"].clone()), (json!(2), json!([])), "{v}");
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][0]["values"]["Client"], json!({"text": "Typed by hand"}), "{sets}");
}
/// A variable whose object is locked reports no value, so Update keeps the row's value for it
/// rather than dropping it from the row.
#[test]
fn updating_keeps_a_value_the_art_does_not_report() {
let mut s = session();
let t = text(&mut s, "First");
let r = rect(&mut s);
s.execute("variable.define", &json!({"name": "Label", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Label", "ids": [t.0]})).unwrap();
s.execute("variable.define", &json!({"name": "Mark", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Mark", "ids": [r.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "Row", "values": {"Label": "Second", "Mark": true}})).unwrap();
s.execute("dataset.select", &json!({"name": "Row"})).unwrap();
s.execute("object.setProps", &json!({"ids": [t.0], "locked": true})).unwrap();
s.execute("object.setProps", &json!({"ids": [r.0], "visible": false})).unwrap();
for params in [json!({}), json!({"all": true})] {
let v = s.execute("dataset.update", &params).unwrap();
assert_eq!((v["values"].clone(), v["left"].clone()), (json!(1), json!(["Label"])), "{params}: {v}");
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][0]["values"], json!({"Label": {"text": "Second"}, "Mark": {"visible": false}}), "{params}: {sets}");
}
}
/// A variable defined after a row was captured stays out of that row on Update, and joins it
/// with `all: true`.
#[test]
fn a_variable_defined_after_a_row_joins_it_only_with_all() {
let mut s = session();
let name = text(&mut s, "First job");
let note = text(&mut s, "First note");
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [name.0]})).unwrap();
s.execute("dataset.capture", &json!({"name": "Job 1"})).unwrap();
s.execute("variable.define", &json!({"name": "Note", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Note", "ids": [note.0]})).unwrap();
s.execute("text.editRange", &json!({"id": name.0, "insert": "Second job"})).unwrap();
let v = s.execute("dataset.update", &json!({})).unwrap();
assert_eq!((v["values"].clone(), v["left"].clone()), (json!(1), json!(["Note"])), "{v}");
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][0]["values"], json!({"Name": {"text": "Second job"}}), "{sets}");
s.execute("dataset.update", &json!({"all": true})).unwrap();
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][0]["values"], json!({"Name": {"text": "Second job"}, "Note": {"text": "First note"}}), "{sets}");
}
/// Renaming carries the variable's bindings and its values in every row with it — a name is
/// how a variable is referred to, so leaving the old one behind would orphan both.
#[test]
+6
View File
@@ -972,6 +972,12 @@ already walks Image Trace. Short agent entry points:
{"name":"run_command","arguments":{"command":"imageTrace.presets","params":{}}}
```
`dataset.update {all?}` writes what the bound objects hold now into the active data set, for each variable that the
data set holds. A variable whose objects report no value, because they are locked, inside a hidden or locked layer
or group, or deleted, keeps the data set's value. With `all: true` it also adds every other variable that the
objects report a value for. The reply gives `values`, the number of values it wrote, and `left`, the variables it
did not write.
## Graphic styles
A graphic style holds an appearance (fills, strokes, effects) plus opacity, blend mode, isolate and knockout.
+1
View File
@@ -396,6 +396,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Architecture:** 19+ crates with enforced layering (`cargo xtask layers`). Every action is a command (~400 engine + ~50 UI). Undo is unlimited via structural sharing. `command.batch` runs several commands as one transaction.
- **Automation:**
- Variables: Update Data Set writes what the art holds into the active data set for the variables that data set holds, and keeps the data set's value for a variable whose objects are locked or inside a hidden or locked layer or group; `dataset.update {all: true}` also adds the other variables that the art reports a value for.
- Actions panel (record/playback, persisted), generic parameter dialogs for every "…" command.
- Variables (data merge): text and visibility variables bound to objects (an object may carry several), dataset rows applied in one undo step (`variable.*`, `dataset.*`) from a Variables panel and Window › Variables, with dialogs to define, bind and edit, Make Text/Visibility Dynamic, Capture/Update Data Set, and a row in italics when an edit leaves the art not matching it; bindings let go of deleted art and locked art is left alone; every command, like the rest, is drivable over MCP and the CLI.
- JSON-lines control channel with real egui pointer and keyboard injection.