Merge track r1-ui into next

docs/whats-new.md: kept both tracks' entries, newest first, one 2026-10-18 heading.
This commit is contained in:
aiongg
2026-10-21 14:54:06 +00:00
35 changed files with 1562 additions and 185 deletions
+25 -8
View File
@@ -77,13 +77,22 @@ fn aspect_tuple(c: &str, x: f64, y: f64) -> Result<(u32, u32)> {
Ok((a as u32, b as u32))
}
fn set_aspect(d: &mut DevelopSettings, w: f64, h: f64, aspect: Option<(u32, u32)>) {
/// How a new aspect ratio is turned (issue #2253).
#[derive(Clone, Copy, PartialEq)]
enum Turn {
/// As the crop is now: a named ratio ("2x3", "original"…) lands landscape on a landscape crop
/// and portrait on a portrait one (after X), as in Lightroom. A square crop follows the image.
LikeCrop,
/// As given: a custom `[w, h]` taller than wide is a portrait crop.
AsGiven,
}
fn set_aspect(d: &mut DevelopSettings, w: f64, h: f64, aspect: Option<(u32, u32)>, turn: Turn) {
let r = d.crop.geometry.rect_px(w, h);
let landscape = if (r.width() - r.height()).abs() > 1e-6 * r.width().max(r.height()) { r.width() > r.height() } else { w >= h };
let aspect = aspect.map(|(aw, ah)| if turn == Turn::LikeCrop && (aw >= ah) != landscape { (ah, aw) } else { (aw, ah) });
d.crop.aspect = aspect;
let a = aspect.map(|(aw, ah)| {
// orientation of the crop follows the image (landscape image → landscape crop)
let (aw, ah) = (aw as f64, ah as f64);
if (w >= h) == (aw >= ah) { aw / ah } else { ah / aw }
});
let a = aspect.map(|(aw, ah)| aw as f64 / ah as f64);
let angle = d.crop.geometry.angle;
d.crop.geometry = crop_fit_angle(w, h, angle, a);
}
@@ -628,11 +637,19 @@ pub fn specs() -> Vec<CommandSpec> {
"Crop Aspect",
[],
None,
"{aspect: \"original\"|\"free\"|\"current\" (lock the crop's present shape)|\"toggle\" (lock ↔ free)|\"1x1\"|\"4x5\"|\"8.5x11\"|\"5x7\"|\"2x3\"|\"4x3\"|\"16x9\"|\"16x10\"|[w,h]}",
"{aspect: \"original\"|\"free\"|\"current\" (lock the crop's present shape)|\"toggle\" (lock ↔ free)|\"1x1\"|\"4x5\"|\"8.5x11\"|\"5x7\"|\"2x3\"|\"4x3\"|\"16x9\"|\"16x10\"|[w,h], orientation?: \"crop\"|\"given\"} — a named ratio takes the crop's present orientation (landscape, or portrait after X); [w,h] is used as given (taller than wide → portrait); `orientation` overrides either",
has_active,
|s, p| {
let id = active(s, "crop.aspect")?;
let (w, h) = crop_dims(s, id);
// checked first, whatever the aspect: a bad value is an error on every path
let turn = match p.get("orientation") {
Some(Value::String(o)) if o == "crop" => Turn::LikeCrop,
Some(Value::String(o)) if o == "given" => Turn::AsGiven,
Some(_) => return Err(bad("crop.aspect", "orientation is \"crop\" or \"given\"")),
None if matches!(p.get("aspect"), Some(Value::Array(_))) => Turn::AsGiven,
None => Turn::LikeCrop,
};
let locked = s.develop_of(id).is_some_and(|d| d.crop.aspect.is_some());
let lock_current = match p.get("aspect").and_then(Value::as_str) {
Some("current") => true,
@@ -667,7 +684,7 @@ pub fn specs() -> Vec<CommandSpec> {
if aspect.is_none() {
d.crop.aspect = None;
} else {
set_aspect(d, w, h, aspect);
set_aspect(d, w, h, aspect, turn);
}
Ok(())
})