mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 18:27:02 -06:00
Merge pull request #2226 from dotCooCoo/feat/update-data-set-keeps-own-values
Variables: Update Data Set writes only the variables the active data set holds
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -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", ¶ms).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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user