diff --git a/crates/engine/src/cmd/atn_cmds/steps_content_aware.rs b/crates/engine/src/cmd/atn_cmds/steps_content_aware.rs index 945cb8ee23..db5dc59a64 100644 --- a/crates/engine/src/cmd/atn_cmds/steps_content_aware.rs +++ b/crates/engine/src/cmd/atn_cmds/steps_content_aware.rs @@ -6,11 +6,12 @@ use super::{D, Descriptor, Map, Value}; const CMD: &str = "edit.contentAwareFill"; const SOURCE: &str = "_atnCafDescriptor"; -// The measured row names these values; its ellipses are not additional enum bindings. +// The measured row bounds these values; its ellipses are not additional enum bindings. const SAMPLING: &[(&str, &str)] = &[("cafSamplingRegionRectangular", "rectangular")]; const COLOR: &[(&str, &str)] = &[("cafColorAdaptationDefault", "default")]; const ROTATION: &[(&str, &str)] = &[("cafRotationAmountNone", "none")]; -const OUTPUT: &[(&str, &str)] = &[("cafOutputToNewLayer", "new")]; +// Current Layer is also explicit in the primary Action Manager example cited in docs/atn.md. +const OUTPUT: &[(&str, &str)] = &[("cafOutputToNewLayer", "new"), ("cafOutputToCurrentLayer", "current")]; const KEYS: &[&str] = &[ "cafSamplingRegion", "cafSampleAllLayers", diff --git a/crates/engine/tests/it/atn_content_aware_fill.rs b/crates/engine/tests/it/atn_content_aware_fill.rs index d8175c0b5d..0bb798d2bd 100644 --- a/crates/engine/tests/it/atn_content_aware_fill.rs +++ b/crates/engine/tests/it/atn_content_aware_fill.rs @@ -69,6 +69,11 @@ fn caf_settings_roundtrip_seven_fields_and_boolean_choices() { } cases.push((d, p)); } + let mut current = base.clone(); + current.items[6].1 = enm("cafOutput", "cafOutputToCurrentLayer"); + let mut p = params(); + p["output"] = json!("current"); + cases.push((current, p)); for (d, p) in cases { let action = load(d.clone()); assert_eq!(action.steps, vec![(CMD.into(), p.clone())]); @@ -129,7 +134,6 @@ fn caf_unrepresented_descriptors_are_kept_and_never_partially_play() { (3, "cafRotationAmount", "cafRotationAmountHigh"), (3, "cafRotationAmount", "cafRotationAmountFull"), (3, "cafRotationAmount", "unknown"), - (6, "cafOutput", "cafOutputToCurrentLayer"), (6, "cafOutput", "cafOutputToDuplicateLayer"), ] { let mut d = base.clone(); @@ -170,9 +174,15 @@ fn caf_native_exports_preserve_effective_defaults_and_refuse_unrepresented_optio let a = load(exported(p).unwrap().descriptor.unwrap()); assert_eq!(a.steps, vec![(CMD.into(), params())]); } + let mut current = params(); + current["output"] = json!("current"); + for p in [json!({"sampling":"rectangular"}), current.clone()] { + let a = load(exported(p).unwrap().descriptor.unwrap()); + assert_eq!(a.steps, vec![(CMD.into(), current.clone())]); + } for (key, value) in [ ("sampling", json!("auto")), ("sampling", json!("custom")), ("sampling", json!("unknown")), - ("output", json!("current")), ("output", json!("duplicate")), + ("output", json!("duplicate")), ("colorAdaptation", json!("none")), ("colorAdaptation", json!("high")), ("colorAdaptation", json!("veryHigh")), ("rotationAdaptation", json!("low")), ("rotationAdaptation", json!("medium")), ("rotationAdaptation", json!("high")), ("rotationAdaptation", json!("full")), @@ -184,7 +194,7 @@ fn caf_native_exports_preserve_effective_defaults_and_refuse_unrepresented_optio p[key] = value; assert!(exported(p.clone()).is_none(), "{p}"); } - for p in [json!({}), json!({"sampling":"rectangular"}), json!({"output":"new"}), json!([])] { + for p in [json!({}), json!({"output":"new"}), json!([])] { assert!(exported(p.clone()).is_none(), "unrepresented native default: {p}"); } } @@ -237,7 +247,7 @@ fn caf_action_matches_native_pixels_and_undo_at_each_depth() { assert!(s.redo()); assert_eq!(s.active().unwrap().doc, after); - // Current-layer native calls still work, although their ATN form is outside this increment. + // Current Layer keeps one layer and matches the ordinary native operation as well. let mut current = session(depth); let mut p = params(); p["output"] = json!("current"); @@ -245,6 +255,32 @@ fn caf_action_matches_native_pixels_and_undo_at_each_depth() { assert_eq!(current.active().unwrap().doc.layers.len(), 1); let center = current.active().unwrap().doc.layers[0].surface().unwrap().rgba(3, 3); assert!(center.iter().all(|v| (*v - 1.0).abs() < 0.01), "{depth}: native Current Layer control failed: {center:?}"); + let mut recorded = session(depth); + let before = recorded.active().unwrap().doc.clone(); + let source = recorded.active().unwrap().active_layer.unwrap(); + let past = recorded.active().unwrap().history.past_len(); + let mut d = descriptor(); + d.items[6].1 = enm("cafOutput", "cafOutputToCurrentLayer"); + recorded.actions.list.push(load(d)); + let result = recorded.execute("actions.play", json!({"action":0})).unwrap(); + assert_eq!(result["ran"], 1); + assert!(result.get("failed").is_none(), "{depth}: {result}"); + let after = recorded.active().unwrap().doc.clone(); + assert_eq!(after.layers.len(), before.layers.len()); + assert_eq!(after.selection, before.selection); + assert_eq!(recorded.active().unwrap().active_layer, Some(source)); + assert_eq!(recorded.active().unwrap().history.past_len(), past + 1); + let expected = current.active().unwrap().doc.layers[0].surface().unwrap(); + let actual = after.layer(source).unwrap().surface().unwrap(); + for y in 0..8 { + for x in 0..8 { + assert_eq!(actual.rgba(x, y), expected.rgba(x, y), "{depth}: Current Layer pixel{x},{y}"); + } + } + assert!(recorded.undo()); + assert_eq!(recorded.active().unwrap().doc, before); + assert!(recorded.redo()); + assert_eq!(recorded.active().unwrap().doc, after); } } diff --git a/docs/atn.md b/docs/atn.md index cc605c60cf..e7d1f41c1e 100644 --- a/docs/atn.md +++ b/docs/atn.md @@ -170,9 +170,11 @@ the reference application. ### Content-Aware Fill recordings -`cafWorkspace` maps the measured Rectangular / Default color adaptation / None rotation / -New Layer form to `edit.contentAwareFill`, retaining all seven keys and the three booleans: +`cafWorkspace` maps Rectangular / Default color adaptation / None rotation with New Layer or +Current Layer output to `edit.contentAwareFill`, retaining all seven keys and the three booleans: Sample All Layers, Scale and Mirror. The selection comes from the preceding selection step. +New Layer is in the measured reference; Current Layer and the enumeration type IDs are explicit +in [Stephen Marsh's published Action Manager example](https://community.adobe.com/questions-712/expand-and-content-aware-fill-functions-are-not-working-via-javascript-1172085). | Recorded key, in order | Supported value | Native parameter | |---|---|---| @@ -182,15 +184,15 @@ Sample All Layers, Scale and Mirror. The selection comes from the preceding sele | `cafRotationAmount` | `cafRotationAmountNone` | `rotationAdaptation: "none"` | | `cafScale` | boolean | `scale` | | `cafMirror` | boolean | `mirror` | -| `cafOutput` | `cafOutputToNewLayer` | `output: "new"` | +| `cafOutput` | `cafOutputToNewLayer`, `cafOutputToCurrentLayer` | `output: "new"`, `"current"` | Descriptor types, source identity and key order survive save while its native settings are unchanged. Native export writes the seven keys explicitly; the known native boolean, color -adaptation and rotation defaults are supported when sampling is Rectangular and output is New -Layer. A native call using Auto or Current Layer defaults cannot yet be exported. +adaptation, rotation and Current Layer output defaults are supported when sampling is explicitly +Rectangular. A native call using the Auto sampling default cannot yet be exported. The reference's ellipses do not establish additional enum forms. Auto/Custom sampling, -other adaptation levels, Current/Duplicate Layer output, absent keys, unknown settings and +other adaptation levels, Duplicate Layer output, absent keys, unknown settings and malformed or repeated fields remain complete kept steps. Native layer targets, sampling/fill edits and nondefault seeds also refuse export rather than lose their settings. Playback uses the existing native fill and its selection, authorization, job and Undo/Redo behavior. The