//! 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 { 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 { s.doc().unwrap().doc.parent_of(id) } fn children(s: &Session, id: NodeId) -> Vec { 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 { 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 { 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(), "", "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"); } /// Preferences › Units › Identify Objects By › XML ID (#394): `layer.setProps` takes a valid XML /// id and stores the object name; Object Name (the default) keeps free text. The Layers label is /// the XML id when that preference is on. #[test] fn identify_objects_by_xml_id_names_layers() { let mut s = session(); let a = rect(&mut s, 0.0); run(&mut s, "layer.setProps", json!({"id": a.0, "name": "My Rect #2"})); assert_eq!(node(&s, a).display_name(), "My Rect #2"); assert_eq!(node(&s, a).identify_label(true), "My_Rect__x23_2"); run(&mut s, "prefs.set", json!({"key": "identifyObjectsBy", "value": "xmlId"})); assert!(s.execute("layer.setProps", &json!({"id": a.0, "name": "My Rect"})).is_err(), "spaces are refused"); run(&mut s, "layer.setProps", json!({"id": a.0, "name": "My_Rect__x23_2"})); assert_eq!(node(&s, a).display_name(), "My Rect #2", "the XML id is stored as the object name"); run(&mut s, "prefs.set", json!({"key": "identifyObjectsBy", "value": "objectName"})); run(&mut s, "layer.setProps", json!({"id": a.0, "name": "My Rect"})); assert_eq!(node(&s, a).display_name(), "My Rect"); } #[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); } /// `node.move` into a hidden or locked layer deselects the object it moves, here the key object. /// Undo puts the object back and selects it again as the key object. #[test] fn node_move_into_a_hidden_or_locked_layer_deselects_the_object() { for prop in ["visible", "locked"] { let mut s = session(); let layer = s.doc().unwrap().doc.layers[0].id; let (a, b) = (rect(&mut s, 0.0), rect(&mut s, 100.0)); let other = id(&run(&mut s, "layer.new", json!({}))); run(&mut s, "layer.setProps", json!({"ids": [other.0], prop: prop == "locked"})); run(&mut s, "select.set", json!({"ids": [a.0, b.0]})); run(&mut s, "select.key", json!({"id": b.0})); run(&mut s, "node.move", json!({"id": b.0, "parent": other.0, "index": 0})); assert_eq!(parent(&s, b), Some(other), "{prop}"); let sel = s.doc().unwrap().selection.clone(); assert_eq!((sel.objects, sel.key), (vec![a], None), "{prop}: b, the key object, is deselected"); run(&mut s, "edit.undo", json!({})); assert_eq!(parent(&s, b), Some(layer), "{prop}"); let sel = s.doc().unwrap().selection.clone(); assert_eq!((sel.objects, sel.key), (vec![a, b], Some(b)), "{prop}: undo selects b again as the key object"); } } #[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"); } /// The selected art in rows dragged into a hidden layer or group is deselected, the key object /// included. Undo puts the rows back and selects the art again. #[test] fn rows_dragged_into_a_hidden_layer_or_group_deselect_their_art() { let mut s = session(); let (layer, sub, a, g, b, c) = tree(&mut s); let hidden = id(&run(&mut s, "layer.new", json!({"visible": false}))); run(&mut s, "select.set", json!({"ids": [a.0, b.0, c.0]})); run(&mut s, "select.key", json!({"id": a.0})); // The sublayer, holding a and the group of b, goes into the hidden layer. run(&mut s, "layer.move", json!({"ids": [sub.0], "target": hidden.0})); assert_eq!(parent(&s, sub), Some(hidden)); let sel = s.doc().unwrap().selection.clone(); assert_eq!((sel.objects, sel.key), (vec![c], None), "a, the key object, and b are hidden"); run(&mut s, "edit.undo", json!({})); assert_eq!(parent(&s, sub), Some(layer)); let sel = s.doc().unwrap().selection.clone(); assert_eq!((sel.objects, sel.key), (vec![a, b, c], Some(a)), "undo selects them again"); // c goes into the group once the group is hidden. run(&mut s, "object.setProps", json!({"ids": [g.0], "visible": false})); assert_eq!(s.doc().unwrap().selection.objects, vec![a, c], "b is in the hidden group"); run(&mut s, "layer.move", json!({"ids": [c.0], "target": g.0, "place": "inside"})); assert_eq!(parent(&s, c), Some(g)); let sel = s.doc().unwrap().selection.clone(); assert_eq!((sel.objects, sel.key), (vec![a], None), "c is hidden, and a single selected object has no key object"); } #[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::>()})); 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]); // A layer and its sublayer: the sublayer merges into its layer, never the other way round. run(&mut s, "layer.merge", json!({"ids": [layer.0, sub.0]})); assert!(s.doc().unwrap().doc.node(sub).is_none()); assert_eq!(children(&s, layer), vec![c, a, g]); run(&mut s, "edit.undo", json!({})); 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![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); } /// Merge Selected with every row highlighted (a layer, its sublayer and another layer, in panel /// order) merges the layers into the last top-level one instead of refusing a sublayer target (#2383). #[test] fn merge_selected_with_all_layer_rows_highlighted_merges_into_the_last_top_level_layer() { for reversed in [false, true] { let mut s = session(); let (layer, sub, a, _, _, c) = tree(&mut s); let l2 = id(&run(&mut s, "layer.new", json!({"top": true}))); let d = rect(&mut s, 500.0); // Panel order, top first: Layer 2, Layer 1, its sublayer. let mut rows = vec![l2.0, layer.0, sub.0]; if reversed { rows.reverse(); } run(&mut s, "layer.highlight", json!({"ids": rows})); let into = id(&run(&mut s, "layer.merge", json!({}))); let st = s.doc().unwrap(); assert_eq!(st.doc.layers.len(), 1, "reversed: {reversed}"); assert_eq!(into, st.doc.layers[0].id); for n in [a, c, d] { assert!(st.doc.layer_of(n) == Some(into), "{n:?} reversed: {reversed}"); } assert!(st.doc.node(sub).is_some() || st.doc.node(layer).is_some() || st.doc.node(l2).is_some()); } } /// Collect in New Layer puts the new layer beside the topmost row when that row is directly in a /// layer, and on top of the layer holding its group when it is in a group. When that row is in a /// hidden or locked layer, the selected art it collects from another layer is deselected, the key /// object included, and undo selects it again. #[test] fn collect_in_new_layer_inside_a_hidden_or_locked_layer_deselects_the_art_it_moves() { for prop in ["visible", "locked"] { let mut s = session(); let (x, z) = (rect(&mut s, 0.0), rect(&mut s, 100.0)); let top = id(&run(&mut s, "layer.new", json!({}))); let y = rect(&mut s, 200.0); run(&mut s, "layer.setProps", json!({"ids": [top.0], prop: prop == "locked"})); run(&mut s, "select.set", json!({"ids": [x.0, z.0]})); run(&mut s, "select.key", json!({"id": x.0})); let col = id(&run(&mut s, "layer.collectInNew", json!({"ids": [x.0, y.0]}))); assert_eq!((parent(&s, col), children(&s, col)), (Some(top), vec![x, y]), "{prop}"); let sel = s.doc().unwrap().selection.clone(); assert_eq!((sel.objects, sel.key), (vec![z], None), "{prop}: x, the key object, is deselected"); run(&mut s, "edit.undo", json!({})); let sel = s.doc().unwrap().selection.clone(); assert_eq!((sel.objects, sel.key), (vec![x, z], Some(x)), "{prop}: undo selects x again as the key object"); } } /// Merge Selected into a hidden or locked layer deselects the selected art it moves there, the key /// object included, and undo selects it again. #[test] fn merge_selected_into_a_hidden_or_locked_layer_deselects_the_art_it_moves() { for prop in ["visible", "locked"] { let mut s = session(); let layer = s.doc().unwrap().doc.layers[0].id; let (x, z) = (rect(&mut s, 0.0), rect(&mut s, 100.0)); let into = id(&run(&mut s, "layer.new", json!({}))); run(&mut s, "layer.setProps", json!({"ids": [into.0], prop: prop == "locked"})); run(&mut s, "select.set", json!({"ids": [x.0, z.0]})); run(&mut s, "select.key", json!({"id": x.0})); run(&mut s, "layer.merge", json!({"ids": [x.0, into.0]})); assert_eq!((parent(&s, x), parent(&s, z)), (Some(into), Some(layer)), "{prop}"); let sel = s.doc().unwrap().selection.clone(); assert_eq!((sel.objects, sel.key), (vec![z], None), "{prop}: x, the key object, is deselected"); run(&mut s, "edit.undo", json!({})); let sel = s.doc().unwrap().selection.clone(); assert_eq!((sel.objects, sel.key), (vec![x, z], Some(x)), "{prop}: undo selects x again as the key object"); } } /// Flatten Artwork into a locked layer deselects the selected art it moves there, the key object /// included, and undo selects it again. #[test] fn flatten_artwork_into_a_locked_layer_deselects_the_art_it_moves() { let mut s = session(); let layer = s.doc().unwrap().doc.layers[0].id; let (x, z) = (rect(&mut s, 0.0), rect(&mut s, 100.0)); let into = id(&run(&mut s, "layer.new", json!({"locked": true}))); run(&mut s, "select.set", json!({"ids": [x.0, z.0]})); run(&mut s, "select.key", json!({"id": x.0})); run(&mut s, "layer.flatten", json!({"id": into.0})); assert_eq!((parent(&s, x), parent(&s, z)), (Some(into), Some(into))); assert!(s.doc().unwrap().doc.node(layer).is_none(), "Layer 1 was merged into the locked layer"); let sel = s.doc().unwrap().selection.clone(); assert_eq!((sel.objects, sel.key), (vec![], None), "x and z are locked"); run(&mut s, "edit.undo", json!({})); let sel = s.doc().unwrap().selection.clone(); assert_eq!((sel.objects, sel.key), (vec![x, z], Some(x)), "undo selects them again with the key object"); } #[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![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 = 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()); } /// #785: explicit malformed layer targets must never become the current-layer default. #[test] fn layer_delete_rejects_invalid_explicit_targets() { let mut unexpected = vec![]; for params in [json!({"id": "1"}), json!({"ids": [1, "2"]}), json!({"id": "$1.id"})] { let mut s = session(); run(&mut s, "layer.new", json!({})); run(&mut s, "layer.new", json!({})); let before = layer_target_snapshot(&s); let result = s.execute("layer.delete", ¶ms); if result.is_ok() { let names: Vec<_> = s.doc().unwrap().doc.layers.iter().map(|l| l.display_name()).collect(); unexpected.push(format!("{params}: {result:?}; remaining layers {names:?}")); } else { assert_eq!(layer_target_snapshot(&s), before, "{params}"); } } assert!(unexpected.is_empty(), "malformed requests succeeded: {}", unexpected.join("; ")); } fn layer_target_snapshot(s: &Session) -> Value { let st = s.doc().unwrap(); let history = |entries: &[HistoryEntry]| entries.iter().map(|e| json!({"label": e.label, "doc": e.doc, "selection": e.selection})).collect::>(); json!({"doc": st.doc, "selection": st.selection, "revision": st.revision, "activeLayer": st.active_layer, "rows": st.layer_rows, "undo": history(&st.history.undo), "redo": history(&st.history.redo)}) } fn layer_target_fixture() -> (Session, Vec) { let mut s = session(); let first = s.doc().unwrap().doc.layers[0].id; rect(&mut s, 0.0); let second = id(&run(&mut s, "layer.new", json!({}))); rect(&mut s, 40.0); let third = id(&run(&mut s, "layer.new", json!({}))); rect(&mut s, 80.0); run(&mut s, "layer.highlight", json!({"ids": [first.0, second.0]})); (s, vec![first, second, third]) } fn malformed_layer_targets(first: NodeId, second: NodeId) -> Vec { let mut params = vec![]; for bad in [json!("1"), json!("$1.id"), json!(1.0), json!(1.5), json!(-1), Value::Null, json!(true), json!({}), json!([])] { params.push(json!({"id": bad})); params.push(json!({"ids": [first.0, bad]})); } for bad in [Value::Null, json!("1"), json!(1), json!(false), json!({})] { params.push(json!({"ids": bad, "id": first.0})); } for missing in [0, u64::MAX] { params.push(json!({"id": missing})); params.push(json!({"ids": [first.0, second.0, missing]})); } params } fn assert_layer_target_error(s: &mut Session, command: &str, params: Value) { let before = layer_target_snapshot(s); let error = s.execute(command, ¶ms).unwrap_err(); assert!(error.to_string().contains(command), "{command} {params}: {error}"); assert_eq!(layer_target_snapshot(s), before, "{command} {params}: rejected targets must leave all state unchanged"); } /// #785: shared row commands reject malformed or unknown targets before any partial edits. #[test] fn layer_target_validation_keeps_document_selection_revision_and_history_unchanged() { let (_, rows) = layer_target_fixture(); for command in [ "layer.delete", "layer.duplicate", "layer.setProps", "layer.template", "layer.merge", "layer.reverse", "layer.hideOthers", "layer.outlineOthers", "layer.lockOthers", ] { for mut params in malformed_layer_targets(rows[0], rows[1]) { let (mut s, _) = layer_target_fixture(); if command == "layer.setProps" { params["name"] = json!("Changed"); } assert_layer_target_error(&mut s, command, params); } } } /// These commands have their own selectors, not the shared row-default parser. #[test] fn layer_collect_and_flatten_validate_their_own_explicit_targets() { let (_, rows) = layer_target_fixture(); for params in malformed_layer_targets(rows[0], rows[1]) { let command = if params.get("ids").is_some() { "layer.collectInNew" } else { "layer.flatten" }; let (mut s, _) = layer_target_fixture(); assert_layer_target_error(&mut s, command, params); } } /// Truncation limits work, not validation: a malformed or unknown tail cannot be ignored. #[test] fn layer_target_arrays_are_fully_validated_before_the_row_limit() { // The existing panel row limit is 100,000; exercise the public command beyond it. const ROW_LIMIT: usize = 100_000; for tail in [json!("$1.id"), json!(u64::MAX)] { let (mut s, rows) = layer_target_fixture(); let mut values = vec![json!(rows[0].0); ROW_LIMIT + 1]; *values.last_mut().unwrap() = tail; assert_layer_target_error(&mut s, "layer.delete", json!({"ids": values})); } let (mut s, rows) = layer_target_fixture(); let mut values = vec![json!(rows[0].0); ROW_LIMIT + 1]; *values.last_mut().unwrap() = json!(rows[1].0); run(&mut s, "layer.setProps", json!({"ids": values, "name": "Within limit"})); assert_eq!(node(&s, rows[0]).name.as_deref(), Some("Within limit")); assert_ne!(node(&s, rows[1]).name.as_deref(), Some("Within limit"), "the valid tail remains beyond the work cap"); } #[test] fn layer_target_defaults_and_valid_explicit_precedence_are_preserved() { let (mut s, rows) = layer_target_fixture(); run(&mut s, "layer.setProps", json!({"name": "Highlighted"})); assert_eq!(node(&s, rows[0]).name.as_deref(), Some("Highlighted")); assert_eq!(node(&s, rows[1]).name.as_deref(), Some("Highlighted")); run(&mut s, "layer.setProps", json!({"id": rows[2].0, "name": "Single"})); assert_eq!(node(&s, rows[2]).name.as_deref(), Some("Single")); let before = undos(&s); run(&mut s, "layer.setProps", json!({"ids": [rows[1].0, rows[0].0, rows[1].0], "id": "$1.id", "name": "Explicit"})); assert_eq!(node(&s, rows[0]).name.as_deref(), Some("Explicit")); assert_eq!(node(&s, rows[1]).name.as_deref(), Some("Explicit")); assert_eq!(node(&s, rows[2]).name.as_deref(), Some("Single")); assert_eq!(undos(&s), before + 1, "duplicates remain one edit"); assert_layer_target_error(&mut s, "layer.setProps", json!({"ids": [], "id": rows[2].0, "name": "Must not fall back"})); run(&mut s, "layer.setCurrent", json!({"id": rows[2].0})); run(&mut s, "layer.highlight", json!({"ids": []})); run(&mut s, "layer.setProps", json!({"name": "Current"})); assert_eq!(node(&s, rows[2]).name.as_deref(), Some("Current")); let before = undos(&s); run(&mut s, "layer.delete", json!({})); assert!(s.doc().unwrap().doc.node(rows[2]).is_none()); assert_eq!(undos(&s), before + 1); } #[test] fn layer_collect_and_flatten_keep_valid_and_omitted_target_behavior() { let (mut s, rows) = layer_target_fixture(); let r = run(&mut s, "layer.flatten", json!({"id": rows[2].0})); assert_eq!(r["id"], json!(rows[2].0)); assert_eq!(s.doc().unwrap().doc.layers.len(), 1); let (mut s, _) = layer_target_fixture(); let selected = s.doc().unwrap().selection.objects.clone(); s.doc_mut().unwrap().layer_rows.clear(); let r = run(&mut s, "layer.collectInNew", json!({})); let collected = id(&r); assert!(selected.iter().all(|id| parent(&s, *id) == Some(collected))); }