mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 17:32:42 -06:00
Merge pull request #2780 from dotCooCoo/feat/visibility-variables-drive-locked-art
Variables: a visibility variable shows and hides locked art
This commit is contained in:
@@ -138,9 +138,10 @@ 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, 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; a variable of unknown kind drives nothing.
|
||||
/// Refuse what a variable of `kind` can't be bound to: art on a hidden layer or in a hidden group,
|
||||
/// art on a locked artboard, and, unless `kind` is visibility, art that is locked or on a locked
|
||||
/// layer or in a locked group; a text variable only drives type; an image variable only drives an
|
||||
/// image object; a variable of unknown kind drives nothing.
|
||||
fn check_bindable(doc: &vectorcraft_doc::Document, ids: &[NodeId], kind: VariableKind, cmd: &str) -> Result<()> {
|
||||
if kind == VariableKind::Unknown {
|
||||
return Err(bad(cmd, "a variable of unknown kind can't be bound to objects"));
|
||||
@@ -149,7 +150,7 @@ fn check_bindable(doc: &vectorcraft_doc::Document, ids: &[NodeId], kind: Variabl
|
||||
for id in ids {
|
||||
match doc.node(*id) {
|
||||
None => return Err(EngineError::NoNode(*id)),
|
||||
Some(_) if !doc.variable_target_with(&locks, *id) => return Err(bad(cmd, not_a_target(doc, *id))),
|
||||
Some(_) if !doc.variable_target_with(&locks, *id, kind) => return Err(bad(cmd, not_a_target(doc, *id, kind))),
|
||||
Some(n) if kind == VariableKind::Text && !matches!(n.kind, NodeKind::Text(_)) => {
|
||||
return Err(bad(cmd, format!("node {} is not type", id.0)));
|
||||
}
|
||||
@@ -165,14 +166,16 @@ fn check_bindable(doc: &vectorcraft_doc::Document, ids: &[NodeId], kind: Variabl
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Why `variable_target` refuses `id`: the object is locked, a group or layer that holds it is
|
||||
/// 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 {
|
||||
/// Why `variable_target` refuses `id` for a variable of `kind`: the object is locked, a group or
|
||||
/// layer that holds it is locked, one that holds it is hidden, or it is on a locked artboard,
|
||||
/// checked in that order. For a visibility variable, the object and group lock checks are skipped.
|
||||
fn not_a_target(doc: &vectorcraft_doc::Document, id: NodeId, kind: VariableKind) -> 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();
|
||||
if doc.node(id).is_some_and(|n| n.locked) {
|
||||
let locks = kind != VariableKind::Visibility;
|
||||
if locks && doc.node(id).is_some_and(|n| n.locked) {
|
||||
format!("node {} is locked", id.0)
|
||||
} else if holders.iter().any(|n| n.locked) {
|
||||
} else if locks && holders.iter().any(|n| n.locked) {
|
||||
format!("node {} is in a locked group or layer", id.0)
|
||||
} else if holders.iter().any(|n| !n.visible) {
|
||||
format!("node {} is in a hidden group or layer", id.0)
|
||||
@@ -511,8 +514,9 @@ 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, on a locked artboard, or gone) keeps the row's value. With `all`, every other variable the
|
||||
/// variable the row holds. A variable whose objects report no value (gone, or, for a variable
|
||||
/// other than a visibility variable, locked or inside a locked layer or group, or, for any
|
||||
/// variable, on a locked artboard) 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.
|
||||
@@ -588,14 +592,16 @@ fn apply_dataset(s: &mut Session, name: &str) -> Result<Value> {
|
||||
}
|
||||
// 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)> = {
|
||||
// keyed by whether a text value is meant for it, which checks every frame of its story,
|
||||
// and by whether a visibility value is, which ignores locks.
|
||||
let targets: BTreeSet<(NodeId, bool, 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));
|
||||
let kind = d.variables.variable(var).map_or(VariableKind::Unknown, |v| v.kind);
|
||||
let text = kind == VariableKind::Text;
|
||||
if if text { d.story_target_with(&locks, *id) } else { d.dataset_target_with(&locks, *id, kind) } {
|
||||
out.insert((*id, text, kind == VariableKind::Visibility));
|
||||
}
|
||||
}
|
||||
out
|
||||
@@ -617,12 +623,13 @@ fn apply_dataset(s: &mut Session, name: &str) -> Result<Value> {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// 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)) {
|
||||
// Art that is gone, art on a locked artboard, and art that is locked or in a locked
|
||||
// layer or group, is skipped and counted, like the rest, except that a visibility value
|
||||
// shows and hides locked art off a locked artboard. 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, kind == VariableKind::Visibility)) {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -759,7 +766,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Bind Variable",
|
||||
["Window", "Variables"],
|
||||
None,
|
||||
"{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. A type object holds one text variable: binding it to another text variable is refused, and the error names the one it has. Variables of other kinds can share an object, and binding an object to a variable 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, art on a locked artboard is refused, and so are locked art and art in a locked layer or group except by a visibility variable. A type object holds one text variable: binding it to another text variable is refused, and the error names the one it has. Variables of other kinds can share an object, and binding an object to a variable it already has changes nothing → {bound, already}",
|
||||
has_doc,
|
||||
bind
|
||||
),
|
||||
@@ -815,7 +822,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, 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}",
|
||||
"{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 (gone, on a locked artboard, or, for a variable other than a visibility variable, locked or inside a locked layer or group) 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
|
||||
),
|
||||
|
||||
@@ -516,7 +516,7 @@ fn unbind_reports_what_it_actually_removed() {
|
||||
assert!(v["variables"].get("bindings").is_none_or(|v| v == &json!({})), "{v}");
|
||||
}
|
||||
|
||||
/// Locked art is the document's to rewrite only while it isn't locked.
|
||||
/// Locked type takes no new text variable binding, and a row leaves its text alone.
|
||||
#[test]
|
||||
fn locked_art_is_neither_bound_nor_rewritten() {
|
||||
let mut s = session();
|
||||
@@ -634,6 +634,90 @@ fn binding_errors_say_whether_a_layer_or_group_is_hidden_or_locked() {
|
||||
assert!(e.contains(&format!("node {} is locked", t.0)), "{e}");
|
||||
}
|
||||
|
||||
/// A visibility variable binds a locked layer and a locked object inside a locked group, and a row
|
||||
/// hides and shows them in one undo step without unlocking them. List, Capture and Update Data Set
|
||||
/// read whether they show.
|
||||
#[test]
|
||||
fn a_visibility_variable_shows_and_hides_locked_art() {
|
||||
let mut s = session();
|
||||
let badge = rect(&mut s);
|
||||
let other = rect(&mut s);
|
||||
let g = group(&mut s, &[badge, other]);
|
||||
let layer = NodeId(s.execute("layer.new", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
rect(&mut s);
|
||||
s.execute("object.setProps", &json!({"ids": [badge.0], "locked": true})).unwrap();
|
||||
s.execute("object.setProps", &json!({"ids": [g.0], "locked": true})).unwrap();
|
||||
s.execute("layer.setProps", &json!({"id": layer.0, "locked": true})).unwrap();
|
||||
s.execute("variable.define", &json!({"name": "Layer", "kind": "visibility"})).unwrap();
|
||||
s.execute("variable.define", &json!({"name": "Badge", "kind": "visibility"})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": "Layer", "ids": [layer.0]})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": "Badge", "ids": [badge.0]})).unwrap();
|
||||
s.execute("dataset.new", &json!({"name": "Shown", "values": {"Layer": true, "Badge": true}})).unwrap();
|
||||
s.execute("dataset.new", &json!({"name": "Hidden", "values": {"Layer": false, "Badge": false}})).unwrap();
|
||||
let shows = |s: &Session, id: NodeId| s.doc().unwrap().doc.node(id).unwrap().visible;
|
||||
let locked = |s: &Session| [layer, g, badge].iter().all(|id| s.doc().unwrap().doc.node(*id).unwrap().locked);
|
||||
|
||||
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!(!shows(&s, layer) && !shows(&s, badge), "the row hides the locked layer and the locked object");
|
||||
assert!(locked(&s), "the row leaves the locks as they were");
|
||||
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!("Hidden")], "List reads the locked art: {sets}");
|
||||
assert_eq!(s.execute("dataset.capture", &json!({"name": "Captured"})).unwrap()["values"], 2);
|
||||
let sets = s.execute("dataset.list", &json!({})).unwrap();
|
||||
assert_eq!(sets["datasets"][2]["values"], json!({"Badge": {"visible": false}, "Layer": {"visible": false}}), "{sets}");
|
||||
|
||||
// Undo puts back what the row hid.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(shows(&s, layer) && shows(&s, badge), "undo shows them again");
|
||||
|
||||
let v = s.execute("dataset.select", &json!({"name": "Hidden"})).unwrap();
|
||||
assert_eq!(v["applied"], 2, "{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}");
|
||||
assert!(shows(&s, layer) && shows(&s, badge), "the row shows them");
|
||||
assert!(locked(&s));
|
||||
|
||||
// Update writes the locked layer's visibility into the active row.
|
||||
s.execute("layer.setProps", &json!({"id": layer.0, "visible": false})).unwrap();
|
||||
let v = s.execute("dataset.update", &json!({})).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"], json!({"Badge": {"visible": true}, "Layer": {"visible": false}}), "{sets}");
|
||||
}
|
||||
|
||||
/// Text and graph variables refuse locked art and art in a locked layer. A visibility variable is
|
||||
/// refused for art on a hidden layer that is also locked, and the error says the art is in a hidden
|
||||
/// group or layer.
|
||||
#[test]
|
||||
fn locked_art_refuses_text_and_graph_variables_and_a_hidden_layer_refuses_visibility() {
|
||||
let mut s = session();
|
||||
let layer = NodeId(s.execute("layer.new", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
let t = text(&mut s, "x");
|
||||
let r = rect(&mut s);
|
||||
s.execute("object.setProps", &json!({"ids": [r.0], "locked": true})).unwrap();
|
||||
s.execute("layer.setProps", &json!({"id": layer.0, "locked": true})).unwrap();
|
||||
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
|
||||
s.execute("variable.define", &json!({"name": "Chart", "kind": "graph"})).unwrap();
|
||||
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
|
||||
let bind = |s: &mut Session, var: &str, id: NodeId| s.execute("variable.bind", &json!({"variable": var, "ids": [id.0]}));
|
||||
|
||||
let e = bind(&mut s, "Name", t).unwrap_err().to_string();
|
||||
assert!(e.contains(&format!("node {} is in a locked group or layer", t.0)), "{e}");
|
||||
let e = bind(&mut s, "Chart", r).unwrap_err().to_string();
|
||||
assert!(e.contains(&format!("node {} is locked", r.0)), "{e}");
|
||||
bind(&mut s, "Show", t).unwrap();
|
||||
assert!(s.doc().unwrap().doc.variables.objects_of("Name").is_empty());
|
||||
|
||||
s.execute("layer.setProps", &json!({"id": layer.0, "visible": false})).unwrap();
|
||||
let e = bind(&mut s, "Show", r).unwrap_err().to_string();
|
||||
assert!(e.contains(&format!("node {} is in a hidden group or layer", r.0)), "{e}");
|
||||
// The hidden layer's own visibility is what the variable drives.
|
||||
bind(&mut s, "Show", layer).unwrap();
|
||||
}
|
||||
|
||||
/// Make Text Dynamic / Make Visibility Dynamic: what the panel's buttons run. One step from a
|
||||
/// selection to a bound variable, named after what it drives.
|
||||
#[test]
|
||||
@@ -848,7 +932,7 @@ fn updating_writes_only_the_variables_the_row_holds() {
|
||||
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
|
||||
/// A text 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() {
|
||||
@@ -1201,8 +1285,8 @@ fn a_hidden_object_comes_back_and_is_captured_hidden() {
|
||||
}
|
||||
|
||||
/// A row selected while a layer is hidden rewrites the type on it and applies its other values,
|
||||
/// and leaves the layer hidden: showing the layer shows that row's text. A layer that is locked
|
||||
/// as well as hidden is still left alone.
|
||||
/// and leaves the layer hidden: showing the layer shows that row's text. On a layer that is locked
|
||||
/// as well as hidden, the type keeps its text and the row's visibility value applies.
|
||||
#[test]
|
||||
fn a_row_rewrites_type_on_a_hidden_layer_but_not_on_a_locked_one() {
|
||||
let mut s = session();
|
||||
@@ -1230,12 +1314,12 @@ fn a_row_rewrites_type_on_a_hidden_layer_but_not_on_a_locked_one() {
|
||||
s.execute("layer.setProps", &json!({"id": layer.0, "visible": true})).unwrap();
|
||||
assert_eq!(plain(&s, t), "Bob", "showing the layer shows the row's text");
|
||||
|
||||
// Locked and hidden: the row skips both objects and the type keeps its text.
|
||||
// Locked and hidden: the row skips the type, which keeps its text, and shows the rectangle.
|
||||
s.execute("layer.setProps", &json!({"id": layer.0, "visible": false, "locked": true})).unwrap();
|
||||
let v = s.execute("dataset.select", &json!({"name": "A"})).unwrap();
|
||||
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(0), Some(2)), "{v}");
|
||||
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(1), Some(1)), "{v}");
|
||||
assert_eq!(plain(&s, t), "Bob", "the type on the locked layer keeps its text");
|
||||
assert!(!s.doc().unwrap().doc.node(r).unwrap().visible);
|
||||
assert!(s.doc().unwrap().doc.node(r).unwrap().visible, "the row's visibility value applies on the locked layer");
|
||||
}
|
||||
|
||||
/// Type inside a hidden group is rewritten too, and List and Capture Data Set read it.
|
||||
|
||||
Reference in New Issue
Block a user