206 lines
10 KiB
Rust
206 lines
10 KiB
Rust
//! 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::<Vec<_>>()
|
|
};
|
|
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::<Vec<_>>()
|
|
};
|
|
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));
|
|
}
|