diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index a00f90952..62a17bda3 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 }; @@ -860,17 +862,20 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho Some(Some(name)) if remember => Some(*layers.entry(name).or_insert_with(|| layer_named(d, name))), _ => None, }; + // In a clip group or a clipping layer the bottom child is the clipping path: art pasted + // in back goes above it, not below it. + let floor = |d: &Document, par: Option| usize::from(par.and_then(|p| d.node(p)).is_some_and(Node::clips)); let (par, idx) = match (into, anchor, layer) { // Inside the object Draw Inside draws into, on top of what is in it already. (Some(group), ..) => (Some(group), usize::MAX), // Next to the anchor, unless the object goes back to another layer. (None, Some((par, i, _)), l) if l.is_none() || l == par => { let k = placed.get(&par).copied().unwrap_or(0); - (par, if front { i + 1 + k } else { i + k }) + (par, if front { i + 1 + k } else { i.max(floor(d, par)) + k }) } (None, _, l) => { let par = l.or(parent); - (par, if back { placed.get(&par).copied().unwrap_or(0) } else { usize::MAX }) + (par, if back { floor(d, par) + placed.get(&par).copied().unwrap_or(0) } else { usize::MAX }) } }; *placed.entry(par).or_default() += 1; @@ -900,6 +905,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..e7ddbc7c0 100644 --- a/crates/engine/src/tests/clipboard.rs +++ b/crates/engine/src/tests/clipboard.rs @@ -563,6 +563,75 @@ 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_in_back_of_a_clipping_path_keeps_it_the_clipping_path() { + let mut s = session(); + let children = |s: &Session, g: NodeId| doc(s).node(g).unwrap().children().unwrap().iter().map(|n| n.id).collect::>(); + let loose = rect(&mut s, 200.0, 0.0, 10.0, 10.0); + let other = rect(&mut s, 220.0, 0.0, 10.0, 10.0); + sel(&mut s, &[other]); + let group = id_of(&s.execute("object.group", &json!({})).unwrap()); + // A path as the clipping path, and a compound path as the clipping path (a group pasted in back + // of a member goes beside the compound path). + let art = rect(&mut s, 0.0, 0.0, 100.0, 100.0); + let mask = rect(&mut s, 10.0, 10.0, 50.0, 50.0); + sel(&mut s, &[art, mask]); + let g = id_of(&s.execute("object.clippingMask.make", &json!({})).unwrap()); + let art2 = rect(&mut s, 0.0, 120.0, 100.0, 100.0); + let (a, b) = (rect(&mut s, 10.0, 130.0, 20.0, 20.0), rect(&mut s, 40.0, 130.0, 20.0, 20.0)); + sel(&mut s, &[a, b]); + let c = id_of(&s.execute("object.compoundPath.make", &json!({})).unwrap()); + sel(&mut s, &[art2, c]); + let g2 = id_of(&s.execute("object.clippingMask.make", &json!({})).unwrap()); + for (g, clipping, target, copies) in [(g, mask, mask, vec![loose, group]), (g2, c, a, vec![group])] { + assert_eq!(children(&s, g)[0], clipping); + for copied in copies { + copy(&mut s, &[copied]); + sel(&mut s, &[target]); + let pasted = ids_of(&s.execute("edit.pasteInBack", &json!({})).unwrap()); + let ch = children(&s, g); + assert_eq!(ch[0], clipping, "the clipping path stays the bottom child"); + assert_eq!(&ch[1..2], &pasted[..], "right above the clipping path"); + } + } +} + #[test] fn paste_on_all_artboards_keeps_the_offset_to_the_source_artboard() { let mut s = Session::new();