From b1abc73e1679b2fb7a34e7d0b9a41894cfb281d8 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 20 Oct 2026 18:20:24 -0700 Subject: [PATCH] Isolation mode: object.isolate checks a Shaper Group's source group object.isolate on a Shaper Group isolates the group's source group, but the hidden and locked check ran on the Shaper Group itself, so a source group hidden or locked with object.setProps was still isolated. The check now runs on the object that isolation mode enters. --- crates/engine/src/cmd/object.rs | 7 ++++--- crates/engine/src/tests_menucmds.rs | 4 ++++ crates/engine/src/tests_shaper.rs | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index 4719a4e4f..292ac3607 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -1068,15 +1068,16 @@ fn isolate(s: &mut Session, p: &Value) -> Result { if matches!(st.doc.node(id).ok_or(EngineError::NoNode(id))?.kind, NodeKind::Text(_)) { return Err(bad("object.isolate", "type objects can't be isolated")); } + let isolation = st.doc.node(id).filter(|n| n.extras().shaper.is_some()).and_then(|n| n.children()).and_then(|c| c.first()).map_or(id, |n| n.id); // A hidden or locked object, one in a hidden or locked group or layer, and a guide path while // Lock Guides is on aren't isolated either: a click with the Selection tool doesn't select them. - if !st.doc.is_editable(id) { + // A Shaper Group isolates its source group, so that group is the one checked. + if !st.doc.is_editable(isolation) { return Err(bad("object.isolate", "the object is hidden or locked")); } - if guides_locked && crate::is_guide_path(&st.doc, id) { + if guides_locked && crate::is_guide_path(&st.doc, isolation) { return Err(EngineError::Other("guides are locked".into())); } - let isolation = st.doc.node(id).filter(|n| n.extras().shaper.is_some()).and_then(|n| n.children()).and_then(|c| c.first()).map_or(id, |n| n.id); st.isolation = Some(isolation); st.selection.clear(); st.revision += 1; diff --git a/crates/engine/src/tests_menucmds.rs b/crates/engine/src/tests_menucmds.rs index dec4a6563..449438312 100644 --- a/crates/engine/src/tests_menucmds.rs +++ b/crates/engine/src/tests_menucmds.rs @@ -2077,8 +2077,12 @@ fn guide_path(s: &mut Session, x: f64) -> NodeId { fn exit_isolation_leaves_a_locked_guide_path_deselected() { let mut s = session(); let g = guide_path(&mut s, 0.0); + // A locked guide path can't be isolated; one isolated before Lock Guides went on stays isolated. s.execute("view.guides.lock", &json!({"locked": true})).unwrap(); + assert!(s.execute("object.isolate", &json!({"id": g.0})).is_err()); + s.execute("view.guides.lock", &json!({"locked": false})).unwrap(); s.execute("object.isolate", &json!({"id": g.0})).unwrap(); + s.execute("view.guides.lock", &json!({"locked": true})).unwrap(); s.execute("object.exitIsolation", &json!({})).unwrap(); assert!(selected(&s).is_empty()); let _ = s.execute("object.move", &json!({"dx": 10, "dy": 0})); diff --git a/crates/engine/src/tests_shaper.rs b/crates/engine/src/tests_shaper.rs index 244aba6a5..0de25e814 100644 --- a/crates/engine/src/tests_shaper.rs +++ b/crates/engine/src/tests_shaper.rs @@ -311,6 +311,23 @@ fn shaper_construction_taps_stay_in_the_isolated_group() { assert_eq!(s.doc().unwrap().isolation, Some(sources)); } +/// `object.isolate` on a Shaper Group whose source group is locked or hidden is refused, and +/// isolation mode stays off. +#[test] +fn shaper_isolate_refuses_a_locked_or_hidden_source_group() { + for (key, on) in [("locked", true), ("visible", false)] { + let mut s = session(); + rectangle(&mut s, 0.0, "#ff0000"); + rectangle(&mut s, 50.0, "#0000ff"); + let g = scribble(&mut s, &[[25.0, 20.0], [75.0, 35.0], [30.0, 50.0], [125.0, 65.0]]); + let sources = node(&s, g).children().unwrap()[0].id; + s.execute("object.setProps", &json!({"ids": [sources.0], key: on})).unwrap(); + let r = s.execute("object.isolate", &json!({"id": g.0})).map_err(|e| e.to_string()); + assert_eq!(r, Err("invalid parameters for `object.isolate`: the object is hidden or locked".into()), "{key}"); + assert_eq!(s.doc().unwrap().isolation, None, "{key}"); + } +} + #[test] fn shaper_lines_keep_free_angles_with_or_without_shift() { for shift in [false, true] {