Merge track r3-t09

# Conflicts:
#	docs/parity-checklist.md
#	docs/raw-parity.md
#	docs/ui-parity.md
#	docs/whats-new.md
This commit is contained in:
aiongg
2026-10-31 02:31:25 +00:00
47 changed files with 1369 additions and 117 deletions
+30
View File
@@ -97,6 +97,10 @@ impl ControlSpec {
}
pub fn format(&self, v: f64) -> String {
let s = format!("{:.*}", self.decimals as usize, v);
if v.is_finite() && !s.bytes().any(|b| matches!(b, b'1'..=b'9')) {
// Rounds to zero: no sign.
return s.trim_start_matches('-').to_string();
}
if v > 0.0 && self.min < 0.0 { format!("+{s}") } else { s }
}
/// Where `v` sits on the slider track, from 0 (left end) to 1 (right end). Linear, except for the Kelvin
@@ -410,6 +414,9 @@ pub static INDEXED: &[ControlSpec] = &[
fn split_indexed(id: &str) -> Option<(&str, usize, &str)> {
let mut it = id.splitn(3, '.');
let (fam, i, field) = (it.next()?, it.next()?, it.next()?);
if i.is_empty() || !i.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
Some((fam, i.parse().ok()?, field))
}
@@ -493,6 +500,17 @@ pub fn in_section(section: Section) -> impl Iterator<Item = &'static ControlSpec
mod tests {
use super::*;
#[test]
fn format_shows_no_sign_on_a_value_that_rounds_to_zero() {
let e = find("light.exposure").unwrap();
assert_eq!(e.format(0.004), "0.00");
assert_eq!(e.format(-0.004), "0.00");
assert_eq!(e.format(0.5), "+0.50");
assert_eq!(e.format(-0.5), "-0.50");
assert_eq!(e.format(f64::INFINITY), "+inf");
assert_eq!(e.format(f64::NEG_INFINITY), "-inf");
}
#[test]
fn every_control_reads_its_default() {
let s = DevelopSettings::default();
@@ -538,6 +556,18 @@ mod tests {
}
}
#[test]
fn signed_indexes_are_unknown_ids() {
let mut s = DevelopSettings::default();
s.point_colors.push(Default::default());
let before = s.point_colors[0].hue_shift;
assert!(!set(&mut s, "pointColor.+0.hueShift", 10.0), "signed index");
assert_eq!(s.point_colors[0].hue_shift, before);
assert_eq!(get(&s, "pointColor.+0.hueShift"), None);
assert!(find("pointColor.+0.hueShift").is_none());
assert!(find("redEye.+0.darken").is_none());
}
#[test]
fn formatting() {
let e = find("light.exposure").unwrap();
+27 -1
View File
@@ -155,7 +155,8 @@ impl DevelopSettings {
}
}
/// Reset every control in `section` to its default.
/// Reset every control in `section` to its default, and the settings of the section that are not
/// controls (the creative profile, the curve, the white balance mode).
pub fn reset_section(&mut self, section: Section) {
for c in controls::in_section(section) {
controls::set(self, c.id, c.default);
@@ -167,6 +168,11 @@ impl DevelopSettings {
}
Section::Color => self.wb.mode = WbMode::AsShot,
Section::Detail => self.enhance.denoise_on = None,
// the creative profile is not a control: the chosen look goes back to the default one,
// and so does the camera's own local tone mapping
Section::Profile => {
self.profile = Profile::default();
}
_ => {}
}
}
@@ -326,6 +332,26 @@ mod tests {
assert!(s.is_unedited());
}
#[test]
fn resetting_the_profile_section_restores_the_default_profile() {
// the chosen creative profile is not a control, so resetting the section must restore it too
let mut s = DevelopSettings::default();
s.profile.id = "lc.muted.matte-soft".into();
s.profile.amount = 40.0;
s.reset_section(Section::Profile);
assert_eq!(s.profile.id, "lc.color", "the creative profile is reset to the default one");
assert_eq!(s.profile.amount, 100.0);
}
#[test]
fn resetting_the_profile_section_clears_the_camera_local_tone() {
// the camera's local tone mapping is not a control either: a Profile reset switches it off
let mut s = DevelopSettings::default();
s.profile.camera_local_tone = true;
s.reset_section(Section::Profile);
assert!(!s.profile.camera_local_tone);
}
#[test]
fn section_toggles() {
let mut s = DevelopSettings::default();
+5
View File
@@ -134,6 +134,7 @@ pub struct DevelopSettings {
pub orientation: Orientation,
pub masks: Vec<Mask>,
pub spots: Vec<Spot>,
/// Red eye corrections; the first [`MAX_RED_EYES`] render.
pub red_eye: Vec<RedEye>,
pub lens_blur: LensBlur,
pub enhance: Enhance,
@@ -1069,6 +1070,10 @@ impl Default for Spot {
}
}
/// Most red eye / pet eye corrections a photo renders. Every output pixel goes through every eye, so render cost is
/// pixels x eyes; a sidecar, preset or agent with thousands could stall a render for hours (issue #2556).
pub const MAX_RED_EYES: usize = 64;
/// A red eye / pet eye correction: the user's ellipse (centre normalized, radii as fractions of
/// the long edge); the pupil inside it is found automatically.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]