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