mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 23:05:05 +00:00
A click on a Layers row runs layer.setCurrent, which makes the row's layer current whether it is hidden or locked and keeps the selection. Object › Arrange › Send to Current Layer then moved the selected objects into that layer, or into a sublayer of a hidden or locked layer, and left them selected. In that case drawing, typing and pasting put new art on the topmost layer that is shown and unlocked, and since #1039 they fail with "the target layer is locked or hidden" when no layer is shown and unlocked. Send to Current Layer now fails with that error when the current layer is hidden or locked, or is inside a hidden or locked layer. It makes the check that DocState::target_parent makes for new art (Document::is_editable) before it moves anything or records an undo step. The objects stay where they are and stay selected. The message already has a row in the cs, de, es, fr, it, ja, pt-br, ru and uk catalogs. Docs: docs/target-app-parity.md.
617 lines
30 KiB
Rust
617 lines
30 KiB
Rust
//! The Layers panel's commands: rows at every depth (layers, sublayers, groups, objects) are
|
|
//! clickable, highlighted and targeted; their art is selected through the selection column; rows
|
|
//! move, copy and reparent with validation; and the panel menu's operations (merge, flatten,
|
|
//! release to layers, reverse, template, others/all) are single undo steps.
|
|
|
|
use serde_json::{Value, json};
|
|
use vectorcraft_doc::{LayerColor, Node, NodeId, NodeKind};
|
|
|
|
use super::*;
|
|
|
|
fn session() -> Session {
|
|
let mut s = Session::new();
|
|
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
|
s
|
|
}
|
|
|
|
fn id(v: &Value) -> NodeId {
|
|
NodeId(v["id"].as_u64().unwrap())
|
|
}
|
|
|
|
fn ids(v: &Value) -> Vec<NodeId> {
|
|
v["ids"].as_array().unwrap().iter().map(|i| NodeId(i.as_u64().unwrap())).collect()
|
|
}
|
|
|
|
fn rect(s: &mut Session, x: f64) -> NodeId {
|
|
id(&s.execute("shape.rectangle", &json!({"x": x, "y": 0, "width": 20, "height": 20})).unwrap())
|
|
}
|
|
|
|
fn run(s: &mut Session, cmd: &str, p: Value) -> Value {
|
|
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"))
|
|
}
|
|
|
|
fn node(s: &Session, id: NodeId) -> Node {
|
|
s.doc().unwrap().doc.node(id).unwrap().clone()
|
|
}
|
|
|
|
fn parent(s: &Session, id: NodeId) -> Option<NodeId> {
|
|
s.doc().unwrap().doc.parent_of(id)
|
|
}
|
|
|
|
fn children(s: &Session, id: NodeId) -> Vec<NodeId> {
|
|
node(s, id).children().unwrap().iter().map(|c| c.id).collect()
|
|
}
|
|
|
|
fn undos(s: &Session) -> usize {
|
|
s.doc().unwrap().history.undo.len()
|
|
}
|
|
|
|
/// Layer 1 holding a sublayer `sub` with a rectangle `a` and a group `g` of rectangle `b`, plus a
|
|
/// rectangle `c` on Layer 1 itself → (layer, sub, a, g, b, c). Nothing selected.
|
|
fn tree(s: &mut Session) -> (NodeId, NodeId, NodeId, NodeId, NodeId, NodeId) {
|
|
let layer = s.doc().unwrap().doc.layers[0].id;
|
|
let c = rect(s, 300.0);
|
|
let sub = id(&run(s, "layer.newSublayer", json!({"name": "Sub"})));
|
|
let a = rect(s, 0.0);
|
|
let b = rect(s, 100.0);
|
|
run(s, "select.set", json!({"ids": [b.0]}));
|
|
let g = id(&run(s, "object.group", json!({})));
|
|
run(s, "select.none", json!({}));
|
|
(layer, sub, a, g, b, c)
|
|
}
|
|
|
|
#[test]
|
|
fn art_in_a_sublayer_is_selected_as_objects_not_the_sublayer() {
|
|
let mut s = session();
|
|
let (_, sub, a, g, _, c) = tree(&mut s);
|
|
assert_eq!(parent(&s, a), Some(sub), "new art went into the current sublayer");
|
|
let n = run(&mut s, "select.all", json!({}))["count"].as_u64().unwrap();
|
|
assert_eq!(n, 3);
|
|
let sel = s.doc().unwrap().selection.objects.clone();
|
|
assert!(sel.contains(&a) && sel.contains(&g) && sel.contains(&c) && !sel.contains(&sub), "{sel:?}");
|
|
// The Selection tool's click takes the object in the sublayer.
|
|
let h = vectorcraft_doc::hit::hit_test(&s.doc().unwrap().doc, vectorcraft_geom::Point::new(10.0, 10.0), Default::default()).unwrap();
|
|
assert_eq!(h.top_object(None), a);
|
|
// Deleting it works (layers were skipped by Clear before).
|
|
run(&mut s, "select.set", json!({"ids": [a.0]}));
|
|
run(&mut s, "edit.clear", json!({}));
|
|
assert!(s.doc().unwrap().doc.node(a).is_none());
|
|
}
|
|
|
|
/// #1002: with no layer shown and unlocked, new art is refused (the document unchanged) rather
|
|
/// than put on a hidden or locked layer; another layer that takes art still gets it.
|
|
#[test]
|
|
fn new_art_is_refused_when_no_layer_is_shown_and_unlocked() {
|
|
for prop in ["visible", "locked"] {
|
|
let mut s = session();
|
|
let layer = s.doc().unwrap().doc.layers[0].id;
|
|
let copied = rect(&mut s, 0.0);
|
|
run(&mut s, "select.set", json!({"ids": [copied.0]}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
run(&mut s, "select.none", json!({}));
|
|
run(&mut s, "layer.setProps", json!({"ids": [layer.0], prop: prop == "locked"}));
|
|
let before = s.doc().unwrap().doc.clone();
|
|
for (cmd, p) in [
|
|
("shape.ellipse", json!({"x": 10, "y": 10, "width": 20, "height": 20})),
|
|
("shape.polarGrid", json!({"x": 10, "y": 10, "width": 20, "height": 20})),
|
|
("text.create", json!({"x": 10, "y": 40, "text": "A"})),
|
|
("edit.paste", json!({})),
|
|
] {
|
|
let e = s.execute(cmd, &p).unwrap_err();
|
|
assert_eq!(e.to_string(), "the target layer is locked or hidden", "{prop} {cmd}");
|
|
}
|
|
assert_eq!(s.doc().unwrap().doc.layers, before.layers, "{prop}: nothing was added");
|
|
let other = id(&run(&mut s, "layer.new", json!({})));
|
|
run(&mut s, "layer.setCurrent", json!({"id": layer.0}));
|
|
let n = rect(&mut s, 50.0);
|
|
assert_eq!(parent(&s, n), Some(other), "{prop}");
|
|
}
|
|
}
|
|
|
|
/// Send to Current Layer fails with the error that new art gets (#1002) when the current layer is
|
|
/// hidden or locked, or is inside a hidden or locked layer: the objects stay where they were and stay
|
|
/// selected, and no undo step is recorded. Once the layer is shown and unlocked again, the command
|
|
/// moves them.
|
|
#[test]
|
|
fn send_to_current_layer_refuses_a_hidden_or_locked_current_layer() {
|
|
for prop in ["visible", "locked"] {
|
|
let mut s = session();
|
|
let layer = s.doc().unwrap().doc.layers[0].id;
|
|
let a = rect(&mut s, 0.0);
|
|
let other = id(&run(&mut s, "layer.new", json!({})));
|
|
let sub = id(&run(&mut s, "layer.newSublayer", json!({})));
|
|
assert_eq!(parent(&s, sub), Some(other), "the sublayer is in the new layer");
|
|
run(&mut s, "layer.setProps", json!({"ids": [other.0], prop: prop == "locked"}));
|
|
run(&mut s, "select.set", json!({"ids": [a.0]}));
|
|
for current in [other, sub] {
|
|
// A click on the layer's row makes that layer current and keeps the selection.
|
|
run(&mut s, "layer.setCurrent", json!({"id": current.0}));
|
|
let (before, undo) = (s.doc().unwrap().doc.clone(), undos(&s));
|
|
let e = s.execute("object.arrange.sendToCurrentLayer", &json!({})).unwrap_err();
|
|
assert_eq!(e.to_string(), "the target layer is locked or hidden", "{prop} {current:?}");
|
|
assert_eq!(s.doc().unwrap().doc.layers, before.layers, "{prop} {current:?}: nothing moved");
|
|
assert_eq!(parent(&s, a), Some(layer), "{prop} {current:?}");
|
|
assert_eq!(undos(&s), undo, "{prop} {current:?}: no undo step");
|
|
assert_eq!(s.doc().unwrap().selection.objects, vec![a], "{prop} {current:?}");
|
|
}
|
|
run(&mut s, "layer.setProps", json!({"ids": [other.0], prop: prop != "locked"}));
|
|
run(&mut s, "object.arrange.sendToCurrentLayer", json!({}));
|
|
assert_eq!(parent(&s, a), Some(sub), "{prop}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn clicking_rows_at_any_depth_highlights_them_and_sets_the_current_layer() {
|
|
let mut s = session();
|
|
let (layer, sub, a, g, b, c) = tree(&mut s);
|
|
let before = undos(&s);
|
|
for (row, current) in [(layer, layer), (sub, sub), (g, sub), (b, sub), (a, sub), (c, layer)] {
|
|
let r = run(&mut s, "layer.setCurrent", json!({"id": row.0}));
|
|
assert_eq!(NodeId(r["layer"].as_u64().unwrap()), current);
|
|
let st = s.doc().unwrap();
|
|
assert_eq!((st.highlighted_rows(), st.current_layer()), (vec![row], Some(current)));
|
|
}
|
|
assert_eq!(undos(&s), before, "clicking rows is not an undo step");
|
|
// New art goes into the current sublayer.
|
|
run(&mut s, "layer.setCurrent", json!({"id": a.0}));
|
|
let n = rect(&mut s, 50.0);
|
|
assert_eq!(parent(&s, n), Some(sub));
|
|
assert!(s.execute("layer.setCurrent", &json!({"id": 9999})).is_err());
|
|
// Shift/Ctrl: several rows.
|
|
run(&mut s, "layer.highlight", json!({"ids": [a.0, g.0], "mode": "set"}));
|
|
run(&mut s, "layer.highlight", json!({"ids": [c.0], "mode": "add"}));
|
|
run(&mut s, "layer.highlight", json!({"ids": [a.0], "mode": "toggle"}));
|
|
let st = s.doc().unwrap();
|
|
assert_eq!((st.highlighted_rows(), st.current_layer()), (vec![g, c], Some(layer)));
|
|
assert!(s.execute("layer.highlight", &json!({"ids": [a.0], "mode": "nope"})).is_err());
|
|
assert_eq!(document_rows(&s), vec![g.0, c.0], "document.inspect lists the rows");
|
|
}
|
|
|
|
fn document_rows(s: &Session) -> Vec<u64> {
|
|
crate::inspect::document(s)["layerRows"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn selecting_art_makes_its_layer_current() {
|
|
let mut s = session();
|
|
let (layer, sub, a, _, _, c) = tree(&mut s);
|
|
run(&mut s, "select.set", json!({"ids": [c.0]}));
|
|
assert_eq!(s.doc().unwrap().current_layer(), Some(layer));
|
|
run(&mut s, "select.set", json!({"ids": [a.0]}));
|
|
assert_eq!(s.doc().unwrap().current_layer(), Some(sub));
|
|
let n = rect(&mut s, 200.0);
|
|
assert_eq!(parent(&s, n), Some(sub));
|
|
// A locked sublayer takes no new art: it goes to the top layer.
|
|
run(&mut s, "select.none", json!({}));
|
|
run(&mut s, "layer.setProps", json!({"id": sub.0, "locked": true}));
|
|
run(&mut s, "layer.setCurrent", json!({"id": sub.0}));
|
|
let n = rect(&mut s, 200.0);
|
|
assert_eq!(parent(&s, n), Some(layer));
|
|
}
|
|
|
|
#[test]
|
|
fn the_selection_column_selects_a_rows_art_and_shift_adds_or_removes_it() {
|
|
let mut s = session();
|
|
let (layer, sub, a, g, _, c) = tree(&mut s);
|
|
run(&mut s, "layer.setCurrent", json!({"id": sub.0}));
|
|
let sel = |s: &Session| s.doc().unwrap().selection.objects.clone();
|
|
run(&mut s, "layer.selectAll", json!({"id": layer.0}));
|
|
assert_eq!(sel(&s), vec![c, a, g], "the layer's art, its sublayer's too");
|
|
run(&mut s, "layer.selectAll", json!({"id": sub.0}));
|
|
assert_eq!(sel(&s), vec![a, g]);
|
|
run(&mut s, "layer.selectAll", json!({"id": c.0, "add": true}));
|
|
assert_eq!(sel(&s), vec![a, g, c]);
|
|
run(&mut s, "layer.selectAll", json!({"id": sub.0, "add": true}));
|
|
assert_eq!(sel(&s), vec![c], "all selected already: removed");
|
|
assert_eq!(s.doc().unwrap().highlighted_rows(), vec![sub], "the highlighted row stays");
|
|
// Locked rows select nothing.
|
|
run(&mut s, "layer.setProps", json!({"id": sub.0, "locked": true}));
|
|
run(&mut s, "layer.selectAll", json!({"id": a.0}));
|
|
assert!(sel(&s).is_empty());
|
|
// Targeting a layer selects its sublayers' art too.
|
|
run(&mut s, "layer.setProps", json!({"id": sub.0, "locked": false}));
|
|
assert_eq!(ids_sel(&run(&mut s, "layer.target", json!({"id": layer.0}))), vec![c, a, g]);
|
|
}
|
|
|
|
fn ids_sel(v: &Value) -> Vec<NodeId> {
|
|
v["selected"].as_array().unwrap().iter().map(|i| NodeId(i.as_u64().unwrap())).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn visibility_and_lock_per_row_are_undoable() {
|
|
let mut s = session();
|
|
let (_, sub, a, g, b, _) = tree(&mut s);
|
|
run(&mut s, "select.set", json!({"ids": [a.0, g.0]}));
|
|
let before = undos(&s);
|
|
run(&mut s, "layer.setProps", json!({"ids": [b.0, sub.0], "visible": false}));
|
|
assert!(!node(&s, b).visible && !node(&s, sub).visible);
|
|
assert!(s.doc().unwrap().selection.is_empty(), "hidden art is deselected");
|
|
assert_eq!(undos(&s), before + 1, "one step for several rows");
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert!(node(&s, b).visible && node(&s, sub).visible);
|
|
run(&mut s, "layer.setProps", json!({"id": a.0, "locked": true, "name": "Box"}));
|
|
assert!(node(&s, a).locked);
|
|
assert_eq!(node(&s, a).display_name(), "Box");
|
|
run(&mut s, "layer.setProps", json!({"id": a.0, "name": ""}));
|
|
assert_eq!(node(&s, a).display_name(), "<Rectangle>", "an empty name goes back to the generated one");
|
|
run(&mut s, "layer.setProps", json!({"id": sub.0, "name": ""}));
|
|
assert_eq!(node(&s, sub).display_name(), "Sub", "a layer keeps its name");
|
|
}
|
|
|
|
#[test]
|
|
fn object_set_props_deselects_the_objects_it_hides_or_locks() {
|
|
let mut s = session();
|
|
let (a, b) = (rect(&mut s, 0.0), rect(&mut s, 100.0));
|
|
for (prop, value) in [("visible", false), ("locked", true)] {
|
|
run(&mut s, "select.set", json!({"ids": [a.0, b.0]}));
|
|
run(&mut s, "select.key", json!({"id": b.0}));
|
|
let mut p = json!({"ids": [b.0]});
|
|
p[prop] = json!(value);
|
|
run(&mut s, "object.setProps", p);
|
|
assert!(!s.doc().unwrap().doc.is_editable(b));
|
|
let sel = s.doc().unwrap().selection.clone();
|
|
assert_eq!((sel.objects, sel.key), (vec![a], None), "{prop}: {value} deselects the key object");
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert!(s.doc().unwrap().doc.is_editable(b));
|
|
let sel = s.doc().unwrap().selection.clone();
|
|
assert_eq!((sel.objects, sel.key), (vec![a, b], Some(b)), "undoing {prop}: {value} selects it again as the key");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn object_set_props_deselects_the_objects_inside_a_group_or_layer_it_hides_or_locks() {
|
|
let mut s = session();
|
|
let (_, sub, a, g, b, c) = tree(&mut s);
|
|
run(&mut s, "select.set", json!({"ids": [a.0, b.0, c.0]}));
|
|
run(&mut s, "object.setProps", json!({"ids": [g.0], "locked": true}));
|
|
assert_eq!(s.doc().unwrap().selection.objects, vec![a, c], "b is in the locked group");
|
|
run(&mut s, "select.key", json!({"id": a.0}));
|
|
assert_eq!(s.doc().unwrap().selection.key, Some(a));
|
|
run(&mut s, "object.setProps", json!({"ids": [sub.0], "visible": false}));
|
|
let sel = s.doc().unwrap().selection.clone();
|
|
assert_eq!((sel.objects, sel.key), (vec![c], None), "a, the key object, is in the hidden sublayer");
|
|
}
|
|
|
|
#[test]
|
|
fn object_set_props_that_hides_or_locks_nothing_keeps_the_selection() {
|
|
let mut s = session();
|
|
let layer = s.doc().unwrap().doc.layers[0].id;
|
|
let x = rect(&mut s, 0.0);
|
|
run(&mut s, "select.set", json!({"ids": [x.0]}));
|
|
run(&mut s, "object.lock", json!({}));
|
|
run(&mut s, "layer.setProps", json!({"id": layer.0, "locked": true}));
|
|
run(&mut s, "object.unlockAll", json!({}));
|
|
let st = s.doc().unwrap();
|
|
assert!(st.selection.objects == vec![x] && !st.doc.is_editable(x), "Unlock All selects x, and its layer still locks it");
|
|
for p in [json!({"opacity": 50}), json!({"visible": true, "locked": false})] {
|
|
run(&mut s, "object.setProps", p.clone());
|
|
assert_eq!(s.doc().unwrap().selection.objects, vec![x], "{p} keeps x selected");
|
|
}
|
|
assert_eq!(node(&s, x).opacity, 0.5);
|
|
}
|
|
|
|
#[test]
|
|
fn object_set_props_that_hides_or_locks_deselects_objects_hidden_or_locked_before() {
|
|
let mut s = session();
|
|
let (x, y, z) = (rect(&mut s, 0.0), rect(&mut s, 100.0), rect(&mut s, 200.0));
|
|
run(&mut s, "object.setProps", json!({"ids": [x.0], "visible": false}));
|
|
run(&mut s, "select.set", json!({"ids": [x.0, y.0, z.0]}));
|
|
assert_eq!(s.doc().unwrap().selection.objects, vec![x, y, z], "select.set selects the hidden x");
|
|
run(&mut s, "object.setProps", json!({"ids": [y.0], "locked": true}));
|
|
assert_eq!(s.doc().unwrap().selection.objects, vec![z], "locking y deselects x, hidden before the call, too");
|
|
}
|
|
|
|
#[test]
|
|
fn object_set_props_that_changes_nothing_deselects_what_it_leaves_hidden_or_locked() {
|
|
let mut s = session();
|
|
let (x, y) = (rect(&mut s, 0.0), rect(&mut s, 100.0));
|
|
for (prop, value) in [("visible", false), ("locked", true)] {
|
|
let mut p = json!({"ids": [x.0]});
|
|
p[prop] = json!(value);
|
|
run(&mut s, "object.setProps", p.clone());
|
|
run(&mut s, "select.set", json!({"ids": [x.0, y.0]}));
|
|
run(&mut s, "select.key", json!({"id": x.0}));
|
|
let before = undos(&s);
|
|
run(&mut s, "object.setProps", p);
|
|
assert_eq!(undos(&s), before, "{prop}: {value} again records no undo step");
|
|
let sel = s.doc().unwrap().selection.clone();
|
|
assert_eq!((sel.objects, sel.key), (vec![y], None), "{prop}: {value} again deselects x, the key object");
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert!(s.doc().unwrap().doc.is_editable(x));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn layer_options_validate_and_template_locks_and_dims() {
|
|
let mut s = session();
|
|
let (layer, ..) = tree(&mut s);
|
|
for bad in [json!({"color": 27}), json!({"color": "#zz"}), json!({"color": [1]}), json!({"locked": "yes"}), json!({"dimImages": "half"})] {
|
|
let mut p = bad.clone();
|
|
p["id"] = json!(layer.0);
|
|
assert!(s.execute("layer.setProps", &p).is_err(), "{bad}");
|
|
}
|
|
let color = |s: &Session| match node(s, layer).kind {
|
|
NodeKind::Layer { color, .. } => color,
|
|
_ => panic!("not a layer"),
|
|
};
|
|
run(&mut s, "layer.setProps", json!({"id": layer.0, "color": "red"}));
|
|
assert_eq!(color(&s), LayerColor::Preset(1));
|
|
run(&mut s, "layer.setProps", json!({"id": layer.0, "color": "#123456"}));
|
|
assert_eq!(color(&s), LayerColor::Custom([0x12, 0x34, 0x56]));
|
|
run(&mut s, "layer.setProps", json!({"id": layer.0, "color": 4}));
|
|
assert_eq!(color(&s), LayerColor::Preset(4));
|
|
run(&mut s, "layer.template", json!({"ids": [layer.0]}));
|
|
let n = node(&s, layer);
|
|
assert!(n.is_template() && n.locked);
|
|
assert!(matches!(n.kind, NodeKind::Layer { dim_images: Some(50), .. }));
|
|
run(&mut s, "layer.template", json!({"ids": [layer.0]}));
|
|
let n = node(&s, layer);
|
|
assert!(!n.is_template() && !n.locked);
|
|
assert!(matches!(n.kind, NodeKind::Layer { dim_images: None, .. }));
|
|
run(&mut s, "layer.setProps", json!({"id": layer.0, "preview": false, "dimImages": 30, "printable": false}));
|
|
assert!(matches!(node(&s, layer).kind, NodeKind::Layer { preview: false, dim_images: Some(30), printable: false, .. }));
|
|
let summary = &crate::inspect::document(&s)["layers"][0];
|
|
assert_eq!((summary["preview"].clone(), summary["dimImages"].clone()), (json!(false), json!(30)));
|
|
}
|
|
|
|
#[test]
|
|
fn new_layers_go_above_the_current_one_at_its_level() {
|
|
let mut s = session();
|
|
let (layer, sub, _, g, ..) = tree(&mut s);
|
|
run(&mut s, "layer.setCurrent", json!({"id": sub.0}));
|
|
let n = id(&run(&mut s, "layer.new", json!({})));
|
|
assert_eq!(parent(&s, n), Some(layer), "a sublayer beside the current sublayer");
|
|
assert_eq!(children(&s, layer).iter().position(|c| *c == n), children(&s, layer).iter().position(|c| *c == sub).map(|i| i + 1));
|
|
assert_eq!(s.doc().unwrap().highlighted_rows(), vec![n]);
|
|
let top = id(&run(&mut s, "layer.new", json!({"top": true, "name": "Top", "color": "green", "locked": true})));
|
|
let st = s.doc().unwrap();
|
|
assert_eq!(st.doc.layers.last().unwrap().id, top);
|
|
assert!(st.doc.layers.last().unwrap().locked);
|
|
// Sublayers only go into layers.
|
|
assert!(s.execute("layer.newSublayer", &json!({"parent": g.0})).is_err());
|
|
let inner = id(&run(&mut s, "layer.newSublayer", json!({"parent": sub.0})));
|
|
assert_eq!(parent(&s, inner), Some(sub));
|
|
// Names count sublayers: no two layers share one.
|
|
let mut names = vec![];
|
|
s.doc().unwrap().doc.walk(|n| {
|
|
if n.is_layer() {
|
|
names.push(n.display_name());
|
|
}
|
|
});
|
|
let unique: std::collections::HashSet<&String> = names.iter().collect();
|
|
assert_eq!(unique.len(), names.len(), "{names:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn rows_move_above_below_and_into_layers_and_groups() {
|
|
let mut s = session();
|
|
let (layer, sub, a, g, b, c) = tree(&mut s);
|
|
let before = undos(&s);
|
|
// Into the group: on top of its contents.
|
|
run(&mut s, "layer.move", json!({"ids": [a.0], "target": g.0, "place": "inside"}));
|
|
assert_eq!(children(&s, g), vec![b, a]);
|
|
// Below b, in the group.
|
|
run(&mut s, "layer.move", json!({"ids": [a.0], "target": b.0, "place": "below"}));
|
|
assert_eq!(children(&s, g), vec![a, b]);
|
|
// Above the sublayer, out into Layer 1.
|
|
run(&mut s, "layer.move", json!({"ids": [a.0], "target": sub.0, "place": "above"}));
|
|
assert_eq!(parent(&s, a), Some(layer));
|
|
assert_eq!(undos(&s), before + 3);
|
|
// Out of the layer into a new top-level layer: an object beside a top-level layer goes in it.
|
|
let l2 = id(&run(&mut s, "layer.new", json!({"top": true})));
|
|
run(&mut s, "layer.move", json!({"ids": [c.0], "target": l2.0, "place": "above"}));
|
|
assert_eq!(parent(&s, c), Some(l2));
|
|
// A sublayer becomes a top-level layer, and back.
|
|
run(&mut s, "layer.move", json!({"ids": [sub.0], "target": l2.0, "place": "above"}));
|
|
assert_eq!(parent(&s, sub), None);
|
|
assert_eq!(s.doc().unwrap().doc.layers.last().unwrap().id, sub);
|
|
run(&mut s, "layer.move", json!({"ids": [sub.0], "target": layer.0}));
|
|
assert_eq!(parent(&s, sub), Some(layer));
|
|
// Several rows keep their stacking order.
|
|
run(&mut s, "layer.move", json!({"ids": [g.0, a.0], "target": l2.0}));
|
|
assert_eq!(children(&s, l2), vec![c, a, g]);
|
|
// Undo walks it back.
|
|
for _ in 0..4 {
|
|
run(&mut s, "edit.undo", json!({}));
|
|
}
|
|
assert_eq!(parent(&s, sub), Some(layer));
|
|
}
|
|
|
|
#[test]
|
|
fn moves_are_validated() {
|
|
let mut s = session();
|
|
let (layer, sub, a, g, b, _) = tree(&mut s);
|
|
let doc_before = s.doc().unwrap().doc.clone();
|
|
let bad = [
|
|
json!({"ids": [sub.0], "target": g.0, "place": "inside"}),
|
|
json!({"ids": [layer.0], "target": a.0, "place": "inside"}),
|
|
json!({"ids": [layer.0], "target": sub.0, "place": "above"}),
|
|
json!({"ids": [sub.0], "target": sub.0}),
|
|
json!({"ids": [g.0], "target": b.0, "place": "inside"}),
|
|
json!({"ids": [a.0], "target": b.0, "place": "inside"}),
|
|
json!({"ids": [a.0], "target": 9999}),
|
|
json!({"ids": [9999], "target": a.0}),
|
|
json!({"ids": [a.0], "target": g.0, "place": "sideways"}),
|
|
json!({"ids": [], "target": g.0}),
|
|
json!({"target": g.0}),
|
|
];
|
|
for p in bad {
|
|
assert!(s.execute("layer.move", &p).is_err(), "{p}");
|
|
}
|
|
// A locked group takes nothing.
|
|
run(&mut s, "layer.setProps", json!({"id": g.0, "locked": true}));
|
|
assert!(s.execute("layer.move", &json!({"ids": [a.0], "target": g.0})).is_err());
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert!(std::sync::Arc::ptr_eq(&s.doc().unwrap().doc, &doc_before), "nothing changed");
|
|
// node.move: layers only at the top level or in layers.
|
|
assert!(s.execute("node.move", &json!({"id": sub.0, "parent": g.0, "index": 0})).is_err());
|
|
assert!(s.execute("node.move", &json!({"id": a.0, "index": 0})).is_err());
|
|
assert!(s.execute("node.move", &json!({"id": layer.0, "parent": sub.0, "index": 0})).is_err());
|
|
run(&mut s, "node.move", json!({"id": sub.0, "index": 5}));
|
|
assert_eq!(parent(&s, sub), None);
|
|
}
|
|
|
|
#[test]
|
|
fn alt_drag_copies_rows_and_the_selected_art_moves_between_layers() {
|
|
let mut s = session();
|
|
let (layer, sub, a, g, _, c) = tree(&mut s);
|
|
let count = s.doc().unwrap().doc.node_count();
|
|
let copies = ids(&run(&mut s, "layer.move", json!({"ids": [sub.0], "target": layer.0, "place": "above", "copy": true})));
|
|
assert_eq!(copies.len(), 1);
|
|
assert_eq!(s.doc().unwrap().doc.node_count(), count + 4, "the sublayer and its art (a, the group, b), copied");
|
|
assert_eq!(parent(&s, copies[0]), None, "a copy of the sublayer at the top level");
|
|
// Dragging the selected-art square: the selection moves into another layer.
|
|
run(&mut s, "select.set", json!({"ids": [a.0, g.0]}));
|
|
run(&mut s, "layer.move", json!({"ids": [a.0, g.0], "target": layer.0}));
|
|
assert_eq!(children(&s, layer), vec![c, sub, a, g]);
|
|
assert_eq!(s.doc().unwrap().selection.objects, vec![a, g], "it stays selected");
|
|
}
|
|
|
|
#[test]
|
|
fn delete_and_duplicate_rows() {
|
|
let mut s = session();
|
|
let (layer, sub, a, g, _, c) = tree(&mut s);
|
|
run(&mut s, "layer.highlight", json!({"ids": [a.0, g.0]}));
|
|
let copies = ids(&run(&mut s, "layer.duplicate", json!({})));
|
|
assert_eq!(copies.len(), 2);
|
|
assert_eq!(children(&s, sub).len(), 4);
|
|
assert_eq!(s.doc().unwrap().highlighted_rows(), copies);
|
|
run(&mut s, "layer.duplicate", json!({"id": sub.0}));
|
|
assert!(children(&s, layer).iter().any(|l| node(&s, *l).display_name() == "Sub copy"));
|
|
run(&mut s, "layer.highlight", json!({"ids": copies.iter().map(|c| c.0).collect::<Vec<_>>()}));
|
|
assert_eq!(run(&mut s, "layer.delete", json!({}))["deleted"], 2);
|
|
assert_eq!(children(&s, sub), vec![a, g]);
|
|
run(&mut s, "layer.delete", json!({"ids": [c.0, a.0]}));
|
|
assert!(s.doc().unwrap().doc.node(c).is_none() && s.doc().unwrap().doc.node(a).is_none());
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert!(s.doc().unwrap().doc.node(c).is_some());
|
|
assert!(s.execute("layer.delete", &json!({"ids": [layer.0]})).is_err(), "the last layer stays");
|
|
run(&mut s, "layer.setCurrent", json!({"id": sub.0}));
|
|
run(&mut s, "layer.delete", json!({}));
|
|
assert!(s.doc().unwrap().doc.node(sub).is_none());
|
|
assert_eq!(s.doc().unwrap().current_layer(), Some(layer), "the current layer moves on");
|
|
}
|
|
|
|
#[test]
|
|
fn collect_merge_flatten_and_reverse() {
|
|
let mut s = session();
|
|
let (layer, sub, a, g, _, c) = tree(&mut s);
|
|
// Collect two rows of the sublayer in a new sublayer of it.
|
|
run(&mut s, "layer.highlight", json!({"ids": [a.0, g.0]}));
|
|
let col = id(&run(&mut s, "layer.collectInNew", json!({})));
|
|
assert_eq!((parent(&s, col), children(&s, col)), (Some(sub), vec![a, g]));
|
|
// Reverse their order.
|
|
run(&mut s, "layer.reverse", json!({"ids": [a.0, g.0]}));
|
|
assert_eq!(children(&s, col), vec![g, a]);
|
|
assert!(s.execute("layer.reverse", &json!({"ids": [a.0]})).is_err());
|
|
run(&mut s, "edit.undo", json!({}));
|
|
// Merge the sublayer into Layer 1 (listed last).
|
|
run(&mut s, "layer.merge", json!({"ids": [col.0, layer.0]}));
|
|
assert!(s.doc().unwrap().doc.node(col).is_none());
|
|
assert_eq!(children(&s, layer), vec![c, sub, a, g]);
|
|
assert!(s.execute("layer.merge", &json!({"ids": [layer.0, sub.0]})).is_err(), "not into its own sublayer");
|
|
assert!(s.execute("layer.merge", &json!({"ids": [layer.0]})).is_err());
|
|
// Flatten: other visible layers merge in, hidden ones go, templates stay.
|
|
let l2 = id(&run(&mut s, "layer.new", json!({"top": true})));
|
|
let d = rect(&mut s, 0.0);
|
|
let l3 = id(&run(&mut s, "layer.new", json!({"top": true, "visible": false})));
|
|
let l4 = id(&run(&mut s, "layer.new", json!({"top": true, "template": true})));
|
|
let before = undos(&s);
|
|
let r = run(&mut s, "layer.flatten", json!({"id": layer.0}));
|
|
assert_eq!(r["discarded"], 1);
|
|
let st = s.doc().unwrap();
|
|
assert_eq!(st.doc.layers.iter().map(|l| l.id).collect::<Vec<_>>(), vec![layer, l4]);
|
|
assert!(st.doc.node(l2).is_none() && st.doc.node(l3).is_none());
|
|
assert_eq!(parent(&s, d), Some(layer));
|
|
assert_eq!(undos(&s), before + 1);
|
|
}
|
|
|
|
#[test]
|
|
fn release_to_layers_sequence_and_build() {
|
|
let mut s = session();
|
|
let layer = s.doc().unwrap().doc.layers[0].id;
|
|
let (a, b, c) = (rect(&mut s, 0.0), rect(&mut s, 50.0), rect(&mut s, 100.0));
|
|
let new = ids(&run(&mut s, "layer.releaseToLayers", json!({"id": layer.0})));
|
|
assert_eq!(new.len(), 3);
|
|
assert_eq!(children(&s, layer), new);
|
|
assert_eq!(new.iter().map(|l| children(&s, *l)).collect::<Vec<_>>(), vec![vec![a], vec![b], vec![c]]);
|
|
run(&mut s, "edit.undo", json!({}));
|
|
let new = ids(&run(&mut s, "layer.releaseToLayers", json!({"id": layer.0, "build": true})));
|
|
let counts: Vec<usize> = new.iter().map(|l| children(&s, *l).len()).collect();
|
|
assert_eq!(counts, vec![1, 2, 3], "the layers add up");
|
|
assert_eq!(children(&s, new[2]), vec![a, b, c], "the top one keeps the originals");
|
|
run(&mut s, "edit.undo", json!({}));
|
|
// A group's objects go into sublayers where the group was.
|
|
run(&mut s, "select.set", json!({"ids": [a.0, b.0]}));
|
|
let g = id(&run(&mut s, "object.group", json!({})));
|
|
let new = ids(&run(&mut s, "layer.releaseToLayersBuild", json!({"id": g.0})));
|
|
assert_eq!(new.len(), 2);
|
|
assert!(s.doc().unwrap().doc.node(g).is_none());
|
|
assert_eq!(children(&s, layer), vec![new[0], new[1], c]);
|
|
assert!(s.execute("layer.releaseToLayers", &json!({"id": c.0})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn hide_lock_and_outline_others_and_their_all_counterparts() {
|
|
let mut s = session();
|
|
let l1 = s.doc().unwrap().doc.layers[0].id;
|
|
let l2 = id(&run(&mut s, "layer.new", json!({})));
|
|
let l3 = id(&run(&mut s, "layer.new", json!({})));
|
|
run(&mut s, "layer.setCurrent", json!({"id": l2.0}));
|
|
let flags = |s: &Session, f: fn(&Node) -> bool| [l1, l2, l3].map(|l| f(&node(s, l)));
|
|
assert_eq!(run(&mut s, "layer.hideOthers", json!({}))["count"], 2);
|
|
assert_eq!(flags(&s, |n| n.visible), [false, true, false]);
|
|
run(&mut s, "layer.showAll", json!({}));
|
|
assert_eq!(flags(&s, |n| n.visible), [true; 3]);
|
|
run(&mut s, "layer.lockOthers", json!({}));
|
|
assert_eq!(flags(&s, |n| n.locked), [true, false, true]);
|
|
run(&mut s, "layer.unlockAll", json!({}));
|
|
assert_eq!(flags(&s, |n| n.locked), [false; 3]);
|
|
run(&mut s, "layer.outlineOthers", json!({}));
|
|
let preview = |n: &Node| matches!(n.kind, NodeKind::Layer { preview: true, .. });
|
|
assert_eq!(flags(&s, preview), [false, true, false]);
|
|
run(&mut s, "layer.previewAll", json!({}));
|
|
assert_eq!(flags(&s, preview), [true; 3]);
|
|
assert_eq!(run(&mut s, "layer.previewAll", json!({}))["count"], 0, "nothing to do: no undo step");
|
|
}
|
|
|
|
/// #1158: Alt-click on a hidden (locked) layer's eye (lock) isolates it: it shows (unlocks) as the
|
|
/// others hide (lock), in one undo step.
|
|
#[test]
|
|
fn hide_and_lock_others_show_and_unlock_the_layer_kept() {
|
|
let mut s = session();
|
|
let l1 = s.doc().unwrap().doc.layers[0].id;
|
|
let l2 = id(&run(&mut s, "layer.new", json!({})));
|
|
let flags = |s: &Session, f: fn(&Node) -> bool| [l1, l2].map(|l| f(&node(s, l)));
|
|
run(&mut s, "layer.setProps", json!({"ids": [l2.0], "visible": false, "locked": true}));
|
|
assert_eq!(run(&mut s, "layer.hideOthers", json!({"ids": [l2.0]}))["count"], 2);
|
|
assert_eq!(flags(&s, |n| n.visible), [false, true]);
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert_eq!(flags(&s, |n| n.visible), [true, false], "one undo step");
|
|
run(&mut s, "layer.lockOthers", json!({"ids": [l2.0]}));
|
|
assert_eq!(flags(&s, |n| n.locked), [true, false]);
|
|
}
|
|
|
|
#[test]
|
|
fn locate_object_highlights_its_row() {
|
|
let mut s = session();
|
|
let (layer, sub, _, g, b, _) = tree(&mut s);
|
|
run(&mut s, "select.set", json!({"ids": [g.0]}));
|
|
let r = run(&mut s, "layer.locate", json!({}));
|
|
assert_eq!(r["ancestry"], json!([layer.0, sub.0, g.0]));
|
|
assert_eq!(s.doc().unwrap().highlighted_rows(), vec![g]);
|
|
assert_eq!(run(&mut s, "layer.locate", json!({"id": b.0}))["id"], b.0);
|
|
run(&mut s, "select.none", json!({}));
|
|
assert!(s.execute("layer.locate", &json!({})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn the_clipping_mask_button_acts_on_the_highlighted_group() {
|
|
let mut s = session();
|
|
let (_, _, a, g, _, _) = tree(&mut s);
|
|
run(&mut s, "layer.move", json!({"ids": [a.0], "target": g.0}));
|
|
run(&mut s, "layer.setCurrent", json!({"id": g.0}));
|
|
assert_eq!(run(&mut s, "layer.clippingMask.toggle", json!({}))["clip"], true);
|
|
assert!(node(&s, g).clips());
|
|
}
|