mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 17:44:08 +00:00
Variables: art on a locked artboard is not bound or rewritten by a data set
Squashed from #5118 onto main.
This commit is contained in:
@@ -138,13 +138,15 @@ fn names_param(p: &Value, cmd: &str, fallback: Option<String>, nothing: &str) ->
|
||||
}
|
||||
}
|
||||
|
||||
/// Refuse what a variable of `kind` can't be bound to: art that is locked or on a hidden or locked
|
||||
/// layer; a text variable only drives type; an image variable only drives an image object.
|
||||
/// Refuse what a variable of `kind` can't be bound to: art that is locked, on a hidden or locked
|
||||
/// layer, or on a locked artboard; a text variable only drives type; an image variable only drives
|
||||
/// an image object.
|
||||
fn check_bindable(doc: &vectorcraft_doc::Document, ids: &[NodeId], kind: VariableKind, cmd: &str) -> Result<()> {
|
||||
let locks = doc.locked_artboards();
|
||||
for id in ids {
|
||||
match doc.node(*id) {
|
||||
None => return Err(EngineError::NoNode(*id)),
|
||||
Some(_) if !doc.variable_target(*id) => return Err(bad(cmd, not_a_target(doc, *id))),
|
||||
Some(_) if !doc.variable_target_with(&locks, *id) => return Err(bad(cmd, not_a_target(doc, *id))),
|
||||
Some(n) if kind == VariableKind::Text && !matches!(n.kind, NodeKind::Text(_)) => {
|
||||
return Err(bad(cmd, format!("node {} is not type", id.0)));
|
||||
}
|
||||
@@ -161,7 +163,7 @@ fn check_bindable(doc: &vectorcraft_doc::Document, ids: &[NodeId], kind: Variabl
|
||||
}
|
||||
|
||||
/// Why `variable_target` refuses `id`: the object is locked, a group or layer that holds it is
|
||||
/// locked, or one that holds it is hidden, checked in that order.
|
||||
/// locked, one that holds it is hidden, or it is on a locked artboard, checked in that order.
|
||||
fn not_a_target(doc: &vectorcraft_doc::Document, id: NodeId) -> String {
|
||||
let holders: Vec<&vectorcraft_doc::Node> =
|
||||
doc.ancestry(id).unwrap_or_default().into_iter().filter(|i| *i != id).filter_map(|i| doc.node(i)).collect();
|
||||
@@ -169,8 +171,10 @@ fn not_a_target(doc: &vectorcraft_doc::Document, id: NodeId) -> String {
|
||||
format!("node {} is locked", id.0)
|
||||
} else if holders.iter().any(|n| n.locked) {
|
||||
format!("node {} is in a locked group or layer", id.0)
|
||||
} else {
|
||||
} else if holders.iter().any(|n| !n.visible) {
|
||||
format!("node {} is in a hidden group or layer", id.0)
|
||||
} else {
|
||||
format!("node {} is on a locked artboard", id.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,7 +487,7 @@ fn dataset_capture(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 locked
|
||||
/// layer or group, or gone) keeps the row's value. With `all`, every other variable the
|
||||
/// layer or group, on a locked artboard, 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. With every bound object locked, no value is
|
||||
/// written; an update that writes no value adds no undo step.
|
||||
@@ -557,6 +561,20 @@ 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())));
|
||||
}
|
||||
// Which bound objects the row may rewrite, measured once before any of them changes, so
|
||||
// text that grows onto a locked artboard part way through is still rewritten. Each id is
|
||||
// keyed by whether a text value is meant for it, which checks every frame of its story.
|
||||
let targets: BTreeSet<(NodeId, bool)> = {
|
||||
let locks = d.locked_artboards();
|
||||
let mut out = BTreeSet::new();
|
||||
for (id, var) in &pairs {
|
||||
let text = d.variables.variable(var).is_some_and(|v| v.kind == VariableKind::Text);
|
||||
if if text { d.story_target_with(&locks, *id) } else { d.dataset_target_with(&locks, *id) } {
|
||||
out.insert((*id, text));
|
||||
}
|
||||
}
|
||||
out
|
||||
};
|
||||
// Whether an object is bound to a variable that this row has a value for (the reply's `warning`).
|
||||
let bound = pairs.iter().any(|(_, var)| ds.values.contains_key(var));
|
||||
let mut applied = 0usize;
|
||||
@@ -574,12 +592,12 @@ fn apply_dataset(s: &mut Session, name: &str) -> Result<Value> {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// Locked art, art in a locked layer or group, and text whose object is gone, can't be
|
||||
// rewritten: skipped and counted, like the rest. Hidden art is rewritten, in a hidden
|
||||
// layer or group too, and stays hidden unless a visibility value shows it. A text value
|
||||
// rewrites the whole story of threaded text, so it skips when any frame of it is locked.
|
||||
let target = if kind == VariableKind::Text { d.story_target(id) } else { d.dataset_target(id) };
|
||||
if !target {
|
||||
// Locked art, art in a locked layer or group or on a locked artboard, and text whose
|
||||
// object is gone, can't be rewritten: skipped and counted, like the rest. Hidden art
|
||||
// is rewritten, in a hidden layer or group too, and stays hidden unless a visibility
|
||||
// value shows it. A text value rewrites the whole story of threaded text, so it skips
|
||||
// when any frame of it is locked or on a locked artboard.
|
||||
if !targets.contains(&(id, kind == VariableKind::Text)) {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -772,7 +790,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Update Data Set",
|
||||
["Window", "Variables"],
|
||||
None,
|
||||
"{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 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}",
|
||||
"{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 locked layer or group, on a locked artboard, 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
|
||||
),
|
||||
|
||||
@@ -438,6 +438,68 @@ fn locked_art_is_neither_bound_nor_rewritten() {
|
||||
assert!(!s.doc().unwrap().doc.node(r).unwrap().visible, "the unlocked rectangle still applied");
|
||||
}
|
||||
|
||||
/// The art on a locked artboard acts as locked art: a data set skips it, Update Data Set reads
|
||||
/// nothing from it, and it takes no new binding. Unlocking the artboard lets the data set rewrite it.
|
||||
#[test]
|
||||
fn art_on_a_locked_artboard_is_neither_bound_nor_rewritten() {
|
||||
let mut s = session();
|
||||
let t = NodeId(s.execute("text.create", &json!({"x": 2050, "y": 50, "text": "cat"})).unwrap()["id"].as_u64().unwrap());
|
||||
let fresh = NodeId(s.execute("text.create", &json!({"x": 2100, "y": 100, "text": "new"})).unwrap()["id"].as_u64().unwrap());
|
||||
let r = rect(&mut s);
|
||||
s.execute("artboard.new", &json!({"x": 2000, "y": 0, "width": 400, "height": 400})).unwrap();
|
||||
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
|
||||
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
|
||||
s.execute("variable.define", &json!({"name": "Box", "kind": "visibility"})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": "Show", "ids": [t.0]})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": "Box", "ids": [r.0]})).unwrap();
|
||||
s.execute("dataset.capture", &json!({"name": "A"})).unwrap();
|
||||
s.execute("dataset.new", &json!({"name": "B", "values": {"Name": "Bob", "Show": false, "Box": false}})).unwrap();
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
s.execute("artboard.setProps", &json!({"index": 1, "locked": true})).unwrap();
|
||||
|
||||
let e = s.execute("variable.bind", &json!({"variable": "Name", "ids": [fresh.0]})).unwrap_err().to_string();
|
||||
assert!(e.contains(&format!("node {} is on a locked artboard", fresh.0)), "{e}");
|
||||
let v = s.execute("dataset.select", &json!({"name": "B"})).unwrap();
|
||||
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(1), Some(2)), "{v}");
|
||||
assert_eq!(plain(&s, t), "cat", "the text on the locked artboard was left alone");
|
||||
assert!(s.doc().unwrap().doc.node(t).unwrap().visible, "and still shows");
|
||||
assert!(!s.doc().unwrap().doc.node(r).unwrap().visible, "the rectangle off it applied");
|
||||
// Update Data Set keeps the row's values for the art it can't read.
|
||||
let v = s.execute("dataset.update", &json!({})).unwrap();
|
||||
assert_eq!(v["values"], 1, "{v}");
|
||||
let row = s.doc().unwrap().doc.variables.dataset("B").unwrap().values.clone();
|
||||
assert_eq!(row.get("Name"), Some(&vectorcraft_doc::DataValue::Text("Bob".into())));
|
||||
assert_eq!(row.get("Show"), Some(&vectorcraft_doc::DataValue::Visible(false)));
|
||||
|
||||
s.execute("artboard.setProps", &json!({"index": 1, "locked": false})).unwrap();
|
||||
let v = s.execute("dataset.select", &json!({"name": "B"})).unwrap();
|
||||
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(3), Some(0)), "{v}");
|
||||
assert_eq!(plain(&s, t), "Bob");
|
||||
}
|
||||
|
||||
/// A locked artboard elsewhere in the document does not stop threaded type on an unlocked artboard
|
||||
/// from having its whole story rewritten, and type on the locked artboard is still left alone.
|
||||
#[test]
|
||||
fn threaded_type_on_an_unlocked_artboard_is_rewritten_beside_a_locked_artboard() {
|
||||
let (mut s, a, b, story) = threaded(200);
|
||||
let far = NodeId(s.execute("text.create", &json!({"x": 2050, "y": 50, "text": "cat"})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("artboard.new", &json!({"x": 2000, "y": 0, "width": 400, "height": 400})).unwrap();
|
||||
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
|
||||
s.execute("variable.define", &json!({"name": "Tail", "kind": "text"})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": "Name", "ids": [a.0]})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": "Tail", "ids": [far.0]})).unwrap();
|
||||
s.execute("dataset.new", &json!({"name": "B", "values": {"Name": "Bob", "Tail": "dog"}})).unwrap();
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
s.execute("artboard.setProps", &json!({"index": 1, "locked": true})).unwrap();
|
||||
|
||||
let v = s.execute("dataset.select", &json!({"name": "B"})).unwrap();
|
||||
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(1), Some(1)), "{v}");
|
||||
assert_eq!((plain(&s, a), plain(&s, b)), ("Bob".to_string(), String::new()), "the whole story was rewritten");
|
||||
assert_eq!(plain(&s, far), "cat", "the text on the locked artboard was left alone");
|
||||
assert!(!story.is_empty());
|
||||
}
|
||||
|
||||
/// Binding fails for locked art and for art in a hidden or locked group or layer, and the
|
||||
/// error names which of the three applies.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user