Isolation mode: Lock, Unlock All, Hide and Show All work on the isolated art

Squashed from #4849 onto main.
This commit is contained in:
dotCooCoo
2026-11-01 01:21:53 +07:00
committed by bflatastic
parent f1a6a9ff67
commit b46cdc4e4d
12 changed files with 511 additions and 226 deletions
+17 -5
View File
@@ -566,17 +566,29 @@ pub(crate) fn selected_roots(s: &Session) -> Result<Vec<NodeId>> {
/// rest still go to the compound path, which holds the paint.
pub(crate) fn selected_roots_to_transform(s: &Session) -> Result<Vec<NodeId>> {
let st = s.doc()?;
Ok(roots_within(&st.doc, st.selection.in_paint_order(&st.doc), true))
Ok(roots_within(&st.doc, st.selection.in_paint_order(&st.doc), |_| true))
}
/// [`selected_roots`] for the selected objects inside the isolated object `root`
/// ([`super::inside_isolation`]): `root` itself and objects outside it are left out before the
/// objects held by another selected object are dropped. A selected path of a compound path stands
/// for its compound unless the compound is `root` or `members` is set (as in
/// [`selected_roots_to_transform`]).
pub(crate) fn selected_roots_inside(s: &Session, root: NodeId, members: bool) -> Result<Vec<NodeId>> {
let st = s.doc()?;
let ids = st.selection.in_paint_order(&st.doc).into_iter().filter(|id| super::inside_isolation(&st.doc, root, *id)).collect();
Ok(roots_within(&st.doc, ids, |compound| members || compound == root))
}
/// The top-level objects among `ids` (in paint order): compound-path members stand for their
/// compound, and ids whose ancestor is also listed, and layers, are dropped.
pub(crate) fn roots_of(doc: &Document, ids: Vec<NodeId>) -> Vec<NodeId> {
roots_within(doc, ids, false)
roots_within(doc, ids, |_| false)
}
/// [`roots_of`]; with `members`, a compound path's member listed stays itself.
fn roots_within(doc: &Document, ids: Vec<NodeId>, members: bool) -> Vec<NodeId> {
/// [`roots_of`]; a compound path's member listed stays itself when `members` holds for the
/// compound's id.
fn roots_within(doc: &Document, ids: Vec<NodeId>, members: impl Fn(NodeId) -> bool) -> Vec<NodeId> {
// Every id's place in the tree, found in one walk (a lookup per id is a walk per id).
let paths = doc.index_paths(ids.iter().copied());
// A compound-path member acts as its compound (compounds are one object).
@@ -586,7 +598,7 @@ fn roots_within(doc: &Document, ids: Vec<NodeId>, members: bool) -> Vec<NodeId>
let path = paths.get(&id);
let parent = path.and_then(|p| p.split_last()).map(|(_, up)| up).filter(|up| !up.is_empty());
match parent.and_then(|up| doc.node_at(up).map(|n| (up, n))) {
Some((up, n)) if matches!(n.kind, vectorcraft_doc::NodeKind::Compound { .. }) && !members => (n.id, Some(up.to_vec())),
Some((up, n)) if matches!(n.kind, vectorcraft_doc::NodeKind::Compound { .. }) && !members(n.id) => (n.id, Some(up.to_vec())),
_ => (id, path.cloned()),
}
})
+48 -23
View File
@@ -15,8 +15,8 @@ use vectorcraft_render::encode::{RasterExportOptions, RasterFormat};
use super::edit::{duplicate_in, selected_roots, selected_roots_to_transform};
use super::{
CommandSpec, EngineError, Result, Session, bad, check_numbers, check_switches, cmd, f64_or, fileio, find_command, has_doc, has_selection,
has_selection_outside_isolation, switch_or,
CommandSpec, EngineError, Result, Session, bad, check_numbers, check_switches, cmd, f64_or, fileio, find_command, has_doc, has_selected_member,
has_selection, switch_or,
};
/// Session-level (not saved) state owned by the menu commands.
@@ -43,8 +43,8 @@ pub fn specs() -> Vec<CommandSpec> {
"All Artwork Above",
["Object", "Lock"],
None,
"{} lock objects in the same layer that are above and overlap the selection, deselecting every selected object left locked or hidden (the key object too) → {count}",
has_selection_outside_isolation,
"{} lock objects in the same layer that are above and overlap the selection, deselecting every selected object left locked or hidden (the key object too); in isolation mode, lock the selected objects and the objects above them inside the isolated object → {count}",
has_selected_member,
|s, _| above(s, true)
),
cmd!(
@@ -52,8 +52,8 @@ pub fn specs() -> Vec<CommandSpec> {
"Other Layers",
["Object", "Lock"],
None,
"{} lock every layer that holds no selected object → {count}",
has_selection_outside_isolation,
"{} lock every layer that holds no selected object; in isolation mode, change nothing and return a count of 0 → {count}",
has_selected_member,
|s, _| other_layers(s, true)
),
cmd!(
@@ -61,8 +61,8 @@ pub fn specs() -> Vec<CommandSpec> {
"Unselected Artwork",
["Object", "Lock"],
Some("Cmd+Alt+Shift+2"),
"{} lock unselected artwork in one undo step, including unselected members of partly selected groups, clipping groups and compound paths, and guides while Lock Guides is on; hidden art and art on locked layers stay unlocked; a selected layer's art counts as selected; in isolation, only art inside the isolated object is affected; object.unlockAll unlocks locked art document-wide → {count}",
has_selection,
"{} lock unselected artwork in one undo step, including unselected members of partly selected groups, clipping groups and compound paths, and guides while Lock Guides is on; hidden art and art on locked layers stay unlocked; a selected layer's art counts as selected; in isolation, only art inside the isolated object is affected; object.unlockAll unlocks locked art document-wide, or inside the isolated object in isolation mode → {count}",
has_selected_member,
|s, _| unselected_art(s, true)
),
cmd!(
@@ -70,8 +70,8 @@ pub fn specs() -> Vec<CommandSpec> {
"All Artwork Above",
["Object", "Hide"],
None,
"{} hide objects in the same layer that are above and overlap the selection, deselecting every selected object left hidden or locked (the key object too) → {count}",
has_selection_outside_isolation,
"{} hide objects in the same layer that are above and overlap the selection, deselecting every selected object left hidden or locked (the key object too); in isolation mode, hide the selected objects and the objects above them inside the isolated object → {count}",
has_selected_member,
|s, _| above(s, false)
),
cmd!(
@@ -79,8 +79,8 @@ pub fn specs() -> Vec<CommandSpec> {
"Other Layers",
["Object", "Hide"],
None,
"{} hide every layer that holds no selected object → {count}",
has_selection_outside_isolation,
"{} hide every layer that holds no selected object; in isolation mode, change nothing and return a count of 0 → {count}",
has_selected_member,
|s, _| other_layers(s, false)
),
cmd!(
@@ -88,8 +88,8 @@ pub fn specs() -> Vec<CommandSpec> {
"Unselected Artwork",
["Object", "Hide"],
Some("Cmd+Alt+Shift+3"),
"{} hide, in one undo step, every object select.all takes that is not selected and holds no selected object; in a group that holds one it hides the other members one by one, and a clip group, compound path or other object that holds one stays as it is; the objects of a selected layer count as selected; while Lock Guides is on, guide paths count as selected and stay as they are; object.showAll shows them again; disabled in isolation mode → {count}",
has_selection_outside_isolation,
"{} hide, in one undo step, every object select.all takes that is not selected and holds no selected object; in a group that holds one it hides the other members one by one, and a clip group, compound path or other object that holds one stays as it is; the objects of a selected layer count as selected; while Lock Guides is on, guide paths count as selected and stay as they are; in isolation mode, only art inside the isolated object is affected; object.showAll shows them again → {count}",
has_selected_member,
|s, _| unselected_art(s, false)
),
cmd!(
@@ -243,14 +243,21 @@ fn reset_bounding_box(s: &mut Session, _: &Value) -> Result<Value> {
// ---------- Lock / Hide ----------
fn above(s: &mut Session, lock: bool) -> Result<Value> {
let roots = selected_roots(s)?;
let d = &s.doc()?.doc;
let st = s.doc()?;
let d = &st.doc;
let isolation = super::ordinary_isolation(st);
// In isolation mode a path of the isolated compound path is an object of its own.
let roots = match isolation {
Some(root) => super::edit::selected_roots_inside(s, root, false)?,
None => selected_roots(s)?,
};
let symbols = d.symbol_boxes();
// `selected` holds each selected object's visual bounds, by layer, with the index there of its
// ancestor directly inside the layer. Symbol instances are measured by their art.
// `selected` holds each selected object's visual bounds, by layer (the isolated object in
// isolation mode), with the index there of its ancestor directly inside it. Symbol instances
// are measured by their art.
let mut selected: BTreeMap<NodeId, Vec<(usize, Rect)>> = BTreeMap::new();
for id in &roots {
let Some(layer) = d.layer_of(*id) else { continue };
let Some(layer) = isolation.or_else(|| d.layer_of(*id)) else { continue };
// The selected object's ancestor directly inside the layer.
let anc = d.ancestry(*id).unwrap_or_default();
let Some(pos) = anc.iter().position(|a| *a == layer) else { continue };
@@ -259,10 +266,14 @@ fn above(s: &mut Session, lock: bool) -> Result<Value> {
let Some((_, idx, _)) = d.position(top) else { continue };
selected.entry(layer).or_default().push((idx, b));
}
// The loop walks up each layer once. Each object that is not selected and overlaps a selected
// object below it is locked (Lock) or hidden (Hide).
// The loop walks up each layer once, or the isolated object in isolation mode. Each object that
// is not selected and overlaps a selected object below it is locked (Lock) or hidden (Hide). In
// isolation mode the selected objects are locked or hidden too.
let roots: HashSet<NodeId> = roots.into_iter().collect();
let mut hits: BTreeSet<NodeId> = BTreeSet::new();
if isolation.is_some() {
hits.extend(roots.iter().copied());
}
for (layer, mut below) in selected {
below.sort_by_key(|(idx, _)| *idx);
let mut below = below.into_iter().peekable();
@@ -307,6 +318,11 @@ fn above(s: &mut Session, lock: bool) -> Result<Value> {
fn other_layers(s: &mut Session, lock: bool) -> Result<Value> {
let st = s.doc()?;
// In isolation mode the other layers lie outside the isolated group, layer or sublayer, so the
// document and its undo history stay as they are.
if super::ordinary_isolation(st).is_some() {
return Ok(json!({ "count": 0 }));
}
let keep: Vec<NodeId> = st.selection.objects.iter().filter_map(|id| st.doc.ancestry(*id)).flatten().collect();
let others: Vec<NodeId> = st.doc.layers.iter().map(|l| l.id).filter(|l| !keep.contains(l)).collect();
let n = others.len();
@@ -370,10 +386,19 @@ fn unselected(
}
fn unselected_art(s: &mut Session, lock: bool) -> Result<Value> {
let roots = selected_roots_to_transform(s)?;
// In isolation mode the isolated object itself does not count as a selected object.
let iso = super::ordinary_isolation(s.doc()?);
let roots = match iso {
Some(root) => super::edit::selected_roots_inside(s, root, true)?,
None => selected_roots_to_transform(s)?,
};
let guides_locked = s.menu.guides_locked;
let st = s.doc()?;
let ids = unselected(&st.doc, st.isolation, &st.selection.objects, &roots, guides_locked, lock);
let selection: Vec<NodeId> = match iso {
Some(root) => st.selection.objects.iter().copied().filter(|id| super::inside_isolation(&st.doc, root, *id)).collect(),
None => st.selection.objects.to_vec(),
};
let ids = unselected(&st.doc, st.isolation, &selection, &roots, guides_locked, lock);
// With nothing to lock (Lock) or hide (Hide), the document and its undo history stay as they are.
if !ids.is_empty() {
s.edit(if lock { "Lock" } else { "Hide" }, |d, _| {
+34 -6
View File
@@ -143,7 +143,7 @@ pub(super) fn ungroup(s: &mut Session, _: &Value) -> Result<Value> {
}
pub(super) fn lock(s: &mut Session, _: &Value) -> Result<Value> {
let ids = selected_roots(s)?;
let ids = selected_lock_hide_roots(s)?;
// With every selected object already locked, the document and its undo history stay as they
// are, and the objects are deselected.
let d = &s.doc()?.doc;
@@ -163,6 +163,16 @@ pub(super) fn lock(s: &mut Session, _: &Value) -> Result<Value> {
ok()
}
/// The selected objects ([`selected_roots`]) that Lock › Selection and Hide › Selection change: in
/// isolation mode, only those inside the isolated object, where a path of the isolated compound
/// path is an object of its own.
fn selected_lock_hide_roots(s: &Session) -> Result<Vec<NodeId>> {
match crate::cmd::ordinary_isolation(s.doc()?) {
Some(root) => crate::cmd::edit::selected_roots_inside(s, root, false),
None => selected_roots(s),
}
}
fn collect_flag(d: &Document, f: impl Fn(&Node) -> bool) -> Vec<NodeId> {
let mut v = vec![];
d.walk(|n| {
@@ -174,14 +184,28 @@ fn collect_flag(d: &Document, f: impl Fn(&Node) -> bool) -> Vec<NodeId> {
}
/// The objects that `f` picks for `object.unlockAll` and `object.showAll` (`cmd`): in the whole
/// document, or with `scope: "group"` inside the groups that hold the selected objects. A selected
/// group is the group that holds its own members, and the objects of a group inside another such
/// group are taken once. With `scope: "group"` and no selected object in a group, it returns an
/// error.
/// document, inside the isolated object in isolation mode, or with `scope: "group"` inside the
/// groups that hold the selected objects (in isolation mode, only the objects of those groups
/// that are inside the isolated object). A selected group is the group that holds its own
/// members, and the objects of a group inside another such group are taken once. With
/// `scope: "group"` and no selected object in a group, it returns an error.
fn collect_flag_in_scope(s: &Session, cmd: &str, p: &Value, f: impl Fn(&Node) -> bool) -> Result<Vec<NodeId>> {
let st = s.doc()?;
let d = &st.doc;
if choice(p, cmd, "scope", &["document", "group"])? != Some("group") {
if let Some(root) = crate::cmd::ordinary_isolation(st) {
let mut ids = vec![];
if let Some(node) = d.node(root) {
for child in node.children().into_iter().flatten() {
child.walk(&mut |n| {
if !n.is_layer() && f(n) {
ids.push(n.id);
}
});
}
}
return Ok(ids);
}
return Ok(collect_flag(d, f));
}
let is_group = |id: &NodeId| d.node(*id).is_some_and(|n| matches!(n.kind, NodeKind::Group { .. }));
@@ -204,6 +228,10 @@ fn collect_flag_in_scope(s: &Session, cmd: &str, p: &Value, f: impl Fn(&Node) ->
});
}
}
// In isolation mode only the objects inside the isolated object are taken.
if let Some(root) = crate::cmd::ordinary_isolation(st) {
v.retain(|id| crate::cmd::inside_isolation(d, root, *id));
}
Ok(v)
}
@@ -226,7 +254,7 @@ pub(super) fn unlock_all(s: &mut Session, p: &Value) -> Result<Value> {
}
pub(super) fn hide(s: &mut Session, _: &Value) -> Result<Value> {
let ids = selected_roots(s)?;
let ids = selected_lock_hide_roots(s)?;
// With every selected object already hidden, the document and its undo history stay as they
// are, and the objects are deselected.
let d = &s.doc()?.doc;
+9 -9
View File
@@ -6,7 +6,7 @@ use super::props::set_props;
use super::shape::{expand_shape, set_live_shape};
use super::transform::{move_cmd, nudge, reflect, rotate, scale, shear, transform, transform_again};
use super::{CommandSpec, cmd, has_doc, has_multi, has_selection, has_selection_or_guides};
use crate::cmd::{has_selection_outside_isolation, outside_isolation};
use crate::cmd::has_selected_member;
pub fn specs() -> Vec<CommandSpec> {
vec![
@@ -150,8 +150,8 @@ pub fn specs() -> Vec<CommandSpec> {
"Selection",
["Object", "Lock"],
Some("Cmd+2"),
"{ids?: [id…] (selected first; default: the selection)}",
has_selection_outside_isolation,
"{ids?: [id…] (selected first; default: the selection; in isolation mode, only those inside the isolated object)}",
has_selected_member,
lock
),
cmd!(
@@ -159,8 +159,8 @@ pub fn specs() -> Vec<CommandSpec> {
"Unlock All",
["Object"],
Some("Cmd+Alt+2"),
"{scope?: \"document\"|\"group\" (default document; group: only the objects inside the groups that hold the selected objects, a selected group holding its own; the in-window menu item chosen with Shift+Alt)} unlock the locked objects and select them → {count}",
outside_isolation,
"{scope?: \"document\"|\"group\" (default document, which in isolation mode is the isolated object; group: only the objects inside the groups that hold the selected objects, a selected group holding its own; the in-window menu item chosen with Shift+Alt)} unlock the locked objects and select them → {count}",
has_doc,
unlock_all
),
cmd!(
@@ -168,8 +168,8 @@ pub fn specs() -> Vec<CommandSpec> {
"Selection",
["Object", "Hide"],
Some("Cmd+3"),
"{ids?: [id…] (selected first; default: the selection)}",
has_selection_outside_isolation,
"{ids?: [id…] (selected first; default: the selection; in isolation mode, only those inside the isolated object)}",
has_selected_member,
hide
),
cmd!(
@@ -177,8 +177,8 @@ pub fn specs() -> Vec<CommandSpec> {
"Show All",
["Object"],
Some("Cmd+Alt+3"),
"{scope?: \"document\"|\"group\" (default document; group: only the objects inside the groups that hold the selected objects, a selected group holding its own; the in-window menu item chosen with Shift+Alt)} show the hidden objects and select them → {count}",
outside_isolation,
"{scope?: \"document\"|\"group\" (default document, which in isolation mode is the isolated object; group: only the objects inside the groups that hold the selected objects, a selected group holding its own; the in-window menu item chosen with Shift+Alt)} show the hidden objects and select them → {count}",
has_doc,
show_all
),
cmd!(