From d94717c8ca7ed5978a3638ebff49532cdfe27f80 Mon Sep 17 00:00:00 2001 From: yoriswind Date: Mon, 2 Nov 2026 19:47:45 -0800 Subject: [PATCH] Actions: map the measured Content-Aware Fill recording Map the seven-field Rectangular/Default/None/New Layer form to the existing CAF command, retaining source descriptor data only while native settings agree. Keep other enum forms whole and refuse unrepresented native exports. Add focused public regressions, the common round-trip case and supported-form documentation. This is a source-only draft increment: formatting, compilation and runtime validation remain pending under native0. --- crates/engine/src/cmd/atn_cmds.rs | 1 + crates/engine/src/cmd/atn_cmds/events.rs | 2 + .../src/cmd/atn_cmds/steps_content_aware.rs | 99 +++++++ crates/engine/src/cmd/atn_cmds/tests.rs | 5 + .../engine/tests/it/atn_content_aware_fill.rs | 264 ++++++++++++++++++ crates/engine/tests/it/main.rs | 1 + docs/atn.md | 32 ++- 7 files changed, 402 insertions(+), 2 deletions(-) create mode 100644 crates/engine/src/cmd/atn_cmds/steps_content_aware.rs create mode 100644 crates/engine/tests/it/atn_content_aware_fill.rs diff --git a/crates/engine/src/cmd/atn_cmds.rs b/crates/engine/src/cmd/atn_cmds.rs index 47f4b688fb..eaa0e61acf 100644 --- a/crates/engine/src/cmd/atn_cmds.rs +++ b/crates/engine/src/cmd/atn_cmds.rs @@ -34,6 +34,7 @@ mod fields; mod files; mod steps_channels; +mod steps_content_aware; mod steps_distort; mod steps_doc; mod steps_gallery; diff --git a/crates/engine/src/cmd/atn_cmds/events.rs b/crates/engine/src/cmd/atn_cmds/events.rs index a29b4a1957..b3a8fb93f2 100644 --- a/crates/engine/src/cmd/atn_cmds/events.rs +++ b/crates/engine/src/cmd/atn_cmds/events.rs @@ -4,6 +4,7 @@ use super::descriptors::{desc, get_bool, get_ref, jbool, layer_ref, layer_target use super::fields::{F, Kind, Mapped, fields_from, fields_to, none, step}; use super::steps_channels::{match_color_from, match_color_to, merge_spot_from, merge_spot_to, split_from, split_to, trap_from, trap_to}; +use super::steps_content_aware::{caf_from, caf_to}; use super::steps_distort::{PINCH, POLAR, SPHERIZE, TWIRL, ZIGZAG, keys_from, keys_to, wave_from, wave_to}; use super::steps_doc::{ align_from, align_to, canvas_size_from, canvas_size_to, close_from, close_to, conditional_from, conditional_to, convert_mode_from, convert_mode_to, @@ -857,6 +858,7 @@ pub(super) const EVENTS: &[Event] = &[ from: |d| profile_from(d, "edit.convertToProfile"), to: profile_to, }, + Event { code: "", name: "cafWorkspace", label: "Content-Aware Fill", ids: &["edit.contentAwareFill"], from: caf_from, to: caf_to }, // File and actions. Event { code: "Cls ", name: "close", label: "Close", ids: &["file.close"], from: close_from, to: close_to }, Event { code: "Expr", name: "export", label: "Export", ids: &["file.export.saveForWebLegacy"], from: export_from, to: export_to }, diff --git a/crates/engine/src/cmd/atn_cmds/steps_content_aware.rs b/crates/engine/src/cmd/atn_cmds/steps_content_aware.rs new file mode 100644 index 0000000000..945cb8ee23 --- /dev/null +++ b/crates/engine/src/cmd/atn_cmds/steps_content_aware.rs @@ -0,0 +1,99 @@ +//! Recorded Content-Aware Fill settings; selection and job/history behavior belong to the command. +use super::descriptors::{desc, enm, jstr}; +use super::fields::{Mapped, step}; +use super::tables::lookup_back; +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. +const SAMPLING: &[(&str, &str)] = &[("cafSamplingRegionRectangular", "rectangular")]; +const COLOR: &[(&str, &str)] = &[("cafColorAdaptationDefault", "default")]; +const ROTATION: &[(&str, &str)] = &[("cafRotationAmountNone", "none")]; +const OUTPUT: &[(&str, &str)] = &[("cafOutputToNewLayer", "new")]; +const KEYS: &[&str] = &[ + "cafSamplingRegion", + "cafSampleAllLayers", + "cafColorAdaptationLevel", + "cafRotationAmount", + "cafScale", + "cafMirror", + "cafOutput", +]; +const PARAMS: &[&str] = &["sampling", "sampleAllLayers", "colorAdaptation", "rotationAdaptation", "scale", "mirror", "output"]; + +/// All seven settings are recorded. Missing keys have no measured Photoshop default here. +fn recorded(d: &Descriptor) -> Option { + if d.items.len() != KEYS.len() || KEYS.iter().any(|name| d.items.iter().filter(|(key, _)| key.is(name)).count() != 1) { + return None; + } + let mut p = Map::new(); + for (key, param, table) in [ + (KEYS[0], PARAMS[0], SAMPLING), + (KEYS[2], PARAMS[2], COLOR), + (KEYS[3], PARAMS[3], ROTATION), + (KEYS[6], PARAMS[6], OUTPUT), + ] { + let D::Enumerated { type_id, value } = d.get(key)? else { return None }; + if !type_id.is(key) { + return None; + } + p.insert(param.into(), table.iter().find(|(wire, _)| value.is(wire))?.1.into()); + } + for (key, param) in [(KEYS[1], PARAMS[1]), (KEYS[4], PARAMS[4]), (KEYS[5], PARAMS[5])] { + let D::Boolean(value) = d.get(key)? else { return None }; + p.insert(param.into(), (*value).into()); + } + Some(Value::Object(p)) +} + +/// Write only represented native options, making their defaults explicit in all seven fields. +fn descriptor(p: &Value) -> Option { + let object = p.as_object()?; + if !super::events::pixel_target(p) + || object.keys().any(|key| !PARAMS.contains(&key.as_str()) && !matches!(key.as_str(), "target" | "seed" | SOURCE)) + || p.get("seed").is_some_and(|seed| seed.as_u64() != Some(1)) + { + return None; + } + let enumeration = |key: &str, param: &str, table: &[(&str, &str)], default: &str| { + let value = match p.get(param) { + None => default, + Some(value) => value.as_str()?, + }; + Some(enm(key, lookup_back(table, value)?)) + }; + let boolean = |param: &str| match p.get(param) { + None => Some(D::Boolean(false)), + Some(value) => value.as_bool().map(D::Boolean), + }; + Some( + desc("null") + .with(KEYS[0], enumeration(KEYS[0], PARAMS[0], SAMPLING, "auto")?) + .with(KEYS[1], boolean(PARAMS[1])?) + .with(KEYS[2], enumeration(KEYS[2], PARAMS[2], COLOR, "default")?) + .with(KEYS[3], enumeration(KEYS[3], PARAMS[3], ROTATION, "none")?) + .with(KEYS[4], boolean(PARAMS[4])?) + .with(KEYS[5], boolean(PARAMS[5])?) + .with(KEYS[6], enumeration(KEYS[6], PARAMS[6], OUTPUT, "current")?), + ) +} + +pub(super) fn caf_from(d: &Descriptor) -> Mapped { + let Some(mut p) = recorded(d) else { return Mapped::Unsupported { harmless: false } }; + if descriptor(&p).as_ref() != Some(d) { + // Key order, descriptor identity and ID encoding are wire data, not native options. + p[SOURCE] = photocraft_paint::tile::b64_encode(&d.to_bytes()).into(); + } + step(CMD, p) +} + +pub(super) fn caf_to(_: &str, p: &Value) -> Option { + let canonical = descriptor(p)?; + if p.get(SOURCE).is_none() { + return Some(canonical); + } + let source = Descriptor::from_bytes(&photocraft_paint::tile::b64_decode(jstr(p, SOURCE)?)?).ok()?; + // Edited native options must never be overwritten by stale preservation metadata. + (descriptor(&recorded(&source)?)? == canonical).then_some(source) +} diff --git a/crates/engine/src/cmd/atn_cmds/tests.rs b/crates/engine/src/cmd/atn_cmds/tests.rs index f153ee83af..3abe96dec6 100644 --- a/crates/engine/src/cmd/atn_cmds/tests.rs +++ b/crates/engine/src/cmd/atn_cmds/tests.rs @@ -494,6 +494,11 @@ fn every_mapped_command_exports_and_imports_back() { ("select.rect", json!({"x":1,"y":2,"width":3,"height":4,"mode":"intersect"})), ("edit.pasteSpecial.pasteInto", json!({})), ("edit.pasteSpecial.pasteOutside", json!({})), + ( + "edit.contentAwareFill", + json!({"sampling":"rectangular","sampleAllLayers":true,"colorAdaptation":"default", + "rotationAdaptation":"none","scale":true,"mirror":false,"output":"new"}), + ), ("edit.fade", json!({"opacity":50,"mode":"Multiply"})), ("layer.linkLayers", json!({})), ("layer.distribute.leftEdges", json!({})), diff --git a/crates/engine/tests/it/atn_content_aware_fill.rs b/crates/engine/tests/it/atn_content_aware_fill.rs new file mode 100644 index 0000000000..d8175c0b5d --- /dev/null +++ b/crates/engine/tests/it/atn_content_aware_fill.rs @@ -0,0 +1,264 @@ +//! Recorded CAF settings use the native operation; unsupported descriptors stay whole. +use photocraft_engine::{ + Session, + actions_cmds::Action, + atn_cmds::{export, import}, +}; +use photocraft_psd::atn::{self, AtnAction, AtnEvent, AtnFile, AtnItem}; +use photocraft_psd::descriptor::{Descriptor, Id, UnicodeString, Value as D}; +use serde_json::{Value, json}; + +const CMD: &str = "edit.contentAwareFill"; + +fn enm(key: &str, value: &str) -> D { + D::Enumerated { type_id: Id::new(key), value: Id::new(value) } +} + +fn descriptor() -> Descriptor { + Descriptor::new("null") + .with("cafSamplingRegion", enm("cafSamplingRegion", "cafSamplingRegionRectangular")) + .with("cafSampleAllLayers", D::Boolean(false)) + .with("cafColorAdaptationLevel", enm("cafColorAdaptationLevel", "cafColorAdaptationDefault")) + .with("cafRotationAmount", enm("cafRotationAmount", "cafRotationAmountNone")) + .with("cafScale", D::Boolean(false)) + .with("cafMirror", D::Boolean(false)) + .with("cafOutput", enm("cafOutput", "cafOutputToNewLayer")) +} + +fn params() -> Value { + json!({"sampling":"rectangular","sampleAllLayers":false,"colorAdaptation":"default", + "rotationAdaptation":"none","scale":false,"mirror":false,"output":"new"}) +} + +fn item(d: Descriptor) -> AtnItem { + AtnItem { event: AtnEvent::name("cafWorkspace"), dictionary: b"Content-Aware Fill".to_vec(), descriptor: Some(d), ..Default::default() } +} + +fn file(item: AtnItem) -> AtnFile { + AtnFile { + name: UnicodeString::new_nul("S"), + actions: vec![AtnAction { name: UnicodeString::new_nul("A"), items: vec![item], ..Default::default() }], + ..Default::default() + } +} + +fn load(d: Descriptor) -> Action { + let loaded = import(&atn::write(&file(item(d))).unwrap()).unwrap(); + assert!(loaded.unsupported.is_empty()); + loaded.actions.into_iter().next().unwrap() +} + +fn exported(p: Value) -> Option { + let a = Action::new("A", vec![(CMD.into(), p)]); + let bytes = export("S", &[&a], &[], false).ok()?.0; + atn::parse(&bytes).ok()?.actions.into_iter().next()?.items.into_iter().next() +} + +/// Each field retains its native meaning, recorded type and position, including false booleans. +#[test] +fn caf_settings_roundtrip_seven_fields_and_boolean_choices() { + let base = descriptor(); + let mut cases = Vec::new(); + for bits in 0..8 { + let mut d = base.clone(); + let mut p = params(); + for (bit, index, param) in [(0, 1, "sampleAllLayers"), (1, 4, "scale"), (2, 5, "mirror")] { + let on = bits & (1 << bit) != 0; + d.items[index].1 = D::Boolean(on); + p[param] = json!(on); + } + cases.push((d, p)); + } + for (d, p) in cases { + let action = load(d.clone()); + assert_eq!(action.steps, vec![(CMD.into(), p.clone())]); + assert_eq!(exported(p).unwrap(), item(d.clone())); + let bytes = export("S", &[&action], &[], false).unwrap().0; + assert_eq!(atn::parse(&bytes).unwrap().actions[0].items[0], item(d)); + } +} + +/// A noncanonical source order is preservation data, and cannot overwrite an edited option. +#[test] +fn caf_source_order_and_flags_survive_without_reviving_stale_settings() { + let mut d = descriptor(); + d.items.reverse(); + let mut source = item(d); + source.enabled = false; + source.with_dialog = true; + source.dialog_options = 2; + let imported = import(&atn::write(&file(source.clone())).unwrap()).unwrap(); + assert!(imported.unsupported.is_empty()); + let a = &imported.actions[0]; + assert!(a.steps[0].1.get("_atnCafDescriptor").is_some()); + let bytes = export("S", &[a], &[], false).unwrap().0; + assert_eq!(atn::parse(&bytes).unwrap().actions[0].items[0], source); + let mut edited = a.steps[0].1.clone(); + edited["mirror"] = json!(true); + assert!(exported(edited).is_none()); + let mut damaged = a.steps[0].1.clone(); + damaged["_atnCafDescriptor"] = json!("not base64"); + assert!(exported(damaged).is_none()); +} + +/// Missing defaults, malformed/repeated fields and unrepresented workspace state stay complete. +#[test] +fn caf_unrepresented_descriptors_are_kept_and_never_partially_play() { + let base = descriptor(); + let mut cases = vec![Descriptor::new("null"), base.clone().with("extra", D::Boolean(true))]; + for i in 0..base.items.len() { + let mut missing = base.clone(); + missing.items.remove(i); + cases.push(missing); + let mut repeated = base.clone(); + repeated.items.push(repeated.items[i].clone()); + cases.push(repeated); + let mut wrong = base.clone(); + wrong.items[i].1 = D::Integer(0); + cases.push(wrong); + } + for (i, key, value) in [ + (0, "cafSamplingRegion", "cafSamplingRegionAuto"), + (0, "cafSamplingRegion", "cafSamplingRegionCustom"), + (2, "cafColorAdaptationLevel", "cafColorAdaptationNone"), + (2, "cafColorAdaptationLevel", "cafColorAdaptationHigh"), + (2, "cafColorAdaptationLevel", "cafColorAdaptationVeryHigh"), + (2, "cafColorAdaptationLevel", "unknown"), + (3, "cafRotationAmount", "cafRotationAmountLow"), + (3, "cafRotationAmount", "cafRotationAmountMedium"), + (3, "cafRotationAmount", "cafRotationAmountHigh"), + (3, "cafRotationAmount", "cafRotationAmountFull"), + (3, "cafRotationAmount", "unknown"), + (6, "cafOutput", "cafOutputToCurrentLayer"), + (6, "cafOutput", "cafOutputToDuplicateLayer"), + ] { + let mut d = base.clone(); + d.items[i].1 = enm(key, value); + cases.push(d); + } + let mut wrong_class = base.clone(); + wrong_class.items[0].1 = enm("unrelatedEnum", "cafSamplingRegionRectangular"); + cases.push(wrong_class); + for d in cases { + let source = item(d); + let imported = import(&atn::write(&file(source.clone())).unwrap()).unwrap(); + assert_eq!(imported.unsupported.len(), 1); + assert_eq!(imported.actions[0].steps[0].0, "actions.photoshopStep"); + let saved = export("S", &[&imported.actions[0]], &[], false).unwrap().0; + assert_eq!(atn::parse(&saved).unwrap().actions[0].items[0], source); + let mut s = Session::new(); + s.execute("file.new", json!({"width":8,"height":8})).unwrap(); + let before = s.active().unwrap().doc.clone(); + let past = s.active().unwrap().history.past_len(); + s.actions.list.extend(imported.actions); + let result = s.execute("actions.play", json!({"action":0})).unwrap(); + assert_eq!(result["ran"], 0); + assert!(result.get("failed").is_some()); + assert_eq!(s.active().unwrap().doc, before); + assert_eq!(s.active().unwrap().history.past_len(), past); + } +} + +/// Known native defaults become explicit values; unsupported defaults/options cannot disappear. +#[test] +fn caf_native_exports_preserve_effective_defaults_and_refuse_unrepresented_options() { + for p in [ + json!({"sampling":"rectangular","output":"new"}), + json!({"sampling":"rectangular","output":"new","seed":1,"target":"pixels"}), + params(), + ] { + let a = load(exported(p).unwrap().descriptor.unwrap()); + assert_eq!(a.steps, vec![(CMD.into(), params())]); + } + for (key, value) in [ + ("sampling", json!("auto")), ("sampling", json!("custom")), ("sampling", json!("unknown")), + ("output", json!("current")), ("output", json!("duplicate")), + ("colorAdaptation", json!("none")), ("colorAdaptation", json!("high")), ("colorAdaptation", json!("veryHigh")), + ("rotationAdaptation", json!("low")), ("rotationAdaptation", json!("medium")), + ("rotationAdaptation", json!("high")), ("rotationAdaptation", json!("full")), + ("sampleAllLayers", json!(0)), ("scale", Value::Null), ("mirror", json!("true")), ("seed", json!(2)), + ("layer", json!(1)), ("margin", json!(10)), ("area", json!([0,0,4,4])), ("channel", json!(0)), + ("samplingStrokes", json!([])), ("fillEdits", json!([])), ("target", json!("mask")), ("_atnCafDescriptor", Value::Null), + ] { + let mut p = params(); + p[key] = value; + assert!(exported(p.clone()).is_none(), "{p}"); + } + for p in [json!({}), json!({"sampling":"rectangular"}), json!({"output":"new"}), json!([])] { + assert!(exported(p.clone()).is_none(), "unrepresented native default: {p}"); + } +} + +fn session(depth: u32) -> Session { + let mut s = Session::new(); + s.execute("file.new", json!({"width":8,"height":8,"depth":depth,"background":"white"})).unwrap(); + s.execute("tools.setColors", json!({"foreground":"#ff0000"})).unwrap(); + s.execute("select.rect", json!({"x":3,"y":3,"width":2,"height":2})).unwrap(); + s.execute("edit.fill", json!({"contents":"foreground"})).unwrap(); + s +} + +/// Native and imported fills produce the same real pixels, source/selection state and Undo/Redo. +#[test] +fn caf_action_matches_native_pixels_and_undo_at_each_depth() { + for depth in [8, 16, 32] { + let mut s = session(depth); + let before = s.active().unwrap().doc.clone(); + let source = s.active().unwrap().active_layer.unwrap(); + let past = s.active().unwrap().history.past_len(); + s.actions.list.push(load(descriptor())); + let result = s.execute("actions.play", json!({"action":0})).unwrap(); + assert!(result.get("failed").is_none(), "{depth}: {result}"); + assert_eq!(result["ran"], 1); + let after = s.active().unwrap().doc.clone(); + assert_eq!(after.layers.len(), before.layers.len() + 1); + assert_eq!(after.selection, before.selection); + assert_eq!(s.active().unwrap().history.past_len(), past + 1); + assert_eq!(after.layer(source), before.layer(source), "new output keeps its source unchanged"); + let id = s.active().unwrap().active_layer.unwrap(); + let pixels = after.layer(id).unwrap().surface().unwrap(); + let center = pixels.rgba(3, 3); + assert!(center.iter().all(|v| (*v - 1.0).abs() < 0.01), "{depth}: hole was not filled: {center:?}"); + assert_eq!(pixels.rgba(0, 0)[3], 0.0, "new output is transparent outside the selection"); + let mut native = session(depth); + native.execute(CMD, params()).unwrap(); + let expected = &native.active().unwrap().doc; + assert_eq!(after.layers.len(), expected.layers.len()); + for (a, b) in after.layers.iter().zip(&expected.layers) { + let (a, b) = (a.surface().unwrap(), b.surface().unwrap()); + for y in 0..8 { + for x in 0..8 { + assert_eq!(a.rgba(x, y), b.rgba(x, y), "{depth}: pixel{x},{y}"); + } + } + } + assert!(s.undo()); + assert_eq!(s.active().unwrap().doc, before); + assert!(s.redo()); + assert_eq!(s.active().unwrap().doc, after); + + // Current-layer native calls still work, although their ATN form is outside this increment. + let mut current = session(depth); + let mut p = params(); + p["output"] = json!("current"); + current.execute(CMD, p).unwrap(); + 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:?}"); + } +} + +/// The adapter cannot manufacture a selection or turn a refused native operation into an edit. +#[test] +fn caf_action_without_selection_fails_without_an_edit() { + let mut s = Session::new(); + s.execute("file.new", json!({"width":8,"height":8})).unwrap(); + let before = s.active().unwrap().doc.clone(); + let past = s.active().unwrap().history.past_len(); + s.actions.list.push(load(descriptor())); + let result = s.execute("actions.play", json!({"action":0})).unwrap(); + assert_eq!(result["ran"], 0); + assert!(result.get("failed").is_some()); + assert_eq!(s.active().unwrap().doc, before); + assert_eq!(s.active().unwrap().history.past_len(), past); +} diff --git a/crates/engine/tests/it/main.rs b/crates/engine/tests/it/main.rs index 467b04720d..b8fe161da5 100644 --- a/crates/engine/tests/it/main.rs +++ b/crates/engine/tests/it/main.rs @@ -2,6 +2,7 @@ mod affinity; mod artboard_export_names; +mod atn_content_aware_fill; mod atn_crystallize_cell_size; mod atn_reference_forms; mod atn_ripple_amount; diff --git a/docs/atn.md b/docs/atn.md index 99f883fc98..cc605c60cf 100644 --- a/docs/atn.md +++ b/docs/atn.md @@ -18,7 +18,7 @@ layers and documents, newer and older events and files, and a measured reference map yet, with everything needed to implement them without the reference application. -## Supported events (144) +## Supported events (145) | Code | String ID | Label | PhotoCraft command(s) | |---|---|---|---| @@ -45,6 +45,7 @@ the reference application. | (string ID only) | collapseAllGroupsEvent | Collapse All Groups | `layer.setExpanded` | | `ClrB` | colorBalance | Color Balance | `image.adjustments.colorBalance` | | `ClrR` | colorRange | Color Range | `select.colorRange` | +| (string ID only) | cafWorkspace | Content-Aware Fill | `edit.contentAwareFill` | | `Cntc` | contract | Contract | `select.modify.contract` | | `CnvM` | convertMode | Convert Mode | `image.mode.rgb`, `image.mode.grayscale`, `image.mode.cmyk`, `image.mode.lab`, `image.mode.multichannel`, `image.mode.bits8`, `image.mode.bits16`, `image.mode.bits32`, `image.mode.bitmap` | | (string ID only) | convertToProfile | Convert to Profile | `edit.convertToProfile` | @@ -167,6 +168,34 @@ the reference application. | `Wave` | wave | Wave | `filter.distort.wave` | | `ZgZg` | zigZag | ZigZag | `filter.distort.zigZag` | +### 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: +Sample All Layers, Scale and Mirror. The selection comes from the preceding selection step. + +| Recorded key, in order | Supported value | Native parameter | +|---|---|---| +| `cafSamplingRegion` | `cafSamplingRegionRectangular` | `sampling: "rectangular"` | +| `cafSampleAllLayers` | boolean | `sampleAllLayers` | +| `cafColorAdaptationLevel` | `cafColorAdaptationDefault` | `colorAdaptation: "default"` | +| `cafRotationAmount` | `cafRotationAmountNone` | `rotationAdaptation: "none"` | +| `cafScale` | boolean | `scale` | +| `cafMirror` | boolean | `mirror` | +| `cafOutput` | `cafOutputToNewLayer` | `output: "new"` | + +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. + +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 +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 +remaining CAF forms and the other recorded tools in #10710 are still separate work. + ### Settings retained by mapped events - Transform keeps its recorded `FTcs` reference point. Show with `TglO` true maps to @@ -460,7 +489,6 @@ the recorded steps back from the saved Actions panel. Ranges come from the dialo | Shear (`Shr `) | `ShrP` list of `Pnt ` (`Hrzn`, `Vrtc` doubles), `UndA` (`WrpA` wrap around, `RptE` repeat edge pixels), `ShrS` long, `ShrE` long | The dialog writes whole numbers: `Hrzn` -64..63 (dragging past the grid clamps), `Vrtc` 1..128 with the first point at 1 and the last at 128; dragging a point out of the grid deletes it. `ShrS` is 0 and `ShrE` the last point's index (count - 1). Script runs take any value unclamped and crashed on far-off `ShrE`/`ShrS` or a first `Vrtc` below 1. | | Gradient tool, classic (`Grdn` / `gradientClassEvent`) | `From`, `T ` points in pixels; `Opct` percent, `Md ` blend mode, `Type` (`GrdT`: `Lnr `, `Rdl `, `Angl`, `Rflc`, `Dmnd`), `Rvrs` bool, `Dthr` bool, `UsMs` bool (transparency), `gradientsInterpolationMethod` (`Perc` perceptual, …), `Grad` gradient object (as in Gradient Map) | Recorded in that order. `Opct`, `Md ` and `Rvrs` appear only when they differ from 100 %, Normal and off; `Dthr` and `UsMs` only when on. The five types are the only accepted values. A step without `Grad` fails. | | Gradient tool, default mode | two steps: Make (`null` = class `contentLayer`, `Usng` = `Type` gradient layer object) then Set (`null` = `contentLayer` target, `T ` = the same object plus `Algn` false) | The object: `Dthr`, `gradientsInterpolationMethod`, `Angl` angle, `Type`, `Scl ` percent, `Ofst` (`Hrzn`, `Vrtc` percent), `Grad`. The new tool makes a gradient fill layer; Make holds the first click, Set the finished drag. | -| Content-Aware Fill (`cafWorkspace`) | `cafSamplingRegion` (`cafSamplingRegionRectangular`, …), `cafSampleAllLayers` bool, `cafColorAdaptationLevel` (`cafColorAdaptationDefault`, …), `cafRotationAmount` (`cafRotationAmountNone`, …), `cafScale` bool, `cafMirror` bool, `cafOutput` (`cafOutputToNewLayer`, …) | Runs on the selection, which is recorded as its own step before it. | | Perspective Warp (`perspectiveWarpTransform`) | `null` = layer target, `Md ` (`perspectiveWarpMode`: `warp`), `referenceRect` rectangle in pixels, `vertices` list of points in pixels (the layout quad), `warpedVertices` (the same points after warping), `quads` list of `indices` (4 vertex indexes each) | Layout and warp are one step. | | Print (`print`) then Print One Copy (`printOneCopy`) | `print`: `null` = reference `printOutput` property of the target document; `printOutput` (`PstS`, `Inte`, `printSixteenBit`, `printerName` text, `printProofSetup`), `printViewProofSetup`, `printOutputOptions` (caption, marks, `Bckg` colour, `BrdT`, `Bld `, `Rslt`, `vectorData`, `PgPs`, `Left`, `Top `, `Scl `, crop rectangle), `osSpecificPrintInfo` raw data, `Cptn` text. `printOneCopy` has no keys. | Pressing Print in the dialog records both steps. | | Create Droplet (`CrtD`) | `In ` alias: the droplet file; `Fldr` bool (include subfolders); `Usng` reference to the action; then the Batch keys below (destination, file naming, error log) when set | The same settings as Batch, with the droplet file instead of a source folder. |