Record a duplicate's inPlace only when a copy is an artboard

Alt-drag, Cmd+Opt-drag and Free Transform on a copy duplicate with
`inPlace: true` for every layer, but the flag only changes anything for
an artboard. With in-place duplicates now refused on strict .atn export,
a recorded copy of a plain layer could no longer be exported, although a
plain Duplicate replays it exactly. Recording now drops `inPlace` when no
copy is an artboard, as `layer.select` already rewrites its recorded
params.

Refs #7200

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kushagra Saxena
2026-10-19 21:17:40 +05:30
co-authored by Claude Opus 5.5
parent 2401039887
commit aa54979583
3 changed files with 56 additions and 2 deletions
+37
View File
@@ -1025,6 +1025,43 @@ mod tests {
}
}
/// An Alt-drag copy records `inPlace` (#7200), which only matters for an artboard: a plain
/// layer's recorded copy must still export to .atn, while an artboard's must be refused rather
/// than exported as a copy beside the original.
#[test]
fn recorded_in_place_duplicate_keeps_in_place_only_for_artboards() {
let mut s = Session::new();
s.execute("file.new", json!({"width": 8, "height": 8})).unwrap();
s.execute("actions.record", json!({"name": "Layer copy"})).unwrap();
s.execute("layer.duplicate", json!({"inPlace": true})).unwrap();
s.execute("actions.stop", json!({})).unwrap();
s.execute("layer.new.artboard", json!({"name": "Board", "rect": [0, 0, 8, 8]})).unwrap();
s.execute("actions.record", json!({"name": "Board copy"})).unwrap();
s.execute("layer.duplicate", json!({"inPlace": true})).unwrap();
s.execute("actions.stop", json!({})).unwrap();
assert_eq!(s.actions.list[0].steps, vec![("layer.duplicate".to_string(), json!({}))]);
assert_eq!(s.actions.list[1].steps, vec![("layer.duplicate".to_string(), json!({"inPlace": true}))]);
assert!(crate::atn_cmds::export("Audit", &[&s.actions.list[0]], &[], false).is_ok());
assert!(crate::atn_cmds::export("Audit", &[&s.actions.list[1]], &[], false).is_err());
// Several selected layers are copied together: one artboard among them keeps `inPlace`.
let mut s = Session::new();
s.execute("file.new", json!({"width": 8, "height": 8})).unwrap();
let photo = s.active().unwrap().active_layer.unwrap();
s.execute("layer.new.layer", json!({"name": "Logo"})).unwrap();
s.execute("layer.select", json!({"layer": photo.0, "mode": "add"})).unwrap();
s.execute("actions.record", json!({"name": "Layers copy"})).unwrap();
s.execute("layer.duplicate", json!({"inPlace": true})).unwrap();
s.execute("actions.stop", json!({})).unwrap();
assert_eq!(s.active().unwrap().selected_layers().len(), 2, "both layers were copied");
s.execute("layer.new.artboard", json!({"name": "Board", "rect": [0, 0, 8, 8]})).unwrap();
s.execute("layer.select", json!({"layer": photo.0, "mode": "add"})).unwrap();
s.execute("actions.record", json!({"name": "Mixed copy"})).unwrap();
s.execute("layer.duplicate", json!({"inPlace": true})).unwrap();
s.execute("actions.stop", json!({})).unwrap();
assert_eq!(s.actions.list[0].steps, vec![("layer.duplicate".to_string(), json!({}))]);
assert_eq!(s.actions.list[1].steps, vec![("layer.duplicate".to_string(), json!({"inPlace": true}))]);
}
#[test]
fn named_selection_preserves_modes_and_duplicate_occurrences() {
let mut s = Session::new();
+14
View File
@@ -183,6 +183,20 @@ pub(crate) fn place_copy(doc: &mut Document, id: LayerId) -> Result<()> {
Ok(())
}
/// Record a Duplicate's `inPlace` only when a copy (the selection after it) is an artboard: for
/// any other layer it changes nothing, and Photoshop's Duplicate can't express it, so a recorded
/// Alt-drag copy of a plain layer must stay exportable to .atn (#7200).
pub(crate) fn recorded_duplicate(s: &Session, mut p: Value) -> Value {
let Some(st) = s.active() else { return p };
if st.selected_layers().into_iter().any(|id| st.doc.layer(id).is_some_and(|l| l.artboard().is_some())) {
return p;
}
if let Some(fields) = p.as_object_mut() {
fields.remove("inPlace");
}
p
}
/// Grow the canvas so every artboard fits. Photoshop's artboard documents grow in every
/// direction: a board left of or above the origin shifts the whole document (boards, their
/// contents, guides, paths…) right/down by its overhang, then the canvas grows to show it. An
+5 -2
View File
@@ -799,8 +799,11 @@ impl Session {
self.journal.truncate(from);
}
if !crate::brush_cmds::coalesce_journal(self, id, &params) {
let params =
if id == "layer.select" && self.actions.recording.is_some() { crate::layer_multi_cmds::recorded_selection(self, params) } else { params };
let params = match id {
"layer.select" if self.actions.recording.is_some() => crate::layer_multi_cmds::recorded_selection(self, params),
"layer.duplicate" if self.actions.recording.is_some() => crate::artboard_cmds::recorded_duplicate(self, params),
_ => params,
};
self.journal.push((id.to_string(), params));
}
}