Keep the target's paint when pasting Merge Paths (#1681)

This commit is contained in:
MarekSpodBiedry
2026-10-18 10:57:23 +02:00
parent 4f49017329
commit 0e7f525a81
2 changed files with 38 additions and 1 deletions
+19 -1
View File
@@ -142,6 +142,8 @@ pub(crate) fn paste_contents(s: &mut Session, p: &Value) -> Result<Value> {
}
let sel = selected_contents(s);
let acts_on_above = clip.iter().all(|g| !matches!(g.match_id.as_str(), "group" | "rect" | "ellipse" | "star" | "path"));
// Merge Paths clears the draws above it, so at the bottom it would erase the paints (#1681).
let has_merge = clip.iter().any(|g| g.match_id == "merge");
let label = match clip.as_slice() {
[g] => format!("Paste {}", g.name),
_ => "Paste".to_string(),
@@ -164,7 +166,11 @@ pub(crate) fn paste_contents(s: &mut Session, p: &Value) -> Result<Value> {
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 };
let at = match unselected {
true if has_merge => merge_spot(parent),
true if acts_on_above => parent.children.len(),
_ => at,
};
for (k, g) in clip.iter().enumerate() {
let mut g = g.clone();
g.reassign_uids(&mut next);
@@ -180,6 +186,18 @@ pub(crate) fn paste_contents(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"contents": pasted.iter().map(|(_, u)| *u).collect::<Vec<_>>()}))
}
/// Where Merge Paths goes in `contents` when nothing is selected: before the first paint, so the
/// paints still draw the merged path (the renderer drops the draws above a Merge Paths), else after
/// the last path item.
fn merge_spot(contents: &PropGroup) -> usize {
let is = |n: &Node, ids: &[&str]| matches!(n, Node::Group(g) if ids.contains(&g.match_id.as_str()));
let paints = ["fill", "stroke", "gfill", "gstroke"];
match contents.children.iter().position(|n| is(n, &paints)) {
Some(i) => i,
None => contents.children.iter().rposition(|n| is(n, &["rect", "ellipse", "star", "path"])).map_or(0, |i| i + 1),
}
}
/// Edit ▸ Duplicate with shape items selected: each is copied in place, above the original and
/// named as After Effects does ("Rectangle 1" → "Rectangle 2"), and the copies are selected.
pub(crate) fn duplicate_contents(s: &mut Session, items: &[(LayerId, Uid)]) -> Result<Value> {
+19
View File
@@ -1556,6 +1556,25 @@ fn pasted_path_operator_goes_below_the_shapes_it_acts_on() {
assert_eq!(contents_names(&s, b), ["Polystar 1", "Trim Paths 1"]);
}
/// Pasting Merge Paths onto a painted layer with nothing selected keeps the paint: the renderer
/// drops the draws above a Merge Paths, so it goes between the path and the paints (#1681).
#[test]
fn pasted_merge_paths_keeps_the_targets_paint() {
let mut s = comp();
let a = s.execute("layer.newShape", json!({"kind": "none", "name": "A"})).unwrap()["layer"].as_u64().unwrap();
let merge = s.execute("layer.addShapeItem", json!({"layer": a, "kind": "merge"})).unwrap()["uid"].as_u64().unwrap();
let b = s.execute("layer.newShape", json!({"kind": "none", "name": "B"})).unwrap()["layer"].as_u64().unwrap();
s.execute("layer.addShapeItem", json!({"layer": b, "kind": "rect"})).unwrap();
s.execute("layer.addShapeItem", json!({"layer": b, "kind": "fill"})).unwrap();
let before = opaque_centroid(&s).2;
s.execute("prop.select", json!({"layer": a, "prop": merge})).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), ["Rectangle Path 1", "Merge Paths 1", "Fill 1"]);
assert_eq!(opaque_centroid(&s).2, before);
}
/// Edit ▸ Duplicate with shape items selected duplicates them in their layer, above the
/// original and named "Rectangle 2", not the layer (#227).
#[test]