LR-CROP-STRAIGHTEN: every way of setting the crop angle keeps the crop inside the photo (#742)

develop.set / develop.adjust / develop.quickAdjust / develop.resetControl on crop.angle
(MCP set_develop, the control channel, lightcraft-cli render --set) and crop.set {angle}
now turn the crop as crop.straighten does, through the new CropGeometry::with_angle:
kept if it still fits, else the largest crop of its aspect inside the rotated photo.
Before, they left the full rect, and the export took in smeared edge pixels in the corners.
This commit is contained in:
Jason Baldwin
2026-10-10 16:01:25 +02:00
parent b70ae1741e
commit b8f868830b
7 changed files with 211 additions and 18 deletions
+36 -14
View File
@@ -37,6 +37,17 @@ fn crop_dims(s: &Session, id: PhotoId) -> (f64, f64) {
if o.swaps_axes() { (h, w) } else { (w, h) }
}
/// After a generic control write (`develop.set`, `develop.adjust`, …) that touched `crop.angle`:
/// refit the crop as `crop.straighten` does, from the crop it had `before`, so the box never takes in
/// canvas outside the photo (issue #742).
fn refit_crop_angle(touched: bool, before: CropGeometry, d: &mut DevelopSettings, (w, h): (f64, f64)) {
if touched {
d.crop.geometry = before.with_angle(d.crop.geometry.angle, w, h);
}
}
const CROP_ANGLE: &str = "crop.angle";
/// `v` as a list of numbers; `None` unless it is an array of nothing but numbers.
fn numbers(v: Option<&Value>) -> Option<Vec<f64>> {
v?.as_array()?.iter().map(Value::as_f64).collect()
@@ -146,7 +157,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Set Develop Values",
[],
None,
"{control?: controlId, value?: number, values?: {controlId: number}, ids?: [photo]} — see develop.controls",
"{control?: controlId, value?: number, values?: {controlId: number}, ids?: [photo]} — see develop.controls; `crop.angle` turns the crop as crop.straighten does (kept if it still fits, else the largest of its aspect inside the photo)",
has_active,
|s, p| {
let mut vals: Vec<(String, f64)> = Vec::new();
@@ -167,7 +178,9 @@ pub fn specs() -> Vec<CommandSpec> {
}
}
let label = if vals.len() == 1 { controls::find(&vals[0].0).map(|c| c.label).unwrap_or("Edit").to_string() } else { "Edit".into() };
let apply = |d: &mut DevelopSettings, info: &lightcraft_pipeline::SourceInfo| {
let angle = vals.iter().any(|(k, _)| k == CROP_ANGLE);
let apply = |d: &mut DevelopSettings, info: &lightcraft_pipeline::SourceInfo, dims: (f64, f64)| {
let before = d.crop.geometry;
if d.wb.mode == WbMode::AsShot && vals.iter().any(|(k, _)| k == "wb.temp" || k == "wb.tint") {
d.wb.temp = info.as_shot_temp;
d.wb.tint = info.as_shot_tint;
@@ -178,6 +191,7 @@ pub fn specs() -> Vec<CommandSpec> {
d.wb.mode = WbMode::Custom;
}
}
refit_crop_angle(angle, before, d, dims);
};
if let Some(ids) = p.get("ids").and_then(Value::as_array) {
let ids: Vec<PhotoId> = ids.iter().filter_map(Value::as_u64).map(PhotoId).collect();
@@ -187,7 +201,7 @@ pub fn specs() -> Vec<CommandSpec> {
.filter_map(|(id, d)| {
let mut d = (*d).clone();
let info = s.source_info(id);
apply(&mut d, &info);
apply(&mut d, &info, crop_dims(s, id));
s.develop_op(id, d, &label)
})
.collect();
@@ -196,8 +210,9 @@ pub fn specs() -> Vec<CommandSpec> {
}
let id = active(s, "develop.set")?;
let info = s.source_info(id);
let dims = crop_dims(s, id);
edit(s, "develop.set", &label, |d| {
apply(d, &info);
apply(d, &info, dims);
Ok(())
})
}
@@ -206,8 +221,10 @@ pub fn specs() -> Vec<CommandSpec> {
let c = str_param(p, "control").ok_or_else(|| bad("develop.adjust", "missing control"))?.to_string();
let spec = controls::find(&c).ok_or_else(|| bad("develop.adjust", "unknown control"))?;
let delta = f64_req(p, "delta", "develop.adjust")?;
let info = s.source_info(active(s, "develop.adjust")?);
let id = active(s, "develop.adjust")?;
let (info, dims) = (s.source_info(id), crop_dims(s, id));
edit(s, "develop.adjust", spec.label, |d| {
let before = d.crop.geometry;
if c == "wb.temp" || c == "wb.tint" {
if d.wb.mode == WbMode::AsShot {
d.wb.temp = info.as_shot_temp;
@@ -217,6 +234,7 @@ pub fn specs() -> Vec<CommandSpec> {
}
let v = controls::get(d, &c).unwrap_or(spec.default);
controls::set(d, &c, v + delta);
refit_crop_angle(c == CROP_ANGLE, before, d, dims);
Ok(())
})
}),
@@ -302,8 +320,11 @@ pub fn specs() -> Vec<CommandSpec> {
cmd!("develop.resetControl", "Reset Control", [], None, "{control}", has_active, |s, p| {
let c = str_param(p, "control").ok_or_else(|| bad("develop.resetControl", "missing control"))?.to_string();
let spec = controls::find(&c).ok_or_else(|| bad("develop.resetControl", "unknown control"))?;
let dims = crop_dims(s, active(s, "develop.resetControl")?);
edit(s, "develop.resetControl", spec.label, |d| {
let before = d.crop.geometry;
controls::set(d, &c, spec.default);
refit_crop_angle(c == CROP_ANGLE, before, d, dims);
Ok(())
})
}),
@@ -489,10 +510,12 @@ pub fn specs() -> Vec<CommandSpec> {
}
d.crop.geometry.rect = Rect::new(v[0].min(v[2]), v[1].min(v[3]), v[0].max(v[2]), v[1].max(v[3]));
}
if let Some(a) = p.get("angle").and_then(Value::as_f64) {
d.crop.geometry.angle = a.clamp(-45.0, 45.0);
}
d.crop.geometry = d.crop.geometry.constrained(w, h);
let angle = p.get("angle").and_then(Value::as_f64).map(|a| a.clamp(-45.0, 45.0));
d.crop.geometry = match (p.get("rect"), angle) {
// a new angle alone turns the crop as Straighten does (issue #742)
(None, Some(a)) => d.crop.geometry.with_angle(a, w, h),
(_, a) => CropGeometry { angle: a.unwrap_or(d.crop.geometry.angle), ..d.crop.geometry }.constrained(w, h),
};
Ok(())
})
}),
@@ -548,11 +571,8 @@ pub fn specs() -> Vec<CommandSpec> {
let (w, h) = crop_dims(s, id);
let angle = f64_req(p, "angle", "crop.straighten")?.clamp(-45.0, 45.0);
edit(s, "crop.straighten", "Straighten", |d| {
let r = d.crop.geometry.rect_px(w, h);
let fit = crop_fit_angle(w, h, angle, Some(r.width() / r.height()));
// keep the current crop size if it still fits, else shrink
let cand = CropGeometry { rect: d.crop.geometry.rect, angle };
d.crop.geometry = if cand.is_within_image(w, h) { cand } else { fit };
// keep the current crop if it still fits, else the largest of its aspect that does
d.crop.geometry = d.crop.geometry.with_angle(angle, w, h);
Ok(())
})
}
@@ -852,8 +872,10 @@ pub fn specs() -> Vec<CommandSpec> {
nd.wb.temp = info.as_shot_temp;
nd.wb.tint = info.as_shot_tint;
}
let before = nd.crop.geometry;
let v = controls::get(&nd, &ctl).unwrap_or(spec.default);
controls::set(&mut nd, &ctl, v + delta);
refit_crop_angle(ctl == CROP_ANGLE, before, &mut nd, crop_dims(s, id));
if ctl == "wb.temp" || ctl == "wb.tint" {
nd.wb.mode = WbMode::Custom;
}