From 405cc83caeaa068d2536969a67e889a9c6a1d42e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 1 Nov 2026 20:41:57 -0800 Subject: [PATCH] Select: Link Block Series ignores selected type that Select All cannot reach Type selected by id on a hidden, locked or template layer, or outside the isolated group, no longer counts as selected type: the command stays off and does not select that frame's thread. The check reads the object's ancestry, so the Select menu does not walk the whole document. --- crates/engine/src/cmd/docmenu.rs | 17 +++++++++++------ crates/engine/src/cmd/select.rs | 19 +++++++++++++++++++ crates/engine/src/tests/menucmds.rs | 16 ++++++++++++++++ docs/mcp.md | 5 +++-- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/crates/engine/src/cmd/docmenu.rs b/crates/engine/src/cmd/docmenu.rs index 41b8f3de5..b44d47637 100644 --- a/crates/engine/src/cmd/docmenu.rs +++ b/crates/engine/src/cmd/docmenu.rs @@ -11,7 +11,7 @@ use vectorcraft_geom::Vec2; use super::edit::selected_roots; use super::pathops::Beside; -use super::select::{selectable_ids, walk_selectable}; +use super::select::{is_selectable, selectable_ids, walk_selectable}; use super::{ CommandSpec, EngineError, Point, Result, Session, bad, bool_or, checked_ids_param, cmd, f64_or, f64_req, has_doc, has_selection, id_param, ok, str_param, @@ -32,7 +32,7 @@ pub fn specs() -> Vec { "Link Block Series", ["Select", "Same"], None, - "{} select every frame of the threads of the selected type objects (and of the visible, unlocked type inside selected groups); type in no thread selects itself; frames that are hidden or locked, inside a hidden or locked object or layer, on a template layer, or outside the isolated group or layer are left out → {count}", + "{} select every frame of the threads of the selected type objects (and of the type inside selected groups); type in no thread selects itself; type and frames that are hidden or locked, inside a hidden or locked object or layer, on a template layer, or outside the isolated group or layer neither count as selected nor are selected → {count}", has_text_selection, same_link_block_series ), @@ -322,8 +322,9 @@ fn same_text(s: &mut Session, cmd: &str, eq: fn(&CharStyle, &CharStyle) -> bool) finish_same(s, cmd, ids) } -/// The type objects among the selected objects and inside them, in selection order, leaving out -/// every hidden or locked object with what it holds. +/// The type objects among the selected objects and inside them, in selection order, keeping only +/// those [`walk_selectable`] visits: none that is hidden, locked, on a hidden, locked or template +/// layer, or outside the isolated group. fn selected_text(s: &Session) -> Vec { fn inside(n: &Node, out: &mut Vec) { if !n.visible || n.locked { @@ -338,8 +339,12 @@ fn selected_text(s: &Session) -> Vec { } let Some(st) = s.active() else { return vec![] }; let mut out = vec![]; - for n in st.selection.objects.iter().filter_map(|id| st.doc.node(*id)) { - inside(n, &mut out); + for id in &st.selection.objects { + if is_selectable(&st.doc, st.isolation, *id) + && let Some(n) = st.doc.node(*id) + { + inside(n, &mut out); + } } out } diff --git a/crates/engine/src/cmd/select.rs b/crates/engine/src/cmd/select.rs index a98a78392..1f76151bb 100644 --- a/crates/engine/src/cmd/select.rs +++ b/crates/engine/src/cmd/select.rs @@ -249,6 +249,25 @@ pub(crate) fn selectable_ids(d: &Document, iso: Option) -> BTreeSet, id: NodeId) -> bool { + let Some(a) = d.ancestry(id) else { return false }; + if !a.iter().all(|i| d.node(*i).is_some_and(|n| n.visible && !n.locked && !n.is_template())) { + return false; + } + match iso.and_then(|i| d.node(i)) { + // Inside the isolated container, or the single object isolated. + Some(c) => a.iter().position(|i| *i == c.id).is_some_and(|p| p + 1 < a.len() || !c.is_container()), + None => { + let locks = d.locked_artboards(); + // The object Select All takes: the outermost one that is not a layer. + let top = a.iter().filter_map(|i| d.node(*i)).find(|n| !n.is_layer()); + top.is_some_and(|t| locks.is_empty() || !locks.holds(t)) + } + } +} + fn all(s: &mut Session, _: &Value) -> Result { // While the Type tool edits text, Select All takes all of that text and leaves the art // selection as it is (Illustrator: text cursor in a text object). diff --git a/crates/engine/src/tests/menucmds.rs b/crates/engine/src/tests/menucmds.rs index a3ad38a98..7bac52cc9 100644 --- a/crates/engine/src/tests/menucmds.rs +++ b/crates/engine/src/tests/menucmds.rs @@ -1993,6 +1993,22 @@ fn select_same_link_block_series_selects_the_frames_of_a_thread() { assert!(s.execute("select.same.linkBlockSeries", &json!({})).is_err(), "{props}"); assert_eq!(selected(&s), vec![g], "{props}"); } + s.execute("layer.setProps", &json!({"id": d.0, "locked": false})).unwrap(); + // A frame on a hidden layer, selected by id, is not type to start from. + let l1 = s.doc().unwrap().doc.layers[0].id; + s.execute("layer.setProps", &json!({"id": l1.0, "visible": false})).unwrap(); + sel(&mut s, &[a]); + assert!(!find_command("select.same.linkBlockSeries").unwrap().info(&s).enabled); + assert!(s.execute("select.same.linkBlockSeries", &json!({})).is_err()); + s.execute("layer.setProps", &json!({"id": l1.0, "visible": true})).unwrap(); + // In an isolated group, only the frames inside it count and are selected. + s.execute("object.isolate", &json!({"id": g.0})).unwrap(); + sel(&mut s, &[d]); + assert_eq!(s.execute("select.same.linkBlockSeries", &json!({})).unwrap()["count"], 1); + assert_eq!(selected(&s), vec![d]); + sel(&mut s, &[a]); + assert!(!find_command("select.same.linkBlockSeries").unwrap().info(&s).enabled); + assert!(s.execute("select.same.linkBlockSeries", &json!({})).is_err()); } /// #1737: Select > Same reads type's fill and stroke from its characters (the first run), so type diff --git a/docs/mcp.md b/docs/mcp.md index 174960488..19b072579 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -4886,8 +4886,9 @@ doesn't exist, a `port` other than `in` or `out`, and a side where no frame of a `select.same.linkBlockSeries` (Select › Same › Link Block Series) selects every frame of the threads that the selected type objects, and the type inside selected groups, are in, in thread order, and returns `{count}`. Type in no thread selects itself, and other selected objects are left out. With frames of several threads selected, it selects the frames -of all of them. Hidden or locked type inside a selected group does not count as selected, and hidden or locked frames of -a thread are left out. It is off while no type is selected, and records no undo step. +of all of them. Type that is hidden or locked, inside a hidden or locked group or layer, on a template layer, or outside +the isolated group does not count as selected, and such frames of a thread are left out. It is off while no other type +is selected, and records no undo step. In every thread, a paragraph that continues from one frame into the next has its first line, first-line indent and bullet or number in the frame where it starts. In the next frame its first line starts where its wrapped lines start,