Merge pull request #1325 from dotCooCoo/feat/select-menu-selectable-only
Select: Select All skips hidden isolated groups, and menu commands stay within what it selects
This commit is contained in:
commit
2b7d5f964f
|
|
@ -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() }))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() }))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -679,6 +679,21 @@ fn select_direction_handles_selects_all_anchors() {
|
|||
assert_eq!(s.doc().unwrap().selection.anchors.get(&a).unwrap().len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direction_handles_leave_out_hidden_and_locked_paths() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
let hidden = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||||
let locked = rect(&mut s, 40.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[a, hidden, locked]);
|
||||
let g = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
s.execute("object.setProps", &json!({"ids": [hidden.0], "visible": false})).unwrap();
|
||||
s.execute("object.setProps", &json!({"ids": [locked.0], "locked": true})).unwrap();
|
||||
sel(&mut s, &[g]);
|
||||
assert_eq!(s.execute("select.object.directionHandles", &json!({})).unwrap()["anchors"], 4);
|
||||
assert_eq!(selected(&s), vec![a]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_point_and_area_text() {
|
||||
let mut s = session();
|
||||
|
|
@ -692,6 +707,214 @@ fn select_point_and_area_text() {
|
|||
assert_eq!(s.execute("select.object.brushStrokes", &json!({})).unwrap()["count"], 0);
|
||||
}
|
||||
|
||||
/// Select › Object and Select › Same leave out hidden and locked objects, and the objects in hidden
|
||||
/// or locked groups and sublayers, as Select All does.
|
||||
#[test]
|
||||
fn select_menu_commands_leave_out_hidden_and_locked_art() {
|
||||
let mut s = session();
|
||||
let layer = s.doc().unwrap().doc.layers[0].id;
|
||||
let shown = text(&mut s, "shown");
|
||||
let hidden = text(&mut s, "hidden");
|
||||
let locked = text(&mut s, "locked");
|
||||
let in_hidden_group = text(&mut s, "in a hidden group");
|
||||
let in_locked_group = text(&mut s, "in a locked group");
|
||||
let open = id_of(&s.execute("shape.line", &json!({"x1": 0, "y1": 0, "x2": 10, "y2": 10})).unwrap());
|
||||
let hidden_open = id_of(&s.execute("shape.line", &json!({"x1": 0, "y1": 20, "x2": 10, "y2": 30})).unwrap());
|
||||
sel(&mut s, &[in_hidden_group]);
|
||||
let hidden_group = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
sel(&mut s, &[in_locked_group]);
|
||||
let locked_group = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
let hidden_sub = id_of(&s.execute("layer.newSublayer", &json!({"parent": layer.0})).unwrap());
|
||||
let in_hidden_sub = text(&mut s, "in a hidden sublayer");
|
||||
let locked_sub = id_of(&s.execute("layer.newSublayer", &json!({"parent": layer.0})).unwrap());
|
||||
let in_locked_sub = text(&mut s, "in a locked sublayer");
|
||||
assert_eq!(s.doc().unwrap().doc.parent_of(in_hidden_sub), Some(hidden_sub));
|
||||
assert_eq!(s.doc().unwrap().doc.parent_of(in_locked_sub), Some(locked_sub));
|
||||
s.execute("object.setProps", &json!({"ids": [hidden.0, hidden_open.0, hidden_group.0], "visible": false})).unwrap();
|
||||
s.execute("object.setProps", &json!({"ids": [locked.0, locked_group.0], "locked": true})).unwrap();
|
||||
s.execute("layer.setProps", &json!({"ids": [hidden_sub.0], "visible": false})).unwrap();
|
||||
s.execute("layer.setProps", &json!({"ids": [locked_sub.0], "locked": true})).unwrap();
|
||||
assert_eq!(s.execute("select.object.textObjects", &json!({})).unwrap()["count"], 1);
|
||||
assert_eq!(selected(&s), vec![shown]);
|
||||
assert_eq!(s.execute("select.object.pointText", &json!({})).unwrap()["count"], 1);
|
||||
assert_eq!(selected(&s), vec![shown]);
|
||||
assert_eq!(s.execute("select.same.fontSize", &json!({})).unwrap()["count"], 1);
|
||||
assert_eq!(selected(&s), vec![shown]);
|
||||
s.execute("select.object.openPaths", &json!({})).unwrap();
|
||||
assert_eq!(selected(&s), vec![open]);
|
||||
}
|
||||
|
||||
/// Next Object Above and Below pass over the hidden and locked objects and the sublayers beside the
|
||||
/// selected object.
|
||||
#[test]
|
||||
fn next_object_above_and_below_pass_over_what_cant_be_selected() {
|
||||
let mut s = session();
|
||||
let layer = s.doc().unwrap().doc.layers[0].id;
|
||||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
let hidden = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||||
let locked = rect(&mut s, 40.0, 0.0, 10.0, 10.0);
|
||||
let sub = id_of(&s.execute("layer.newSublayer", &json!({"parent": layer.0})).unwrap());
|
||||
s.execute("layer.setCurrent", &json!({"id": layer.0})).unwrap();
|
||||
let b = rect(&mut s, 60.0, 0.0, 10.0, 10.0);
|
||||
let order: Vec<NodeId> = s.doc().unwrap().doc.layers[0].children().unwrap().iter().map(|n| n.id).collect();
|
||||
assert_eq!(order, vec![a, hidden, locked, sub, b]);
|
||||
s.execute("object.setProps", &json!({"ids": [hidden.0], "visible": false})).unwrap();
|
||||
s.execute("object.setProps", &json!({"ids": [locked.0], "locked": true})).unwrap();
|
||||
sel(&mut s, &[a]);
|
||||
s.execute("select.nextAbove", &json!({})).unwrap();
|
||||
assert_eq!(selected(&s), vec![b]);
|
||||
s.execute("select.nextBelow", &json!({})).unwrap();
|
||||
assert_eq!(selected(&s), vec![a]);
|
||||
// Nothing that can be selected below it: the selection stays.
|
||||
s.execute("select.nextBelow", &json!({})).unwrap();
|
||||
assert_eq!(selected(&s), vec![a]);
|
||||
}
|
||||
|
||||
/// In isolation mode Select › Same and Select › Object select only objects in the isolated group.
|
||||
#[test]
|
||||
fn select_menu_commands_stay_inside_the_isolated_group() {
|
||||
let mut s = session();
|
||||
let outside = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
let _text_outside = text(&mut s, "outside");
|
||||
let a = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||||
let b = rect(&mut s, 40.0, 0.0, 10.0, 10.0);
|
||||
let text_inside = text(&mut s, "inside");
|
||||
for id in [outside, a, b] {
|
||||
fill(&mut s, id, "#ff0000");
|
||||
}
|
||||
sel(&mut s, &[a, b, text_inside]);
|
||||
let g = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
s.execute("object.isolate", &json!({"id": g.0})).unwrap();
|
||||
sel(&mut s, &[a]);
|
||||
s.execute("select.same.fillColor", &json!({})).unwrap();
|
||||
assert_eq!(selected(&s), vec![a, b]);
|
||||
s.execute("select.object.textObjects", &json!({})).unwrap();
|
||||
assert_eq!(selected(&s), vec![text_inside]);
|
||||
s.execute("select.object.pointText", &json!({})).unwrap();
|
||||
assert_eq!(selected(&s), vec![text_inside]);
|
||||
s.execute("select.same.fontSize", &json!({})).unwrap();
|
||||
assert_eq!(selected(&s), vec![text_inside]);
|
||||
// All on Same Layers takes the objects of the isolated group.
|
||||
sel(&mut s, &[a]);
|
||||
s.execute("select.object.allOnSameLayers", &json!({})).unwrap();
|
||||
assert_eq!(selected(&s), vec![a, b, text_inside]);
|
||||
}
|
||||
|
||||
/// In isolation mode a recalled selection leaves out the objects outside the isolated group, and
|
||||
/// Next Object Above selects nothing outside it.
|
||||
#[test]
|
||||
fn recalled_selections_and_next_object_stay_inside_the_isolated_group() {
|
||||
let mut s = session();
|
||||
let outside = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
let _above_outside = rect(&mut s, 0.0, 20.0, 10.0, 10.0);
|
||||
let a = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||||
let b = rect(&mut s, 40.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[outside, a]);
|
||||
s.execute("select.save", &json!({"name": "Both"})).unwrap();
|
||||
sel(&mut s, &[a, b]);
|
||||
let g = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
s.execute("object.isolate", &json!({"id": g.0})).unwrap();
|
||||
s.execute("select.recall", &json!({"name": "Both"})).unwrap();
|
||||
assert_eq!(selected(&s), vec![a]);
|
||||
// An object outside the group (selected by id) has no neighbor in it: the selection stays.
|
||||
sel(&mut s, &[outside]);
|
||||
s.execute("select.nextAbove", &json!({})).unwrap();
|
||||
assert_eq!(selected(&s), vec![outside]);
|
||||
}
|
||||
|
||||
/// In isolation mode Open Paths and All on Same Layers select nothing when the isolated group, or
|
||||
/// the layer it is on, is hidden or locked.
|
||||
#[test]
|
||||
fn select_menu_commands_leave_out_an_isolated_group_hidden_or_locked_since() {
|
||||
let mut s = session();
|
||||
let layer = s.doc().unwrap().doc.layers[0].id;
|
||||
let a = id_of(&s.execute("shape.line", &json!({"x1": 0, "y1": 0, "x2": 10, "y2": 10})).unwrap());
|
||||
let b = id_of(&s.execute("shape.line", &json!({"x1": 0, "y1": 20, "x2": 10, "y2": 30})).unwrap());
|
||||
sel(&mut s, &[a, b]);
|
||||
let g = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
s.execute("object.isolate", &json!({"id": g.0})).unwrap();
|
||||
s.execute("select.object.openPaths", &json!({})).unwrap();
|
||||
assert_eq!(selected(&s), vec![a, b]);
|
||||
s.execute("layer.setProps", &json!({"ids": [layer.0], "visible": false})).unwrap();
|
||||
s.execute("select.object.openPaths", &json!({})).unwrap();
|
||||
assert_eq!(selected(&s), vec![]);
|
||||
sel(&mut s, &[a]);
|
||||
s.execute("select.object.allOnSameLayers", &json!({})).unwrap();
|
||||
assert_eq!(selected(&s), vec![]);
|
||||
s.execute("layer.setProps", &json!({"ids": [layer.0], "visible": true})).unwrap();
|
||||
s.execute("object.setProps", &json!({"ids": [g.0], "locked": true})).unwrap();
|
||||
s.execute("select.object.openPaths", &json!({})).unwrap();
|
||||
assert_eq!(selected(&s), vec![]);
|
||||
}
|
||||
|
||||
/// The selection after command `c` runs with nothing selected.
|
||||
fn select_from_none(s: &mut Session, c: &str) -> Vec<NodeId> {
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
s.execute(c, &json!({})).unwrap();
|
||||
selected(s)
|
||||
}
|
||||
|
||||
/// In isolation mode Select All, All on Active Artboard and Inverse select nothing while the
|
||||
/// isolated group is hidden or locked, or the layer it is on is hidden or a template.
|
||||
#[test]
|
||||
fn select_all_leaves_out_an_isolated_group_hidden_or_locked_since() {
|
||||
let mut s = session();
|
||||
let layer = s.doc().unwrap().doc.layers[0].id;
|
||||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
let b = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[a, b]);
|
||||
let g = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
s.execute("object.isolate", &json!({"id": g.0})).unwrap();
|
||||
let commands = ["select.all", "select.allOnArtboard", "select.inverse"];
|
||||
for c in commands {
|
||||
assert_eq!(select_from_none(&mut s, c), vec![a, b], "{c}");
|
||||
}
|
||||
let changes = [
|
||||
("object.setProps", json!({"ids": [g.0], "visible": false}), json!({"ids": [g.0], "visible": true})),
|
||||
("object.setProps", json!({"ids": [g.0], "locked": true}), json!({"ids": [g.0], "locked": false})),
|
||||
("layer.setProps", json!({"ids": [layer.0], "visible": false}), json!({"ids": [layer.0], "visible": true})),
|
||||
// A template layer that has been unlocked again.
|
||||
("layer.setProps", json!({"ids": [layer.0], "template": true, "locked": false}), json!({"ids": [layer.0], "template": false})),
|
||||
];
|
||||
for (cmd, on, off) in changes {
|
||||
s.execute(cmd, &on).unwrap();
|
||||
for c in commands {
|
||||
assert_eq!(select_from_none(&mut s, c), vec![], "{c} after {cmd} {on}");
|
||||
}
|
||||
s.execute(cmd, &off).unwrap();
|
||||
for c in commands {
|
||||
assert_eq!(select_from_none(&mut s, c), vec![a, b], "{c} after {cmd} {off}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern editing and opacity mask editing isolate a new layer: Select All, All on Active
|
||||
/// Artboard and Inverse select the art on it.
|
||||
#[test]
|
||||
fn select_all_in_pattern_and_mask_editing_selects_the_art_being_edited() {
|
||||
let mut s = session();
|
||||
let commands = ["select.all", "select.allOnArtboard", "select.inverse"];
|
||||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[r]);
|
||||
s.execute("object.pattern.make", &json!({"name": "P"})).unwrap();
|
||||
let tile = selected(&s);
|
||||
assert_eq!(tile.len(), 1);
|
||||
for c in commands {
|
||||
assert_eq!(select_from_none(&mut s, c), tile, "{c} in pattern editing");
|
||||
}
|
||||
s.execute("object.pattern.done", &json!({})).unwrap();
|
||||
let obj = rect(&mut s, 0.0, 0.0, 200.0, 200.0);
|
||||
let m = rect(&mut s, 0.0, 0.0, 100.0, 200.0);
|
||||
sel(&mut s, &[obj, m]);
|
||||
s.execute("transparency.makeOpacityMask", &json!({})).unwrap();
|
||||
s.execute("transparency.editOpacityMask", &json!({})).unwrap();
|
||||
let mask = selected(&s);
|
||||
assert_eq!(mask.len(), 1);
|
||||
for c in commands {
|
||||
assert_eq!(select_from_none(&mut s, c), mask, "{c} in opacity mask editing");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_and_recall_selection() {
|
||||
let mut s = session();
|
||||
|
|
@ -756,6 +979,30 @@ fn saved_selections_live_in_the_document() {
|
|||
assert_eq!(s.execute("select.recall", &json!({"name": "Pair"})).unwrap()["count"], 1);
|
||||
}
|
||||
|
||||
/// A recall leaves out the objects hidden or locked since the save, themselves or through their
|
||||
/// group or sublayer.
|
||||
#[test]
|
||||
fn recalled_selections_leave_out_objects_hidden_or_locked_since() {
|
||||
let mut s = session();
|
||||
let layer = s.doc().unwrap().doc.layers[0].id;
|
||||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
let hidden = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||||
let locked = rect(&mut s, 40.0, 0.0, 10.0, 10.0);
|
||||
let in_group = rect(&mut s, 60.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[in_group]);
|
||||
let g = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
let sub = id_of(&s.execute("layer.newSublayer", &json!({"parent": layer.0})).unwrap());
|
||||
let in_sub = rect(&mut s, 80.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[a, hidden, locked, in_group, in_sub]);
|
||||
s.execute("select.save", &json!({"name": "All"})).unwrap();
|
||||
s.execute("object.setProps", &json!({"ids": [hidden.0, g.0], "visible": false})).unwrap();
|
||||
s.execute("object.setProps", &json!({"ids": [locked.0], "locked": true})).unwrap();
|
||||
s.execute("layer.setProps", &json!({"ids": [sub.0], "locked": true})).unwrap();
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
assert_eq!(s.execute("select.recall", &json!({"name": "All"})).unwrap()["count"], 1);
|
||||
assert_eq!(selected(&s), vec![a]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_selection_applies_several_edits_as_one_step() {
|
||||
let mut s = session();
|
||||
|
|
|
|||
13
docs/mcp.md
13
docs/mcp.md
|
|
@ -750,12 +750,23 @@ that were already hidden or locked. The key object is cleared when it is one of
|
|||
them again with the key object. When the call changes nothing, it records no undo step and still deselects them.
|
||||
Other `object.setProps` calls leave the selection unchanged.
|
||||
|
||||
These Select menu commands select only the objects that `select.all` selects and the objects inside them: every
|
||||
`select.same.*` and `select.object.*` command except `select.object.slices`, `select.nextAbove`, `select.nextBelow`
|
||||
and `select.recall`. They leave out hidden and locked objects with the objects inside them, the objects on hidden,
|
||||
locked or template layers and sublayers and, in isolation mode, everything outside the isolated group or layer. When
|
||||
the isolated group or layer, or a group or layer it is in, is hidden, locked or a template layer, they also leave out
|
||||
everything in it, and `select.all`, `select.allOnArtboard` and `select.inverse` select nothing.
|
||||
`select.nextAbove` and `select.nextBelow` select the nearest such object above or below the first selected object
|
||||
among the objects beside it in its group or layer, passing over hidden and locked objects and sublayers. When there
|
||||
is no such object, the selection stays as it is.
|
||||
|
||||
## Saved selections
|
||||
|
||||
Select → Save Selection… keeps the selected objects under a name, in the document: `select.save {name?}` (default
|
||||
the first free "Selection N"; an existing name is replaced with the current selection; at most 25 per document,
|
||||
names up to 255 characters) → `{name}`, one undo step. `select.savedList` lists the names in menu order, and
|
||||
`select.recall {name}` selects those objects again (ones deleted since are left out) → `{count}`. Edit Selection…
|
||||
`select.recall {name}` selects those objects again, except the ones deleted since and the ones hidden, locked or
|
||||
outside the isolated group or layer (see Object selection) → `{count}`. Edit Selection…
|
||||
renames and deletes: `select.editSaved {name, newName?, delete?}`, or several at once as
|
||||
`{edits: [{name, newName?, delete?}…]}` in one undo step, where every `name` is the name before the edit and the
|
||||
names must stay unique. In the desktop app the saved selections are listed at the bottom of the Select menu
|
||||
|
|
|
|||
|
|
@ -417,6 +417,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
|||
The command palette lists `select.key` as Cancel Key Object, and a pick cancels the key object (#541).
|
||||
Agent selection commands validate every object id before changing the selection and report the resulting count and ids (#784).
|
||||
Agents read the key object: `select.key` returns it, and `document.inspect` and the `ui.pointer` and `pointer_gesture` replies report it as `key`. `select.key` with an id that isn't a selected top-level object returns an error and leaves the key unchanged.
|
||||
The Select menu's Same commands, its Object commands that select art, Next Object Above and Below and saved selections select only objects that Select All selects and objects inside them: nothing hidden or locked, nothing in a hidden or locked group, layer or sublayer, nothing on a template layer, and in isolation mode nothing outside the isolated group or layer. In isolation mode Select All, All on Active Artboard and Inverse select nothing while the isolated group or layer, or a group or layer it is in, is hidden, locked or a template layer.
|
||||
Align and Distribute Spacing keep a compound path or selected group that contains the key object in place, as they keep the key object itself.
|
||||
Anchor Align moves the selected points to their bounds or the artboard (#1180). Repeating it, or moving by at most 1e-9 pt on both axes as whole-object Align does, leaves the document, Undo, Redo and saved state unchanged; actual moves still take one undo step. `crates/engine/src/tests.rs` covers repeated alignment, rounding-only alignment, unchanged handles, saved state, command recording and Undo/Redo.
|
||||
Clear deletes individual selected compound-path members without deleting their siblings, and explicit object ids work without a selection; Cut still removes the same objects it copies (#786).
|
||||
|
|
|
|||
Loading…
Reference in New Issue