Select: Select All skips hidden isolated groups, and menu commands stay within what it selects

Select All, All on Active Artboard and Inverse select from selectable:
the visible, unlocked objects on visible, unlocked, non-template
layers and sublayers, or in isolation mode the visible, unlocked
objects in the isolated group or layer and in its visible, unlocked,
non-template sublayers. Other Select menu commands did not use it:

- Clipping Masks, All Text Objects, Stray Points and Open Paths
  selected hidden and locked objects and the objects in hidden or
  locked groups and sublayers.
- Point Text Objects, Area Text Objects, Brush Strokes, Bristle Brush
  Strokes and the Same commands for symbols, fonts and text colors
  checked an object's own flags and its layer's, but not those of the
  groups and sublayers between them.
- Next Object Above and Below selected the object beside the selected
  one even when it was hidden, locked or a sublayer.
- A saved selection picked from the Select menu selected again the
  objects hidden or locked since it was saved.
- Direction Handles direct-selected the hidden and locked paths in a
  selected group.
- In isolation mode, the commands in the first two bullets, the other
  Same commands, All on Same Layers and a recalled saved selection
  selected art outside the isolated group or layer. Next Object Above
  and Below and Direction Handles did so only when the selection
  already held something that was not inside the isolated group or
  layer.

In isolation mode selectable checked the flags of the isolated group's
or layer's objects and sublayers, but not those of the group or layer
itself or of the groups and layers it is in. Select All, All on Active
Artboard and Inverse still selected the isolated group's or layer's
objects after it, or a group or layer it is in, was hidden or locked.

In isolation mode selectable now returns nothing while the isolated
group or layer, or a group or layer it is in, is hidden, locked or a
template layer. walk_selectable visits the objects selectable returns
and the objects inside them, leaving out hidden and locked objects
with what they hold. It visits nothing in a hidden, locked or template
layer, sublayer or group. The commands in the bullets above now select
from it. All on Same Layers takes the objects on the selected objects'
layers that selectable returns. Next Object Above and Below pass over
the objects walk_selectable leaves out, and leave the selection as it
is when none is left in that direction.

