From 3c80b314e150a53cb4c1b664ffc8f4c85e41d781 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 06:52:05 -0800 Subject: [PATCH] Select: All on Same Layers takes the layers of objects that can be selected Select > Object > All on Same Layers took the layer of every selected object, including one that select.set had kept while it is hidden or locked, on a template layer, on a locked artboard, or outside the isolated group. That object's layer then decided which art was selected. The command now takes the layers only of the selected objects that the Select commands could select. When the selection holds no such object, it clears the selection. --- crates/engine/src/cmd/select.rs | 12 ++++++-- crates/engine/src/tests/menucmds.rs | 44 +++++++++++++++++++++++++++++ docs/mcp.md | 5 ++++ docs/target-app-parity.md | 1 + 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/select.rs b/crates/engine/src/cmd/select.rs index 5874c76ab..25e0123ca 100644 --- a/crates/engine/src/cmd/select.rs +++ b/crates/engine/src/cmd/select.rs @@ -819,8 +819,16 @@ fn same_attribute(s: &mut Session, p: &Value) -> Result { fn same_layers(s: &mut Session, _: &Value) -> Result { let st = s.doc()?; - // The layers or sublayers the selected objects are on (art in their sublayers too). - let layers: BTreeSet = st.selection.objects.iter().filter_map(|id| st.doc.layer_containing(*id)).collect(); + // The layers or sublayers the selected objects are on (art in their sublayers too). A selected + // object that `is_selectable` does not accept adds no layer. + let locks = st.doc.locked_artboards(); + let layers: BTreeSet = st + .selection + .objects + .iter() + .filter(|id| is_selectable(&st.doc, st.isolation, &locks, **id)) + .filter_map(|id| st.doc.layer_containing(*id)) + .collect(); let mut on = BTreeSet::new(); for l in layers.iter().filter_map(|l| st.doc.node(*l)) { l.walk(&mut |n| { diff --git a/crates/engine/src/tests/menucmds.rs b/crates/engine/src/tests/menucmds.rs index 8c18eae43..d14502659 100644 --- a/crates/engine/src/tests/menucmds.rs +++ b/crates/engine/src/tests/menucmds.rs @@ -2801,6 +2801,50 @@ fn select_menu_commands_leave_out_an_isolated_group_hidden_or_locked_since() { assert_eq!(selected(&s), vec![]); } +/// All on Same Layers takes the layers only of the selected objects that can be selected: an +/// object that is hidden or locked, on a template layer, or outside the isolated group adds no +/// layer. When the selection holds no such object, the command selects nothing. +#[test] +fn select_all_on_same_layers_skips_objects_that_cannot_be_selected() { + let mut s = session(); + let [hidden, a] = [0.0, 20.0].map(|x| rect(&mut s, x, 0.0, 10.0, 10.0)); + s.execute("layer.new", &json!({"top": true})).unwrap(); + let [b, c, d] = [0.0, 20.0, 40.0].map(|x| rect(&mut s, x, 40.0, 10.0, 10.0)); + let same = |s: &mut Session, from: &[NodeId]| { + sel(s, from); + assert_eq!(selected(s), from, "select.set keeps {from:?}"); + s.execute("select.object.allOnSameLayers", &json!({})).unwrap(); + let mut v = selected(s); + v.sort(); + v + }; + assert_eq!(same(&mut s, &[a, b]), vec![hidden, a, b, c, d]); + // A hidden or locked object on the first layer adds no layer. + for (on, off) in [ + (json!({"id": hidden.0, "visible": false}), json!({"id": hidden.0, "visible": true})), + (json!({"id": hidden.0, "locked": true}), json!({"id": hidden.0, "locked": false})), + ] { + s.execute("layer.setProps", &on).unwrap(); + assert_eq!(same(&mut s, &[hidden, b]), vec![b, c, d], "{on}"); + s.execute("layer.setProps", &off).unwrap(); + } + // An object on a template layer adds no layer. + let l3 = id_of(&s.execute("layer.new", &json!({"top": true})).unwrap()); + let on_template = rect(&mut s, 0.0, 80.0, 10.0, 10.0); + s.execute("layer.setProps", &json!({"id": l3.0, "template": true})).unwrap(); + assert_eq!(same(&mut s, &[on_template, b]), vec![b, c, d]); + // An object outside the isolated group adds no layer, even though its layer holds the group. + sel(&mut s, &[c, d]); + let g = id_of(&s.execute("object.group", &json!({})).unwrap()); + s.execute("object.isolate", &json!({"id": g.0})).unwrap(); + assert_eq!(same(&mut s, &[b]), vec![]); + assert_eq!(same(&mut s, &[c]), vec![c, d]); + s.execute("object.exitIsolation", &json!({})).unwrap(); + // With no object that can be selected, nothing is selected. + s.execute("layer.setProps", &json!({"id": hidden.0, "visible": false})).unwrap(); + assert_eq!(same(&mut s, &[hidden, on_template]), vec![]); +} + /// The selection after command `c` runs with nothing selected. fn select_from_none(s: &mut Session, c: &str) -> Vec { s.execute("select.none", &json!({})).unwrap(); diff --git a/docs/mcp.md b/docs/mcp.md index bd73b2658..3c57dc208 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1490,6 +1490,11 @@ template layer, on a locked artboard while nothing is isolated, or outside the i such as a symbol instance just placed, is not passed over for being on a locked artboard while it stays selected. When the selection holds no other object, they return the error "nothing selected". +`select.object.allOnSameLayers` selects the objects on the layers and sublayers of the selected objects that can be +selected. A selected object that is hidden or locked, inside a hidden or locked group or layer, on a template layer, on +a locked artboard while nothing is isolated, or outside the isolated group adds no layer. When the selection holds no +other object, the command clears the selection. + ## Shaper Groups `shaper.releaseSource {source, group?}` takes the original shape `source` out of its Shaper Group: the shape becomes a diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index fe801adcc..38be922ad 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -940,6 +940,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Select › Same › Text Fill & Stroke Color selects the type whose characters match both the fill and the stroke of the selected type (`select.same.textFillAndStrokeColor`). The menu entry is listed in [Select objects by characteristics](https://helpx.adobe.com/illustrator/desktop/manage-objects/select-objects/select-objects-by-characteristics.html); requiring one run of characters to match both is VectorCraft's own behavior. The Select › Same font and text color commands take the type to match from the selected type that can be selected: type that is hidden or locked, on a template layer, or outside the isolated group is passed over. This is VectorCraft's own behavior. Select › Same › Fill Color, Stroke Color, Fill & Stroke, Stroke Weight, Opacity, Blending Mode, Appearance, Shape, Graphic Style, Appearance Attribute and Symbol Instance compare the other objects with the first selected object that can be selected, of any kind: an object that is hidden or locked, on a template layer, on a locked artboard while nothing is isolated (except new art while it stays selected), or outside the isolated group is passed over. This is VectorCraft's own behavior. + Select › Object › All on Same Layers takes the layers only of the selected objects that can be selected: an object that is hidden or locked, on a template layer, on a locked artboard while nothing is isolated, or outside the isolated group adds no layer. This is VectorCraft's own behavior. Object › Flatten Transparency takes a frame of threaded text out of the thread in the same way when Convert All Text to Outlines outlines it or when the frame becomes flat regions or an image, and the other frames keep the text they hold. New Symbol and Redefine Symbol on a frame of threaded text take it out of the thread with the text it shows, which the symbol's art holds, and the other frames keep the text they hold. When the last frame goes into the symbol, the text it has no room for goes after the text of the last frame that stays. When every frame of a thread goes into the symbol, the copy of the last frame keeps all its text. With Optical Margin Alignment on, or Roman Hanging Punctuation on in the first paragraph, the glyphs at the ends of left-to-right type on a path hang outside its brackets, measured along the path. The type hangs at the start bracket when its line is placed there, hangs at the end bracket when the line is placed there, and hangs at both when it is centered. Glyphs that hang past the end bracket do not make the type overflow, and a hang stops at an open path's end. SVG export writes such type as outlines.