Crop away the corners a straighten exposed, once the slider is let go

Turning a rectangle inside its own bounds exposes its corners: there is no
source pixel out there, and the shader renders it black. Nothing in the
render prevents that, deliberately — a free angle does not change the
output size, which is what leaves the frame where the user put it while
the slider moves. Correct during the drag; four black wedges on the
finished photograph.

Letting go of the slider now pulls the crop inside the area the angle
leaves defined. `Framing::max_inscribed_crop` already computed that
bound and had no caller; this is the caller its doc comment described.

**Once, at the end of the gesture.** Applied per frame it would shrink
the crop on every step of the slider and never grow it back, so a user
who overshot to 20° and came back to 3° would be left with a crop
ratcheted down by the excursion rather than by the angle they settled on.
Per gesture it is bounded by the angles actually rested at, and undo steps
back through them.

**The crop is fitted into the bound, not replaced by it.** A crop placed
deliberately off-centre is a decision, and an automatic correction that
recentred it would undo the user's work to fix a problem they did not
have. `CropRect::fitted_into` scales only as far as the bound demands and
then slides the rect the shortest distance needed to be inside — so a
ratio locked in the crop panel survives the straighten too, since the
shape is never touched.

It returns the rect unchanged, bit for bit, when nothing needed to move.
That matters more than it looks: this runs on every release of the
slider, including releases at zero, and a rect that drifted by a rounding
error each time would be an edit recorded for no reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-29 13:49:19 +02:00
co-authored by Claude Opus 5
parent e6ad906bc1
commit d9eb8faffd
6 changed files with 233 additions and 32 deletions
+34
View File
@@ -3315,6 +3315,40 @@ impl DevelopSession {
.record(&self.graph, Edit::Action(labels::step::FLIP_V));
}
/// TRACES: FR-DEV-3
/// Pull the crop inside the area a straightening angle leaves defined.
///
/// Turning a rectangle inside its own bounds exposes the corners: there is
/// no source pixel out there and the shader renders it black. Nothing in
/// the render prevents it — a free angle deliberately does *not* change the
/// output size, so that straightening a horizon leaves the frame where the
/// user put it — which is correct for the drag and leaves black wedges in
/// the corners of the finished photograph.
///
/// This is the correction, and it runs when the gesture **finishes**.
/// Applied continuously it would shrink the crop on every frame of the
/// slider and never grow it back, so a user who overshot to 20° and came
/// back to 3° would be left with a crop ratcheted down by the excursion
/// rather than by the angle they settled on. Once per gesture, that is
/// bounded: the crop is only ever as small as the angles actually rested
/// at, and undo steps back through them.
///
/// The crop keeps its own shape — so a locked ratio survives — and keeps
/// the side of the frame it was on; see [`CropRect::fitted_into`] for why
/// it is not simply replaced by the inscribed rectangle.
pub fn auto_crop_to_angle(&mut self) {
let (sw, sh) = self.demosaiced.size();
let bound = self.graph.framing().max_inscribed_crop(sw, sh);
// Upright, so every corner is already defined and there is nothing to
// pull in from. Returning early rather than fitting into a full rect
// matters: it keeps letting go of a slider at zero from touching the
// crop at all.
if bound.is_full() {
return;
}
self.set_crop(self.graph.crop().fitted_into(bound));
}
/// Set the straightening angle, in degrees.
pub fn set_angle(&mut self, degrees: f32) {
self.graph.set_param(