mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 06:31:41 +00:00
Guides: removing and editing artboard guides visits each layer once
Deleting ruler guides and moving or scaling an artboard's guides asked each layer whether its whole subtree held a matching guide, then asked every sublayer the same question again on the way down. A layer nested N deep was scanned N times, so a chain of N layers cost N(N+1)/2 layer scans. One read walk now records which children match and which sublayers hold a match, and the edit visits and copies only those layers. The guides removed, the count returned, the selection, the order the guides are changed in and the sharing of untouched layers are the same as before. The same walk replaces the repeated subtree scans in the other guide edits in this file: - tidying the ruler guides of an opened file (guides below art, in objects, in opacity-mask art, in symbols and patterns) - saving the guides as a list - removing an editing layer while keeping its guides
This commit is contained in:
@@ -183,8 +183,8 @@ impl Document {
|
||||
self.layers.retain(|l| !l.is_ruler_guide());
|
||||
gone -= self.layers.len();
|
||||
for l in &mut self.layers {
|
||||
if layer_untidy(l) {
|
||||
gone += tidy_layer(Arc::make_mut(l));
|
||||
if let Some(found) = untidy(l, &Node::is_ruler_guide) {
|
||||
gone += tidy_layer(Arc::make_mut(l), found);
|
||||
}
|
||||
}
|
||||
let others = self
|
||||
@@ -198,8 +198,8 @@ impl Document {
|
||||
if root.is_ruler_guide() {
|
||||
*root = Arc::new(Node::group(root.id, vec![]));
|
||||
gone += 1;
|
||||
} else if any_guide(root) {
|
||||
gone += drop_guides_below(Arc::make_mut(root));
|
||||
} else if let Some(found) = guides_below(root, &Node::is_ruler_guide) {
|
||||
gone += drop_guides_below(Arc::make_mut(root), found);
|
||||
}
|
||||
}
|
||||
gone
|
||||
@@ -272,8 +272,8 @@ impl Document {
|
||||
return;
|
||||
}
|
||||
for l in &mut self.layers {
|
||||
if l.holds_ruler_guides() {
|
||||
Arc::make_mut(l).strip_ruler_guides();
|
||||
if let Some(held) = picked_in_layers(l, &Node::is_ruler_guide) {
|
||||
drop_picked(l, held);
|
||||
}
|
||||
}
|
||||
self.saved_guides.extend(guides);
|
||||
@@ -347,16 +347,14 @@ impl Document {
|
||||
/// saved guide whose layer is gone opens. Returns the removed node, without the guides.
|
||||
pub fn remove_edit_layer(&mut self, id: NodeId) -> Result<Arc<Node>, crate::DocError> {
|
||||
let mut layer = self.remove(id)?;
|
||||
if !layer.holds_ruler_guides() {
|
||||
return Ok(layer);
|
||||
}
|
||||
let Some(held) = picked_in_layers(&layer, &Node::is_ruler_guide) else { return Ok(layer) };
|
||||
let mut guides = vec![];
|
||||
layer.walk(&mut |c| {
|
||||
if c.is_ruler_guide() {
|
||||
guides.push(Arc::new(c.clone()));
|
||||
}
|
||||
});
|
||||
Arc::make_mut(&mut layer).strip_ruler_guides();
|
||||
drop_picked(&mut layer, held);
|
||||
let home = self.default_layer().unwrap_or_else(|| self.add_layer(None));
|
||||
self.children_mut(Some(home))?.extend(guides);
|
||||
Ok(layer)
|
||||
@@ -418,21 +416,11 @@ impl Document {
|
||||
if gone.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
fn drop(layer: &mut Arc<Node>, gone: &HashSet<NodeId>) {
|
||||
fn holds(n: &Node, gone: &HashSet<NodeId>) -> bool {
|
||||
n.is_layer() && n.children().is_some_and(|ch| ch.iter().any(|c| gone.contains(&c.id) || holds(c, gone)))
|
||||
}
|
||||
if !holds(layer, gone) {
|
||||
return;
|
||||
}
|
||||
let Some(ch) = Arc::make_mut(layer).children_mut() else { return };
|
||||
ch.retain(|c| !(c.is_ruler_guide() && gone.contains(&c.id)));
|
||||
for c in ch.iter_mut().filter(|c| c.is_layer()) {
|
||||
drop(c, gone);
|
||||
}
|
||||
}
|
||||
let pick = |n: &Node| n.is_ruler_guide() && gone.contains(&n.id);
|
||||
for l in &mut self.layers {
|
||||
drop(l, &gone);
|
||||
if let Some(held) = picked_in_layers(l, &pick) {
|
||||
drop_picked(l, held);
|
||||
}
|
||||
}
|
||||
sel.guides.retain(|id| !gone.contains(id));
|
||||
if sel.key_guide.is_some_and(|k| gone.contains(&k)) {
|
||||
@@ -444,29 +432,11 @@ impl Document {
|
||||
/// Change the line of every ruler guide on artboard `id` with `f` (whether it is vertical, its
|
||||
/// position).
|
||||
fn edit_artboard_guides(&mut self, id: u32, f: &mut impl FnMut(bool, &mut f64)) {
|
||||
fn visit(layer: &mut Arc<Node>, id: u32, f: &mut impl FnMut(bool, &mut f64)) {
|
||||
fn holds(n: &Node, id: u32) -> bool {
|
||||
n.is_layer()
|
||||
&& n.children().is_some_and(|ch| {
|
||||
ch.iter().any(|c| matches!(c.kind, NodeKind::RulerGuide { artboard: Some(a), .. } if a == id) || holds(c, id))
|
||||
})
|
||||
}
|
||||
if !holds(layer, id) {
|
||||
return;
|
||||
}
|
||||
let Some(ch) = Arc::make_mut(layer).children_mut() else { return };
|
||||
for c in ch.iter_mut() {
|
||||
if matches!(c.kind, NodeKind::RulerGuide { artboard: Some(a), .. } if a == id) {
|
||||
if let NodeKind::RulerGuide { vertical, pos, .. } = &mut Arc::make_mut(c).kind {
|
||||
f(*vertical, pos);
|
||||
}
|
||||
} else if c.is_layer() {
|
||||
visit(c, id, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
let pick = |n: &Node| matches!(n.kind, NodeKind::RulerGuide { artboard: Some(a), .. } if a == id);
|
||||
for l in &mut self.layers {
|
||||
visit(l, id, f);
|
||||
if let Some(held) = picked_in_layers(l, &pick) {
|
||||
edit_picked(l, held, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,65 +550,172 @@ impl Document {
|
||||
}
|
||||
}
|
||||
|
||||
/// Does `n`, or anything below it (opacity-mask art included), hold a ruler guide?
|
||||
fn any_guide(n: &Node) -> bool {
|
||||
let mut any = false;
|
||||
crate::swatches::walk_with_masks(n, &mut |m| any |= m.is_ruler_guide());
|
||||
any
|
||||
/// What a walk picked in a node: its children by index in order, each with what was picked
|
||||
/// inside it, and what was picked in its opacity-mask art. Empty for a node picked itself.
|
||||
#[derive(Debug, Default)]
|
||||
struct Picked {
|
||||
children: Vec<(usize, Picked)>,
|
||||
mask: Option<Box<Picked>>,
|
||||
}
|
||||
|
||||
/// Remove every ruler guide below `n`, in its members and its opacity-mask art (`n` itself
|
||||
/// stays). Mask art that is a ruler guide becomes an empty group. Returns how many went.
|
||||
fn drop_guides_below(n: &mut Node) -> usize {
|
||||
let mut gone = drop_mask_guides(n);
|
||||
if let Some(ch) = n.children_mut() {
|
||||
let before = ch.len();
|
||||
ch.retain(|c| !c.is_ruler_guide());
|
||||
gone += before - ch.len();
|
||||
for c in ch.iter_mut().filter(|c| any_guide(c)) {
|
||||
gone += drop_guides_below(Arc::make_mut(c));
|
||||
impl Picked {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.children.is_empty() && self.mask.is_none()
|
||||
}
|
||||
|
||||
/// `self`, or `None` when nothing was picked.
|
||||
fn some(self) -> Option<Self> {
|
||||
(!self.is_empty()).then_some(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The children of `n`, if it is a layer, that `pick` picks (with nothing inside), and its
|
||||
/// sublayers holding one (with what they hold), or `None` when it holds none. `pick` looks at
|
||||
/// each child of the layers once.
|
||||
fn picked_in_layers(n: &Node, pick: &impl Fn(&Node) -> bool) -> Option<Picked> {
|
||||
if !n.is_layer() {
|
||||
return None;
|
||||
}
|
||||
let children = n
|
||||
.children()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.enumerate()
|
||||
.filter_map(|(i, c)| if pick(c) { Some((i, Picked::default())) } else { picked_in_layers(c, pick).map(|p| (i, p)) })
|
||||
.collect();
|
||||
Picked { children, mask: None }.some()
|
||||
}
|
||||
|
||||
/// Remove the children of `layer` and of its sublayers that [`picked_in_layers`] picked (`held`).
|
||||
/// Only the layers holding one are copied, and only those are visited.
|
||||
fn drop_picked(layer: &mut Arc<Node>, held: Picked) {
|
||||
let Some(ch) = Arc::make_mut(layer).children_mut() else { return };
|
||||
let mut held = held.children.into_iter().peekable();
|
||||
let mut next = 0;
|
||||
ch.retain_mut(|c| {
|
||||
let at = next;
|
||||
next += 1;
|
||||
let Some((_, inner)) = held.next_if(|(i, _)| *i == at) else { return true };
|
||||
// A sublayer holds something; a child picked itself has nothing inside.
|
||||
if inner.is_empty() {
|
||||
return false;
|
||||
}
|
||||
drop_picked(c, inner);
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
/// Change with `f` (whether it is vertical, its position) the line of each ruler guide of `layer`
|
||||
/// and of its sublayers that [`picked_in_layers`] picked (`held`), in the order they lie in. Only
|
||||
/// the layers holding one, and the guides, are copied, and only those layers are visited.
|
||||
fn edit_picked(layer: &mut Arc<Node>, held: Picked, f: &mut impl FnMut(bool, &mut f64)) {
|
||||
let Some(ch) = Arc::make_mut(layer).children_mut() else { return };
|
||||
for (i, inner) in held.children {
|
||||
let Some(c) = ch.get_mut(i) else { continue };
|
||||
if !inner.is_empty() {
|
||||
edit_picked(c, inner, f);
|
||||
} else if let NodeKind::RulerGuide { vertical, pos, .. } = &mut Arc::make_mut(c).kind {
|
||||
f(*vertical, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `n` picked itself when it is a ruler guide by `is_guide`, else the ruler guides below it
|
||||
/// ([`guides_below`]).
|
||||
fn guide_or_below(n: &Node, is_guide: &impl Fn(&Node) -> bool) -> Option<Picked> {
|
||||
if is_guide(n) { Some(Picked::default()) } else { guides_below(n, is_guide) }
|
||||
}
|
||||
|
||||
/// The ruler guides below `n`, by `is_guide`: those among its members and its opacity-mask art
|
||||
/// (each with nothing inside), and the members and mask art holding one further down (with what
|
||||
/// they hold), or `None` when there is none. `is_guide` looks at each node below `n` once, and at
|
||||
/// nothing inside a ruler guide.
|
||||
fn guides_below(n: &Node, is_guide: &impl Fn(&Node) -> bool) -> Option<Picked> {
|
||||
let mask = n.extras().mask.as_deref().and_then(|m| guide_or_below(&m.art, is_guide)).map(Box::new);
|
||||
let children = n.children().into_iter().flatten().enumerate().filter_map(|(i, c)| guide_or_below(c, is_guide).map(|p| (i, p))).collect();
|
||||
Picked { children, mask }.some()
|
||||
}
|
||||
|
||||
/// Remove the ruler guides below `n` that [`guides_below`] `found`, in its members and its
|
||||
/// opacity-mask art (`n` itself stays). Mask art that is a ruler guide becomes an empty group.
|
||||
/// Only the nodes holding one are copied, and only those are visited. Returns how many went.
|
||||
fn drop_guides_below(n: &mut Node, found: Picked) -> usize {
|
||||
let mut gone = drop_mask_guides(n, found.mask);
|
||||
if let Some(ch) = n.children_mut() {
|
||||
let mut found = found.children.into_iter().peekable();
|
||||
let mut next = 0;
|
||||
ch.retain_mut(|c| {
|
||||
let at = next;
|
||||
next += 1;
|
||||
let Some((_, inner)) = found.next_if(|(i, _)| *i == at) else { return true };
|
||||
if inner.is_empty() {
|
||||
gone += 1;
|
||||
return false;
|
||||
}
|
||||
gone += drop_guides_below(Arc::make_mut(c), inner);
|
||||
true
|
||||
});
|
||||
}
|
||||
gone
|
||||
}
|
||||
|
||||
/// Remove every ruler guide in the opacity-mask art of `n`. Mask art that is a ruler guide
|
||||
/// becomes an empty group. Returns how many went.
|
||||
fn drop_mask_guides(n: &mut Node) -> usize {
|
||||
/// Remove the ruler guides `found` in the opacity-mask art of `n` ([`guides_below`]'s mask
|
||||
/// part). Mask art that is a ruler guide becomes an empty group. Returns how many went.
|
||||
fn drop_mask_guides(n: &mut Node, found: Option<Box<Picked>>) -> usize {
|
||||
let Some(found) = found else { return 0 };
|
||||
let Some(mask) = n.extras_if_any().and_then(|e| e.mask.as_deref_mut()) else { return 0 };
|
||||
if mask.art.is_ruler_guide() {
|
||||
if found.is_empty() {
|
||||
mask.art = Arc::new(Node::group(mask.art.id, vec![]));
|
||||
1
|
||||
} else if any_guide(&mask.art) {
|
||||
drop_guides_below(Arc::make_mut(&mut mask.art))
|
||||
} else {
|
||||
0
|
||||
drop_guides_below(Arc::make_mut(&mut mask.art), *found)
|
||||
}
|
||||
}
|
||||
|
||||
/// Does layer `l` hold a ruler guide out of place: below art, in an object or in mask art, or so
|
||||
/// in one of its sublayers?
|
||||
fn layer_untidy(l: &Node) -> bool {
|
||||
let Some(ch) = l.children() else { return false };
|
||||
let start = Node::guide_run_start(ch);
|
||||
l.extras().mask.as_deref().is_some_and(|m| any_guide(&m.art))
|
||||
|| ch.iter().take(start).any(|c| c.is_ruler_guide() || if c.is_layer() { layer_untidy(c) } else { any_guide(c) })
|
||||
/// The ruler guides, by `is_guide`, that layer `l` holds out of place, or `None` when it holds
|
||||
/// none: those below art (each with nothing inside), those in its mask art and in its objects
|
||||
/// ([`guides_below`]), and its sublayers holding one out of place (with what they hold).
|
||||
/// `is_guide` looks at each node once; the guides at the end of a layer are in place and are
|
||||
/// not looked at.
|
||||
fn untidy(l: &Node, is_guide: &impl Fn(&Node) -> bool) -> Option<Picked> {
|
||||
let ch = l.children()?;
|
||||
let mask = l.extras().mask.as_deref().and_then(|m| guide_or_below(&m.art, is_guide)).map(Box::new);
|
||||
let children = ch
|
||||
.iter()
|
||||
.enumerate()
|
||||
.take(Node::guide_run_start(ch))
|
||||
.filter_map(|(i, c)| {
|
||||
let found = if is_guide(c) {
|
||||
Some(Picked::default())
|
||||
} else if c.is_layer() {
|
||||
untidy(c, is_guide)
|
||||
} else {
|
||||
guides_below(c, is_guide)
|
||||
};
|
||||
found.map(|p| (i, p))
|
||||
})
|
||||
.collect();
|
||||
Picked { children, mask }.some()
|
||||
}
|
||||
|
||||
/// [`Document::tidy_ruler_guides`] for layer `l`. Returns how many guides were removed.
|
||||
fn tidy_layer(l: &mut Node) -> usize {
|
||||
let mut gone = drop_mask_guides(l);
|
||||
/// [`Document::tidy_ruler_guides`] for layer `l`, whose guides out of place [`untidy`] `found`.
|
||||
/// Only the nodes holding one are copied, and only those are visited. Returns how many guides
|
||||
/// were removed.
|
||||
fn tidy_layer(l: &mut Node, found: Picked) -> usize {
|
||||
let mut gone = drop_mask_guides(l, found.mask);
|
||||
let Some(ch) = l.children_mut() else { return gone };
|
||||
for c in ch.iter_mut() {
|
||||
if c.is_layer() {
|
||||
if layer_untidy(c) {
|
||||
gone += tidy_layer(Arc::make_mut(c));
|
||||
}
|
||||
} else if !c.is_ruler_guide() && any_guide(c) {
|
||||
gone += drop_guides_below(Arc::make_mut(c));
|
||||
let mut below_art = false;
|
||||
for (i, inner) in found.children {
|
||||
let Some(c) = ch.get_mut(i) else { continue };
|
||||
if inner.is_empty() {
|
||||
below_art = true;
|
||||
} else if c.is_layer() {
|
||||
gone += tidy_layer(Arc::make_mut(c), inner);
|
||||
} else {
|
||||
gone += drop_guides_below(Arc::make_mut(c), inner);
|
||||
}
|
||||
}
|
||||
if ch.iter().take(Node::guide_run_start(ch)).any(|c| c.is_ruler_guide()) {
|
||||
if below_art {
|
||||
let (guides, art): (Vec<_>, Vec<_>) = std::mem::take(ch).into_iter().partition(|c| c.is_ruler_guide());
|
||||
*ch = art;
|
||||
ch.extend(guides);
|
||||
@@ -698,3 +775,286 @@ impl std::fmt::Debug for GuideCache {
|
||||
f.write_str("GuideCache")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Appearance, OpacityMask, Symbol};
|
||||
use std::cell::Cell;
|
||||
use vectorcraft_geom::shapes;
|
||||
|
||||
/// The sublayers in [`nested`]'s chain: with the top-level layer around them and the objects in
|
||||
/// the innermost, the deepest a document goes ([`crate::MAX_DEPTH`]).
|
||||
const CHAIN: u64 = 30;
|
||||
|
||||
fn rect(id: u64) -> Arc<Node> {
|
||||
Arc::new(Node::path(NodeId(id), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art()))
|
||||
}
|
||||
|
||||
fn guide(id: u64, artboard: Option<u32>) -> Arc<Node> {
|
||||
Arc::new(Node::new(NodeId(id), NodeKind::RulerGuide { vertical: false, pos: id as f64, artboard }))
|
||||
}
|
||||
|
||||
fn group(id: u64, children: Vec<Arc<Node>>) -> Arc<Node> {
|
||||
Arc::new(Node::group(NodeId(id), children))
|
||||
}
|
||||
|
||||
/// `n` with `art` as its opacity-mask art.
|
||||
fn masked(n: Arc<Node>, art: Arc<Node>) -> Arc<Node> {
|
||||
let mut n = (*n).clone();
|
||||
n.extras_mut().mask = Some(Box::new(OpacityMask { art, clip: true, invert: false, disabled: false, linked: true }));
|
||||
Arc::new(n)
|
||||
}
|
||||
|
||||
/// A sublayer like a new document's first layer, holding `children`.
|
||||
fn sublayer(id: u64, children: Vec<Arc<Node>>) -> Arc<Node> {
|
||||
let mut l = (*Document::new(1.0, 1.0).layers[0]).clone();
|
||||
l.id = NodeId(id);
|
||||
if let Some(ch) = l.children_mut() {
|
||||
*ch = children;
|
||||
}
|
||||
Arc::new(l)
|
||||
}
|
||||
|
||||
/// A document whose first layer holds `children`.
|
||||
fn doc(children: Vec<Arc<Node>>) -> Document {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
if let Some(ch) = d.layers.first_mut().and_then(|l| Arc::make_mut(l).children_mut()) {
|
||||
*ch = children;
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
/// The ids of the nodes in the layers, in tree order.
|
||||
fn ids(d: &Document) -> Vec<u64> {
|
||||
fn walk(n: &Node, out: &mut Vec<u64>) {
|
||||
out.push(n.id.0);
|
||||
for c in n.children().into_iter().flatten() {
|
||||
walk(c, out);
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
for l in &d.layers {
|
||||
walk(l, &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// [`CHAIN`] sublayers, each inside the one before and holding an object, a canvas guide and a
|
||||
/// guide of artboard 7 (the innermost: object 150, guides 200 and 201). Beside the chain, a
|
||||
/// sublayer with a canvas guide only. Returns the document and that sublayer.
|
||||
fn nested() -> (Document, Arc<Node>) {
|
||||
let mut chain = sublayer(100, vec![rect(150), guide(200, None), guide(201, Some(7))]);
|
||||
for depth in 1..CHAIN {
|
||||
chain = sublayer(100 + depth, vec![rect(600 + depth), chain, guide(300 + depth * 2, None), guide(301 + depth * 2, Some(7))]);
|
||||
}
|
||||
let still = sublayer(500, vec![rect(502), guide(501, None)]);
|
||||
(doc(vec![chain, Arc::clone(&still)]), still)
|
||||
}
|
||||
|
||||
/// How many children the layers of [`nested`] hold: the first layer's 2, the untouched
|
||||
/// sublayer's 2, the innermost sublayer's 3 and 4 in each sublayer around it.
|
||||
const NESTED_CHILDREN: usize = 2 + 2 + 3 + (CHAIN as usize - 1) * 4;
|
||||
|
||||
fn on_artboard_7(n: &Node) -> bool {
|
||||
matches!(n.kind, NodeKind::RulerGuide { artboard: Some(7), .. })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_guides_looks_at_each_child_of_nested_layers_once() {
|
||||
let (mut d, still) = nested();
|
||||
assert!(!d.clone().limit_depth(), "the chain is as deep as a document goes");
|
||||
let mut deeper = doc(vec![sublayer(999, d.layers[0].children().cloned().unwrap_or_default())]);
|
||||
assert!(deeper.limit_depth(), "a document nested deeper is brought within the cap when it is read");
|
||||
let mut whole = d.clone();
|
||||
let looked = Cell::new(0_usize);
|
||||
let pick = |n: &Node| {
|
||||
looked.set(looked.get() + 1);
|
||||
on_artboard_7(n)
|
||||
};
|
||||
let held = picked_in_layers(&d.layers[0], &pick).unwrap();
|
||||
assert_eq!(looked.get(), NESTED_CHILDREN, "each child of the layers is looked at once");
|
||||
drop_picked(&mut d.layers[0], held);
|
||||
assert_eq!(looked.get(), NESTED_CHILDREN, "removing looks at nothing again");
|
||||
assert!(d.layers[0].children().is_some_and(|c| Arc::ptr_eq(&c[1], &still)), "a sublayer with nothing to remove is not copied");
|
||||
|
||||
// The command's own entry point removes the same guides.
|
||||
let before = whole.clone();
|
||||
let mut sel = Selection::default();
|
||||
sel.set_guides([NodeId(200), NodeId(201), NodeId(501)]);
|
||||
sel.objects.push(NodeId(150));
|
||||
sel.key_guide = Some(NodeId(201));
|
||||
assert_eq!(whole.retain_guides(&mut sel, |g| g.artboard != Some(7)), CHAIN as usize);
|
||||
assert_eq!(ids(&whole), ids(&d));
|
||||
assert_eq!(ids(&whole).len(), ids(&before).len() - CHAIN as usize);
|
||||
assert!(whole.ruler_guides().iter().all(|g| g.artboard.is_none()));
|
||||
assert_eq!(whole.ruler_guides().len(), CHAIN as usize + 1);
|
||||
assert_eq!((sel.objects.as_slice(), sel.guides.as_slice(), sel.key_guide), (&[NodeId(150)][..], &[NodeId(200), NodeId(501)][..], None));
|
||||
assert!(whole.layers[0].children().is_some_and(|c| Arc::ptr_eq(&c[1], &still)));
|
||||
// The document as it was (an undo step's copy) still holds every guide.
|
||||
assert_eq!(before.ruler_guides().len(), CHAIN as usize * 2 + 1);
|
||||
|
||||
// Saving the guides as a list, and removing a layer but not its guides, take the guides
|
||||
// out of the nested layers too.
|
||||
let mut saved = before.clone();
|
||||
saved.guides_to_saved();
|
||||
assert_eq!(saved.saved_guides, before.ruler_guides());
|
||||
assert_eq!(ids(&saved).len(), 1 + CHAIN as usize * 2 + 2);
|
||||
assert!(saved.layers[0].children().is_some_and(|c| !Arc::ptr_eq(&c[1], &still) && c[1].children().is_some_and(|c| c.len() == 1)));
|
||||
let mut edited = before.clone();
|
||||
let chain = edited.remove_edit_layer(NodeId(100 + CHAIN - 1)).unwrap();
|
||||
assert!(!chain.holds_ruler_guides());
|
||||
assert_eq!(edited.ruler_guides().len(), before.ruler_guides().len(), "the removed layer's guides stay in the document");
|
||||
assert_eq!(edited.guides_on(edited.layers[0].id).len(), CHAIN as usize * 2);
|
||||
|
||||
// Keeping every guide changes nothing and copies no layer.
|
||||
let kept = whole.clone();
|
||||
assert_eq!(whole.retain_guides(&mut sel, |_| true), 0);
|
||||
assert!(Arc::ptr_eq(&whole.layers[0], &kept.layers[0]));
|
||||
// Removing every guide leaves the objects.
|
||||
assert_eq!(whole.retain_guides(&mut sel, |_| false), CHAIN as usize + 1);
|
||||
assert!(whole.ruler_guides().is_empty() && sel.guides.is_empty());
|
||||
assert_eq!(ids(&whole).len(), 1 + CHAIN as usize * 2 + 2);
|
||||
assert_eq!(sel.objects, [NodeId(150)]);
|
||||
// A document without layers, and one without guides, have nothing to remove.
|
||||
let mut empty = Document::new(10.0, 10.0);
|
||||
assert_eq!(empty.retain_guides(&mut sel, |_| false), 0);
|
||||
empty.layers.clear();
|
||||
assert_eq!(empty.retain_guides(&mut sel, |_| false), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_an_artboards_guides_looks_at_each_child_of_nested_layers_once() {
|
||||
let (mut d, still) = nested();
|
||||
let mut whole = d.clone();
|
||||
let looked = Cell::new(0_usize);
|
||||
let pick = |n: &Node| {
|
||||
looked.set(looked.get() + 1);
|
||||
on_artboard_7(n)
|
||||
};
|
||||
let held = picked_in_layers(&d.layers[0], &pick).unwrap();
|
||||
assert_eq!(looked.get(), NESTED_CHILDREN, "each child of the layers is looked at once");
|
||||
let mut seen = vec![];
|
||||
edit_picked(&mut d.layers[0], held, &mut |vertical, pos: &mut f64| {
|
||||
seen.push((vertical, *pos));
|
||||
*pos += 9.0;
|
||||
});
|
||||
assert_eq!(looked.get(), NESTED_CHILDREN, "changing looks at nothing again");
|
||||
// A sublayer comes before its layer's own guides, so the innermost guide is changed first.
|
||||
let order: Vec<(bool, f64)> = [201.0].into_iter().chain((1..CHAIN).map(|depth| (301 + depth * 2) as f64)).map(|pos| (false, pos)).collect();
|
||||
assert_eq!(seen, order, "each guide of the artboard is changed once, in the order the guides lie in");
|
||||
assert!(d.layers[0].children().is_some_and(|c| Arc::ptr_eq(&c[1], &still)), "a sublayer with no guide of the artboard is not copied");
|
||||
|
||||
// The public moves change the same guides by the same amount.
|
||||
let before = whole.clone();
|
||||
whole.move_artboard_guides(7, Vec2::new(5.0, 9.0));
|
||||
assert_eq!(whole, d);
|
||||
assert_eq!(ids(&whole), ids(&before));
|
||||
for g in whole.ruler_guides() {
|
||||
let id = g.id.map_or(0, |id| id.0) as f64;
|
||||
assert_eq!(g.pos, if g.artboard == Some(7) { id + 9.0 } else { id }, "guide {id}");
|
||||
}
|
||||
assert!(whole.layers[0].children().is_some_and(|c| Arc::ptr_eq(&c[1], &still)));
|
||||
assert_eq!(before.ruler_guide(NodeId(201)).map(|g| g.pos), Some(201.0), "the document as it was keeps its guides");
|
||||
whole.map_artboard_guides(7, Affine::scale(2.0));
|
||||
assert_eq!(whole.ruler_guide(NodeId(201)).map(|g| g.pos), Some(420.0));
|
||||
assert_eq!(whole.ruler_guide(NodeId(200)).map(|g| g.pos), Some(200.0), "a canvas guide stays");
|
||||
|
||||
// An artboard without guides, and a document without layers, change nothing and copy no layer.
|
||||
let kept = whole.clone();
|
||||
whole.move_artboard_guides(99, Vec2::new(5.0, 9.0));
|
||||
assert!(Arc::ptr_eq(&whole.layers[0], &kept.layers[0]));
|
||||
let mut empty = Document::new(10.0, 10.0);
|
||||
empty.layers.clear();
|
||||
empty.move_artboard_guides(7, Vec2::new(5.0, 9.0));
|
||||
assert!(empty.layers.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guides_in_objects_are_found_by_looking_at_each_node_once() {
|
||||
// Groups, each inside the one before and holding an object and a guide. Beside them, an
|
||||
// object whose mask art holds a guide, one whose mask art is a guide, and a group with none.
|
||||
let mut chain = group(100, vec![rect(150), guide(200, None)]);
|
||||
for depth in 1..CHAIN {
|
||||
chain = group(100 + depth, vec![rect(600 + depth), chain, guide(300 + depth, None)]);
|
||||
}
|
||||
let still = group(700, vec![rect(701)]);
|
||||
let root = group(
|
||||
1,
|
||||
vec![chain, masked(rect(800), group(801, vec![guide(802, None), rect(803)])), masked(rect(810), guide(811, None)), Arc::clone(&still)],
|
||||
);
|
||||
let looked = Cell::new(0_usize);
|
||||
let is_guide = |n: &Node| {
|
||||
looked.set(looked.get() + 1);
|
||||
n.is_ruler_guide()
|
||||
};
|
||||
// The chain's groups, objects and guides; a masked object, its mask group and the two in
|
||||
// it; a masked object and its mask guide; the last group and its object.
|
||||
let nodes = CHAIN as usize * 3 + 4 + 2 + 2;
|
||||
let found = guides_below(&root, &is_guide).unwrap();
|
||||
assert_eq!(looked.get(), nodes, "each node below the root is looked at once");
|
||||
let mut cleared = (*root).clone();
|
||||
assert_eq!(drop_guides_below(&mut cleared, found), CHAIN as usize + 2);
|
||||
assert_eq!(looked.get(), nodes, "removing looks at nothing again");
|
||||
assert!(guides_below(&cleared, &Node::is_ruler_guide).is_none());
|
||||
let ch = cleared.children().unwrap();
|
||||
assert!(Arc::ptr_eq(&ch[3], &still), "a group without guides is not copied");
|
||||
assert_eq!(
|
||||
ch[1].extras().mask.as_deref().map(|m| m.art.children().map(|c| c.iter().map(|c| c.id.0).collect::<Vec<_>>())),
|
||||
Some(Some(vec![803]))
|
||||
);
|
||||
assert_eq!(ch[2].extras().mask.as_deref().map(|m| (m.art.id, m.art.children().map(Vec::len))), Some((NodeId(811), Some(0))));
|
||||
|
||||
// Opening a file removes the same guides from an object on a layer and from a symbol's art.
|
||||
let mut d = doc(vec![Arc::clone(&root), guide(900, None)]);
|
||||
d.symbols = vec![Symbol { name: "s".into(), art: Arc::clone(&root), dynamic: false }];
|
||||
assert_eq!(d.tidy_ruler_guides(), (CHAIN as usize + 2) * 2);
|
||||
assert_eq!(d.layers[0].children().map(|c| (*c[0]).clone()), Some(cleared.clone()));
|
||||
assert_eq!(*d.symbols[0].art, cleared);
|
||||
assert_eq!(d.ruler_guides().iter().map(|g| g.id).collect::<Vec<_>>(), [Some(NodeId(900))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guides_out_of_place_are_found_by_looking_at_each_node_once() {
|
||||
// Sublayers, each inside the one before, with their guides in place; the innermost also
|
||||
// holds a guide below its art. Beside them, a group holding a guide, a sublayer with its
|
||||
// guide in place, and a sublayer whose mask art holds a guide.
|
||||
let mut chain = sublayer(100, vec![guide(200, None), rect(150), guide(201, None)]);
|
||||
for depth in 1..CHAIN {
|
||||
chain = sublayer(100 + depth, vec![rect(600 + depth), chain, guide(300 + depth, None)]);
|
||||
}
|
||||
let still = sublayer(500, vec![rect(502), guide(501, None)]);
|
||||
let with_mask = masked(sublayer(510, vec![rect(513)]), group(511, vec![guide(512, None)]));
|
||||
let mut d = doc(vec![chain, group(700, vec![rect(701), guide(702, None)]), Arc::clone(&still), with_mask, guide(900, None)]);
|
||||
let mut whole = d.clone();
|
||||
let looked = Cell::new(0_usize);
|
||||
let is_guide = |n: &Node| {
|
||||
looked.set(looked.get() + 1);
|
||||
n.is_ruler_guide()
|
||||
};
|
||||
// Below the guides at the end of each layer: the first layer's 4, the group's 2, the
|
||||
// untouched sublayer's 1, the masked sublayer's mask group, its guide and the object, 2 in
|
||||
// the innermost sublayer and 2 in each sublayer around it.
|
||||
let nodes = 4 + 2 + 1 + 3 + 2 + (CHAIN as usize - 1) * 2;
|
||||
let found = untidy(&d.layers[0], &is_guide).unwrap();
|
||||
assert_eq!(looked.get(), nodes, "each node below the guides in place is looked at once");
|
||||
assert_eq!(tidy_layer(Arc::make_mut(&mut d.layers[0]), found), 2);
|
||||
assert_eq!(looked.get(), nodes, "tidying looks at nothing again");
|
||||
assert!(d.layers[0].children().is_some_and(|c| Arc::ptr_eq(&c[2], &still)), "a sublayer with its guides in place is not copied");
|
||||
assert!(untidy(&d.layers[0], &Node::is_ruler_guide).is_none());
|
||||
|
||||
// Opening a file does the same.
|
||||
let before = whole.clone();
|
||||
assert_eq!(whole.tidy_ruler_guides(), 2);
|
||||
assert_eq!(whole, d);
|
||||
let all = ids(&whole);
|
||||
assert!(!all.contains(&702) && all.len() == ids(&before).len() - 1);
|
||||
let innermost = whole.node(NodeId(100)).and_then(|n| n.children()).map(|c| c.iter().map(|c| c.id.0).collect::<Vec<_>>());
|
||||
assert_eq!(innermost, Some(vec![150, 200, 201]), "a guide below art moves up to the other guides");
|
||||
assert!(whole.node(NodeId(510)).is_some_and(|n| n.extras().mask.as_deref().is_some_and(|m| m.art.children().is_some_and(Vec::is_empty))));
|
||||
// A tidy document is left alone, and no layer is copied.
|
||||
let kept = whole.clone();
|
||||
assert_eq!(whole.tidy_ruler_guides(), 0);
|
||||
assert!(Arc::ptr_eq(&whole.layers[0], &kept.layers[0]));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user