Give back what a straighten took, when the angle comes back
Build and test / Desktop (Linux) (push) Successful in 2h16m6s
Build and test / Layer separation (push) Successful in 52s
🐳 Android image / Build and push (push) Successful in 4s
Build and test / android-image (push) Successful in 4s
Traceability / Requirement traces (push) Successful in 51s
Build and test / Android (aarch64) (push) Successful in 47m10s

The auto-crop only ever shrank. Straighten to 20 degrees and the corners
are cropped away correctly; come back to 3, or all the way to zero, and
the crop stays at the size 20 degrees demanded. Nothing on screen explains
why the photograph is still small, and the only way back was undo.

The cause was that each correction was computed from the previous
correction's output, so it accumulated: every angle the slider rested at
took its cut and none was ever returned. The fix is to stop accumulating
and recompute. The applied crop is now always the user's own rectangle
fitted into the current angle's safe area, so as the angle falls and that
area opens up the crop grows back — and stops, exactly, at the rectangle
they chose. At zero the safe area is the whole frame and the fit is the
identity, which is what carries it the last of the way home. There is
deliberately no early exit for the upright case now: that exit is
precisely what would strand the crop small.

**The intent is remembered as a pair, so it repairs itself.** The session
keeps `(applied, intended)` — what the correction wrote, and what it was
derived from — and trusts the remembered intent only while the graph still
holds `applied`. Every other route to the crop leaves something else
there: a handle dragged, a ratio chosen, a sidecar loaded, a paste, an
undo. That mismatch is the signal the memory is stale, and the current
rectangle becomes the new intent. The alternative was a write into this
field from each of those paths, which is the kind of bookkeeping that is
correct until someone adds a seventh path.

Dragging a handle therefore *is* the user choosing, including at a
non-zero angle: the correction will not later grow the crop past what they
dragged it to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-29 19:16:43 +02:00
co-authored by Claude Opus 5
parent ff6c313bab
commit 5133e53bc8
3 changed files with 146 additions and 35 deletions
+56
View File
@@ -1932,6 +1932,62 @@ mod tests {
);
}
#[test]
fn fitting_into_the_whole_frame_is_the_identity() {
// What makes the straighten correction reversible: at zero degrees the
// safe area is the whole frame, so recomputing the crop from the
// user's own rectangle hands it straight back, bit for bit.
for rect in [
CropRect::default(),
CropRect {
x: 0.1,
y: 0.2,
width: 0.3,
height: 0.4,
},
CropRect {
x: 0.0,
y: 0.0,
width: 1.0,
height: 0.5,
},
] {
assert_eq!(rect.fitted_into(CropRect::default()), rect, "{rect:?}");
}
}
#[test]
fn a_crop_recomputed_from_intent_grows_back_as_the_angle_falls() {
// The whole reason the correction is recomputed rather than
// accumulated. Taking each angle's fit from the *previous* fit
// ratchets — the excursion to 20 degrees is never given back — where
// taking it from the user's own rectangle every time returns it.
let intended = CropRect::default();
let bound_at = |deg: f32| {
let mut f = Framing::new();
f.set_param(ANGLE, deg);
f.max_inscribed_crop(6000, 4000)
};
let out = intended.fitted_into(bound_at(20.0));
let back = intended.fitted_into(bound_at(3.0));
assert!(
back.width > out.width && back.height > out.height,
"3 degrees should give more back than 20 took: {out:?} -> {back:?}"
);
// And all the way home.
assert_eq!(intended.fitted_into(bound_at(0.0)), intended);
// The ratcheting version, for contrast: chaining the fits never
// recovers, which is the behaviour this arrangement exists to avoid.
let chained = out.fitted_into(bound_at(3.0));
assert!(
chained.width <= out.width + 1e-6,
"a chained fit must not grow — that is the bug being pinned"
);
}
#[test]
fn parameters_round_trip() {
let mut f = Framing::new();