The params texts of Direction Handles and Recall Selection,
docs/mcp.md (Object selection, Saved selections) and one line in the
evidence list of docs/target-app-parity.md describe the change.
This commit is contained in:
dotCooCoo
2026-10-12 09:05:14 +07:00
committed by bflatastic
parent b94c13bfbb
commit d3be78000e
5 changed files with 354 additions and 46 deletions
+19 -12
View File
@@ -10,6 +10,7 @@ use vectorcraft_doc::{CharStyle, Guide, Node, NodeId, NodeKind, SavedSelection,
use vectorcraft_geom::Vec2;
use super::edit::selected_roots;
use super::select::{selectable_ids, walk_selectable};
use super::*;
pub fn specs() -> Vec<CommandSpec> {
@@ -57,7 +58,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Direction Handles",
["Select", "Object"],
None,
"{} direct-select every anchor (showing all handles) of the selected paths → {anchors}",
"{} direct-select every anchor (showing all handles) of the selected paths and the paths inside the selected objects, leaving out paths that are hidden or locked, inside a hidden or locked object or layer, on a template layer, or outside the isolated group or layer → {anchors}",
has_selection,
direction_handles
),
@@ -103,7 +104,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Recall Selection",
["Select"],
None,
"{name} select the objects of a saved selection (those since deleted are left out) → {count}",
"{name} select the objects of a saved selection, leaving out those deleted since and those that are hidden or locked, inside a hidden or locked object or layer, on a template layer, or outside the isolated group or layer → {count}",
has_saved_selection,
recall_selection
),
@@ -243,13 +244,12 @@ impl Session {
fn candidates(s: &Session, f: impl Fn(&Node) -> bool) -> Result<Vec<NodeId>> {
let st = s.doc()?;
let mut ids = vec![];
for l in st.doc.layers.iter().filter(|l| l.visible && !l.locked) {
l.walk(&mut |n| {
if !n.is_container() && n.visible && !n.locked && f(n) {
ids.push(n.id);
}
});
}
walk_selectable(&st.doc, st.isolation, |n| {
if !n.is_container() && f(n) {
ids.push(n.id);
}
true
});
Ok(ids)
}
@@ -304,12 +304,17 @@ fn by_kind(s: &mut Session, f: fn(&Node) -> bool) -> Result<Value> {
fn direction_handles(s: &mut Session, _: &Value) -> Result<Value> {
let roots = selected_roots(s)?;
let d = &s.doc()?.doc;
let st = s.doc()?;
let d = &st.doc;
// Only paths that `walk_selectable` visits.
let can = selectable_ids(d, st.isolation);
let mut items: Vec<(NodeId, BTreeSet<(usize, usize)>)> = vec![];
for r in &roots {
if let Some(n) = d.node(*r) {
n.walk(&mut |c| {
if let Some(p) = c.path_data() {
if can.contains(&c.id)
&& let Some(p) = c.path_data()
{
items.push((c.id, p.anchors().map(|(si, ai, _)| (si, ai)).collect()));
}
});
@@ -352,7 +357,9 @@ fn recall_selection(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_param(p, "name").ok_or_else(|| bad(C, "missing name"))?;
let ids = s.doc()?.doc.saved_selections.iter().find(|x| x.name == name).map(|x| x.objects.clone());
let ids = ids.ok_or_else(|| bad(C, format!("no saved selection `{name}`")))?;
s.select(|d, sel| sel.set(ids.into_iter().filter(|id| d.node(*id).is_some())))?;
let st = s.doc()?;
let can = selectable_ids(&st.doc, st.isolation);
s.select(|_, sel| sel.set(ids.into_iter().filter(|id| can.contains(id))))?;
Ok(json!({ "count": s.doc()?.selection.len() }))
}
+75 -33
View File
@@ -188,14 +188,53 @@ pub fn specs() -> Vec<CommandSpec> {
/// Selectable objects: children of visible, unlocked layers (or of the isolation container).
/// The objects Select All takes: those of the isolated container, else of every layer (looking
/// through sublayers, which are not objects).
/// through sublayers, which are not objects). In isolation mode there are none while the isolated
/// container, or a group or layer it is in, is hidden, locked or a template layer.
fn selectable(d: &Document, iso: Option<NodeId>) -> Vec<NodeId> {
match iso.and_then(|i| d.node(i)) {
Some(c) => c.layer_art(true),
Some(c) if d.ancestry(c.id).is_some_and(|a| a.iter().all(|i| d.node(*i).is_some_and(|n| n.visible && !n.locked && !n.is_template()))) => {
c.layer_art(true)
}
Some(_) => vec![],
None => d.selectable_art(),
}
}
/// Visit, in paint order, the objects Select All takes ([`selectable`]) and the objects inside
/// them, leaving out every hidden or locked object with what it holds. `f` returns whether to
/// visit the objects inside the one it is given.
pub(crate) fn walk_selectable(d: &Document, iso: Option<NodeId>, mut f: impl FnMut(&Node) -> bool) {
fn inside(n: &Node, f: &mut impl FnMut(&Node) -> bool) {
if n.visible && !n.locked && f(n) {
for c in n.children().into_iter().flatten() {
inside(c, f);
}
}
}
fn find(n: &Node, tops: &BTreeSet<NodeId>, f: &mut impl FnMut(&Node) -> bool) {
if tops.contains(&n.id) {
return inside(n, f);
}
for c in n.children().into_iter().flatten() {
find(c, tops, f);
}
}
let tops: BTreeSet<NodeId> = selectable(d, iso).into_iter().collect();
for l in &d.layers {
find(l, &tops, &mut f);
}
}
/// The objects [`walk_selectable`] visits.
pub(crate) fn selectable_ids(d: &Document, iso: Option<NodeId>) -> BTreeSet<NodeId> {
let mut ids = BTreeSet::new();
walk_selectable(d, iso, |n| {
ids.insert(n.id);
true
});
ids
}
fn all(s: &mut Session, _: &Value) -> Result<Value> {
// 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).
@@ -259,12 +298,17 @@ fn inverse(s: &mut Session, _: &Value) -> Result<Value> {
fn step(s: &mut Session, dir: i64) -> Result<Value> {
let st = s.doc()?;
let Some(cur) = st.selection.objects.first().copied() else { return ok() };
let Some((par, idx, n)) = st.doc.position(cur) else { return ok() };
let j = idx as i64 + dir;
if j < 0 || j >= n as i64 {
return ok();
}
let Some(id) = st.doc.children(par).and_then(|c| c.get(j as usize)).map(|n| n.id) else { return ok() };
let Some((par, idx, _)) = st.doc.position(cur) else { return ok() };
let Some(siblings) = st.doc.children(par) else { return ok() };
// The nearest sibling in that direction that `walk_selectable` visits: hidden and locked
// objects and sublayers are passed over.
let can = selectable_ids(&st.doc, st.isolation);
let next = if dir > 0 {
siblings.iter().skip(idx + 1).find(|n| can.contains(&n.id))
} else {
siblings.iter().take(idx).rev().find(|n| can.contains(&n.id))
};
let Some(id) = next.map(|n| n.id) else { return ok() };
s.select(|_, sel| sel.set([id]))?;
ok()
}
@@ -475,26 +519,18 @@ fn same(s: &mut Session, cmd: &str, eq: impl Fn(&Node, &Node) -> bool) -> Result
select_where(s, cmd, &json!({}), |_, n| !n.is_container() && eq(n, &refn))
}
/// Select the visible, unlocked objects in visible, unlocked layers that `f` accepts (not looking
/// inside accepted ones); Select > Reselect repeats `cmd` with `p`.
/// Select the objects [`walk_selectable`] visits that `f` accepts (not looking inside accepted
/// ones); Select > Reselect repeats `cmd` with `p`.
fn select_where(s: &mut Session, cmd: &str, p: &Value, f: impl Fn(&Document, &Node) -> bool) -> Result<Value> {
fn visit(d: &Document, n: &Node, f: &impl Fn(&Document, &Node) -> bool, ids: &mut Vec<NodeId>) {
// Hidden or locked objects and sublayers (and what they hold) are out of reach.
if !n.visible || n.locked || n.is_template() {
return;
}
if !n.is_layer() && f(d, n) {
return ids.push(n.id);
}
for c in n.children().into_iter().flatten() {
visit(d, c, f, ids);
}
}
let st = s.doc()?;
let mut ids = vec![];
for l in st.doc.layers.iter().filter(|l| l.visible && !l.locked) {
visit(&st.doc, l, &f, &mut ids);
}
walk_selectable(&st.doc, st.isolation, |n| {
if f(&st.doc, n) {
ids.push(n.id);
return false;
}
true
});
let from = st.selection.objects.clone();
s.select(|_, sel| sel.set(ids.iter().copied()))?;
s.doc_mut()?.last_selection_cmd = Some((cmd.to_string(), p.clone(), from));
@@ -543,7 +579,14 @@ fn same_layers(s: &mut Session, _: &Value) -> Result<Value> {
let st = s.doc()?;
// The layers or sublayers the selected objects are on (art in their sublayers too).
let layers: BTreeSet<NodeId> = st.selection.objects.iter().filter_map(|id| st.doc.layer_containing(*id)).collect();
let ids: Vec<NodeId> = layers.iter().filter_map(|l| st.doc.node(*l)).flat_map(|l| l.layer_art(true)).collect();
let mut on = BTreeSet::new();
for l in layers.iter().filter_map(|l| st.doc.node(*l)) {
l.walk(&mut |n| {
on.insert(n.id);
});
}
// The objects Select All takes there (in isolation mode, those in the isolated group or layer).
let ids: Vec<NodeId> = selectable(&st.doc, st.isolation).into_iter().filter(|id| on.contains(id)).collect();
s.select(|_, sel| sel.set(ids.iter().copied()))?;
ok()
}
@@ -551,13 +594,12 @@ fn same_layers(s: &mut Session, _: &Value) -> Result<Value> {
fn by_kind(s: &mut Session, f: fn(&Node) -> bool) -> Result<Value> {
let st = s.doc()?;
let mut ids = vec![];
for l in st.doc.layers.iter().filter(|l| l.visible && !l.locked) {
l.walk(&mut |n| {
if f(n) {
ids.push(n.id)
}
});
}
walk_selectable(&st.doc, st.isolation, |n| {
if f(n) {
ids.push(n.id);
}
true
});
s.select(|_, sel| sel.set(ids.iter().copied()))?;
Ok(json!({ "count": ids.len() }))
}