From e67277ebe7eb9f191380a43e4ad6204eaf162794 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 01:19:31 -0800 Subject: [PATCH] Hide: Unselected Artwork keeps groups that hold a selected layer or a locked guide A layer nested in a group whose art was all locked counted for nothing, so the group was hidden or locked with the layer inside it. The selected layer now counts as selected itself. While Lock Guides was on, a guide path that was locked itself, or sat in a locked object, was not kept, so the group around it was hidden. Guide paths inside locked objects are now kept too. --- crates/engine/src/cmd/menucmds.rs | 22 ++++++++++++-- crates/engine/src/tests/menucmds.rs | 45 +++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/menucmds.rs b/crates/engine/src/cmd/menucmds.rs index 73cf96c9b..5077408e4 100644 --- a/crates/engine/src/cmd/menucmds.rs +++ b/crates/engine/src/cmd/menucmds.rs @@ -347,7 +347,7 @@ fn other_layers(s: &mut Session, lock: bool) -> Result { } /// Returns the objects [`super::select::walk_selectable`] visits that are not among the selected objects `roots` and -/// hold none of them. A selected layer's objects count as selected. The walk goes into a plain group, but not a Shaper +/// hold none of them. A selected layer and its objects count as selected. The walk goes into a plain group, but not a Shaper /// group, that holds a selected object. With `lock`, it also goes into a clip group or compound path that holds one, /// and includes guide paths while `guides_locked`. Without `lock`, guide paths and the objects that hold one are left /// out while `guides_locked`; a clip group, Shaper group, compound path and every other object that holds a selected @@ -360,14 +360,32 @@ fn unselected( guides_locked: bool, lock: bool, ) -> Vec { + /// Pushes the visible guide paths in `n`, `n` included, locked ones too. + fn shown_guides(n: &vectorcraft_doc::Node, out: &mut Vec) { + if !n.visible { + return; + } + if matches!(n.kind, NodeKind::Path { guide: true, .. }) { + out.push(n.id); + } + for c in n.children().into_iter().flatten() { + shown_guides(c, out); + } + } let mut roots: HashSet = roots.iter().copied().collect(); - roots.extend(selection.iter().filter_map(|id| d.node(*id)).filter(|n| n.is_layer()).flat_map(|l| l.layer_art(true))); + let layers = || selection.iter().filter_map(|id| d.node(*id)).filter(|n| n.is_layer()); + roots.extend(layers().map(|l| l.id)); + roots.extend(layers().flat_map(|l| l.layer_art(true))); let mut kept: Vec = roots.iter().copied().collect(); if guides_locked && !lock { super::select::walk_selectable(d, iso, |n| { if matches!(n.kind, NodeKind::Path { guide: true, .. }) { kept.push(n.id); } + // The walk skips locked objects, so the guide paths in them are looked for here. + for c in n.children().into_iter().flatten().filter(|c| c.locked) { + shown_guides(c, &mut kept); + } true }); } diff --git a/crates/engine/src/tests/menucmds.rs b/crates/engine/src/tests/menucmds.rs index e47cabc5f..b5cb95255 100644 --- a/crates/engine/src/tests/menucmds.rs +++ b/crates/engine/src/tests/menucmds.rs @@ -865,6 +865,51 @@ fn hide_unselected_artwork_keeps_groups_with_locked_guides_and_the_selected_laye assert_eq!(selected(&s), [other]); } +/// While Lock Guides is on, Hide › Unselected Artwork keeps a group visible when it holds a guide path that is locked +/// itself, and hides the group's other members one by one. +#[test] +fn hide_unselected_artwork_keeps_groups_with_a_locked_guide_path() { + let mut s = session(); + let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0); + let guide = rect(&mut s, 40.0, 0.0, 10.0, 10.0); + let x = rect(&mut s, 60.0, 0.0, 10.0, 10.0); + sel(&mut s, &[guide, x]); + let g = id_of(&s.execute("object.group", &json!({})).unwrap()); + sel(&mut s, &[guide]); + s.execute("view.guides.make", &json!({})).unwrap(); + s.execute("layer.setProps", &json!({"id": guide.0, "locked": true})).unwrap(); + s.execute("view.guides.lock", &json!({"locked": true})).unwrap(); + sel(&mut s, &[a]); + assert_eq!(s.execute("object.hide.unselected", &json!({})).unwrap(), json!({"count": 1})); + assert_eq!([a, g, guide, x].map(|id| node(&s, id).visible), [true, true, true, false]); +} + +/// Hide › Unselected Artwork keeps a group visible when it holds a selected layer whose art is all locked, and hides the +/// group's other members one by one. +#[test] +fn hide_unselected_artwork_keeps_a_group_that_holds_a_selected_layer() { + let mut s = session(); + let b = rect(&mut s, 0.0, 0.0, 10.0, 10.0); + let y = rect(&mut s, 20.0, 0.0, 10.0, 10.0); + let z = rect(&mut s, 40.0, 0.0, 10.0, 10.0); + sel(&mut s, &[y]); + let g = id_of(&s.execute("object.group", &json!({})).unwrap()); + let sub = s + .edit("nest", |d, _| { + let id = d.alloc_id(); + d.insert(Some(g), usize::MAX, Node::layer(id, "Sub", vectorcraft_doc::LayerColor::Preset(1))) + .map_err(|e| EngineError::Other(e.to_string()))?; + let art = d.remove(z).map_err(|e| EngineError::Other(e.to_string()))?; + d.insert(Some(id), 0, (*art).clone()).map_err(|e| EngineError::Other(e.to_string()))?; + Ok(id) + }) + .unwrap(); + s.execute("layer.setProps", &json!({"id": z.0, "locked": true})).unwrap(); + sel(&mut s, &[sub]); + assert_eq!(s.execute("object.hide.unselected", &json!({})).unwrap(), json!({"count": 2})); + assert_eq!([b, g, y, sub, z].map(|id| node(&s, id).visible), [false, true, false, true, true]); +} + /// Lock Unselected Artwork locks `b`, `n` in the group that holds selected `m`, `p` on another layer, and the clip /// group's unselected members `c1` and clipping path `c3` in one undo step labeled "Lock". The clip group container, /// `c2`, the selected objects, group `g`, hidden object `h` and art `q` on a locked layer stay unlocked. `k` was locked