fix: 切り抜きの初期比率をOriginalでロック #991

This commit is contained in:
haranishi
2026-10-12 02:17:34 +09:00
parent 76b5377868
commit 6ff9b95fb9
3 changed files with 50 additions and 7 deletions
+39 -3
View File
@@ -2240,6 +2240,42 @@ mod tests {
assert_eq!(h.app.session.develop_of(photo).unwrap(), before);
}
#[test]
fn crop_starts_locked_to_original_without_editing_and_remembers_unlock() {
use crate::state::UiState;
for saved in [json!({}), json!({"cropDefaultAspect": null})] {
let mut h = demo([1200.0, 800.0]);
h.app.ui = serde_json::from_value::<UiState>(saved).unwrap();
let photos: Vec<_> =
h.app.session.visible_cloned().into_iter().filter(|id| !h.app.session.catalog.photo(*id).unwrap().is_edited()).collect();
let photo = photos[0];
h.app.run("library.select", json!({"ids": [photo.0]})).unwrap();
h.app.run("panel.crop", json!({})).unwrap();
let before = h.app.session.develop_of(photo).unwrap();
let p = h.app.session.catalog.photo(photo).unwrap();
let original = (p.width * 100, p.height * 100);
let undo = h.app.session.undo.len();
h.settle(SETTLE);
assert_eq!(h.app.crop_tool_aspect(), Some(original));
assert_eq!(h.app.session.develop_of(photo).unwrap(), before);
assert_eq!(h.app.session.undo.len(), undo);
h.app.run("develop.beginInteraction", json!({"label": "Crop"})).unwrap();
h.app.begin_crop().unwrap();
h.app.run("crop.drag", json!({"handle": "bottomRight", "from": [0.8, 0.8], "to": [0.7, 0.6]})).unwrap();
h.app.run("develop.endInteraction", json!({})).unwrap();
assert_eq!(h.app.session.develop_of(photo).unwrap().crop.aspect, Some(original));
assert_eq!(h.app.session.undo.len(), undo + 1);
h.app.run("edit.undo", json!({})).unwrap();
assert_eq!(h.app.session.develop_of(photo).unwrap(), before);
h.app.run("crop.aspect", json!({"aspect": "toggle"})).unwrap();
let saved = serde_json::to_value(&h.app.ui).unwrap();
h.app.ui = serde_json::from_value(saved).unwrap();
h.app.run("library.select", json!({"ids": [photos[1].0]})).unwrap();
assert_eq!(h.app.ui.crop_default_aspect, Some(json!("free")));
assert_eq!(h.app.crop_tool_aspect(), None);
}
}
/// While cropping: O cycles the guides, ⇧O mirrors them, A locks / unlocks the aspect.
#[test]
fn crop_keys() {
@@ -2254,12 +2290,12 @@ mod tests {
h.request("ui.key", json!({"key": "O", "shift": true}), t);
assert_eq!(h.app.ui.crop_overlay_orient, 1);
assert_eq!(h.app.ui.mask_overlay, mask_overlay, "O doesn't toggle the mask overlay while cropping");
let locked = |h: &Headless| h.app.session.develop_of(h.app.session.active().unwrap()).unwrap().crop.aspect.is_some();
assert!(!locked(&h));
h.request("ui.key", json!({"key": "A"}), t);
let locked = |h: &Headless| h.app.crop_tool_aspect().is_some();
assert!(locked(&h));
h.request("ui.key", json!({"key": "A"}), t);
assert!(!locked(&h));
h.request("ui.key", json!({"key": "A"}), t);
assert!(locked(&h));
assert!(!h.app.ui.visualize_spots);
}
+8 -2
View File
@@ -548,7 +548,8 @@ 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.
/// Last chosen aspect, retained across photos and launches; Original until a choice is made.
#[serde(deserialize_with = "deserialize_crop_default_aspect")]
pub crop_default_aspect: Option<serde_json::Value>,
pub show_filenames: bool,
/// What the square grid's caption shows: `filename`, `exposure` (shutter · aperture · ISO ·
@@ -1055,7 +1056,7 @@ impl Default for UiState {
mask_overlay_opacity: 50.0,
mask_pins: true,
crop_overlay: CropOverlay::Thirds,
crop_default_aspect: None,
crop_default_aspect: Some(serde_json::json!("original")),
crop_overlay_orient: 0,
show_filenames: true,
grid_info: "filename".into(),
@@ -1203,6 +1204,11 @@ impl UiState {
}
}
fn deserialize_crop_default_aspect<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<serde_json::Value>, D::Error> {
// Legacy null means no choice yet. An explicitly unlocked crop is saved as "free".
Ok(Option::<serde_json::Value>::deserialize(d)?.or_else(|| Some(serde_json::json!("original"))))
}
fn deserialize_favorite_themes<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<String>, D::Error> {
let values = Vec::<String>::deserialize(d)?;
let mut seen = std::collections::BTreeSet::new();
+3 -2
View File
@@ -1,6 +1,6 @@
# Lightroom parity checklist
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (rapid slider entry; LR-PROF-CAMERACOLOR: tone curve from black-and-white camera JPEGs; LR-VIEW-ZOOM, LR-VIEW-COMPARE: Compare and Reference zoom windows; LR-IMP-FORMATS, LR-IMP-CAMERA-COVERAGE: EOS R5 Mark II CR3 decodes; renamed from `parity.md`; Top gaps moved to gaps.md, shortcut notes to ui-parity.md; filesystem history retry/Undo/Redo evidence; renamed from `parity.md`; Top gaps moved to gaps.md, shortcut notes to ui-parity.md) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (LR-CROP-ASPECT: new and legacy unset crop defaults lock to Original; explicit Free is retained) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we
have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good
@@ -246,7 +246,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
| 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, holding Shift while dragging a handle keeps the frame's proportions for that drag (issue #614), 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 a crop tool default, resolving Original per photo and preserving existing crops; opening Crop or browsing never edits a photo: the default applies only when a crop gesture starts, within its single undo step |
| 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, holding Shift while dragging a handle keeps the frame's proportions for that drag (issue #614), 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 a crop tool default, resolving Original per photo and preserving existing crops; without a previous choice (including legacy unset preferences) it starts locked to Original, while an explicit Free choice survives restarts (issue #991); opening Crop or browsing never edits a photo: the default applies only when a crop gesture starts, within its single undo step |
| LR-CROP-STRAIGHTEN | Straighten tool | P0 | ✅ | `cmd:crop.straighten`, `cmd:crop.autoStraighten`, `crates/ui-egui/src/panels/detail.rs` (`rotate_cursor`, `angle_readout`), `crates/ui-egui/src/panels/right.rs` (Angle field), `crates/ui-egui/src/tests_crop_rotate.rs` | Straighten Tool button: drag along a horizon/vertical; double-click or Auto levels automatically. Dragging outside the crop box rotates it: there the pointer is a curved double arrow that bulges away from the side of the box it is on, diagonally off a corner and following a rotated box (`rotate_cursor_turn`, tested by `rotate_cursor_faces_away_from_the_crop_box`) (with ⌘ held, the straighten crosshair), and while rotating the angle shows next to it, kept on the canvas (issue #534); an exact angle goes in the Angle field under Straighten (`cropAngleField`: applies on Return or leaving it, Esc keeps the old angle, takes a decimal comma; a drag on it is one undo step) or by clicking the Straighten value (its hover says so). Every way of setting the angle (rotate handle, Straighten slider, Angle field, `crop.straighten`, `crop.set {angle}`, and the generic `develop.set` / `develop.adjust` / `develop.quickAdjust` / `develop.resetControl` on `crop.angle`, as MCP, the control channel and `lightcraft-cli render --set` use them) keeps the crop if it still fits, else shrinks it to the largest crop of its aspect inside the photo (`CropGeometry::with_angle`, `crates/engine/src/tests_crop_angle.rs`; before issue #742 the generic path left the box, and the export, past the photo with smeared edge pixels in the corners). The dimming outside the box is the photo minus the (rotated) box, drawn as one mesh (`outside_crop_mesh`), so rotating never dims wedges inside the box or doubles them outside (issue #731) |
| 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`, `crates/ui-egui/src/panels/right.rs` | the Straighten slider; an exact angle can also be typed in the Angle field under it (see LR-CROP-STRAIGHTEN); setting `crop.angle` through `develop.set` fits the crop inside the photo as Straighten does (issue #742) |
@@ -787,6 +787,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
| Date | Change | Summary |
|---|---|---|
| 2026-10-12 | minor | LR-CROP-ASPECT: new and legacy unset tool preferences lock to Original; an explicit Free choice remains unlocked across restarts (issue #991); status unchanged |
| 2026-10-11 | minor | KEY-SLIDER-TYPE: honor immediate Return/Escape and keep typed keys in the value editor; add regression references |
| 2026-10-11 | minor | LR-PROF-CAMERACOLOR: raws with a black-and-white camera JPEG take its tone curve (issue #577) |
| 2026-10-11 | minor | LR-VIEW-ZOOM and LR-VIEW-COMPARE: Compare and Reference are sharp at 1:1 (zoom windows per pane, two originals kept decoded while they are open); statuses unchanged |