diff --git a/crates/engine/src/atn_cmds.rs b/crates/engine/src/atn_cmds.rs index 64b2fa262e..0b70a15a76 100644 --- a/crates/engine/src/atn_cmds.rs +++ b/crates/engine/src/atn_cmds.rs @@ -1203,8 +1203,18 @@ fn make_from(d: &Descriptor) -> Mapped { if let Some(n) = using.and_then(|u| get_text(u, "Nm ")) { p.insert("name".into(), n.into()); } - let id = if get_ref(d, "From").is_some() { "layer.groupLayers" } else { "layer.new.group" }; - step(id, Value::Object(p)) + match get_ref(d, "From") { + // A named source layer stays the source (#7236); a target reference and an + // index/id source (our exporter never writes those) keep the old implicit + // import: the group lands on the current selection at playback. + Some(r) => { + if let Some(t) = layer_target(r) { + p.extend(t); + } + step("layer.groupLayers", Value::Object(p)) + } + None => step("layer.new.group", Value::Object(p)), + } } _ => Mapped::Unsupported { harmless: false }, } @@ -1231,7 +1241,9 @@ fn make_to(id: &str, p: &Value) -> Option { } "layer.new.group" => desc("null").with("null", class_ref("layerSection")).with("Usng", D::Descriptor(with_name("layerSection"))), "layer.groupLayers" => { - desc("null").with("null", class_ref("layerSection")).with("From", target_ref("Lyr ")).with("Usng", D::Descriptor(with_name("layerSection"))) + // An explicit source layer goes in by name like every other layer reference; a + // step without one keeps the target-layer form (#7236). + desc("null").with("null", class_ref("layerSection")).with("From", layer_ref(p)).with("Usng", D::Descriptor(with_name("layerSection"))) } "layer.new.layerViaCopy" | "layer.new.layerViaCut" => { let mut d = desc("null").with("Nw ", D::Descriptor(desc("Lyr "))).with("Usng", enm("ArSl", "Slct")); diff --git a/crates/engine/src/atn_cmds/tests.rs b/crates/engine/src/atn_cmds/tests.rs index 7ff734b618..2e89dab8ec 100644 --- a/crates/engine/src/atn_cmds/tests.rs +++ b/crates/engine/src/atn_cmds/tests.rs @@ -604,6 +604,16 @@ fn photo_filter_round_trip(params: Value) -> Option { } } +/// A `Mk` layerSection step exported to .atn and imported back → (id, params), or None. +fn group_round_trip(id: &str, params: &Value) -> Option<(String, Value)> { + let item = export_item(id, params, "S")?; + let e = event_for(&item.event)?; + match (e.from)(item.descriptor.as_ref()?) { + Mapped::Steps(steps) => steps.first().map(|(i, p)| (i.clone(), p.clone())), + _ => None, + } +} + #[test] fn photo_filter_preset_only_step_keeps_its_resolved_color() { // #7235: the Photoshop descriptor keeps only a colour (`phfl`/`PhtF` are colour-only; the io @@ -630,6 +640,44 @@ fn photo_filter_preset_only_step_keeps_its_resolved_color() { assert_eq!(back, json!({"density": 25})); } +#[test] +fn group_layers_keeps_its_explicit_target_through_atn() { + // #7236: a recorded step with an explicit layer groups that layer, not the current + // selection. The source goes into the `From` reference by name and comes back the same + // way; a step with no source layer keeps the implicit form, imported as before. + let (id, p) = group_round_trip("layer.groupLayers", &json!({"layer": "A", "name": "Group"})).unwrap(); + assert_eq!(id, "layer.groupLayers"); + assert_eq!(p, json!({"layer": "A", "name": "Group"})); + // The implicit form survives unchanged: the reference stays the target layer. + let (id, p) = group_round_trip("layer.groupLayers", &json!({"name": "Group"})).unwrap(); + assert_eq!(id, "layer.groupLayers"); + assert_eq!(p, json!({"name": "Group"})); + let (id, p) = group_round_trip("layer.groupLayers", &json!({})).unwrap(); + assert_eq!(id, "layer.groupLayers"); + assert_eq!(p, json!({})); + // Group Without Layers: no `From` at all, imported as a plain new group. + let (id, p) = group_round_trip("layer.new.group", &json!({"name": "G"})).unwrap(); + assert_eq!(id, "layer.new.group"); + assert_eq!(p, json!({"name": "G"})); +} + +#[test] +fn group_layers_from_index_reference_stays_implicit() { + // A Photoshop-authored `From` that names nothing portable (a zero-based index) keeps the + // old implicit import instead of inventing a wrong explicit target (#7236, edge). + let d = desc("null") + .with("null", class_ref("layerSection")) + .with("From", D::Reference(vec![ReferenceItem::Index { class: class("Lyr "), index: 2 }])) + .with("Usng", D::Descriptor(desc("layerSection").with("Nm ", text("Group")))); + match make_from(&d) { + Mapped::Steps(steps) => { + assert_eq!(steps[0].0, "layer.groupLayers"); + assert_eq!(steps[0].1, json!({"name": "Group"})); + } + _ => panic!("an index source imports as the implicit form"), + } +} + #[test] fn unsupported_shapes_become_photoshop_steps_not_errors() { // Image size in two different percentages, a Paint Bucket click in percent of the canvas, a