Refuse strict .atn export of a mask/channel-targeted Free Transform

Fixes #7195

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-19 20:48:58 +00:00
co-authored by Claude Haiku 5.5
parent 3a896a5efc
commit 47f7c97a3b
2 changed files with 32 additions and 0 deletions
+3
View File
@@ -1651,6 +1651,9 @@ fn transform_to(id: &str, p: &Value) -> Option<Descriptor> {
return None;
}
let target = match id {
// The `Trnf` descriptor has no field for a mask, Quick Mask or alpha channel target; a
// layer reference would transform the layer's pixels instead.
"edit.transform" if !pixel_target(p) => return None,
"edit.transform" => layer_ref(p),
"select.transformSelection" => D::Reference(vec![ReferenceItem::Property { class: class("Chnl"), key: code("fsel") }]),
_ => return None,
+29
View File
@@ -1387,3 +1387,32 @@ fn function_keys_keep_shift_and_command() {
}
assert_eq!(super::function_key_name(7, false, false), "F7");
}
/// A Free Transform recorded against an unlinked mask or an alpha channel must not export as a
/// layer transform that moves the layer's pixels on replay (#7195).
#[test]
fn transform_with_mask_or_channel_target_is_not_exported_as_layer_transform() {
for target in [json!("mask"), json!({"channel": 0}), json!("quickMask")] {
let a = Action {
name: "Probe".into(),
steps: vec![("edit.transform".into(), json!({"target": target, "dx": 3, "dy": 2, "scaleX": 100, "interpolation": "nearest"}))],
..Default::default()
};
assert!(export("Targets", &[&a], &[], false).is_err(), "{target} exported");
let (_, skipped) = export("Targets", &[&a], &[], true).unwrap();
assert_eq!(skipped, vec![Skipped { action: "Probe".into(), step: 0, id: "edit.transform".into() }]);
}
for params in [
json!({"dx": 3, "dy": 2, "scaleX": 50, "interpolation": "nearest"}),
json!({"target": "pixels", "dx": 3, "dy": 2, "scaleX": 50, "interpolation": "nearest"}),
] {
let plain = Action { name: "Plain".into(), steps: vec![("edit.transform".into(), params)], ..Default::default() };
let (bytes, skipped) = export("Targets", &[&plain], &[], false).unwrap();
assert!(skipped.is_empty());
let imported = import(&bytes).unwrap();
assert_eq!(imported.actions[0].steps, vec![("edit.transform".to_string(), json!({"dx": 3, "dy": 2, "scaleX": 50, "interpolation": "nearest"}))]);
}
for form in [json!({"quad": [[0, 0], [1, 0], [1, 1], [0, 1]]}), json!({"matrix": [1, 0, 0, 1, 0, 0]}), json!({"rect": [0, 0, 1, 1]})] {
assert!(transform_to("edit.transform", &form).is_none());
}
}