From a8b69338daa0e77dfe91145d600aabfcf9e40ced Mon Sep 17 00:00:00 2001 From: David Mann Date: Wed, 4 Nov 2026 16:33:00 +0100 Subject: [PATCH] Fix-up #11243: Free Transform a Copy of a linked layer makes no copy Measured in the reference application: Ctrl+Alt+T on a linked layer without a selection makes no copy; the box frames and moves the whole link group, and History shows one Free Transform. An unlinked layer still gets its copy. Co-Authored-By: Claude Opus 5.5 --- .../ui-egui/src/tools/transform_tool/begin.rs | 10 +++++++++ .../ui-egui/src/tools/transform_tool/tests.rs | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/crates/ui-egui/src/tools/transform_tool/begin.rs b/crates/ui-egui/src/tools/transform_tool/begin.rs index eb0a06b905..cd3620c31c 100644 --- a/crates/ui-egui/src/tools/transform_tool/begin.rs +++ b/crates/ui-egui/src/tools/transform_tool/begin.rs @@ -255,6 +255,16 @@ pub const WARP_ONE_LAYER: &str = "Warp works on one layer at a time: select a si /// its selected pixels, as Layer via Copy) and transforms the copy (#352). pub fn begin_copy(app: &mut PhotocraftApp, ctx: &egui::Context) -> Result<(), String> { let selection = app.session.active().is_some_and(|d| d.doc.selection.is_some()); + // A linked layer without a selection: the reference application makes no copy and frames the + // whole link group, as Free Transform does (measured, #11217). Unlinked layers get the copy. + let linked = !selection + && app.session.active().is_some_and(|st| { + let targets = photocraft_engine::transform_cmds::selection_targets(st); + photocraft_engine::transform_cmds::box_targets(&st.doc, &targets) != targets + }); + if linked { + return begin(app, ctx); + } let params = if selection { json!({}) } else { json!({"inPlace": true}) }; app.run(if selection { "layer.new.layerViaCopy" } else { "layer.duplicate" }, params)?; let made = app.session.active().and_then(|st| st.active_layer.map(|layer| (st.doc.id, layer))); diff --git a/crates/ui-egui/src/tools/transform_tool/tests.rs b/crates/ui-egui/src/tools/transform_tool/tests.rs index f5bd31566c..18df17bf3a 100644 --- a/crates/ui-egui/src/tools/transform_tool/tests.rs +++ b/crates/ui-egui/src/tools/transform_tool/tests.rs @@ -2090,6 +2090,27 @@ fn linked_layers_app() -> (PhotocraftApp, LayerId, LayerId, LayerId) { (app, a, b, c) } +/// #11217: ⌥⌘T on a linked layer without a selection makes no copy in the reference application +/// (measured): it frames and moves the whole link group, as Free Transform does. An unlinked +/// layer still gets its copy. +#[test] +fn free_transform_a_copy_of_a_linked_layer_boxes_the_link_group_without_a_copy() { + let ctx = egui::Context::default(); + let (mut app, a, b, c) = linked_layers_app(); + let layers = app.session.active().unwrap().doc.layers.len(); + crate::menus::invoke(&mut app, &ctx, "edit.freeTransformCopy", json!({})).unwrap(); + assert_eq!(app.session.active().unwrap().doc.layers.len(), layers, "no copy of a linked layer"); + let t = app.ui.transform.as_ref().unwrap(); + assert_eq!(t.made, None, "nothing made to take back"); + assert_eq!(t.layers, [a.0, b.0], "the box frames the link group"); + cancel(&mut app); + app.session.execute("layer.select", json!({"layer": c.0})).unwrap(); + crate::menus::invoke(&mut app, &ctx, "edit.freeTransformCopy", json!({})).unwrap(); + assert_eq!(app.session.active().unwrap().doc.layers.len(), layers + 1, "an unlinked layer is copied"); + assert_eq!(app.ui.transform.as_ref().unwrap().made, Some(MadeLayer::Copy)); + cancel(&mut app); +} + /// #11217: the box over one selected linked layer frames, previews and commits its whole link /// group, as in Photoshop. It used to frame A alone; B only moved at the commit. #[test]