mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 13:47:21 -06:00
Isolation mode: Lock, Unlock All, Hide and Show All work on the isolated art
Squashed from #4849 onto main.
This commit is contained in:
+15
-13
@@ -182,20 +182,22 @@ pub fn has_multi(s: &Session) -> std::result::Result<(), String> {
|
||||
let st = s.active().ok_or("no document open")?;
|
||||
if st.selection.len() < 2 { Err("select at least two objects".into()) } else { Ok(()) }
|
||||
}
|
||||
/// A document is open and it is not in isolation mode: Object › Lock's and Object › Hide's items,
|
||||
/// Unlock All and Show All are off in isolation mode. Pattern editing mode and opacity mask
|
||||
/// editing isolate their art too, and do not count as isolation mode here while their own layer
|
||||
/// is the isolated one. An object isolated inside them counts.
|
||||
pub fn outside_isolation(s: &Session) -> std::result::Result<(), String> {
|
||||
let st = s.active().ok_or("no document open")?;
|
||||
let editing_layer = |l| st.doc.pattern_edit.as_ref().is_some_and(|e| e.layer == l) || st.doc.mask_edit.as_ref().is_some_and(|e| e.layer == l);
|
||||
if st.isolation.is_some_and(|l| !editing_layer(l)) { Err("exit isolation mode first".into()) } else { Ok(()) }
|
||||
/// The isolated group, layer or object in isolation mode. Pattern editing mode and opacity mask
|
||||
/// editing isolate their own layer, which does not count here; an object isolated inside them
|
||||
/// counts.
|
||||
pub(crate) fn ordinary_isolation(st: &crate::DocState) -> Option<vectorcraft_doc::NodeId> {
|
||||
st.isolation.filter(|l| !st.doc.pattern_edit.as_ref().is_some_and(|e| e.layer == *l) && !st.doc.mask_edit.as_ref().is_some_and(|e| e.layer == *l))
|
||||
}
|
||||
/// Objects are selected ([`has_selection`]) and the document is not in isolation mode
|
||||
/// ([`outside_isolation`]).
|
||||
pub fn has_selection_outside_isolation(s: &Session) -> std::result::Result<(), String> {
|
||||
outside_isolation(s)?;
|
||||
has_selection(s)
|
||||
/// Objects are selected ([`has_selection`]), and in isolation mode ([`ordinary_isolation`]) at
|
||||
/// least one of them is inside the isolated object: Object › Lock's and Object › Hide's items.
|
||||
pub fn has_selected_member(s: &Session) -> std::result::Result<(), String> {
|
||||
let st = s.active().ok_or("no document open")?;
|
||||
let Some(root) = ordinary_isolation(st) else { return has_selection(s) };
|
||||
if st.selection.objects.iter().any(|id| inside_isolation(&st.doc, root, *id)) { Ok(()) } else { Err("nothing selected".into()) }
|
||||
}
|
||||
/// Whether `id` lies inside the isolated object `root` (the root itself does not).
|
||||
pub(crate) fn inside_isolation(d: &vectorcraft_doc::Document, root: vectorcraft_doc::NodeId, id: vectorcraft_doc::NodeId) -> bool {
|
||||
id != root && d.ancestry(id).is_some_and(|a| a.contains(&root))
|
||||
}
|
||||
pub fn can_undo(s: &Session) -> std::result::Result<(), String> {
|
||||
s.active().filter(|d| !d.history.undo.is_empty()).map(|_| ()).ok_or_else(|| "nothing to undo".into())
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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, _| {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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!(
|
||||
|
||||
+344
-138
@@ -316,137 +316,347 @@ fn shown_and_locked(s: &Session, id: NodeId) -> (bool, bool) {
|
||||
(n.visible, n.locked)
|
||||
}
|
||||
|
||||
/// Each of `commands` is off in isolation mode: the command list reports it disabled, its error
|
||||
/// names isolation mode, and so does the error of a call that gives the selection as `ids` (for a
|
||||
/// command that takes them).
|
||||
fn off_in_isolation(s: &mut Session, commands: &[&str]) {
|
||||
let ids: Vec<u64> = selected(s).iter().map(|i| i.0).collect();
|
||||
for c in commands {
|
||||
let spec = find_command(c).unwrap();
|
||||
assert_eq!(spec.info(s).disabled_reason.as_deref(), Some("exit isolation mode first"), "{c}");
|
||||
let given = cmd::params::takes_key(spec.params, "ids").then(|| json!({"ids": ids}));
|
||||
for p in std::iter::once(json!({})).chain(given) {
|
||||
let e = s.execute(c, &p).unwrap_err().to_string();
|
||||
assert!(e.contains("exit isolation mode first"), "{c} {p}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// In isolation mode, Lock › Selection and Hide › Selection are off and change nothing, in an
|
||||
/// isolated group and in an isolated compound path with one of its paths selected. After Exit
|
||||
/// Isolation Mode they lock and hide again.
|
||||
#[test]
|
||||
fn lock_and_hide_selection_are_off_in_isolation_mode() {
|
||||
let mut s = session();
|
||||
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());
|
||||
let outer = rect(&mut s, 100.0, 100.0, 100.0, 100.0);
|
||||
let hole = rect(&mut s, 120.0, 120.0, 20.0, 20.0);
|
||||
sel(&mut s, &[outer, hole]);
|
||||
let compound = id_of(&s.execute("object.compoundPath.make", &json!({})).unwrap());
|
||||
let ids = [a, b, g, outer, hole, compound];
|
||||
let commands = ["object.lock", "object.hide"];
|
||||
for (isolated, member) in [(g, a), (compound, hole)] {
|
||||
s.execute("object.isolate", &json!({"id": isolated.0})).unwrap();
|
||||
sel(&mut s, &[member]);
|
||||
off_in_isolation(&mut s, &commands);
|
||||
for id in ids {
|
||||
assert_eq!(shown_and_locked(&s, id), (true, false), "{id:?} in isolation of {isolated:?}");
|
||||
}
|
||||
assert_eq!(selected(&s), [member]);
|
||||
s.execute("object.exitIsolation", &json!({})).unwrap();
|
||||
}
|
||||
sel(&mut s, &[a]);
|
||||
s.execute("object.lock", &json!({})).unwrap();
|
||||
sel(&mut s, &[outer]);
|
||||
s.execute("object.hide", &json!({})).unwrap();
|
||||
assert_eq!(shown_and_locked(&s, a), (true, true));
|
||||
assert_eq!(shown_and_locked(&s, compound), (false, false));
|
||||
}
|
||||
|
||||
/// In isolation mode, Unlock All and Show All are off: the locked and the hidden objects outside
|
||||
/// and inside the isolated group stay as they are. Pattern editing mode and opacity mask editing
|
||||
/// are not isolation mode for them.
|
||||
#[test]
|
||||
fn unlock_all_and_show_all_are_off_in_isolation_mode() {
|
||||
let mut s = session();
|
||||
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);
|
||||
let c = rect(&mut s, 40.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[a, b, c]);
|
||||
let g = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
let locked = rect(&mut s, 100.0, 0.0, 10.0, 10.0);
|
||||
let hidden = rect(&mut s, 120.0, 0.0, 10.0, 10.0);
|
||||
s.execute("layer.setProps", &json!({"id": locked.0, "locked": true})).unwrap();
|
||||
s.execute("layer.setProps", &json!({"id": hidden.0, "visible": false})).unwrap();
|
||||
s.execute("layer.setProps", &json!({"id": b.0, "locked": true})).unwrap();
|
||||
s.execute("layer.setProps", &json!({"id": c.0, "visible": false})).unwrap();
|
||||
s.execute("object.isolate", &json!({"id": g.0})).unwrap();
|
||||
sel(&mut s, &[a]);
|
||||
off_in_isolation(&mut s, &["object.unlockAll", "object.showAll"]);
|
||||
for (id, want) in [(locked, (true, true)), (hidden, (false, false)), (b, (true, true)), (c, (false, false))] {
|
||||
assert_eq!(shown_and_locked(&s, id), want, "{id:?}");
|
||||
}
|
||||
s.execute("object.exitIsolation", &json!({})).unwrap();
|
||||
s.execute("object.unlockAll", &json!({})).unwrap();
|
||||
s.execute("object.showAll", &json!({})).unwrap();
|
||||
for id in [locked, hidden, b, c] {
|
||||
assert_eq!(shown_and_locked(&s, id), (true, false), "{id:?}");
|
||||
}
|
||||
|
||||
// Pattern editing mode: the tile's art hides and shows again.
|
||||
let r = rect(&mut s, 300.0, 300.0, 10.0, 10.0);
|
||||
sel(&mut s, &[r]);
|
||||
s.execute("object.pattern.make", &json!({"name": "P"})).unwrap();
|
||||
let tile = selected(&s);
|
||||
s.execute("object.hide", &json!({})).unwrap();
|
||||
assert!(!node(&s, tile[0]).visible, "hidden in pattern editing mode");
|
||||
s.execute("object.showAll", &json!({})).unwrap();
|
||||
assert!(node(&s, tile[0]).visible, "shown in pattern editing mode");
|
||||
// An object isolated inside pattern editing mode is isolation mode again.
|
||||
s.execute("object.isolate", &json!({"id": tile[0].0})).unwrap();
|
||||
sel(&mut s, &[tile[0]]);
|
||||
off_in_isolation(&mut s, &["object.unlockAll", "object.showAll"]);
|
||||
s.execute("object.pattern.cancel", &json!({})).unwrap();
|
||||
|
||||
// Opacity mask editing: the mask's art locks and unlocks again.
|
||||
let obj = rect(&mut s, 0.0, 300.0, 200.0, 200.0);
|
||||
let m = rect(&mut s, 0.0, 300.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);
|
||||
s.execute("object.lock", &json!({})).unwrap();
|
||||
assert!(node(&s, mask[0]).locked, "locked in opacity mask editing");
|
||||
s.execute("object.unlockAll", &json!({})).unwrap();
|
||||
assert!(!node(&s, mask[0]).locked, "unlocked in opacity mask editing");
|
||||
}
|
||||
|
||||
/// In isolation mode, Lock › All Artwork Above, Lock › Other Layers, Hide › All Artwork Above and
|
||||
/// Hide › Other Layers are off: the art above and the other layer stay as they are.
|
||||
#[test]
|
||||
fn lock_and_hide_above_and_other_layers_are_off_in_isolation_mode() {
|
||||
/// Check command enablement, targets, selection and history inside an isolated group.
|
||||
fn check_group_isolation_command(command: &str, lock: bool, a_changed: bool, b_changed: bool) {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
let b = rect(&mut s, 200.0, 0.0, 10.0, 10.0);
|
||||
let b = rect(&mut s, 20.0, 20.0, 100.0, 100.0);
|
||||
sel(&mut s, &[a, b]);
|
||||
let g = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
let over = rect(&mut s, 50.0, 50.0, 100.0, 100.0);
|
||||
let out = rect(&mut s, 30.0, 30.0, 100.0, 100.0);
|
||||
let l2 = id_of(&s.execute("layer.new", &json!({})).unwrap());
|
||||
let p = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
s.execute("object.isolate", &json!({"id": g.0})).unwrap();
|
||||
sel(&mut s, &[out]);
|
||||
assert!(!find_command(command).unwrap().info(&s).enabled, "{command}: outside selection");
|
||||
sel(&mut s, &[a]);
|
||||
assert!(find_command(command).unwrap().info(&s).enabled, "{command}");
|
||||
let before = history(&s).0;
|
||||
s.execute(command, &json!({})).unwrap();
|
||||
let changed = |s: &Session, id| if lock { node(s, id).locked } else { !node(s, id).visible };
|
||||
assert_eq!(changed(&s, a), a_changed, "{command}: A");
|
||||
assert_eq!(changed(&s, b), b_changed, "{command}: B");
|
||||
assert!(!changed(&s, out) && !changed(&s, p) && !changed(&s, l2), "{command}: outside");
|
||||
assert_eq!(selected(&s), if a_changed { vec![] } else { vec![a] }, "{command}: selection");
|
||||
let steps = usize::from(a_changed || b_changed);
|
||||
assert_eq!(history(&s).0, before + steps, "{command}: history");
|
||||
if steps != 0 {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(!changed(&s, a) && !changed(&s, b), "{command}: undo");
|
||||
assert_eq!(selected(&s), [a], "{command}: undo selection");
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!((changed(&s, a), changed(&s, b)), (a_changed, b_changed), "{command}: redo");
|
||||
assert_eq!(selected(&s), if a_changed { vec![] } else { vec![a] }, "{command}: redo selection");
|
||||
}
|
||||
if !a_changed && b_changed {
|
||||
let restore = if lock { "object.unlockAll" } else { "object.showAll" };
|
||||
let params = if lock { json!({"id": out.0, "locked": true}) } else { json!({"id": out.0, "visible": false}) };
|
||||
s.execute("layer.setProps", ¶ms).unwrap();
|
||||
assert_eq!(s.execute(restore, &json!({})).unwrap(), json!({"count": 1}));
|
||||
assert!(!changed(&s, b) && changed(&s, out), "{command}: scoped restore");
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the focused group isolation checks for each Lock and Hide command.
|
||||
macro_rules! group_isolation_command_test {
|
||||
($name:ident, $command:literal, $lock:literal, $a:literal, $b:literal) => {
|
||||
#[doc = concat!("Checks ", $command, " targets and history in group isolation.")]
|
||||
#[test]
|
||||
fn $name() {
|
||||
check_group_isolation_command($command, $lock, $a, $b);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
group_isolation_command_test!(lock_selection_in_group_isolation, "object.lock", true, true, false);
|
||||
group_isolation_command_test!(lock_above_in_group_isolation, "object.lock.above", true, true, true);
|
||||
group_isolation_command_test!(lock_other_layers_in_group_isolation, "object.lock.otherLayers", true, false, false);
|
||||
group_isolation_command_test!(lock_unselected_in_group_isolation, "object.lock.unselected", true, false, true);
|
||||
group_isolation_command_test!(hide_selection_in_group_isolation, "object.hide", false, true, false);
|
||||
group_isolation_command_test!(hide_above_in_group_isolation, "object.hide.above", false, true, true);
|
||||
group_isolation_command_test!(hide_other_layers_in_group_isolation, "object.hide.otherLayers", false, false, false);
|
||||
group_isolation_command_test!(hide_unselected_in_group_isolation, "object.hide.unselected", false, false, true);
|
||||
|
||||
/// Lock › Selection, Hide › Selection and All Artwork Above change only the selected path inside
|
||||
/// an isolated compound path, with the other path below it.
|
||||
#[test]
|
||||
fn lock_and_hide_selection_in_isolated_compound_path() {
|
||||
for command in ["object.lock", "object.hide", "object.lock.above", "object.hide.above"] {
|
||||
let mut s = session();
|
||||
let outer = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
let hole = rect(&mut s, 20.0, 20.0, 20.0, 20.0);
|
||||
sel(&mut s, &[outer, hole]);
|
||||
let compound = id_of(&s.execute("object.compoundPath.make", &json!({})).unwrap());
|
||||
let outside = rect(&mut s, 150.0, 0.0, 20.0, 20.0);
|
||||
s.execute("object.isolate", &json!({"id": compound.0})).unwrap();
|
||||
sel(&mut s, &[hole]);
|
||||
assert!(find_command(command).unwrap().info(&s).enabled, "{command}");
|
||||
let before = history(&s).0;
|
||||
let want = if command.ends_with(".above") { json!({"count": 1}) } else { serde_json::Value::Null };
|
||||
assert_eq!(s.execute(command, &json!({})).unwrap(), want, "{command}");
|
||||
assert_eq!(history(&s).0, before + 1, "{command}");
|
||||
assert_eq!(selected(&s), [], "{command}");
|
||||
let expected = if command.starts_with("object.lock") { (true, true) } else { (false, false) };
|
||||
assert_eq!(shown_and_locked(&s, hole), expected, "{command}");
|
||||
for id in [outer, compound, outside] {
|
||||
assert_eq!(shown_and_locked(&s, id), (true, false), "{command}: {id:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lock › Selection, Hide › Selection and All Artwork Above change the whole compound path when
|
||||
/// one of its paths is selected inside an isolated group that holds it.
|
||||
#[test]
|
||||
fn lock_and_hide_a_compound_path_member_in_an_isolated_group() {
|
||||
for command in ["object.lock", "object.hide", "object.lock.above", "object.hide.above"] {
|
||||
let mut s = session();
|
||||
let outer = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
let hole = rect(&mut s, 20.0, 20.0, 20.0, 20.0);
|
||||
sel(&mut s, &[outer, hole]);
|
||||
let compound = id_of(&s.execute("object.compoundPath.make", &json!({})).unwrap());
|
||||
let b = rect(&mut s, 150.0, 0.0, 20.0, 20.0);
|
||||
sel(&mut s, &[compound, b]);
|
||||
let g = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
s.execute("object.isolate", &json!({"id": g.0})).unwrap();
|
||||
sel(&mut s, &[hole]);
|
||||
assert!(find_command(command).unwrap().info(&s).enabled, "{command}");
|
||||
let before = history(&s).0;
|
||||
s.execute(command, &json!({})).unwrap();
|
||||
assert_eq!(history(&s).0, before + 1, "{command}");
|
||||
assert_eq!(selected(&s), [], "{command}");
|
||||
let expected = if command.starts_with("object.lock") { (true, true) } else { (false, false) };
|
||||
assert_eq!(shown_and_locked(&s, compound), expected, "{command}");
|
||||
for id in [outer, hole, b, g] {
|
||||
assert_eq!(shown_and_locked(&s, id), (true, false), "{command}: {id:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that restore commands affect only isolated art and preserve undo and selection.
|
||||
fn check_restore_in_group_isolation(change: &str, restore: &str, lock: bool) {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
let b = rect(&mut s, 20.0, 20.0, 100.0, 100.0);
|
||||
sel(&mut s, &[a, b]);
|
||||
let g = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
let out = rect(&mut s, 30.0, 30.0, 100.0, 100.0);
|
||||
let l2 = id_of(&s.execute("layer.new", &json!({})).unwrap());
|
||||
let p = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
for id in [out, p] {
|
||||
let params = if lock { json!({"id": id.0, "locked": true}) } else { json!({"id": id.0, "visible": false}) };
|
||||
s.execute("layer.setProps", ¶ms).unwrap();
|
||||
}
|
||||
s.execute("object.isolate", &json!({"id": g.0})).unwrap();
|
||||
sel(&mut s, &[a]);
|
||||
off_in_isolation(&mut s, &["object.lock.above", "object.lock.otherLayers", "object.hide.above", "object.hide.otherLayers"]);
|
||||
for id in [a, b, g, over, l2] {
|
||||
assert_eq!(shown_and_locked(&s, id), (true, false), "{id:?}");
|
||||
s.execute(change, &json!({})).unwrap();
|
||||
assert!(selected(&s).is_empty());
|
||||
assert!(find_command(restore).unwrap().info(&s).enabled);
|
||||
let before = history(&s).0;
|
||||
assert_eq!(s.execute(restore, &json!({})).unwrap(), json!({"count": 1}));
|
||||
assert_eq!(history(&s).0, before + 1);
|
||||
assert_eq!(selected(&s), [a]);
|
||||
let changed = |s: &Session, id| if lock { node(s, id).locked } else { !node(s, id).visible };
|
||||
assert!(!changed(&s, a) && !changed(&s, b));
|
||||
assert!(changed(&s, out) && changed(&s, p));
|
||||
let no_op = history(&s).0;
|
||||
assert_eq!(s.execute(restore, &json!({})).unwrap(), json!({"count": 0}));
|
||||
assert_eq!(history(&s).0, no_op);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(changed(&s, a));
|
||||
assert!(selected(&s).is_empty());
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert!(!changed(&s, a) && changed(&s, out) && changed(&s, p) && !changed(&s, l2));
|
||||
assert_eq!(selected(&s), [a]);
|
||||
}
|
||||
|
||||
/// Unlock All restores locked art inside the group after Lock Selection clears selection.
|
||||
#[test]
|
||||
fn unlock_all_in_group_isolation_without_selection() {
|
||||
check_restore_in_group_isolation("object.lock", "object.unlockAll", true);
|
||||
}
|
||||
|
||||
/// Show All restores hidden art inside the group after Hide Selection clears selection.
|
||||
#[test]
|
||||
fn show_all_in_group_isolation_without_selection() {
|
||||
check_restore_in_group_isolation("object.hide", "object.showAll", false);
|
||||
}
|
||||
|
||||
/// With `scope: "group"`, Unlock All and Show All in an isolated compound path inside a group
|
||||
/// change only the compound path's paths, not the group's other objects or a group outside it
|
||||
/// that holds a selected object.
|
||||
#[test]
|
||||
fn group_scope_restore_stays_inside_isolated_compound_path() {
|
||||
for (restore, lock) in [("object.unlockAll", true), ("object.showAll", false)] {
|
||||
let mut s = session();
|
||||
let outer = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
let hole = rect(&mut s, 20.0, 20.0, 20.0, 20.0);
|
||||
sel(&mut s, &[outer, hole]);
|
||||
let compound = id_of(&s.execute("object.compoundPath.make", &json!({})).unwrap());
|
||||
let sibling = rect(&mut s, 150.0, 0.0, 20.0, 20.0);
|
||||
sel(&mut s, &[compound, sibling]);
|
||||
s.execute("object.group", &json!({})).unwrap();
|
||||
let away = rect(&mut s, 300.0, 0.0, 20.0, 20.0);
|
||||
let away_mate = rect(&mut s, 350.0, 0.0, 20.0, 20.0);
|
||||
sel(&mut s, &[away, away_mate]);
|
||||
s.execute("object.group", &json!({})).unwrap();
|
||||
for id in [hole, sibling, away_mate] {
|
||||
let params = if lock { json!({"id": id.0, "locked": true}) } else { json!({"id": id.0, "visible": false}) };
|
||||
s.execute("layer.setProps", ¶ms).unwrap();
|
||||
}
|
||||
s.execute("object.isolate", &json!({"id": compound.0})).unwrap();
|
||||
let changed = |s: &Session, id| if lock { node(s, id).locked } else { !node(s, id).visible };
|
||||
sel(&mut s, &[away]);
|
||||
assert_eq!(s.execute(restore, &json!({"scope": "group"})).unwrap(), json!({"count": 0}), "{restore}: outside selection");
|
||||
assert!(changed(&s, away_mate) && changed(&s, hole) && changed(&s, sibling), "{restore}: outside selection");
|
||||
sel(&mut s, &[outer]);
|
||||
assert_eq!(s.execute(restore, &json!({"scope": "group"})).unwrap(), json!({"count": 1}), "{restore}");
|
||||
assert!(!changed(&s, hole), "{restore}: hole");
|
||||
assert!(changed(&s, sibling) && changed(&s, away_mate), "{restore}: outside");
|
||||
assert_eq!(selected(&s), [hole], "{restore}: selection");
|
||||
}
|
||||
}
|
||||
|
||||
/// Lock › Selection, Hide › Selection and All Artwork Above, with the isolated group and one of
|
||||
/// its members both selected, change the member.
|
||||
#[test]
|
||||
fn lock_and_hide_with_the_isolated_group_selected_change_its_member() {
|
||||
for command in ["object.lock", "object.hide", "object.lock.above", "object.hide.above"] {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
let b = rect(&mut s, 200.0, 0.0, 100.0, 100.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();
|
||||
sel(&mut s, &[g, a]);
|
||||
assert!(find_command(command).unwrap().info(&s).enabled, "{command}");
|
||||
let before = history(&s).0;
|
||||
let want = if command.ends_with(".above") { json!({"count": 1}) } else { serde_json::Value::Null };
|
||||
assert_eq!(s.execute(command, &json!({})).unwrap(), want, "{command}");
|
||||
assert_eq!(history(&s).0, before + 1, "{command}");
|
||||
let expected = if command.starts_with("object.lock") { (true, true) } else { (false, false) };
|
||||
assert_eq!(shown_and_locked(&s, a), expected, "{command}: A");
|
||||
for id in [b, g] {
|
||||
assert_eq!(shown_and_locked(&s, id), (true, false), "{command}: {id:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lock and Hide Unselected with the isolated group and one of its members selected change only its other members.
|
||||
#[test]
|
||||
fn lock_and_hide_unselected_with_the_isolated_group_selected_keep_its_member() {
|
||||
for command in ["object.lock.unselected", "object.hide.unselected"] {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
let b = rect(&mut s, 200.0, 0.0, 100.0, 100.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();
|
||||
sel(&mut s, &[g, a]);
|
||||
assert!(find_command(command).unwrap().info(&s).enabled, "{command}");
|
||||
assert_eq!(s.execute(command, &json!({})).unwrap(), json!({"count": 1}), "{command}");
|
||||
let changed = if command == "object.lock.unselected" { (true, true) } else { (false, false) };
|
||||
assert_eq!(shown_and_locked(&s, b), changed, "{command}: B");
|
||||
for id in [a, g] {
|
||||
assert_eq!(shown_and_locked(&s, id), (true, false), "{command}: {id:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lock and Hide Unselected with the isolated layer and one of its objects selected change the layer's other objects,
|
||||
/// and a selected sublayer inside the isolated layer keeps its art.
|
||||
#[test]
|
||||
fn lock_and_hide_unselected_with_the_isolated_layer_selected_keep_its_object() {
|
||||
for command in ["object.lock.unselected", "object.hide.unselected"] {
|
||||
let mut s = session();
|
||||
let layer = s.doc().unwrap().doc.layers[0].id;
|
||||
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
let b = rect(&mut s, 200.0, 0.0, 100.0, 100.0);
|
||||
let sub = id_of(&s.execute("layer.newSublayer", &json!({"parent": layer.0})).unwrap());
|
||||
s.execute("layer.setCurrent", &json!({"id": sub.0})).unwrap();
|
||||
let c = rect(&mut s, 400.0, 0.0, 100.0, 100.0);
|
||||
s.execute("object.isolate", &json!({"id": layer.0})).unwrap();
|
||||
let changed = if command == "object.lock.unselected" { (true, true) } else { (false, false) };
|
||||
sel(&mut s, &[layer, a]);
|
||||
assert!(find_command(command).unwrap().info(&s).enabled, "{command}");
|
||||
assert_eq!(s.execute(command, &json!({})).unwrap(), json!({"count": 2}), "{command}");
|
||||
for id in [b, c] {
|
||||
assert_eq!(shown_and_locked(&s, id), changed, "{command}: {id:?}");
|
||||
}
|
||||
for id in [a, sub, layer] {
|
||||
assert_eq!(shown_and_locked(&s, id), (true, false), "{command}: {id:?}");
|
||||
}
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
sel(&mut s, &[layer, sub]);
|
||||
assert_eq!(s.execute(command, &json!({})).unwrap(), json!({"count": 2}), "{command}: sublayer");
|
||||
for id in [a, b] {
|
||||
assert_eq!(shown_and_locked(&s, id), changed, "{command}: sublayer, {id:?}");
|
||||
}
|
||||
for id in [c, sub, layer] {
|
||||
assert_eq!(shown_and_locked(&s, id), (true, false), "{command}: sublayer, {id:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern editing keeps Lock Selection and Unlock All available for its tile art.
|
||||
#[test]
|
||||
fn pattern_editing_keeps_lock_and_unlock_available() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[a]);
|
||||
s.execute("object.pattern.make", &json!({"name": "P"})).unwrap();
|
||||
let tile = selected(&s)[0];
|
||||
assert!(find_command("object.lock").unwrap().info(&s).enabled);
|
||||
s.execute("object.lock", &json!({})).unwrap();
|
||||
assert!(node(&s, tile).locked);
|
||||
assert!(find_command("object.unlockAll").unwrap().info(&s).enabled);
|
||||
s.execute("object.unlockAll", &json!({})).unwrap();
|
||||
assert!(!node(&s, tile).locked);
|
||||
assert_eq!(selected(&s), [tile]);
|
||||
}
|
||||
|
||||
/// Opacity mask editing lets Lock and Hide Selection change mask art and restores it with Unlock All and Show All.
|
||||
#[test]
|
||||
fn opacity_mask_editing_keeps_lock_hide_and_restore_available() {
|
||||
let mut s = session();
|
||||
let artwork = rect(&mut s, 0.0, 0.0, 200.0, 200.0);
|
||||
let mask_source = rect(&mut s, 0.0, 0.0, 100.0, 200.0);
|
||||
sel(&mut s, &[artwork, mask_source]);
|
||||
s.execute("transparency.makeOpacityMask", &json!({})).unwrap();
|
||||
s.execute("transparency.editOpacityMask", &json!({})).unwrap();
|
||||
let mask = selected(&s)[0];
|
||||
for (change, restore) in [("object.lock", "object.unlockAll"), ("object.hide", "object.showAll")] {
|
||||
assert!(find_command(change).unwrap().info(&s).enabled, "{change}");
|
||||
s.execute(change, &json!({})).unwrap();
|
||||
assert!(selected(&s).is_empty(), "{change}");
|
||||
assert_eq!(shown_and_locked(&s, mask), if change == "object.lock" { (true, true) } else { (false, false) });
|
||||
assert!(find_command(restore).unwrap().info(&s).enabled, "{restore}");
|
||||
assert_eq!(s.execute(restore, &json!({})).unwrap(), json!({"count": 1}), "{restore}");
|
||||
assert_eq!(shown_and_locked(&s, mask), (true, false), "{restore}");
|
||||
assert_eq!(selected(&s), [mask], "{restore}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Other Layers changes nothing in an isolated layer or sublayer.
|
||||
#[test]
|
||||
fn other_layers_has_no_target_in_isolated_layers_or_sublayers() {
|
||||
for sublayer in [false, true] {
|
||||
let mut s = session();
|
||||
let layer = s.doc().unwrap().doc.layers[0].id;
|
||||
let root = if sublayer { id_of(&s.execute("layer.newSublayer", &json!({"parent": layer.0})).unwrap()) } else { layer };
|
||||
s.execute("layer.setCurrent", &json!({"id": root.0})).unwrap();
|
||||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
let other = id_of(&s.execute("layer.new", &json!({"top": true})).unwrap());
|
||||
s.execute("object.isolate", &json!({"id": root.0})).unwrap();
|
||||
sel(&mut s, &[a]);
|
||||
for command in ["object.lock.otherLayers", "object.hide.otherLayers"] {
|
||||
assert!(find_command(command).unwrap().info(&s).enabled);
|
||||
let before = history(&s).0;
|
||||
assert_eq!(s.execute(command, &json!({})).unwrap(), json!({"count": 0}));
|
||||
assert_eq!(history(&s).0, before);
|
||||
assert_eq!(selected(&s), [a]);
|
||||
assert_eq!(shown_and_locked(&s, other), (true, false));
|
||||
}
|
||||
}
|
||||
s.execute("object.exitIsolation", &json!({})).unwrap();
|
||||
sel(&mut s, &[g]);
|
||||
assert_eq!(s.execute("object.lock.above", &json!({})).unwrap(), json!({"count": 1}));
|
||||
s.execute("object.hide.otherLayers", &json!({})).unwrap();
|
||||
assert_eq!(shown_and_locked(&s, over), (true, true));
|
||||
assert_eq!(shown_and_locked(&s, l2), (false, false));
|
||||
}
|
||||
|
||||
/// How many steps there are to undo and to redo, and whether the document has unsaved changes.
|
||||
@@ -580,12 +790,10 @@ fn hide_unselected_artwork_leaves_the_art_on_a_locked_artboard() {
|
||||
assert_eq!([a, b, on_locked].map(|id| node(&s, id).visible), [true, false, true]);
|
||||
}
|
||||
|
||||
/// In isolation mode Hide › Unselected Artwork is disabled: the menu item is off, and the command returns an error that
|
||||
/// names isolation mode and changes nothing. While Lock Guides is on it leaves the guides as they are. With nothing
|
||||
/// left to hide it records no undo step and returns a count of 0. A compound path that holds a selected path stays as
|
||||
/// it is.
|
||||
/// Hide Unselected affects only isolated art, keeps locked guides, records no step for a no-op, and
|
||||
/// preserves a compound path that holds the selected path.
|
||||
#[test]
|
||||
fn hide_unselected_artwork_keeps_locked_guides_and_is_disabled_in_isolation_mode() {
|
||||
fn hide_unselected_artwork_keeps_locked_guides_and_scopes_isolation() {
|
||||
let mut s = session();
|
||||
let guide = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[guide]);
|
||||
@@ -600,17 +808,15 @@ fn hide_unselected_artwork_keeps_locked_guides_and_is_disabled_in_isolation_mode
|
||||
let shown = |s: &Session, ids: &[NodeId]| ids.iter().map(|id| node(s, *id).visible).collect::<Vec<_>>();
|
||||
s.execute("object.isolate", &json!({"id": g.0})).unwrap();
|
||||
select_with_a_step_to_redo(&mut s, &[m]);
|
||||
let before = history(&s);
|
||||
let info = find_command("object.hide.unselected").unwrap().info(&s);
|
||||
assert!(!info.enabled && info.disabled_reason.as_deref() == Some("exit isolation mode first"), "{info:?}");
|
||||
let e = s.execute("object.hide.unselected", &json!({})).unwrap_err();
|
||||
assert!(matches!(e, EngineError::Disabled(..)) && e.to_string().contains("isolation mode"), "{e}");
|
||||
assert_eq!(shown(&s, &[m, n, out, guide]), [true; 4]);
|
||||
assert_eq!((history(&s), selected(&s)), (before, vec![m]));
|
||||
let before = history(&s).0;
|
||||
assert!(find_command("object.hide.unselected").unwrap().info(&s).enabled);
|
||||
assert_eq!(hide(&mut s), json!({"count": 1}));
|
||||
assert_eq!(history(&s).0, before + 1);
|
||||
assert_eq!(shown(&s, &[m, n, out, guide]), [true, false, true, true]);
|
||||
assert_eq!(selected(&s), [m]);
|
||||
s.execute("object.exitIsolation", &json!({})).unwrap();
|
||||
sel(&mut s, &[m]);
|
||||
assert!(find_command("object.hide.unselected").unwrap().info(&s).enabled);
|
||||
assert_eq!(hide(&mut s), json!({"count": 2}));
|
||||
assert_eq!(hide(&mut s), json!({"count": 1}));
|
||||
assert_eq!(shown(&s, &[n, out, guide]), [false, false, true]);
|
||||
select_with_a_step_to_redo(&mut s, &[m]);
|
||||
let before = history(&s);
|
||||
@@ -621,7 +827,7 @@ fn hide_unselected_artwork_keeps_locked_guides_and_is_disabled_in_isolation_mode
|
||||
sel(&mut s, &[m]);
|
||||
assert_eq!(hide(&mut s), json!({"count": 1}));
|
||||
assert_eq!(shown(&s, &[guide]), [false]);
|
||||
// A compound path that holds the selected path stays as it is with its paths.
|
||||
// The compound path and its paths stay visible when one path is selected.
|
||||
let outer = rect(&mut s, 0.0, 200.0, 100.0, 100.0);
|
||||
let hole = rect(&mut s, 40.0, 240.0, 20.0, 20.0);
|
||||
sel(&mut s, &[outer, hole]);
|
||||
|
||||
Reference in New Issue
Block a user