diff --git a/crates/engine/src/atn_cmds.rs b/crates/engine/src/atn_cmds.rs index 621b5bb4a8..3e4c34b81a 100644 --- a/crates/engine/src/atn_cmds.rs +++ b/crates/engine/src/atn_cmds.rs @@ -1984,6 +1984,10 @@ fn play_from(d: &Descriptor) -> Mapped { fn play_to(_: &str, p: &Value) -> Option { let name = jstr(p, "action")?; + // The Play Action descriptor written here carries no starting step, so a call from a later step is not a whole call. + if p.get("from").is_some_and(|v| !v.is_null() && v.as_f64() != Some(0.0)) { + return None; + } let mut items = vec![ReferenceItem::Name { class: class("Actn"), name: UnicodeString::new_nul(name) }]; if let Some(set) = jstr(p, "set") { items.push(ReferenceItem::Name { class: class("ASet"), name: UnicodeString::new_nul(set) }); diff --git a/crates/engine/src/atn_cmds/tests.rs b/crates/engine/src/atn_cmds/tests.rs index b19dc25d8b..37744831cc 100644 --- a/crates/engine/src/atn_cmds/tests.rs +++ b/crates/engine/src/atn_cmds/tests.rs @@ -1239,6 +1239,23 @@ fn add_noise_with_mask_target_is_not_exported_as_pixel_filter() { } } +/// A Play Action that starts the called action at a later step must not export as a call of the +/// whole action: that would replay the steps the recording deliberately skipped. +#[test] +fn partial_play_action_is_not_exported_as_whole_action_call() { + let partial = Action { name: "Parent".into(), steps: vec![("actions.play".into(), json!({"action": "Child", "from": 1}))], ..Default::default() }; + assert!(export("Set", &[&partial], &[], false).is_err()); + let (_, skipped) = export("Set", &[&partial], &[], true).unwrap(); + assert_eq!(skipped, vec![Skipped { action: "Parent".into(), step: 0, id: "actions.play".into() }]); + for params in [json!({"action": "Child"}), json!({"action": "Child", "from": 0})] { + let whole = Action { name: "Parent".into(), steps: vec![("actions.play".into(), params)], ..Default::default() }; + let (bytes, skipped) = export("Set", &[&whole], &[], false).unwrap(); + assert!(skipped.is_empty()); + let imported = import(&bytes).unwrap(); + assert_eq!(imported.actions[0].steps, vec![("actions.play".to_string(), json!({"action": "Child"}))]); + } +} + /// #7646: an action's function key keeps Photoshop's Shift and Command options through /// `.atn` load and save, as `Shift+F5`, `Cmd+F5` and `Cmd+Shift+F5` shortcuts. #[test]