From e9c26abe4d8377eb16683955f06180ad01f4a76f Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 06:11:07 -0800 Subject: [PATCH] Flatten: held ruler guides are gathered in one walk When a flattened root is a sublayer, its ruler guides were found by checking whether the sublayer held any and then walking its whole subtree again to collect them. They are now collected in one descent through the sublayer and its sublayers, looking at each of their children once. The guides, their order and the layer each one goes to are unchanged. A test-only record of the nodes looked at checks that each one is looked at once and that the guides keep their paint order. --- crates/engine/src/cmd/flatten/apply.rs | 86 +++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 7 deletions(-) diff --git a/crates/engine/src/cmd/flatten/apply.rs b/crates/engine/src/cmd/flatten/apply.rs index ef8425b362..b159fa7361 100644 --- a/crates/engine/src/cmd/flatten/apply.rs +++ b/crates/engine/src/cmd/flatten/apply.rs @@ -797,22 +797,47 @@ fn place_of(d: &Document, roots: &[NodeId]) -> Option { } /// The ruler guides of each of `roots` that is a sublayer (in its sublayers too), with the layer -/// that holds it, in order. Other objects hold none. +/// that holds it, in order. Other objects hold none. Each root, and each child of it and of its +/// sublayers, is looked at once. fn held_guides(d: &Document, roots: impl IntoIterator) -> Vec<(NodeId, Node)> { let mut out = vec![]; for id in roots { let (Some(n), Some(parent)) = (d.node(id), d.parent_of(id)) else { continue }; - if n.holds_ruler_guides() { - n.walk(&mut |c| { - if c.is_ruler_guide() { - out.push((parent, c.clone())); - } - }); + guide_seen(n); + if n.is_layer() { + guides_of(n, parent, &mut out); } } out } +/// Push each ruler guide of `layer` and of its sublayers onto `out` in paint order, with `parent`. +fn guides_of(layer: &Node, parent: NodeId, out: &mut Vec<(NodeId, Node)>) { + for c in layer.children().into_iter().flatten() { + guide_seen(c); + if c.is_ruler_guide() { + out.push((parent, Node::clone(c))); + } else if c.is_layer() { + guides_of(c, parent, out); + } + } +} + +#[cfg(test)] +thread_local! { + /// The nodes that [`held_guides`] has looked at on this thread, in order. + static GUIDES_SEEN: std::cell::RefCell> = const { std::cell::RefCell::new(Vec::new()) }; +} + +/// Note that [`held_guides`] looked at `n`. +#[cfg(test)] +fn guide_seen(n: &Node) { + GUIDES_SEEN.with(|s| s.borrow_mut().push(n.id)); +} + +#[cfg(not(test))] +fn guide_seen(_: &Node) {} + /// An image of `r` (in a clip group when it is clipped). fn raster_node(d: &mut Document, r: Raster) -> Node { let key = unique_key(d, "flattened"); @@ -922,6 +947,53 @@ mod tests { assert_eq!(flat.children(Some(layer)).and_then(|c| c.last()).map(|c| c.id), Some(g)); } + /// The guides of a sublayer and of its sublayers are gathered in paint order with the layer + /// that holds it, looking at the sublayer and each child of it and of its sublayers once. + /// Other roots give none. + #[test] + fn held_guides_look_at_each_node_once() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + let layer = s.doc().unwrap().doc.layers[0].id; + let sublayer = + |s: &mut Session, parent: NodeId| NodeId(s.execute("layer.newSublayer", &json!({"parent": parent.0})).unwrap()["id"].as_u64().unwrap()); + let rect = |s: &mut Session, on: NodeId| { + s.execute("layer.setCurrent", &json!({"id": on.0})).unwrap(); + NodeId(s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap()) + }; + let sub = sublayer(&mut s, layer); + let deep = sublayer(&mut s, sub); + let empty = sublayer(&mut s, layer); + let (a, b, c) = (rect(&mut s, sub), rect(&mut s, deep), rect(&mut s, empty)); + let d = Arc::make_mut(&mut s.doc_mut().unwrap().doc); + let mut guide = |on: NodeId, pos: f64| { + let g = Node::new(d.alloc_id(), NodeKind::RulerGuide { vertical: true, pos, artboard: None }); + d.insert(Some(on), usize::MAX, g).unwrap() + }; + let (g1, g2, g3) = (guide(sub, 1.0), guide(deep, 2.0), guide(sub, 3.0)); + let doc = &s.doc().unwrap().doc; + let ids = |n: &Node| { + let mut v = vec![]; + n.walk(&mut |c| v.push(c.id)); + v + }; + assert_eq!(ids(doc.node(sub).unwrap()), vec![sub, deep, b, g2, a, g1, g3]); + let run = |roots: &[NodeId]| { + GUIDES_SEEN.with(|v| v.borrow_mut().clear()); + let got: Vec<_> = held_guides(doc, roots.iter().copied()).into_iter().map(|(p, g)| (p, g.id)).collect(); + (got, GUIDES_SEEN.with(|v| v.take())) + }; + // Every node of the sublayer is looked at once, and the guides keep their paint order. + assert_eq!(run(&[sub]), (vec![(layer, g2), (layer, g1), (layer, g3)], vec![sub, deep, b, g2, a, g1, g3])); + // A sublayer that holds none gives none; a path holds none; a missing root and a top-level + // layer, which has no parent, are skipped. + assert_eq!(run(&[empty]), (vec![], vec![empty, c])); + assert_eq!(run(&[a]), (vec![], vec![a])); + assert_eq!(run(&[NodeId(u64::MAX), layer]), (vec![], vec![])); + // A deeper sublayer gives its guides to the sublayer that holds it. + assert_eq!(run(&[deep, deep]).0, vec![(sub, g2), (sub, g2)]); + } + /// In a file without transparency with Clip Complex Regions on, art under a group's opaque /// image within one of its pixels joins the group, also where a long image is made at a lower /// resolution and its pixels are wider than at the preset's.