mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 05:57:09 -06:00
Show the remembered crop ratio's box on an untouched photo
Fixes #3029 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
59a5d00c60
commit
a8f353ba64
@@ -313,6 +313,33 @@ impl LightcraftApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// The box the crop tool shows on an untouched photo with a remembered ratio (issue #3029): fitted
|
||||
/// as `begin_crop` (and Return) will apply it, so the box matches the dropdown before any edit.
|
||||
pub(crate) fn crop_tool_preview(&self) -> Option<lightcraft_geom::CropGeometry> {
|
||||
let photo = self.session.active()?;
|
||||
let d = self.session.develop_of(photo)?;
|
||||
if d.crop != lightcraft_develop::Crop::default() || !matches!(self.ui.crop_default_aspect, Some(Value::Array(_))) {
|
||||
return None;
|
||||
}
|
||||
let (aw, ah) = self.crop_tool_aspect()?;
|
||||
let p = self.session.catalog.photo(photo)?;
|
||||
let (w, h) = (f64::from(p.width.max(1)), f64::from(p.height.max(1)));
|
||||
let (w, h) = if d.orientation.swaps_axes() { (h, w) } else { (w, h) };
|
||||
// turned like the photo's full-frame crop, as `crop.aspect` with `orientation: "crop"` does
|
||||
let (aw, ah) = if (aw >= ah) != (w >= h) { (ah, aw) } else { (aw, ah) };
|
||||
Some(lightcraft_geom::crop_fit_angle(w, h, d.crop.geometry.angle, Some(f64::from(aw) / f64::from(ah.max(1)))))
|
||||
}
|
||||
|
||||
/// Return on an untouched photo crops it to the remembered ratio's box the tool showed.
|
||||
pub(crate) fn apply_crop_preview(&mut self) {
|
||||
if self.crop_tool_preview().is_some()
|
||||
&& let Some(aspect) = self.ui.crop_default_aspect.clone()
|
||||
&& let Err(e) = self.session.execute("crop.aspect", &serde_json::json!({"aspect": aspect, "orientation": "crop"}))
|
||||
{
|
||||
self.ui.status = e.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
/// Called only after a crop gesture begins, inside its undo transaction.
|
||||
pub(crate) fn begin_crop(&mut self) -> Result<(), String> {
|
||||
// nothing chosen yet: the photo's own shape, as `crop_tool_aspect` shows it
|
||||
|
||||
@@ -2337,6 +2337,48 @@ fn crop_default_browsing_is_read_only_and_drag_is_one_edit() {
|
||||
assert_eq!(h.app.session.develop_of(photo).unwrap(), before);
|
||||
}
|
||||
|
||||
/// Issue #3029: a ratio picked on one photo is the one the crop tool offers on the next. The box
|
||||
/// drawn on an untouched photo has that ratio too (not the photo's full frame), browsing still
|
||||
/// doesn't edit the photo, and Return crops it to the box shown.
|
||||
#[test]
|
||||
fn crop_box_on_the_next_photo_shows_the_remembered_ratio() {
|
||||
let mut h = demo([1200.0, 800.0]);
|
||||
let t = Duration::from_secs(10);
|
||||
let photos: Vec<_> = h.app.session.visible_cloned().into_iter().filter(|id| !h.app.session.catalog.photo(*id).unwrap().is_edited()).collect();
|
||||
h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [photos[0].0]}}), t);
|
||||
h.request("ui.set", json!({"view": "detail"}), t);
|
||||
h.request("engine.execute", json!({"command": "panel.crop"}), t);
|
||||
h.app.run("crop.aspect", json!({"aspect": "4x5"})).unwrap();
|
||||
h.app.run("tool.done", json!({})).unwrap();
|
||||
h.settle(SETTLE);
|
||||
let photo = photos[1];
|
||||
h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [photo.0]}}), t);
|
||||
let before = h.app.session.catalog.photo(photo).unwrap().clone();
|
||||
let undo = h.app.session.undo.len();
|
||||
h.request("engine.execute", json!({"command": "panel.crop"}), t);
|
||||
h.settle(SETTLE);
|
||||
let after = h.app.session.catalog.photo(photo).unwrap();
|
||||
assert_eq!(after.develop, before.develop, "browsing doesn't edit the photo");
|
||||
assert_eq!(after.history.len(), before.history.len());
|
||||
assert_eq!(h.app.session.undo.len(), undo);
|
||||
let (aw, ah) = h.app.crop_tool_aspect().expect("the remembered ratio");
|
||||
let shown = f64::from(aw) / f64::from(ah);
|
||||
let same = |a: f64, b: f64| (a - b).abs() < 0.02 || (1.0 / a - b).abs() < 0.02;
|
||||
let corner = |h: &Headless, i: usize| {
|
||||
h.app.widgets.iter().find(|(w, _)| *w == format!("cropHandle:{i}")).map(|(_, r)| r.center()).expect("a corner handle")
|
||||
};
|
||||
let (tl, br) = (corner(&h, 0), corner(&h, 2));
|
||||
let drawn = f64::from((br.x - tl.x) / (br.y - tl.y));
|
||||
assert!(same(shown, drawn), "the dropdown says {shown:.3} but the crop box is {drawn:.3}");
|
||||
// Return crops to the box shown
|
||||
h.request("engine.execute", json!({"command": "tool.done"}), t);
|
||||
h.settle(SETTLE);
|
||||
let p = h.app.session.catalog.photo(photo).unwrap();
|
||||
let r = p.develop.crop.geometry.rect;
|
||||
let cropped = r.width() * f64::from(p.width) / (r.height() * f64::from(p.height));
|
||||
assert!(same(shown, cropped), "the dropdown says {shown:.3} but the crop is {cropped:.3}");
|
||||
}
|
||||
|
||||
/// While cropping: O cycles the guides, ⇧O mirrors them, A locks / unlocks the aspect.
|
||||
#[test]
|
||||
fn crop_keys() {
|
||||
|
||||
@@ -957,6 +957,9 @@ fn ui_commands_7(app: &mut LightcraftApp, ctx: &egui::Context, id: &str, p: &Val
|
||||
// Return commits a tool panel (crop, remove, red eye, masking): back to Edit
|
||||
use RightPanel::*;
|
||||
if app.ui.dialog.is_none() && matches!(app.ui.right, Crop | Remove | RedEye | Masking) {
|
||||
if app.ui.right == Crop {
|
||||
app.apply_crop_preview();
|
||||
}
|
||||
let _ = app.session.end_interaction();
|
||||
app.ui.tool.clear();
|
||||
app.ui.right = Edit;
|
||||
|
||||
@@ -36,6 +36,15 @@ pub(super) fn crop_overlay(
|
||||
if resp.hovered() && ui.input(|i| i.modifiers.command) {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
}
|
||||
// an untouched photo shows the remembered ratio's box, which the first drag (or Return) applies
|
||||
let preview;
|
||||
let d = match app.crop_tool_preview() {
|
||||
Some(geometry) => {
|
||||
preview = DevelopSettings { crop: lightcraft_develop::Crop { geometry, ..d.crop }, ..d.clone() };
|
||||
&preview
|
||||
}
|
||||
None => d,
|
||||
};
|
||||
let quad = frame_crop_quad(d, frame);
|
||||
let pts: Vec<Pos2> = quad.iter().map(|q| map.screen(*q)).collect();
|
||||
let p = ui.painter();
|
||||
|
||||
Reference in New Issue
Block a user