Add Vertical and Horizontal perspective sliders to Compose

The keystone existed in framing but nothing in develop could reach it:
framing is presented by its own Compose panel rather than generated, so
new framing parameters get no control until the panel names them.

Compose now has Vertical and Horizontal sliders under Straighten,
mirrored from the session like the angle, recorded as parameter steps
("Vertical Perspective" in the history), cleared by the Compose reset
and by opening the next photograph. Releasing either slider refits the
crop the way releasing the straighten slider does: a keystone alone
needs no crop, but it moves the empty corners of a straightened frame,
so the crop that avoided them before may not after, or may have room
to grow back.
This commit is contained in:
2026-09-24 22:13:12 -04:00
parent 5a500118ae
commit 114d979397
7 changed files with 253 additions and 50 deletions
+51 -2
View File
@@ -641,7 +641,12 @@ export global Framing {
in property <float> max-straighten: 45.0;
in property <bool> flip-h: false;
in property <bool> flip-v: false;
/// Any of crop, angle, rotation or flips differs from neutral — what
/// TRACES: FR-DEV-20
/// The vertical and horizontal keystone, and their travel either way.
in property <float> keystone-v: 0.0;
in property <float> keystone-h: 0.0;
in property <float> max-keystone: 100.0;
/// Any of crop, angle, keystone, rotation or flips differs from neutral — what
/// lights the section's dot and enables its reset. Distinct from the crop
/// rect being non-full: a rotation or a flip is an edit with the crop
/// still full.
@@ -673,6 +678,15 @@ export global Framing {
/// for why it must be the end of the drag and not every frame of it, and
/// `PlainSlider::committed` for why it must not be `drag-changed`.
callback angle-committed(float);
/// TRACES: FR-DEV-20
/// A keystone slider moved. Separate callbacks rather than one taking an
/// axis, so neither slider has to name the other's value.
callback keystone-v-changed(float);
callback keystone-h-changed(float);
/// A keystone gesture is over: refit the crop, as the end of a
/// straightening does, since the two together decide where the empty
/// corners are.
callback keystone-committed();
/// Lock the crop to one of `aspects`, by index.
callback aspect-picked(int);
/// Stand the chosen ratio on its other edge.
@@ -697,7 +711,7 @@ export global Framing {
callback hidden-masks-kept();
}
// Crop, rotation, flips and straightening — the framing controls.
// Crop, rotation, flips, straightening and perspective — the framing controls.
//
// **Why this is hand-built when the rest of the panel is generated.** The
// generic path renders one slider per parameter, which for framing means eight
@@ -803,6 +817,41 @@ export component ComposePanel inherits Rectangle {
reset => { Framing.angle-changed(0); }
}
// TRACES: FR-DEV-20
// Perspective, under the straightening it is used with: the two
// are the corrections a building needs, and the keystone runs
// just before the angle in the frame's own map. Named for the
// lines they make parallel, as every developer names them — the
// word "keystone" is the mechanism, not the task.
PlainSlider {
label: "Vertical";
value: Framing.keystone-v;
default-value: 0.0;
minimum: -Framing.max-keystone;
maximum: Framing.max-keystone;
unit: "";
changed(v) => { Framing.keystone-v-changed(v); }
committed(v) => { Framing.keystone-committed(); }
reset => {
Framing.keystone-v-changed(0);
Framing.keystone-committed();
}
}
PlainSlider {
label: "Horizontal";
value: Framing.keystone-h;
default-value: 0.0;
minimum: -Framing.max-keystone;
maximum: Framing.max-keystone;
unit: "";
changed(v) => { Framing.keystone-h-changed(v); }
committed(v) => { Framing.keystone-committed(); }
reset => {
Framing.keystone-h-changed(0);
Framing.keystone-committed();
}
}
// TRACES: FR-DEV-3
// The ratio the crop is held to.
//