mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 14:16:56 -06:00
Object: Cmd/Ctrl+Alt+Shift+2 locks inside clip groups and isolation, with no menu item
object.lock.unselected is now reached by its key and as a command only: the Object > Lock > Unselected Artwork menu item is removed, and the command stays in the registry, the shortcut editor and automation. In a partly selected clip group or compound path it now locks the unselected members, the clipping path included, and leaves the container and the selected members unlocked. It locks unselected guides while Lock Guides is on. Hidden art and art on a locked layer stay unlocked, and a selected layer's art counts as selected. With a selection in isolation mode the command is enabled and locks only the unselected art inside the isolated group. The other Lock and Hide items keep their isolation behavior. Lock and Hide still share one selector. A lock flag on it selects the clip group, compound path and guide cases above; Hide > Unselected Artwork keeps its behavior, and a test covers both directions. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -61,8 +61,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Unselected Artwork",
|
||||
["Object", "Lock"],
|
||||
Some("Cmd+Alt+Shift+2"),
|
||||
"{} lock, 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 locks 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.unlockAll unlocks them again; disabled in isolation mode → {count}",
|
||||
has_selection_outside_isolation,
|
||||
"{} 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,
|
||||
|s, _| unselected_art(s, true)
|
||||
),
|
||||
cmd!(
|
||||
@@ -330,15 +330,24 @@ fn other_layers(s: &mut Session, lock: bool) -> Result<Value> {
|
||||
Ok(json!({ "count": n }))
|
||||
}
|
||||
|
||||
/// The objects [`super::select::walk_selectable`] visits that are not among the selected objects `roots` and hold none of
|
||||
/// them. The objects of a selected layer count as selected. Guide paths are left out while `guides_locked`, and so are
|
||||
/// the objects that hold one. The walk goes into a plain group that holds a selected object or such a guide path; a
|
||||
/// clip group, a Shaper group, a compound path and every other object that holds one is left out with its members.
|
||||
fn unselected(d: &vectorcraft_doc::Document, iso: Option<NodeId>, selection: &[NodeId], roots: &[NodeId], guides_locked: bool) -> Vec<NodeId> {
|
||||
/// Returns the objects [`super::select::walk_selectable`] visits that are not among the selected objects `roots` and
|
||||
/// hold none of them. A selected layer's objects count as selected. The walk goes into a plain group, but not a Shaper
|
||||
/// group, that holds a selected object. With `lock`, it also goes into a clip group or compound path that holds one,
|
||||
/// and includes guide paths while `guides_locked`. Without `lock`, guide paths and the objects that hold one are left
|
||||
/// out while `guides_locked`; a clip group, Shaper group, compound path and every other object that holds a selected
|
||||
/// object or such a guide are left out with their members.
|
||||
fn unselected(
|
||||
d: &vectorcraft_doc::Document,
|
||||
iso: Option<NodeId>,
|
||||
selection: &[NodeId],
|
||||
roots: &[NodeId],
|
||||
guides_locked: bool,
|
||||
lock: bool,
|
||||
) -> Vec<NodeId> {
|
||||
let mut roots: HashSet<NodeId> = roots.iter().copied().collect();
|
||||
roots.extend(selection.iter().filter_map(|id| d.node(*id)).filter(|n| n.is_layer()).flat_map(|l| l.layer_art(true)));
|
||||
let mut kept: Vec<NodeId> = roots.iter().copied().collect();
|
||||
if guides_locked {
|
||||
if guides_locked && !lock {
|
||||
super::select::walk_selectable(d, iso, |n| {
|
||||
if matches!(n.kind, NodeKind::Path { guide: true, .. }) {
|
||||
kept.push(n.id);
|
||||
@@ -353,7 +362,9 @@ fn unselected(d: &vectorcraft_doc::Document, iso: Option<NodeId>, selection: &[N
|
||||
out.push(n.id);
|
||||
return false;
|
||||
}
|
||||
!roots.contains(&n.id) && matches!(n.kind, NodeKind::Group { clip: false, .. }) && n.extras().shaper.is_none()
|
||||
!roots.contains(&n.id)
|
||||
&& (matches!(n.kind, NodeKind::Group { clip: false, .. }) && n.extras().shaper.is_none()
|
||||
|| lock && (matches!(n.kind, NodeKind::Group { clip: true, .. } | NodeKind::Compound { .. })))
|
||||
});
|
||||
out
|
||||
}
|
||||
@@ -362,7 +373,7 @@ fn unselected_art(s: &mut Session, lock: bool) -> Result<Value> {
|
||||
let roots = 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);
|
||||
let ids = unselected(&st.doc, st.isolation, &st.selection.objects, &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, _| {
|
||||
|
||||
@@ -518,8 +518,9 @@ fn lock_and_hide_with_nothing_to_lock_or_hide_record_no_undo_step() {
|
||||
|
||||
/// Hide › Unselected Artwork hides, in one undo step, each object that is not selected and holds no selected object:
|
||||
/// here `b`, the member `n` of the group that holds the selected `m`, and `p` on another layer. The selected objects,
|
||||
/// that group, the clip group that holds the selected `c2` with all its members, a locked object, the art on a locked
|
||||
/// layer and an object hidden before stay as they are, and so does the selection. Show All shows them again.
|
||||
/// that group, the clip group that holds the selected `c2` with all its members, the compound path that holds the
|
||||
/// selected `hole` with both paths, a guide under Lock Guides, a locked object, the art on a locked layer and an
|
||||
/// object hidden before stay as they are, and so does the selection. Show All shows them again.
|
||||
#[test]
|
||||
fn hide_unselected_artwork_hides_every_object_that_is_not_selected() {
|
||||
let mut s = session();
|
||||
@@ -534,6 +535,14 @@ fn hide_unselected_artwork_hides_every_object_that_is_not_selected() {
|
||||
let c3 = rect(&mut s, 80.0, 0.0, 30.0, 30.0);
|
||||
sel(&mut s, &[c1, c2, c3]);
|
||||
let clip = id_of(&s.execute("object.clippingMask.make", &json!({})).unwrap());
|
||||
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]);
|
||||
let compound = id_of(&s.execute("object.compoundPath.make", &json!({})).unwrap());
|
||||
let guide = rect(&mut s, 200.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[guide]);
|
||||
s.execute("view.guides.make", &json!({})).unwrap();
|
||||
s.execute("view.guides.lock", &json!({"locked": true})).unwrap();
|
||||
let k = rect(&mut s, 120.0, 0.0, 10.0, 10.0);
|
||||
s.execute("layer.setProps", &json!({"id": k.0, "locked": true})).unwrap();
|
||||
let h = rect(&mut s, 140.0, 0.0, 10.0, 10.0);
|
||||
@@ -544,13 +553,13 @@ fn hide_unselected_artwork_hides_every_object_that_is_not_selected() {
|
||||
let q = rect(&mut s, 20.0, 100.0, 10.0, 10.0);
|
||||
s.execute("layer.setProps", &json!({"id": locked_layer.0, "locked": true})).unwrap();
|
||||
let shown = |s: &Session, ids: &[NodeId]| ids.iter().map(|id| node(s, *id).visible).collect::<Vec<_>>();
|
||||
sel(&mut s, &[a, m, c2]);
|
||||
sel(&mut s, &[a, m, c2, hole]);
|
||||
let steps = undo_len(&s);
|
||||
assert_eq!(s.execute("object.hide.unselected", &json!({})).unwrap(), json!({"count": 3}));
|
||||
assert_eq!(shown(&s, &[b, n, p, h]), [false; 4]);
|
||||
assert_eq!(shown(&s, &[a, m, g, c1, c2, c3, clip, k, q]), [true; 9]);
|
||||
assert_eq!(shown(&s, &[a, m, g, c1, c2, c3, clip, compound, outer, hole, guide, k, q]), [true; 13]);
|
||||
assert!(node(&s, k).locked);
|
||||
assert_eq!((undo_len(&s), selected(&s)), (steps + 1, vec![a, m, c2]));
|
||||
assert_eq!((undo_len(&s), selected(&s)), (steps + 1, vec![a, m, c2, hole]));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(shown(&s, &[b, n, p]), [true; 3]);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
@@ -650,11 +659,10 @@ fn hide_unselected_artwork_keeps_groups_with_locked_guides_and_the_selected_laye
|
||||
assert_eq!(selected(&s), [other]);
|
||||
}
|
||||
|
||||
/// Lock › Unselected Artwork locks, in one undo step labeled "Lock", each object that is not selected and holds no
|
||||
/// selected object: here `b`, the member `n` of the group that holds the selected `m`, and `p` on another layer. The
|
||||
/// selected objects, that group, the clip group that holds the selected `c2` with all its members, an object hidden
|
||||
/// before and the art on a locked layer stay unlocked, and the selection stays as it is. Unlock All unlocks them again
|
||||
/// with the object locked before, and selects them.
|
||||
/// Lock Unselected Artwork locks `b`, `n` in the group that holds selected `m`, `p` on another layer, and the clip
|
||||
/// group's unselected members `c1` and clipping path `c3` in one undo step labeled "Lock". The clip group container,
|
||||
/// `c2`, the selected objects, group `g`, hidden object `h` and art `q` on a locked layer stay unlocked. `k` was locked
|
||||
/// before. The selection stays as it is. Unlock All unlocks all six locked objects and selects them.
|
||||
#[test]
|
||||
fn lock_unselected_artwork_locks_every_object_that_is_not_selected() {
|
||||
let mut s = session();
|
||||
@@ -681,29 +689,28 @@ fn lock_unselected_artwork_locks_every_object_that_is_not_selected() {
|
||||
let locked = |s: &Session, ids: &[NodeId]| ids.iter().map(|id| node(s, *id).locked).collect::<Vec<_>>();
|
||||
sel(&mut s, &[a, m, c2]);
|
||||
let steps = undo_len(&s);
|
||||
assert_eq!(s.execute("object.lock.unselected", &json!({})).unwrap(), json!({"count": 3}));
|
||||
assert_eq!(locked(&s, &[b, n, p, k]), [true; 4]);
|
||||
assert_eq!(locked(&s, &[a, m, g, c1, c2, c3, clip, h, q]), [false; 9]);
|
||||
assert_eq!(s.execute("object.lock.unselected", &json!({})).unwrap(), json!({"count": 5}));
|
||||
assert_eq!(locked(&s, &[b, n, p, c1, c3, k]), [true; 6]);
|
||||
assert_eq!(locked(&s, &[a, m, g, c2, clip, h, q]), [false; 7]);
|
||||
assert_eq!((undo_len(&s), selected(&s)), (steps + 1, vec![a, m, c2]));
|
||||
assert_eq!(s.doc().unwrap().history.undo.last().unwrap().label, "Lock");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(locked(&s, &[b, n, p]), [false; 3]);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(locked(&s, &[b, n, p]), [true; 3]);
|
||||
assert_eq!(s.execute("object.unlockAll", &json!({})).unwrap(), json!({"count": 4}));
|
||||
assert_eq!(locked(&s, &[b, n, p, k]), [false; 4]);
|
||||
assert_eq!(s.execute("object.unlockAll", &json!({})).unwrap(), json!({"count": 6}));
|
||||
assert_eq!(locked(&s, &[b, n, p, c1, c3, k]), [false; 6]);
|
||||
let mut unlocked = selected(&s);
|
||||
unlocked.sort();
|
||||
let mut want = vec![b, n, p, k];
|
||||
let mut want = vec![b, n, p, c1, c3, k];
|
||||
want.sort();
|
||||
assert_eq!(unlocked, want);
|
||||
}
|
||||
|
||||
/// In isolation mode Lock › 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 unlocked. With nothing left to
|
||||
/// lock it records no undo step and returns a count of 0. With Lock Guides off it locks a guide path like other art.
|
||||
/// Lock Unselected Artwork affects only the isolated group's other members. Lock Guides does not protect an unselected
|
||||
/// guide from this command. With nothing left to lock it records no undo step.
|
||||
#[test]
|
||||
fn lock_unselected_artwork_keeps_locked_guides_and_is_disabled_in_isolation_mode() {
|
||||
fn lock_unselected_artwork_scopes_isolation_and_locks_guides() {
|
||||
let mut s = session();
|
||||
let guide = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[guide]);
|
||||
@@ -718,26 +725,83 @@ fn lock_unselected_artwork_keeps_locked_guides_and_is_disabled_in_isolation_mode
|
||||
let locked = |s: &Session, ids: &[NodeId]| ids.iter().map(|id| node(s, *id).locked).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.lock.unselected").unwrap().info(&s);
|
||||
assert!(!info.enabled && info.disabled_reason.as_deref() == Some("exit isolation mode first"), "{info:?}");
|
||||
let e = s.execute("object.lock.unselected", &json!({})).unwrap_err();
|
||||
assert!(matches!(e, EngineError::Disabled(..)) && e.to_string().contains("isolation mode"), "{e}");
|
||||
assert_eq!(locked(&s, &[m, n, out, guide]), [false; 4]);
|
||||
assert_eq!((history(&s), selected(&s)), (before, vec![m]));
|
||||
assert!(info.enabled, "{info:?}");
|
||||
let before = undo_len(&s);
|
||||
assert_eq!(lock(&mut s), json!({"count": 1}));
|
||||
assert_eq!(locked(&s, &[m, n, out, guide]), [false, true, false, false]);
|
||||
assert_eq!((undo_len(&s), selected(&s)), (before + 1, vec![m]));
|
||||
s.execute("object.exitIsolation", &json!({})).unwrap();
|
||||
sel(&mut s, &[m]);
|
||||
assert!(find_command("object.lock.unselected").unwrap().info(&s).enabled);
|
||||
assert_eq!(lock(&mut s), json!({"count": 2}));
|
||||
assert_eq!(locked(&s, &[n, out, guide, g, m]), [true, true, false, false, false]);
|
||||
assert_eq!(locked(&s, &[n, out, guide, g, m]), [true, true, true, false, false]);
|
||||
select_with_a_step_to_redo(&mut s, &[m]);
|
||||
let before = history(&s);
|
||||
assert_eq!(lock(&mut s), json!({"count": 0}));
|
||||
assert_eq!(history(&s), before);
|
||||
s.execute("view.guides.lock", &json!({"locked": false})).unwrap();
|
||||
sel(&mut s, &[m]);
|
||||
assert_eq!(lock(&mut s), json!({"count": 1}));
|
||||
assert_eq!(locked(&s, &[guide]), [true]);
|
||||
}
|
||||
|
||||
/// Lock Unselected Artwork locks the unselected path of a partly selected compound path, but needs a selection.
|
||||
#[test]
|
||||
fn lock_unselected_artwork_descends_into_compound_and_needs_selection() {
|
||||
let mut s = session();
|
||||
let outer = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
let hole = rect(&mut s, 40.0, 40.0, 20.0, 20.0);
|
||||
sel(&mut s, &[outer, hole]);
|
||||
let compound = id_of(&s.execute("object.compoundPath.make", &json!({})).unwrap());
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
let before = history(&s);
|
||||
assert!(!find_command("object.lock.unselected").unwrap().info(&s).enabled);
|
||||
assert!(matches!(s.execute("object.lock.unselected", &json!({})), Err(EngineError::Disabled(..))));
|
||||
assert_eq!(history(&s), before);
|
||||
sel(&mut s, &[hole]);
|
||||
assert_eq!(s.execute("object.lock.unselected", &json!({})).unwrap(), json!({"count": 1}));
|
||||
assert!(node(&s, outer).locked);
|
||||
assert!(!node(&s, hole).locked);
|
||||
assert!(!node(&s, compound).locked);
|
||||
assert_eq!(selected(&s), [hole]);
|
||||
}
|
||||
|
||||
/// Lock Unselected Artwork leaves all art on a selected layer unlocked and locks unselected art on another layer.
|
||||
#[test]
|
||||
fn lock_unselected_artwork_keeps_a_selected_layers_art() {
|
||||
let mut s = session();
|
||||
let selected_layer = id_of(&s.execute("layer.new", &json!({})).unwrap());
|
||||
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);
|
||||
s.execute("layer.new", &json!({})).unwrap();
|
||||
let other = rect(&mut s, 40.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[selected_layer]);
|
||||
assert_eq!(s.execute("object.lock.unselected", &json!({})).unwrap(), json!({"count": 1}));
|
||||
assert_eq!([a, b, other].map(|id| node(&s, id).locked), [false, false, true]);
|
||||
assert_eq!(selected(&s), [selected_layer]);
|
||||
}
|
||||
|
||||
/// In a plain group, Lock and Hide Unselected Artwork affect the same members of identical documents.
|
||||
#[test]
|
||||
fn lock_and_hide_unselected_artwork_choose_the_same_objects() {
|
||||
let prepare = || {
|
||||
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 m = rect(&mut s, 40.0, 0.0, 10.0, 10.0);
|
||||
let n = rect(&mut s, 60.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[m, n]);
|
||||
let g = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
sel(&mut s, &[a, m]);
|
||||
(s, [a, b, m, n, g])
|
||||
};
|
||||
let (mut locked, lock_ids) = prepare();
|
||||
let (mut hidden, hide_ids) = prepare();
|
||||
assert_eq!(lock_ids, hide_ids);
|
||||
assert_eq!(locked.execute("object.lock.unselected", &json!({})).unwrap(), json!({"count": 2}));
|
||||
assert_eq!(hidden.execute("object.hide.unselected", &json!({})).unwrap(), json!({"count": 2}));
|
||||
let changed_by_lock: Vec<_> = lock_ids.into_iter().filter(|id| node(&locked, *id).locked).collect();
|
||||
let changed_by_hide: Vec<_> = hide_ids.into_iter().filter(|id| !node(&hidden, *id).visible).collect();
|
||||
assert_eq!(changed_by_lock, changed_by_hide);
|
||||
assert_eq!(changed_by_lock, [lock_ids[1], lock_ids[3]]);
|
||||
}
|
||||
|
||||
// ---------- Transform Each ----------
|
||||
|
||||
Reference in New Issue
Block a user