Layers: Merge Selected with a layer and its sublayer highlighted merges into the last top-level layer (#2383)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76
2026-10-17 13:06:51 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent 2a3efa02f2
commit 1ff7ac7557
2 changed files with 44 additions and 3 deletions
+13 -2
View File
@@ -271,8 +271,19 @@ fn merge(s: &mut Session, p: &Value) -> Result<Value> {
if ids.len() < 2 {
return Err(bad(C, "highlight two or more rows to merge"));
}
let into =
ids.iter().rev().copied().find(|id| d.node(*id).is_some_and(Node::is_layer)).ok_or_else(|| bad(C, "highlight a layer to merge into"))?;
// The last highlighted layer that is not inside another highlighted row: with a layer and its
// sublayer both highlighted (Select All in the panel), the sublayer travels with its layer
// instead of becoming the target (#2383).
let roots: HashSet<NodeId> = row_roots(d, &ids).into_iter().collect();
let is_layer = |id: &NodeId| d.node(*id).is_some_and(Node::is_layer);
let into = ids
.iter()
.rev()
.copied()
.filter(is_layer)
.find(|id| roots.contains(id))
.or_else(|| ids.iter().rev().copied().find(is_layer))
.ok_or_else(|| bad(C, "highlight a layer to merge into"))?;
let around = d.ancestry(into).unwrap_or_default();
// (Rows inside the target layer merge too: only rows inside other listed rows go with those.)
let rest: Vec<NodeId> = ids.iter().copied().filter(|id| *id != into).collect();
+31 -1
View File
@@ -578,7 +578,11 @@ fn collect_merge_flatten_and_reverse() {
run(&mut s, "layer.merge", json!({"ids": [col.0, layer.0]}));
assert!(s.doc().unwrap().doc.node(col).is_none());
assert_eq!(children(&s, layer), vec![c, sub, a, g]);
assert!(s.execute("layer.merge", &json!({"ids": [layer.0, sub.0]})).is_err(), "not into its own sublayer");
// A layer and its sublayer: the sublayer merges into its layer, never the other way round.
run(&mut s, "layer.merge", json!({"ids": [layer.0, sub.0]}));
assert!(s.doc().unwrap().doc.node(sub).is_none());
assert_eq!(children(&s, layer), vec![c, a, g]);
run(&mut s, "edit.undo", json!({}));
assert!(s.execute("layer.merge", &json!({"ids": [layer.0]})).is_err());
// Flatten: other visible layers merge in, hidden ones go, templates stay.
let l2 = id(&run(&mut s, "layer.new", json!({"top": true})));
@@ -595,6 +599,32 @@ fn collect_merge_flatten_and_reverse() {
assert_eq!(undos(&s), before + 1);
}
/// Merge Selected with every row highlighted (a layer, its sublayer and another layer, in panel
/// order) merges the layers into the last top-level one instead of refusing a sublayer target (#2383).
#[test]
fn merge_selected_with_all_layer_rows_highlighted_merges_into_the_last_top_level_layer() {
for reversed in [false, true] {
let mut s = session();
let (layer, sub, a, _, _, c) = tree(&mut s);
let l2 = id(&run(&mut s, "layer.new", json!({"top": true})));
let d = rect(&mut s, 500.0);
// Panel order, top first: Layer 2, Layer 1, its sublayer.
let mut rows = vec![l2.0, layer.0, sub.0];
if reversed {
rows.reverse();
}
run(&mut s, "layer.highlight", json!({"ids": rows}));
let into = id(&run(&mut s, "layer.merge", json!({})));
let st = s.doc().unwrap();
assert_eq!(st.doc.layers.len(), 1, "reversed: {reversed}");
assert_eq!(into, st.doc.layers[0].id);
for n in [a, c, d] {
assert!(st.doc.layer_of(n) == Some(into), "{n:?} reversed: {reversed}");
}
assert!(st.doc.node(sub).is_some() || st.doc.node(layer).is_some() || st.doc.node(l2).is_some());
}
}
/// Collect in New Layer puts the new layer beside the topmost row when that row is directly in a
/// layer, and on top of the layer holding its group when it is in a group. When that row is in a
/// hidden or locked layer, the selected art it collects from another layer is deselected, the key