Files
vectorcraft/crates/doc/src/selection/tests.rs
T

296 lines
11 KiB
Rust

use std::sync::Arc;
use super::*;
use crate::Node;
#[test]
fn basic_ops() {
let mut s = Selection::default();
s.add(NodeId(1));
s.add(NodeId(1));
s.toggle(NodeId(2));
assert_eq!(s.objects, vec![NodeId(1), NodeId(2)]);
s.toggle(NodeId(1));
assert_eq!(s.objects, vec![NodeId(2)]);
s.clear();
assert!(s.is_empty());
}
#[test]
fn guides_are_selected_on_their_own() {
let mut s = Selection::default();
s.add(NodeId(1));
s.set_guides([2, 0, 2]);
assert_eq!((s.objects.clone(), s.guides.clone()), (vec![], vec![2, 0]));
assert!(s.has_objects_or_guides() && s.is_empty());
// Shift-adding an object keeps them; a new selection doesn't.
s.add(NodeId(1));
assert_eq!(s.guides, vec![2, 0]);
s.set([NodeId(1)]);
assert!(s.guides.is_empty());
// Toggled, a guide joins (or leaves) the selected objects.
s.toggle_guide(0);
assert_eq!((s.objects.clone(), s.guides.clone()), (vec![NodeId(1)], vec![0]));
s.toggle_guide(0);
assert_eq!((s.objects.clone(), s.guides.clone()), (vec![NodeId(1)], vec![]));
// Pruning drops guides the document no longer has.
let mut d = Document::new(100.0, 100.0);
d.guides.push(crate::Guide::new(true, 10.0));
s.set_guides([0, 1]);
s.prune(&d);
assert_eq!(s.guides, vec![0]);
}
/// A picked object of a dynamic symbol's instance stays picked while it and the groups and layers
/// around it in the symbol's art are visible, unlocked and not a template. An art edit that hides
/// or locks it, or one around it, or makes a layer around it a template, drops the pick.
#[test]
fn a_picked_symbol_part_is_dropped_once_it_cannot_be_edited() {
let rect = || vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(0.0, 0.0, 10.0, 10.0));
let mut d = Document::new(100.0, 100.0);
let l = d.layers[0].id;
let leaf = Node::path(d.alloc_id(), rect(), crate::Appearance::default_art());
let group = Node::group(d.alloc_id(), vec![Arc::new(leaf)]);
let layer = Node::layer(d.alloc_id(), "Inner", crate::LayerColor::Preset(2));
let root = Node::group(d.alloc_id(), vec![Arc::new(group), Arc::new(layer)]);
d.symbols.push(crate::Symbol { dynamic: true, name: "Parts".into(), art: Arc::new(root.clone()) });
let inst = d.alloc_id();
d.insert(Some(l), 0, Node::new(inst, NodeKind::SymbolInstance { symbol: "Parts".into(), xf: vectorcraft_geom::Affine::IDENTITY })).unwrap();
// The layer in the art holds a path too.
let in_layer = Node::path(d.alloc_id(), rect(), crate::Appearance::default_art());
let mut art = root;
if let Some(NodeKind::Layer { children, .. }) = art.children_mut().and_then(|c| c.get_mut(1)).map(|c| &mut Arc::make_mut(c).kind) {
children.push(Arc::new(in_layer));
}
d.symbols[0].art = Arc::new(art.clone());
let picked = |d: &Document, path: &[u32]| {
let mut s = Selection::default();
s.set_symbol_part(inst, path.to_vec());
s.prune(d);
s.symbol_part.is_some()
};
assert!(picked(&d, &[0, 0]) && picked(&d, &[1, 0]), "editable parts stay picked");
assert!(!picked(&d, &[0]) && !picked(&d, &[1]) && !picked(&d, &[]), "a group or layer does not stay picked");
let edited = |change: &dyn Fn(&mut Node)| {
let mut d = d.clone();
let mut art = art.clone();
change(&mut art);
d.symbols[0].art = Arc::new(art);
d
};
let child = |art: &mut Node, path: &[usize]| -> Node {
let mut n = &*art;
for &i in path {
n = &n.children().unwrap()[i];
}
n.clone()
};
let set = |art: &mut Node, path: &[usize], n: Node| {
let mut at = art;
for &i in path {
at = Arc::make_mut(&mut at.children_mut().unwrap()[i]);
}
*at = n;
};
let hidden = edited(&|art| {
let mut n = child(art, &[0, 0]);
n.visible = false;
set(art, &[0, 0], n);
});
assert!(!picked(&hidden, &[0, 0]), "the part is hidden");
let locked_group = edited(&|art| {
let mut n = child(art, &[0]);
n.locked = true;
set(art, &[0], n);
});
assert!(!picked(&locked_group, &[0, 0]), "the group around it is locked");
assert!(picked(&locked_group, &[1, 0]), "a part outside the locked group stays");
let template = edited(&|art| {
let mut n = child(art, &[1]);
if let NodeKind::Layer { template, .. } = &mut n.kind {
*template = true;
}
set(art, &[1], n);
});
assert!(!picked(&template, &[1, 0]), "the layer around it is a template");
let hidden_root = edited(&|art| art.visible = false);
assert!(!picked(&hidden_root, &[0, 0]), "the art is hidden");
}
#[test]
fn guides_show_and_lock_with_their_layer() {
let mut d = Document::new(100.0, 100.0);
let layer = d.layers[0].id;
let g = crate::Guide { layer: Some(layer), ..crate::Guide::new(true, 10.0) };
assert!(d.guide_shown(&g) && d.guide_editable(&g));
d.node_mut(layer).unwrap().locked = true;
assert!(d.guide_shown(&g) && !d.guide_editable(&g), "locked: shown, not editable");
d.node_mut(layer).unwrap().visible = false;
assert!(!d.guide_shown(&g) && !d.guide_editable(&g), "hidden");
// On no layer, or a layer that's gone: always there.
assert!(d.guide_shown(&crate::Guide::new(false, 5.0)));
let gone = crate::Guide { layer: Some(NodeId(9999)), ..crate::Guide::new(true, 1.0) };
assert!(d.guide_shown(&gone) && d.guide_editable(&gone));
d.guides.push(g);
assert_eq!(d.guides_on(layer).count(), 1);
// Guides on no layer, or a gone one, are adopted by the top visible, unlocked layer.
d.node_mut(layer).unwrap().visible = true;
d.node_mut(layer).unwrap().locked = false;
d.guides.push(crate::Guide::new(false, 5.0));
d.guides.push(gone);
assert_eq!(d.adopt_guides(), 2);
assert!(d.guides.iter().all(|g| g.layer == Some(layer)));
assert_eq!(d.adopt_guides(), 0, "once");
// A guide saved before guides had layers reads as one on no layer.
let old: crate::Guide = serde_json::from_str(r#"{"vertical": true, "pos": 5.0}"#).unwrap();
assert_eq!(old.layer, None);
assert!(!serde_json::to_string(&crate::Guide::new(true, 5.0)).unwrap().contains("layer"), "nothing new written");
}
#[test]
fn selection_changes_clear_the_target() {
let mut s = Selection::default();
s.set([NodeId(2), NodeId(3)]);
s.target = Some(NodeId(1));
assert_eq!(s.subjects(), &[NodeId(1)]);
s.add(NodeId(4));
assert_eq!(s.target, None);
assert_eq!(s.subjects(), &[NodeId(2), NodeId(3), NodeId(4)]);
s.target = Some(NodeId(1));
s.remove(NodeId(2));
assert_eq!(s.target, None);
s.target = Some(NodeId(1));
s.set([NodeId(3)]);
assert_eq!(s.target, None);
}
#[test]
fn a_key_object_needs_two_or_more_selected_objects() {
let mut s = Selection::default();
s.set([NodeId(1), NodeId(2)]);
s.key = Some(NodeId(2));
s.remove(NodeId(1));
assert_eq!(s.key, Some(NodeId(2)), "another object left: remove keeps the key");
s.remove(NodeId(2));
assert_eq!(s.key, None, "the key object itself left");
// Pruning drops objects the document no longer has and clears the key once no other
// selected object lies outside it.
let mut d = Document::new(100.0, 100.0);
let layer = d.layers[0].id;
let mut ids = vec![];
for _ in 0..4 {
let id = d.alloc_id();
ids.push(d.insert(Some(layer), usize::MAX, crate::Node::group(id, vec![])).unwrap());
}
s.set(ids.iter().copied());
s.key = Some(ids[3]);
d.remove(ids[0]).unwrap();
s.prune(&d);
assert_eq!(s.key, Some(ids[3]), "three objects left: the key stays");
s.toggle(ids[1]);
s.prune(&d);
assert_eq!(s.key, Some(ids[3]), "two objects left: the key stays");
s.toggle(ids[2]);
s.prune(&d);
assert_eq!((s.objects.clone(), s.key), (vec![ids[3]], None), "one object left");
// A group key selected with only its own member is cleared too; another selected object
// outside the group keeps it.
let member = d.alloc_id();
d.insert(Some(ids[3]), usize::MAX, crate::Node::group(member, vec![])).unwrap();
s.set([member, ids[3]]);
s.key = Some(ids[3]);
s.prune(&d);
assert_eq!(s.key, None, "a group and its own member");
s.set([member, ids[3], ids[2]]);
s.key = Some(ids[3]);
s.prune(&d);
assert_eq!(s.key, Some(ids[3]), "another object outside the group");
}
fn below(r: &mut u64, n: u64) -> u64 {
*r ^= *r << 13;
*r ^= *r >> 7;
*r ^= *r << 17;
*r % n.max(1)
}
fn random_group(d: &mut Document, r: &mut u64, depth: u64, ids: &mut Vec<NodeId>) -> Arc<Node> {
let id = d.alloc_id();
ids.push(id);
let count = if depth == 0 { 0 } else { 1 + below(r, 4) };
let children = (0..count)
.map(|_| {
let depth = below(r, depth);
random_group(d, r, depth, ids)
})
.collect();
let mut group = Node::group(id, children);
group.locked = below(r, 6) == 0;
group.visible = below(r, 6) != 0;
Arc::new(group)
}
fn random_document(r: &mut u64) -> (Document, Vec<NodeId>) {
let mut d = Document::new(100.0, 100.0);
let mut ids = vec![];
for _ in 0..3 {
let mut layer = Node::layer(d.alloc_id(), "L", crate::LayerColor::Preset(0));
let children = (0..1 + below(r, 4)).map(|_| random_group(&mut d, r, 3, &mut ids)).collect();
if let Some(c) = layer.children_mut() {
*c = children;
}
layer.locked = below(r, 6) == 0;
layer.visible = below(r, 6) != 0;
d.layers.push(Arc::new(layer));
}
(d, ids)
}
fn random_selection(r: &mut u64, ids: &[NodeId]) -> Selection {
let mut s = Selection::default();
let missing = (below(r, 2) == 0).then_some(NodeId(u64::MAX));
s.set(ids.iter().copied().filter(|_| below(r, 2) == 0).chain(missing));
let k = below(r, s.objects.len() as u64) as usize;
s.objects.rotate_left(k);
for &o in &s.objects {
match below(r, 3) {
0 => {
s.anchors.insert(o, BTreeSet::from([(0, 0)]));
}
1 => {
s.anchors.insert(o, BTreeSet::from([(0, 1)]));
s.segments.insert(o, BTreeSet::from([(0, 0)]));
}
_ => {}
}
}
s.key = s.objects.get(below(r, s.objects.len() as u64) as usize).copied();
s.target = ids.get(below(r, ids.len() as u64) as usize).copied().filter(|_| below(r, 2) == 0);
s.slices = ids.iter().copied().take(below(r, 3) as usize).collect();
s.guides = vec![1, 0];
s
}
#[test]
fn deselect_uneditable_matches_removing_one_at_a_time() {
let (mut unchanged, mut partial) = (0, 0);
for seed in 1..400u64 {
let mut r = seed.wrapping_mul(0x9e37_79b9_7f4a_7c15) | 1;
let (d, mut ids) = random_document(&mut r);
if seed % 4 == 0 {
ids.retain(|id| d.is_editable(*id));
}
let s = random_selection(&mut r, &ids);
let mut expected = s.clone();
for o in s.objects.iter().copied().filter(|o| !d.is_editable(*o)) {
expected.remove(o);
}
let mut actual = s.clone();
actual.deselect_uneditable(&d);
assert_eq!(actual, expected, "seed {seed}");
unchanged += usize::from(actual == s);
partial += usize::from(!actual.objects.is_empty() && actual.objects.len() < s.objects.len());
}
assert!(unchanged > 10 && partial > 100, "{unchanged} {partial}");
}