The other hidden "quick transform" shortcut. Cmd+Alt+T (no menu item, in Keyboard Shortcuts as "Free Transform a Copy") duplicates the active layer, or with a selection copies the selected pixels to a new layer (Layer via Copy), and opens Free Transform on the copy. OK leaves the original in place and the transformed copy as one history step (the duplicate's state is folded away); an untouched OK keeps the copy where it is; Esc, or a transform that fails, undoes the copy and leaves nothing to redo. `TransformSession::copy` marks such a session. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7b3d131bc8
commit
d38eefc8f7
|
|
@ -108,6 +108,7 @@
|
|||
Puppet Warp Loutková deformace
|
||||
Perspective Warp Perspektivní deformace
|
||||
Free Transform Volná transformace
|
||||
Free Transform a Copy Volná transformace kopie
|
||||
Transform Transformace
|
||||
Again Opakovat
|
||||
Transform Again on a Copy Duplikovat a opakovat transformaci
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -100,6 +100,7 @@
|
|||
Puppet Warp Deformación de clavos
|
||||
Perspective Warp Deformación en perspectiva
|
||||
Free Transform Transformación libre
|
||||
Free Transform a Copy Transformación libre de una copia
|
||||
Transform Transformar
|
||||
Again Repetir
|
||||
Transform Again on a Copy Duplicar y repetir la transformación
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
|
|
@ -104,6 +104,7 @@
|
|||
Puppet Warp パペットワープ
|
||||
Perspective Warp 遠近法ワープ
|
||||
Free Transform 自由変形
|
||||
Free Transform a Copy コピーを自由変形
|
||||
Transform 変形
|
||||
Again もう一度
|
||||
Transform Again on a Copy 複製してもう一度変形
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -1554,6 +1554,7 @@
|
|||
|
||||
# Transform / warp overlays
|
||||
Free Transform Свободное трансформирование
|
||||
Free Transform a Copy Свободное трансформирование копии
|
||||
Transform Трансформирование
|
||||
Transform Selection Трансформировать выделение
|
||||
Switch between free transform and warp modes Переключение между свободным трансформированием и деформаци
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -99,6 +99,7 @@
|
|||
Puppet Warp 操控变形
|
||||
Perspective Warp 透视变形
|
||||
Free Transform 自由变换
|
||||
Free Transform a Copy 自由变换副本
|
||||
Transform 变换
|
||||
Again 再次变换
|
||||
Transform Again on a Copy 复制并再次变换
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 13.
|
|
|
@ -108,6 +108,7 @@
|
|||
Puppet Warp 操控彎曲
|
||||
Perspective Warp 透視彎曲
|
||||
Free Transform 任意變形
|
||||
Free Transform a Copy 任意變形拷貝
|
||||
Transform 變形
|
||||
Again 再次
|
||||
Transform Again on a Copy 複製並再次變形
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -31,6 +31,7 @@ pub const UI_COMMANDS: &[(&str, &str, &[&str], Option<&str>)] = &[
|
|||
("view.extras", "Extras", &["View"], Some("Cmd+H")),
|
||||
("view.show.targetPath", "Target Path", &["View", "Show"], Some("Cmd+Shift+H")),
|
||||
("view.screenMode.cycle", "Cycle Screen Mode", &[], Some("F")),
|
||||
("edit.freeTransformCopy", "Free Transform a Copy", &[], Some("Cmd+Alt+T")),
|
||||
("view.zoomIn", "Zoom In", &["View"], Some("Cmd+=")),
|
||||
("view.zoomOut", "Zoom Out", &["View"], Some("Cmd+-")),
|
||||
("view.fitOnScreen", "Fit on Screen", &["View"], Some("Cmd+0")),
|
||||
|
|
@ -369,6 +370,7 @@ pub(crate) fn invoke_unguarded(app: &mut PhotocraftApp, ctx: &egui::Context, id:
|
|||
| "edit.transform.skew"
|
||||
| "edit.transform.distort"
|
||||
| "edit.transform.perspective" => crate::transform_tool::begin(app, ctx).map(|_| json!({"transform": app.ui.transform})),
|
||||
"edit.freeTransformCopy" => crate::transform_tool::begin_copy(app, ctx).map(|_| json!({"transform": app.ui.transform})),
|
||||
// Edit › Transform › Warp from the menu: interactive Warp mode (with params: the engine).
|
||||
"edit.transform.warp" | "layer.smartObjects.warp" if params.as_object().is_none_or(|o| o.is_empty()) => {
|
||||
crate::transform_tool::begin_warp(app, ctx).map(|_| json!({"transform": app.ui.transform}))
|
||||
|
|
@ -476,6 +478,7 @@ pub fn is_enabled(app: &PhotocraftApp, id: &str) -> bool {
|
|||
app.session.active().is_some()
|
||||
}
|
||||
"edit.freeTransform"
|
||||
| "edit.freeTransformCopy"
|
||||
| "edit.transform.scale"
|
||||
| "edit.transform.rotate"
|
||||
| "edit.transform.skew"
|
||||
|
|
|
|||
|
|
@ -487,6 +487,10 @@ pub struct TransformSession {
|
|||
/// the Quick Mask by itself (`None`: the layer, with its linked masks).
|
||||
#[serde(default)]
|
||||
pub target: Option<serde_json::Value>,
|
||||
/// Free Transform on a copy (⌥⌘T): the copy was made for this session, so Cancel takes it back
|
||||
/// and OK folds it into the transform's history step (#352).
|
||||
#[serde(default)]
|
||||
pub copy: bool,
|
||||
}
|
||||
|
||||
/// In-progress inline type editing (Type tool). Offsets are character indices.
|
||||
|
|
|
|||
|
|
@ -85,10 +85,42 @@ pub fn begin(app: &mut PhotocraftApp, ctx: &egui::Context) -> Result<(), String>
|
|||
warp: None,
|
||||
selection: false,
|
||||
target: None,
|
||||
copy: false,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Free Transform on a copy (⌥⌘T, no menu item): duplicates the active layer (with a selection,
|
||||
/// 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());
|
||||
app.run(if selection { "layer.new.layerViaCopy" } else { "layer.duplicate" }, json!({}))?;
|
||||
if let Err(e) = begin(app, ctx) {
|
||||
take_back_copy(app);
|
||||
return Err(e);
|
||||
}
|
||||
if let Some(t) = app.ui.transform.as_mut() {
|
||||
t.copy = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Undoes the copy a cancelled or failed ⌥⌘T made, leaving nothing to redo.
|
||||
fn take_back_copy(app: &mut PhotocraftApp) {
|
||||
app.session.undo();
|
||||
if let Some(st) = app.session.active_mut() {
|
||||
st.history.clear_redo();
|
||||
}
|
||||
app.sync_views();
|
||||
}
|
||||
|
||||
/// After ⌥⌘T's transform: the copy and the transform become one history step (the transform's).
|
||||
fn fold_copy(app: &mut PhotocraftApp) {
|
||||
if let Some(st) = app.session.active_mut() {
|
||||
st.history.purge_last();
|
||||
}
|
||||
}
|
||||
|
||||
/// Free Transform of a targeted unlinked layer mask, alpha channel or Quick Mask by itself: the
|
||||
/// box frames its content (within the selection) and previews the moving values in grey over the
|
||||
/// document with them vacated.
|
||||
|
|
@ -138,6 +170,7 @@ fn begin_lone(
|
|||
warp: None,
|
||||
selection: false,
|
||||
target: Some(target),
|
||||
copy: false,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -181,6 +214,7 @@ pub fn begin_selection(app: &mut PhotocraftApp, ctx: &egui::Context) -> Result<(
|
|||
warp: None,
|
||||
selection: true,
|
||||
target: None,
|
||||
copy: false,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -305,30 +339,40 @@ pub fn commit(app: &mut PhotocraftApp) {
|
|||
}
|
||||
return;
|
||||
}
|
||||
if let Some(w) = &t.warp {
|
||||
let copy = t.copy;
|
||||
let r = if let Some(w) = &t.warp {
|
||||
if w.is_identity() {
|
||||
return;
|
||||
}
|
||||
if let Err(e) = app.run("edit.transform.warp", json!({"layer": t.layer, "rect": t.rect, "warp": w, "interpolation": t.interpolation})) {
|
||||
app.run("edit.transform.warp", json!({"layer": t.layer, "rect": t.rect, "warp": w, "interpolation": t.interpolation}))
|
||||
} else {
|
||||
if t.quad == corners(t.rect) {
|
||||
return; // untouched: nothing to do and no history step; a ⌥⌘T copy stays
|
||||
}
|
||||
let mut p = json!({"layer": t.layer, "rect": t.rect, "quad": t.quad, "interpolation": t.interpolation});
|
||||
if let Some(target) = t.target {
|
||||
p["target"] = target;
|
||||
}
|
||||
app.run("edit.transform", p)
|
||||
};
|
||||
match r {
|
||||
Ok(_) if copy => fold_copy(app),
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
if copy {
|
||||
take_back_copy(app);
|
||||
}
|
||||
app.ui.status = e;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if t.quad == corners(t.rect) {
|
||||
return; // untouched: nothing to do (Photoshop adds no history step either)
|
||||
}
|
||||
let mut p = json!({"layer": t.layer, "rect": t.rect, "quad": t.quad, "interpolation": t.interpolation});
|
||||
if let Some(target) = t.target {
|
||||
p["target"] = target;
|
||||
}
|
||||
if let Err(e) = app.run("edit.transform", p) {
|
||||
app.ui.status = e;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(app: &mut PhotocraftApp) {
|
||||
app.ui.transform = None;
|
||||
let copy = app.ui.transform.take().is_some_and(|t| t.copy);
|
||||
app.transform_preview = None;
|
||||
if copy {
|
||||
take_back_copy(app);
|
||||
}
|
||||
}
|
||||
|
||||
/// Which part of the box a document point hits.
|
||||
|
|
@ -1000,6 +1044,7 @@ mod tests {
|
|||
warp: None,
|
||||
selection: false,
|
||||
target: None,
|
||||
copy: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1078,6 +1123,43 @@ mod tests {
|
|||
assert!(app.ui.transform.is_none() && app.transform_preview.is_none());
|
||||
}
|
||||
|
||||
/// #352: ⌥⌘T transforms a copy. OK leaves the original and a moved copy as one history step;
|
||||
/// Cancel leaves no copy and nothing to redo; with a selection the copy holds the selected
|
||||
/// pixels only.
|
||||
#[test]
|
||||
fn free_transform_a_copy() {
|
||||
let ctx = egui::Context::default();
|
||||
let bounds = |app: &PhotocraftApp, i: usize| app.session.active().unwrap().doc.layers[i].surface().unwrap().content_bounds();
|
||||
let mut app = app_with_square(64, photocraft_geom::Rect::new(8, 8, 24, 24));
|
||||
let (layers, steps) = (app.session.active().unwrap().doc.layers.len(), app.session.active().unwrap().history.past_len());
|
||||
crate::menus::invoke(&mut app, &ctx, "edit.freeTransformCopy", json!({})).unwrap();
|
||||
assert!(app.ui.transform.as_ref().unwrap().copy);
|
||||
assert!(!crate::menus::is_enabled(&app, "edit.freeTransformCopy"), "one transform at a time");
|
||||
// Esc: the copy goes too.
|
||||
cancel(&mut app);
|
||||
let st = app.session.active().unwrap();
|
||||
assert_eq!((st.doc.layers.len(), st.history.past_len()), (layers, steps));
|
||||
assert!(!st.history.can_redo(), "nothing to redo");
|
||||
// OK: original in place, a moved copy, one step.
|
||||
crate::menus::invoke(&mut app, &ctx, "edit.freeTransformCopy", json!({})).unwrap();
|
||||
if let Some(t) = app.ui.transform.as_mut() {
|
||||
t.quad = t.quad.map(|[x, y]| [x + 20.0, y]);
|
||||
}
|
||||
commit(&mut app);
|
||||
let st = app.session.active().unwrap();
|
||||
assert_eq!((st.doc.layers.len(), st.history.past_len()), (layers + 1, steps + 1));
|
||||
assert_eq!(bounds(&app, layers - 1), photocraft_geom::Rect::new(8, 8, 24, 24), "the original stays");
|
||||
assert_eq!(bounds(&app, layers), photocraft_geom::Rect::new(28, 8, 44, 24), "the copy moved");
|
||||
app.session.undo();
|
||||
assert_eq!(app.session.active().unwrap().doc.layers.len(), layers, "one undo removes copy and move");
|
||||
// With a selection: Layer via Copy, so only the selected pixels are copied.
|
||||
app.run("select.rect", json!({"x": 8, "y": 8, "width": 8, "height": 16})).unwrap();
|
||||
crate::menus::invoke(&mut app, &ctx, "edit.freeTransformCopy", json!({})).unwrap();
|
||||
assert_eq!(app.ui.transform.as_ref().unwrap().rect, [8.0, 8.0, 16.0, 24.0]);
|
||||
cancel(&mut app);
|
||||
assert_eq!(app.session.active().unwrap().doc.layers.len(), layers);
|
||||
}
|
||||
|
||||
fn app_with_square(size: u32, fill: photocraft_geom::Rect) -> PhotocraftApp {
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
|
||||
app.session.execute("file.new", json!({"width": size, "height": size})).unwrap();
|
||||
|
|
|
|||
Loading…
Reference in New Issue