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();