# Conflicts:
#	docs/parity-checklist.md
This commit is contained in:
Claude
2026-10-11 11:44:45 +00:00
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(())
})
}),
@@ -306,8 +324,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(())
})
}),
@@ -493,10 +514,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(())
})
}),
@@ -552,11 +575,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(())
})
}
@@ -856,8 +876,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;
}