mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-11 02:49:56 -06:00
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:
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user