diff --git a/crates/engine/src/cmd/layerpanel.rs b/crates/engine/src/cmd/layerpanel.rs index f82e2890a..c302a7409 100644 --- a/crates/engine/src/cmd/layerpanel.rs +++ b/crates/engine/src/cmd/layerpanel.rs @@ -271,8 +271,19 @@ fn merge(s: &mut Session, p: &Value) -> Result { 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 = 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 = ids.iter().copied().filter(|id| *id != into).collect(); diff --git a/crates/engine/src/tests_layers.rs b/crates/engine/src/tests_layers.rs index 1e2e7e221..1fc789c80 100644 --- a/crates/engine/src/tests_layers.rs +++ b/crates/engine/src/tests_layers.rs @@ -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