Merge PR #7968: fix(atn): a Group Layers step keeps its explicit layer through .atn (#7236)

Conflicts:
crates/engine/src/atn_cmds/tests.rs: appended test helper and tests (two blocks), kept both
This commit is contained in:
aiongg
2026-10-22 02:25:59 +00:00
2 changed files with 63 additions and 3 deletions
+15 -3
View File
@@ -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<Descriptor> {
}
"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"));
+48
View File
@@ -604,6 +604,16 @@ fn photo_filter_round_trip(params: Value) -> Option<Value> {
}
}
/// 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