Merge pull request #2242 from dotCooCoo/fix/data-set-sets-objects-in-groups

Variables: selecting a data set sets the objects inside a group that it shows
This commit is contained in:
BFlat
2026-10-16 03:16:35 +07:00
committed by GitHub
4 changed files with 126 additions and 3 deletions
+4 -3
View File
@@ -84,9 +84,10 @@ impl DataSet {
///
/// An object may be driven by several variables — the reference app binds the selected object
/// to the selected variable, one click at a time, and never says a second binding replaces the
/// first — so a binding is a set. `dataset.select` applies them in the order the variables are
/// defined, which is what makes two text variables on one object deterministic: the last one
/// wins, and `applied` counts both.
/// first — so a binding is a set. `dataset.select` applies the text values in the order the
/// variables are defined, which is what makes two text variables on one object deterministic:
/// the last one wins, and `applied` counts both. Of two visibility variables on one object, the
/// last one defined wins too.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Variables {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
+22
View File
@@ -413,6 +413,28 @@ fn apply_dataset(s: &mut Session, name: &str) -> Result<Value> {
for v in &d.variables.variables {
pairs.extend(d.variables.objects_of(&v.name).into_iter().map(|id| (id, v.name.clone())));
}
// The row's values go in three passes: the visibility values that show an object,
// outermost object first; the other values, in definition order; the visibility values
// that hide an object, innermost object first. An object inside a group is then rewritten
// after the row shows the group and before the row hides it. An object with several
// visibility variables goes in the pass of the last one's value, and each pass keeps
// definition order otherwise.
let mut shows = std::collections::HashMap::new();
for (id, var) in &pairs {
let kind = d.variables.variable(var).map(|v| v.kind);
if let (Some(VariableKind::Visibility), Some(DataValue::Visible(show))) = (kind, ds.values.get(var)) {
shows.insert(*id, *show);
}
}
let depths = d.index_paths(shows.keys().copied());
pairs.sort_by_cached_key(|(id, var)| {
let depth = depths.get(id).map_or(0, Vec::len);
match (d.variables.variable(var).map(|v| v.kind), shows.get(id)) {
(Some(VariableKind::Visibility), Some(true)) => (0, depth),
(Some(VariableKind::Visibility), Some(false)) => (2, usize::MAX - depth),
_ => (1, 0),
}
});
let mut applied = 0usize;
let mut skipped = 0usize;
for (id, var) in pairs {
+93
View File
@@ -22,6 +22,13 @@ fn rect(s: &mut Session) -> NodeId {
NodeId(r["id"].as_u64().unwrap())
}
/// A group of the objects `ids` → the group's id.
fn group(s: &mut Session, ids: &[NodeId]) -> NodeId {
s.execute("select.set", &json!({"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()})).unwrap();
let r = s.execute("object.group", &json!({})).unwrap();
NodeId(r["id"].as_u64().unwrap())
}
fn plain(s: &Session, id: NodeId) -> String {
match &s.doc().unwrap().doc.node(id).unwrap().kind {
vectorcraft_doc::NodeKind::Text(t) => t.plain_text(),
@@ -671,6 +678,92 @@ fn a_row_that_hides_selected_objects_deselects_them() {
assert_eq!((sel.objects, sel.key), (vec![r, q], Some(r)), "undo selects r again as the key object");
}
/// A row that shows or hides a group also sets the objects inside it in the same selection,
/// whichever variable was defined first.
#[test]
fn a_row_sets_the_objects_inside_a_group_it_shows_or_hides() {
let mut s = session();
let (inner, other) = (rect(&mut s), rect(&mut s));
let g = group(&mut s, &[inner, other]);
s.execute("variable.define", &json!({"name": "Inner", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Inner", "ids": [inner.0]})).unwrap();
s.execute("variable.define", &json!({"name": "Set", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Set", "ids": [g.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "Hidden", "values": {"Inner": false, "Set": false}})).unwrap();
s.execute("dataset.new", &json!({"name": "Shown", "values": {"Inner": true, "Set": true}})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "Hidden"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(2), Some(0)), "{v}");
let v = s.execute("dataset.select", &json!({"name": "Shown"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(2), Some(0)), "{v}");
let doc = &s.doc().unwrap().doc;
assert!(doc.node(g).unwrap().visible && doc.node(inner).unwrap().visible, "the group and the object inside it show");
let sets = s.execute("dataset.list", &json!({})).unwrap();
let matching: Vec<&Value> = sets["datasets"].as_array().unwrap().iter().filter(|d| d["matches"] == json!(true)).map(|d| &d["name"]).collect();
assert_eq!(matching, [&json!("Shown")], "{sets}");
// With the inner object's variable defined last, a row that hides both still hides it.
s.execute("variable.delete", &json!({"names": ["Inner"]})).unwrap();
s.execute("variable.define", &json!({"name": "Inner", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Inner", "ids": [inner.0]})).unwrap();
s.execute("dataset.set", &json!({"name": "Hidden", "values": {"Inner": false, "Set": false}})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "Hidden"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(2), Some(0)), "{v}");
let doc = &s.doc().unwrap().doc;
assert!(!doc.node(g).unwrap().visible && !doc.node(inner).unwrap().visible, "both are hidden");
}
/// A row writes the text of type inside a group that it shows or hides, whichever variable was
/// defined first.
#[test]
fn a_row_writes_type_inside_a_group_it_shows_or_hides() {
let mut s = session();
let t = text(&mut s, "Alice");
let other = rect(&mut s);
let g = group(&mut s, &[t, other]);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("variable.define", &json!({"name": "Set", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Set", "ids": [g.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "Hidden", "values": {"Name": "Bob", "Set": false}})).unwrap();
s.execute("dataset.new", &json!({"name": "Shown", "values": {"Name": "Carol", "Set": true}})).unwrap();
s.execute("dataset.select", &json!({"name": "Hidden"})).unwrap();
assert_eq!(plain(&s, t), "Bob");
let v = s.execute("dataset.select", &json!({"name": "Shown"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(2), Some(0)), "{v}");
assert_eq!(plain(&s, t), "Carol", "the text is written although the group started hidden");
// With the text variable defined last, a row that hides the group still writes the text.
s.execute("variable.delete", &json!({"names": ["Name"]})).unwrap();
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("dataset.set", &json!({"name": "Hidden", "values": {"Name": "Bob", "Set": false}})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "Hidden"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(2), Some(0)), "{v}");
assert_eq!(plain(&s, t), "Bob", "the text is written before the group is hidden");
assert!(!s.doc().unwrap().doc.node(g).unwrap().visible);
}
/// Of two visibility variables on one object, the one defined later decides whether it shows,
/// as with two text variables.
#[test]
fn the_later_visibility_variable_on_an_object_wins() {
let mut s = session();
let r = rect(&mut s);
for name in ["First", "Second"] {
s.execute("variable.define", &json!({"name": name, "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": name, "ids": [r.0]})).unwrap();
}
s.execute("dataset.new", &json!({"name": "Hide", "values": {"First": true, "Second": false}})).unwrap();
s.execute("dataset.new", &json!({"name": "Show", "values": {"First": false, "Second": true}})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "Hide"})).unwrap();
assert_eq!(v["applied"], json!(2), "{v}");
assert!(!s.doc().unwrap().doc.node(r).unwrap().visible);
s.execute("dataset.select", &json!({"name": "Show"})).unwrap();
assert!(s.doc().unwrap().doc.node(r).unwrap().visible);
}
/// Variables and datasets are separate namespaces: renaming a variable leaves a dataset of
/// the same name, and which dataset is active, alone.
#[test]
+7
View File
@@ -965,6 +965,13 @@ already walks Image Trace. Short agent entry points:
- Image Trace: `imageTrace.presets`, `imageTrace.make {id?, preset?, params?, view?}` (or `imageTrace.makeAndExpand`),
`imageTrace.setView`, `imageTrace.expand`, `imageTrace.release`.
`dataset.select`, `dataset.next` and `dataset.prev` reply `{name, applied, skipped}`. They skip an object that is
locked or inside a hidden or locked layer or group. They apply the data set's visibility values that show objects
first, outermost object first, then its other values in the order the variables are defined, then its visibility
values that hide objects, innermost object first. A data set that shows a group sets the objects inside it in the
same call, and one that hides a visible group sets the objects inside it before it hides the group. Of two
variables of the same kind on one object, the one defined last wins.
```json
{"name":"run_command","arguments":{"command":"symbol.list","params":{}}}
{"name":"run_command","arguments":{"command":"brush.list","params":{}}}