Files
vectorcraft/crates/doc/src/selection/tests.rs
T
dotCooCoo 5ac7d97d85 Guides: ruler guides are document objects on their layers
Ruler guides were a list kept beside the layers. Each guide is now an
object of its layer or sublayer, always directly in it and above the
layer's art. These behave as on main:

- Guides draw on screen only. Exports (SVG, PDF, PNG, PSD), Fit All,
  Select All, the art bounds, clicks and marquees, and a layer's
  Pathfinder and Transform effects leave them out. The Layers panel
  lists them as guide rows of their layer, the highest guide.list
  index first.
- Deleting a layer deletes its guides, and merging layers keeps their
  guides in the merged layer.
- The guide commands number guides as guide.list does, in the order
  they were made.

New: each guide takes an id from the document's object ids. An object
command given a guide's id fails with an error, and node.move cannot
put a guide into a group or out of its layers. A guide made while an
opacity mask, a symbol or a pattern is edited moves to the top
visible, unlocked layer when editing ends.

File format: native files still save the guides in the document's
guides list, in every file version, so older builds read them. Each
entry may now also carry the guide's id, name, locked and hidden
flags; older builds ignore those fields. Opening a file puts each
saved guide on its layer (on the top visible, unlocked layer when the
file names none or a layer it does not have), and each saved guide
counts against the object limit for opening a file. A ruler guide node
that a file holds below a layer's art moves up to the layer's guides,
and one anywhere else (in a group or another object, beside the
layers, or in a symbol) is removed. Ruler guides read from an .ai file
become guide objects on the layer they were on, numbered in the order
the file lists them.
2026-11-11 09:52:28 -08:00

311 lines
12 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));
let (g0, g2) = (NodeId(10), NodeId(12));
s.set_guides([g2, g0, g2]);
assert_eq!((s.objects.clone(), s.guides.clone()), (vec![], vec![g2, g0]));
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![g2, g0]);
s.set([NodeId(1)]);
assert!(s.guides.is_empty());
// Toggled, a guide joins (or leaves) the selected objects.
s.toggle_guide(g0);
assert_eq!((s.objects.clone(), s.guides.clone()), (vec![NodeId(1)], vec![g0]));
s.toggle_guide(g0);
assert_eq!((s.objects.clone(), s.guides.clone()), (vec![NodeId(1)], vec![]));
// Pruning drops guides the document no longer has, ids of other objects, and guides that are
// locked or hidden themselves.
let mut d = Document::new(100.0, 100.0);
let layer = d.layers[0].id;
let kept = d.add_ruler_guide(&crate::Guide::new(true, 10.0));
let locked = d.add_ruler_guide(&crate::Guide { locked: true, ..crate::Guide::new(true, 20.0) });
let hidden = d.add_ruler_guide(&crate::Guide { hidden: true, ..crate::Guide::new(true, 30.0) });
s.set_guides([kept, NodeId(9999), layer, locked, hidden]);
s.prune(&d);
assert_eq!(s.guides, vec![kept]);
}
/// 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));
// As a node, a guide shows and locks with its layer and on its own.
let id = d.add_ruler_guide(&g);
let node = d.ruler_guide(id).unwrap();
assert_eq!(d.guides_on(layer), std::slice::from_ref(&node));
assert!(!d.guide_shown(&node) && !d.guide_editable(&node), "its layer is hidden");
d.node_mut(layer).unwrap().visible = true;
d.node_mut(layer).unwrap().locked = false;
assert!(d.guide_shown(&node) && d.guide_editable(&node));
d.node_mut(id).unwrap().locked = true;
let node = d.ruler_guide(id).unwrap();
assert!(d.guide_shown(&node) && !d.guide_editable(&node), "locked itself");
d.node_mut(id).unwrap().visible = false;
assert!(!d.guide_shown(&node), "hidden itself");
// Saved guides on no layer, or a gone one, go on the top visible, unlocked layer.
d.saved_guides.push(crate::Guide::new(false, 5.0));
d.saved_guides.push(gone);
assert_eq!(d.guides_into_nodes(), 2);
assert!(d.ruler_guides().iter().all(|g| g.layer == Some(layer)));
assert_eq!(d.ruler_guides().len(), 3);
assert_eq!(d.guides_into_nodes(), 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![NodeId(1), NodeId(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}");
}