mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-11 16:31:32 -06:00
LR-CROP-ASPECT: remember the last crop aspect as the default
This commit is contained in:
@@ -1572,6 +1572,34 @@ mod tests {
|
||||
assert!(ids(&mut h, "notice:previewOnly:").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crop_remembers_last_aspect_across_photos() {
|
||||
let mut h = demo([1200.0, 800.0]);
|
||||
let photos = h.app.session.visible_cloned();
|
||||
h.app.ui.right = crate::state::RightPanel::Crop;
|
||||
h.app.run("crop.aspect", json!({"aspect": "original"})).unwrap();
|
||||
let saved = serde_json::to_value(&h.app.ui).unwrap();
|
||||
assert_eq!(saved["cropDefaultAspect"], "original");
|
||||
for &photo in &photos[1..3] {
|
||||
h.app.run("library.select", json!({"ids": [photo.0]})).unwrap();
|
||||
h.app.apply_crop_default();
|
||||
let p = h.app.session.catalog.photo(photo).unwrap();
|
||||
assert_eq!(p.develop.crop.aspect, Some((p.width * 100, p.height * 100)));
|
||||
}
|
||||
h.app.run("crop.aspect", json!({"aspect": "1x1"})).unwrap();
|
||||
let existing = h.app.session.develop_of(photos[2]).unwrap().crop;
|
||||
h.app.run("library.select", json!({"ids": [photos[3].0]})).unwrap();
|
||||
h.app.apply_crop_default();
|
||||
assert_eq!(h.app.session.develop_of(photos[3]).unwrap().crop.aspect, Some((100, 100)));
|
||||
h.app.run("crop.aspect", json!({"aspect": "free"})).unwrap();
|
||||
h.app.run("library.select", json!({"ids": [photos[2].0]})).unwrap();
|
||||
h.app.apply_crop_default();
|
||||
assert_eq!(h.app.session.develop_of(photos[2]).unwrap().crop, existing);
|
||||
h.app.ui = serde_json::from_value(saved).unwrap();
|
||||
assert_eq!(h.app.ui.crop_default_aspect, Some(json!("original")));
|
||||
h.settle(SETTLE);
|
||||
}
|
||||
|
||||
/// While cropping: O cycles the guides, ⇧O mirrors them, A locks / unlocks the aspect.
|
||||
#[test]
|
||||
fn crop_keys() {
|
||||
|
||||
@@ -349,6 +349,17 @@ impl LightcraftApp {
|
||||
return r;
|
||||
}
|
||||
let r = self.session.execute(id, ¶ms).map_err(|e| e.to_string());
|
||||
if r.is_ok() && matches!(id, "crop.aspect" | "crop.rotateAspect") {
|
||||
self.ui.crop_default_aspect = if id == "crop.aspect" && params.get("aspect").and_then(Value::as_str) == Some("original") {
|
||||
Some(serde_json::json!("original"))
|
||||
} else {
|
||||
self.session
|
||||
.active()
|
||||
.and_then(|photo| self.session.develop_of(photo))
|
||||
.map(|d| d.crop.aspect.map_or(serde_json::json!("free"), |(w, h)| serde_json::json!([w as f64 / 100.0, h as f64 / 100.0])))
|
||||
};
|
||||
self.ui.crop_default_photo = self.session.active();
|
||||
}
|
||||
if r.is_ok() && id == "photo.label" {
|
||||
let label = params.get("label").and_then(Value::as_str).and_then(lightcraft_catalog::ColorLabel::parse);
|
||||
let text = match label {
|
||||
@@ -378,6 +389,27 @@ impl LightcraftApp {
|
||||
r
|
||||
}
|
||||
|
||||
/// Initialize the crop tool on untouched photos without replacing existing crops.
|
||||
pub(crate) fn apply_crop_default(&mut self) {
|
||||
if self.ui.right != state::RightPanel::Crop {
|
||||
self.ui.crop_default_photo = None;
|
||||
return;
|
||||
}
|
||||
let photo = self.session.active();
|
||||
if self.ui.crop_default_photo == photo {
|
||||
return;
|
||||
}
|
||||
self.ui.crop_default_photo = photo;
|
||||
if let Some(photo) = photo
|
||||
&& self.session.develop_of(photo).is_some_and(|d| d.crop == lightcraft_develop::Crop::default())
|
||||
&& let Some(aspect) = self.ui.crop_default_aspect.clone()
|
||||
&& aspect != serde_json::json!("free")
|
||||
&& let Err(e) = self.session.execute("crop.aspect", &serde_json::json!({"aspect": aspect}))
|
||||
{
|
||||
self.ui.status = e.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a transient toast at the bottom of the canvas (like the reference app's HUD).
|
||||
/// Advance a running slideshow (wrapping around at the end).
|
||||
fn slideshow_tick(&mut self, ctx: &egui::Context) {
|
||||
@@ -695,6 +727,7 @@ impl LightcraftApp {
|
||||
}
|
||||
|
||||
fn logic_inner(&mut self, ctx: &egui::Context) {
|
||||
self.apply_crop_default();
|
||||
if !self.styled {
|
||||
theme::install_fonts_with_chinese(ctx, self.chinese_font.as_ref());
|
||||
theme::apply(ctx);
|
||||
|
||||
@@ -129,6 +129,7 @@ fn custom_aspect(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
|
||||
fn crop(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
app.apply_crop_default();
|
||||
let d = app.session.develop_of(id).unwrap_or_default();
|
||||
header(ui, "Crop");
|
||||
padded(ui, |ui| {
|
||||
|
||||
@@ -321,6 +321,10 @@ pub struct UiState {
|
||||
/// Mirroring of the triangle / spiral crop guides (0..4).
|
||||
pub crop_overlay_orient: u8,
|
||||
pub crop_overlay: CropOverlay,
|
||||
/// Last explicitly chosen aspect, retained across photos and app launches.
|
||||
pub crop_default_aspect: Option<serde_json::Value>,
|
||||
#[serde(skip)]
|
||||
pub crop_default_photo: Option<lightcraft_catalog::PhotoId>,
|
||||
pub show_filenames: bool,
|
||||
/// What the square grid's caption shows: `filename`, `exposure` (shutter · aperture · ISO ·
|
||||
/// focal length) or `date`.
|
||||
@@ -703,6 +707,8 @@ impl Default for UiState {
|
||||
mask_overlay_opacity: 50.0,
|
||||
mask_pins: true,
|
||||
crop_overlay: CropOverlay::Thirds,
|
||||
crop_default_aspect: None,
|
||||
crop_default_photo: None,
|
||||
crop_overlay_orient: 0,
|
||||
show_filenames: true,
|
||||
grid_info: "filename".into(),
|
||||
|
||||
+1
-1
@@ -308,7 +308,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
| Id | Feature | Tier | Status | Evidence | Missing / notes |
|
||||
|---|---|---|---|---|---|
|
||||
| LR-CROP-RECT | Crop rectangle | P0 | ✅ | `cmd:crop.set`, `cmd:crop.drag`, `crates/geom/src/crop_drag.rs`, `crates/ui-egui/src/panels/detail.rs` | double-click inside the box applies the crop (like Return) |
|
||||
| LR-CROP-ASPECT | Aspect ratios | P0 | ✅ | `cmd:crop.aspect`, `cmd:crop.rotateAspect`, `crates/ui-egui/src/panels/right.rs` | menu with presets + custom W × H, visible Lock toggle (also A), button names the active ratio; every handle honours the lock and the drag stops at the image edge instead of shifting the frame (issue #295); "Original" is the photo's own shape (no separate As Shot) |
|
||||
| LR-CROP-ASPECT | Aspect ratios | P0 | ✅ | `cmd:crop.aspect`, `cmd:crop.rotateAspect`, `crates/ui-egui/src/panels/right.rs` | menu with presets + custom W × H, visible Lock toggle (also A), button names the active ratio; every handle honours the lock and the drag stops at the image edge instead of shifting the frame (issue #295); "Original" is the photo's own shape (no separate As Shot); the last chosen aspect is saved as the default for untouched photos in Crop, resolving Original per photo and preserving existing crops |
|
||||
| LR-CROP-STRAIGHTEN | Straighten tool | P0 | ✅ | `cmd:crop.straighten`, `cmd:crop.autoStraighten` | Straighten Tool button: drag along a horizon/vertical; double-click or Auto levels automatically |
|
||||
| LR-CROP-AUTO | Auto straighten | P1 | ✅ | `cmd:crop.autoStraighten` | crop-angle leveling from detected horizon/plumb lines (consensus required) |
|
||||
| LR-CROP-ANGLE | Angle slider | P0 | ✅ | `ctl:crop.angle` | |
|
||||
|
||||
Reference in New Issue
Block a user