From 827e137b5cc0283dd201e52fdca2c2d899117cdb Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 1 Nov 2026 06:42:33 -0800 Subject: [PATCH] Paste: in front or back of a compound path's member, other art goes beside the compound path With a member of a compound path selected, Paste in Front and Paste in Back put the clipboard's objects next to that member, inside the compound path, whatever they were. A pasted group, compound path or type object then sat inside a compound path, which holds only paths. The model-based test `random_command_sequences` stops on this with "compound #N has a non-path child". When the clipboard holds anything other than paths, the objects now go right above or below the compound path. Paths still go next to the member, inside the compound path. --- crates/engine/src/cmd/edit.rs | 15 ++++++++++-- crates/engine/src/tests/clipboard.rs | 36 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index a00f90952..3427e23f1 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -813,11 +813,13 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho && (st.doc.paste_remembers_layers || boards.is_some()) && parent.is_some_and(|p| st.doc.node(p).is_some_and(Node::is_layer)); // Front/back: relative to the selection (top-most / bottom-most selected object); with - // nothing selected, the top / bottom of the current layer. + // nothing selected, the top / bottom of the current layer. Art other than paths goes beside + // the compound path that holds the selected object, not into it. let anchor = if front || back { let order = st.selection.in_paint_order(&st.doc); let a = if front { order.last() } else { order.first() }; - a.and_then(|id| st.doc.position(*id)) + let paths_only = clip.nodes.iter().all(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Path { .. })); + a.map(|id| paste_anchor(&st.doc, *id, paths_only)).and_then(|id| st.doc.position(id)) } else { None }; @@ -900,6 +902,15 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho })) } +/// What pasted art goes beside: `id`, or the compound path holding it when the art isn't only +/// paths (a compound path holds paths only). +fn paste_anchor(doc: &Document, id: NodeId, paths_only: bool) -> NodeId { + match doc.parent_of(id) { + Some(p) if !paths_only && doc.node(p).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Compound { .. })) => p, + _ => id, + } +} + /// The first unlocked layer or sublayer called `name` (topmost first), or a new top layer. fn layer_named(d: &mut Document, name: &str) -> NodeId { let mut found = None; diff --git a/crates/engine/src/tests/clipboard.rs b/crates/engine/src/tests/clipboard.rs index 410a828b9..3336d384e 100644 --- a/crates/engine/src/tests/clipboard.rs +++ b/crates/engine/src/tests/clipboard.rs @@ -563,6 +563,42 @@ fn front_and_back_without_a_selection_use_the_current_layers_ends() { assert_eq!(&l[i - 2..i], &below[..]); } +#[test] +fn front_and_back_beside_a_compound_paths_member_go_beside_the_compound_path() { + 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 c = NodeId(s.execute("object.compoundPath.make", &json!({})).unwrap()["id"].as_u64().unwrap()); + let members = |s: &Session| doc(s).node(c).unwrap().children().unwrap().iter().map(|n| n.id).collect::>(); + let before = members(&s); + let layer = |s: &Session| doc(s).layers[0].children().unwrap().iter().map(|n| n.id).collect::>(); + // A compound path and a group, each pasted with one of the compound path's members selected. + let other = rect(&mut s, 40.0, 0.0, 10.0, 10.0); + sel(&mut s, &[other]); + let group = NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap()); + for copied in [c, group] { + copy(&mut s, &[copied]); + sel(&mut s, &[before[0]]); + let above = ids_of(&s.execute("edit.pasteInFront", &json!({})).unwrap()); + let l = layer(&s); + let i = l.iter().position(|id| *id == c).unwrap(); + assert_eq!(&l[i + 1..i + 2], &above[..], "right above the compound path"); + sel(&mut s, &[before[1]]); + let below = ids_of(&s.execute("edit.pasteInBack", &json!({})).unwrap()); + let l = layer(&s); + let i = l.iter().position(|id| *id == c).unwrap(); + assert_eq!(&l[i - 1..i], &below[..], "right below the compound path"); + assert_eq!(members(&s), before, "the compound path keeps its own members"); + } + // A path still goes in front of the member, inside the compound path. + let loose = rect(&mut s, 60.0, 0.0, 10.0, 10.0); + copy(&mut s, &[loose]); + sel(&mut s, &[before[0]]); + let path = ids_of(&s.execute("edit.pasteInFront", &json!({})).unwrap()); + assert_eq!(members(&s), [before[0], path[0], before[1]]); +} + #[test] fn paste_on_all_artboards_keeps_the_offset_to_the_source_artboard() { let mut s = Session::new();