From f5ff9317d4ad89b499bc1701873192f24db87c30 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 1 Oct 2026 20:56:22 -0400 Subject: [PATCH] =?UTF-8?q?M6.7:=20Object=20=E2=86=92=20Clipping=20Mask=20?= =?UTF-8?q?=E2=86=92=20Edit=20Contents=20/=20Edit=20Clipping=20Path=20(sel?= =?UTF-8?q?ect=20the=20clipped=20art=20or=20the=20mask=20of=20the=20select?= =?UTF-8?q?ed=20clip=20groups,=20also=20from=20inside=20one)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/object.rs | 45 +++++++++++++++++++++++++++++++++ crates/engine/src/tests.rs | 5 ++++ crates/ui-egui/src/menus.rs | 10 +++++++- 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index 585a2f48..6a123e46 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -89,6 +89,24 @@ pub fn specs() -> Vec { cmd!("object.compoundPath.release", "Release", ["Object", "Compound Path"], Some("Cmd+Alt+Shift+8"), "{}", has_selection, compound_release), cmd!("object.clippingMask.make", "Make", ["Object", "Clipping Mask"], Some("Cmd+7"), "{}", has_multi, clip_make), cmd!("object.clippingMask.release", "Release", ["Object", "Clipping Mask"], Some("Cmd+Alt+7"), "{}", has_selection, clip_release), + cmd!( + "object.clippingMask.editContents", + "Edit Contents", + ["Object", "Clipping Mask"], + None, + "{} select the clipped art of the selected clip groups → {count}", + has_selection, + |s, _| clip_edit(s, false) + ), + cmd!( + "object.clippingMask.editMask", + "Edit Clipping Path", + ["Object", "Clipping Mask"], + None, + "{} select the clipping paths of the selected clip groups → {count}", + has_selection, + |s, _| clip_edit(s, true) + ), cmd!("object.isolate", "Enter Isolation Mode", [], None, "{id}", has_doc, isolate), cmd!("object.exitIsolation", "Exit Isolation Mode", [], None, "{}", has_doc, exit_isolation), cmd!( @@ -543,6 +561,33 @@ fn clip_release(s: &mut Session, _: &Value) -> Result { ok() } +/// Selects the clipping path (`mask`) or the clipped contents of every selected clip group (or of the +/// clip group containing a selected object), like Illustrator's Edit Contents / Edit Clipping Path toggle. +fn clip_edit(s: &mut Session, mask: bool) -> Result { + let st = s.doc()?; + let mut ids = vec![]; + for id in st.selection.objects.iter().copied() { + let mut cur = Some(id); + while let Some(c) = cur { + if let Some(NodeKind::Group { children, clip: true }) = st.doc.node(c).map(|n| &n.kind) { + if mask { + ids.extend(children.first().map(|c| c.id)); + } else { + ids.extend(children.iter().skip(1).map(|c| c.id)); + } + break; + } + cur = st.doc.position(c).and_then(|(par, _, _)| par); + } + } + ids.dedup(); + if ids.is_empty() { + return Err(EngineError::Other("select a clip group".into())); + } + s.select(|_, sel| sel.set(ids.iter().copied()))?; + Ok(json!({ "count": ids.len() })) +} + fn isolate(s: &mut Session, p: &Value) -> Result { let id = id_param(p, "id") .or_else(|| s.active().and_then(|d| d.selection.objects.first().copied())) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 923da0bd..10a2ded2 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -178,6 +178,11 @@ fn clipping_and_compound() { assert_eq!(s.doc().unwrap().selection.len(), 2); let g = s.execute("object.clippingMask.make", &json!({})).unwrap()["id"].as_u64().unwrap(); assert_eq!(s.doc().unwrap().doc.node(NodeId(g)).unwrap().kind_label(), "Clip Group"); + // Edit Contents selects the clipped art; Edit Clipping Path the mask (also from inside the group). + s.execute("object.clippingMask.editContents", &json!({})).unwrap(); + assert_eq!(s.doc().unwrap().selection.objects, vec![a]); + s.execute("object.clippingMask.editMask", &json!({})).unwrap(); + assert_eq!(s.doc().unwrap().selection.objects, vec![b]); } #[test] diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 75ff8d96..90c45756 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -782,7 +782,15 @@ pub fn menu_tree() -> Vec<(&'static str, Vec)> { ), sub("Text Wrap", vec![todo("Make"), todo("Release"), todo("Text Wrap Options…")]), Sep, - sub("Clipping Mask", vec![c("Make", "object.clippingMask.make"), c("Release", "object.clippingMask.release"), todo("Edit Contents")]), + sub( + "Clipping Mask", + vec![ + c("Make", "object.clippingMask.make"), + c("Release", "object.clippingMask.release"), + c("Edit Contents", "object.clippingMask.editContents"), + c("Edit Clipping Path", "object.clippingMask.editMask"), + ], + ), sub("Compound Path", vec![c("Make", "object.compoundPath.make"), c("Release", "object.compoundPath.release")]), sub( "Artboards",