From b8d52cd8c47144b6ca1cf39f50613348a4df1ef4 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:36:34 +0200 Subject: [PATCH] Paste path operators below the shapes they act on (#1681) Pasting Trim Paths, Wiggle Paths or a paint into a shape layer with nothing selected put it on top of Contents, where it acts on no shapes. It now goes to the bottom, as Add places it. Also fixes #1629. --- crates/engine/src/commands/prop_groups.rs | 10 ++++++++-- crates/engine/src/tests_menu_cmds.rs | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/commands/prop_groups.rs b/crates/engine/src/commands/prop_groups.rs index e4d80b16..6bb2861a 100644 --- a/crates/engine/src/commands/prop_groups.rs +++ b/crates/engine/src/commands/prop_groups.rs @@ -127,8 +127,9 @@ pub(crate) fn cut_contents(s: &mut Session, items: &[(LayerId, Uid)]) -> Result< } /// Edit ▸ Paste with shape items on the clipboard: copies go into each selected shape layer's -/// Contents, above its topmost selected item (in that item's group), else on top. Names stay -/// unique in their group ("Rectangle 2"…), and the copies are selected. +/// Contents, above its topmost selected item (in that item's group), else on top (paints and path +/// operations alone: at the bottom). Names stay unique in their group ("Rectangle 2"…), and the +/// copies are selected. pub(crate) fn paste_contents(s: &mut Session, p: &Value) -> Result { let c = "edit.paste"; let clip = s.state.contents_clipboard.clone(); @@ -140,6 +141,7 @@ pub(crate) fn paste_contents(s: &mut Session, p: &Value) -> Result { return Err(bad(c, "select a shape layer to paste shape items into")); } let sel = selected_contents(s); + let acts_on_above = clip.iter().all(|g| !matches!(g.match_id.as_str(), "group" | "rect" | "ellipse" | "star" | "path")); let label = match clip.as_slice() { [g] => format!("Paste {}", g.name), _ => "Paste".to_string(), @@ -154,11 +156,15 @@ pub(crate) fn paste_contents(s: &mut Session, p: &Value) -> Result { let parent = l.props.parent_of(*u)?; Some((parent.uid, parent.children.iter().position(|n| n.uid() == *u)?)) }); + let unselected = spot.is_none(); let (parent, at) = match spot { Some((pu, i)) => (l.props.find_group_mut(pu), i), None => (l.props.sub_mut("contents"), 0), }; let parent = parent.ok_or_else(|| bad(c, "the layer has no contents"))?; + // Trim Paths, Wiggle Paths, fills… act on the items above them (as Add places them), + // so with nothing selected they go to the bottom: on top they'd affect nothing (#1681). + let at = if unselected && acts_on_above { parent.children.len() } else { at }; for (k, g) in clip.iter().enumerate() { let mut g = g.clone(); g.reassign_uids(&mut next); diff --git a/crates/engine/src/tests_menu_cmds.rs b/crates/engine/src/tests_menu_cmds.rs index 4e01e380..90bca241 100644 --- a/crates/engine/src/tests_menu_cmds.rs +++ b/crates/engine/src/tests_menu_cmds.rs @@ -1541,6 +1541,21 @@ fn shape_contents_copy_cut_and_paste_between_shape_layers() { assert!(s.execute("edit.paste", json!({})).is_err()); } +/// Pasting Trim Paths into another shape layer with nothing selected puts it below the shapes, +/// where it acts on them (it acts on the items above it); on top it would trim nothing (#1681, #1629). +#[test] +fn pasted_path_operator_goes_below_the_shapes_it_acts_on() { + let mut s = comp(); + let a = s.execute("layer.newShape", json!({"kind": "rect"})).unwrap()["layer"].as_u64().unwrap(); + let trim = s.execute("layer.addShapeItem", json!({"layer": a, "kind": "trim"})).unwrap()["uid"].as_u64().unwrap(); + let b = s.execute("layer.newShape", json!({"kind": "star", "name": "B"})).unwrap()["layer"].as_u64().unwrap(); + s.execute("prop.select", json!({"layer": a, "prop": trim})).unwrap(); + s.execute("edit.copy", json!({})).unwrap(); + s.execute("layer.select", json!({"layers": [b]})).unwrap(); + s.execute("edit.paste", json!({})).unwrap(); + assert_eq!(contents_names(&s, b), ["Polystar 1", "Trim Paths 1"]); +} + /// Edit ▸ Duplicate with shape items selected duplicates them in their layer, above the /// original and named "Rectangle 2", not the layer (#227). #[test]