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
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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user