//! Clear deletes its exact targets, including compound members (#786). #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] use serde_json::json; use vectorcraft_doc::NodeId; use vectorcraft_engine::{EngineError, Session}; fn compound() -> (Session, NodeId, NodeId, NodeId) { let mut s = Session::new(); s.execute("file.new", &json!({})).unwrap(); let mut rectangle = |x| NodeId(s.execute("shape.rectangle", &json!({"x": x, "y": 10, "width": 30, "height": 30})).unwrap()["id"].as_u64().unwrap()); let a = rectangle(10); let b = rectangle(50); s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap(); let c = NodeId(s.execute("object.compoundPath.make", &json!({})).unwrap()["id"].as_u64().unwrap()); (s, a, b, c) } /// The two right-hand corners of a square are indexes 1 and 2. Delete must remove those corners /// and leave the left side, not a diagonal from shifting indexes as the path opens. #[test] fn clear_deletes_every_selected_anchor_of_a_closed_path() { let mut s = Session::new(); s.execute("file.new", &json!({})).unwrap(); let id = NodeId(s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 30, "height": 30})).unwrap()["id"].as_u64().unwrap()); let points = |s: &Session| { s.active().unwrap().doc.node(id).unwrap().path_data().unwrap().subpaths[0].anchors.iter().map(|a| (a.p.x, a.p.y)).collect::>() }; let before = points(&s); s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 1], [0, 2]]})).unwrap(); s.execute("edit.clear", &json!({})).unwrap(); let sp = &s.active().unwrap().doc.node(id).unwrap().path_data().unwrap().subpaths[0]; assert!(!sp.closed); assert_eq!(points(&s), vec![before[3], before[0]]); assert_eq!(s.active().unwrap().selection.objects, vec![id]); assert!(s.active().unwrap().selection.anchors.is_empty()); s.execute("edit.clear", &json!({})).unwrap(); assert!(s.active().unwrap().doc.node(id).is_none(), "a second Delete removes the remainder"); s.execute("edit.undo", &json!({})).unwrap(); s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(points(&s), before); // One corner still opens the square into the other three, in order. s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 1]]})).unwrap(); s.execute("edit.clear", &json!({})).unwrap(); assert_eq!(points(&s), vec![before[2], before[3], before[0]]); } /// A marquee that covers one path completely and only some anchors of another stores the first as /// a whole object. Delete removes that object and the selected anchors of the other. #[test] fn clear_deletes_fully_selected_objects_along_with_partial_anchors() { let mut s = Session::new(); s.execute("file.new", &json!({})).unwrap(); let mut rectangle = |x| NodeId(s.execute("shape.rectangle", &json!({"x": x, "y": 10, "width": 30, "height": 30})).unwrap()["id"].as_u64().unwrap()); let whole = rectangle(10); let partial = rectangle(80); let refs = |s: &Session, id| { s.active().unwrap().doc.node(id).unwrap().path_data().unwrap().anchors().map(|(si, ai, _)| vec![si, ai]).collect::>() }; let all = refs(&s, whole); let one = refs(&s, partial)[0].clone(); let before = refs(&s, partial).len(); s.execute("select.anchorsMany", &json!({"items": [{"id": whole.0, "anchors": all}, {"id": partial.0, "anchors": [one]}]})).unwrap(); assert!(s.active().unwrap().selection.contains(whole) && s.active().unwrap().selection.partial(whole).is_none()); s.execute("edit.clear", &json!({})).unwrap(); assert!(s.active().unwrap().doc.node(whole).is_none()); assert_eq!(refs(&s, partial).len(), before - 1); assert_eq!(s.active().unwrap().selection.objects, vec![partial]); assert!(s.active().unwrap().selection.anchors.is_empty()); s.execute("edit.clear", &json!({})).unwrap(); assert!(s.active().unwrap().doc.node(partial).is_none(), "a second Delete removes what the first left"); s.execute("edit.undo", &json!({})).unwrap(); s.execute("edit.undo", &json!({})).unwrap(); assert!(s.active().unwrap().doc.node(whole).is_some()); assert_eq!(refs(&s, partial).len(), before); } #[test] fn clear_selected_compound_member_preserves_its_sibling_and_is_undoable() { let (mut s, a, b, c) = compound(); s.execute("select.set", &json!({"ids": [a.0]})).unwrap(); s.execute("edit.clear", &json!({})).unwrap(); assert!(s.active().unwrap().doc.node(a).is_none()); assert_eq!(s.active().unwrap().doc.parent_of(b), Some(c)); assert!(s.active().unwrap().selection.is_empty()); assert_eq!(s.execute("edit.undo", &json!({})).unwrap()["undone"], "Clear"); assert_eq!(s.active().unwrap().doc.parent_of(a), Some(c)); assert_eq!(s.active().unwrap().selection.objects, vec![a]); s.execute("edit.redo", &json!({})).unwrap(); assert!(s.active().unwrap().doc.node(a).is_none()); assert_eq!(s.active().unwrap().doc.parent_of(b), Some(c)); } #[test] fn clear_explicit_ids_works_without_a_selection_and_overrides_selected_anchors() { let (mut s, a, b, c) = compound(); s.execute("select.none", &json!({})).unwrap(); let info = s.commands().into_iter().find(|info| info.id == "edit.clear").unwrap(); assert!(!info.enabled, "the menu stays disabled with no selection"); s.execute("edit.clear", &json!({"ids": [a.0]})).unwrap(); assert!(s.active().unwrap().doc.node(a).is_none()); assert_eq!(s.active().unwrap().doc.parent_of(b), Some(c)); s.execute("edit.undo", &json!({})).unwrap(); s.execute("select.anchors", &json!({"id": b.0, "anchors": [[0, 0]]})).unwrap(); let sibling = s.active().unwrap().doc.node(b).unwrap().clone(); s.execute("edit.clear", &json!({"ids": [a.0]})).unwrap(); assert!(s.active().unwrap().doc.node(a).is_none()); assert_eq!(s.active().unwrap().doc.node(b).unwrap(), &sibling); } #[test] fn clear_explicit_targets_are_validated_atomically() { let (mut s, a, _, _) = compound(); s.execute("select.none", &json!({})).unwrap(); for ids in [json!([a.0, 999]), json!([a.0, -1]), json!([a.0, "bad"]), json!([a.0, 2.5]), json!(null), json!(a.0)] { let doc = s.active().unwrap().doc.clone(); let journal = s.journal.clone(); assert!(matches!(s.execute("edit.clear", &json!({"ids": ids})), Err(EngineError::BadParams { .. })), "{ids}"); assert_eq!(s.active().unwrap().doc, doc); assert_eq!(s.journal, journal); } assert!(s.execute("edit.clear", &json!({})).is_err()); assert!(Session::new().execute("edit.clear", &json!({"ids": [a.0]})).is_err()); } #[test] fn clear_compound_and_member_together_deletes_the_compound() { for explicit in [false, true] { let (mut s, a, b, c) = compound(); s.execute("select.set", &json!({"ids": [a.0, c.0]})).unwrap(); s.execute("edit.clear", &if explicit { json!({"ids": [a.0, c.0, c.0]}) } else { json!({}) }).unwrap(); for id in [a, b, c] { assert!(s.active().unwrap().doc.node(id).is_none()); } s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(s.active().unwrap().doc.parent_of(a), Some(c)); assert_eq!(s.active().unwrap().doc.parent_of(b), Some(c)); } } #[test] fn cut_compound_member_keeps_copy_and_delete_targets_consistent() { let (mut s, a, b, c) = compound(); s.execute("select.set", &json!({"ids": [a.0]})).unwrap(); s.execute("edit.cut", &json!({})).unwrap(); assert_eq!(s.clipboard.nodes.len(), 1); assert_eq!(s.clipboard.nodes[0].id, c); assert!(s.active().unwrap().doc.node(c).is_none()); s.execute("edit.pasteInPlace", &json!({})).unwrap(); let pasted = s.active().unwrap().selection.objects[0]; assert_eq!(s.active().unwrap().doc.node(pasted).unwrap().children().unwrap().len(), 2); s.execute("edit.undo", &json!({})).unwrap(); s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(s.active().unwrap().doc.parent_of(a), Some(c)); assert_eq!(s.active().unwrap().doc.parent_of(b), Some(c)); } #[test] fn clear_direct_selected_anchors_keeps_the_rest_of_the_path() { let (mut s, a, b, c) = compound(); s.execute("select.anchors", &json!({"id": a.0, "anchors": [[0, 0]]})).unwrap(); s.execute("edit.clear", &json!({})).unwrap(); assert_eq!(s.active().unwrap().doc.node(a).unwrap().path_data().unwrap().anchor_count(), 3); assert_eq!(s.active().unwrap().doc.parent_of(b), Some(c)); } #[test] fn clear_guides_and_empty_explicit_targets_keep_their_separate_behavior() { let (mut s, a, _, _) = compound(); s.execute("guide.add", &json!({"vertical": true, "pos": 10})).unwrap(); s.execute("guide.select", &json!({"indexes": [0]})).unwrap(); let history = s.active().unwrap().history.undo.len(); let selection = s.active().unwrap().selection.clone(); s.execute("edit.clear", &json!({"ids": []})).unwrap(); assert_eq!(s.active().unwrap().history.undo.len(), history); assert_eq!(s.active().unwrap().selection, selection); assert_eq!(s.active().unwrap().doc.guides.len(), 1); s.execute("edit.clear", &json!({})).unwrap(); assert!(s.active().unwrap().doc.guides.is_empty()); assert!(s.active().unwrap().doc.node(a).is_some()); s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(s.active().unwrap().doc.guides.len(), 1); } #[test] fn clear_keeps_layers_but_deletes_an_explicitly_targeted_child() { let (mut s, a, b, c) = compound(); let layer = s.active().unwrap().doc.layers[0].id; s.execute("select.none", &json!({})).unwrap(); s.execute("edit.clear", &json!({"ids": [layer.0, a.0]})).unwrap(); assert!(s.active().unwrap().doc.node(layer).is_some()); assert!(s.active().unwrap().doc.node(a).is_none()); assert_eq!(s.active().unwrap().doc.parent_of(b), Some(c)); } #[test] fn cut_direct_selected_anchors_retains_anchor_deletion() { let (mut s, a, b, c) = compound(); s.execute("select.anchors", &json!({"id": a.0, "anchors": [[0, 0]]})).unwrap(); s.execute("edit.cut", &json!({})).unwrap(); assert_eq!(s.active().unwrap().doc.node(a).unwrap().path_data().unwrap().anchor_count(), 3); assert_eq!(s.active().unwrap().doc.parent_of(b), Some(c)); }