#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SceneLuminance(f64);
impl SceneLuminance {
pub fn new(l: f64) -> Option<Self> {
if l.is_finite() && l >= 0.0 {
Some(Self(l))
} else {
None
}
}
#[inline]
pub fn get(self) -> f64 {
self.0
}
pub fn to_display(self) -> DisplayLuminance {
let l = self.0;
let ld = l / (1.0 + l);
DisplayLuminance(ld)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DisplayLuminance(f64);
impl DisplayLuminance {
pub fn new(ld: f64) -> Option<Self> {
if ld.is_finite() && (0.0..1.0).contains(&ld) {
Some(Self(ld))
} else {
None
}
}
#[inline]
pub fn get(self) -> f64 {
self.0
}
pub fn to_scene(self) -> SceneLuminance {
let ld = self.0;
let l = ld / (1.0 - ld);
SceneLuminance(l)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scene_constructor_rejects_invalid() {
assert!(SceneLuminance::new(-0.1).is_none());
assert!(SceneLuminance::new(f64::NAN).is_none());
assert!(SceneLuminance::new(f64::INFINITY).is_none());
assert!(SceneLuminance::new(0.0).is_some());
assert!(SceneLuminance::new(1.0e6).is_some());
}
#[test]
fn display_constructor_rejects_invalid() {
assert!(DisplayLuminance::new(-0.01).is_none());
assert!(DisplayLuminance::new(1.0).is_none());
assert!(DisplayLuminance::new(1.001).is_none());
assert!(DisplayLuminance::new(f64::NAN).is_none());
assert!(DisplayLuminance::new(0.0).is_some());
assert!(DisplayLuminance::new(0.99999).is_some());
}
#[test]
fn known_curve_values() {
assert_eq!(SceneLuminance::new(0.0).unwrap().to_display().get(), 0.0);
assert_eq!(SceneLuminance::new(1.0).unwrap().to_display().get(), 0.5);
assert!((SceneLuminance::new(3.0).unwrap().to_display().get() - 0.75).abs() < 1e-15);
assert!((SceneLuminance::new(9.0).unwrap().to_display().get() - 0.9).abs() < 1e-15);
}
#[test]
fn forward_output_is_strictly_below_one() {
let big = SceneLuminance::new(1.0e12).unwrap().to_display().get();
assert!(big < 1.0, "Ld must remain strictly below 1, got {big}");
}
#[test]
fn scene_display_scene_roundtrip() {
let probes: &[f64] = &[
0.0, 1.0e-9, 0.001, 0.018, 0.18, 1.0, 2.5, 10.0, 100.0, 1.0e4, 1.0e6,
];
for &l in probes {
let scene = SceneLuminance::new(l).unwrap();
let back = scene.to_display().to_scene();
let denom = if l > 0.0 { l } else { 1.0 };
let rel = (back.get() - l).abs() / denom;
assert!(
rel < 1e-9,
"round-trip lost precision at L={l}: got {got}, rel err {rel}",
got = back.get(),
);
}
}
#[test]
fn display_scene_display_roundtrip() {
let probes: &[f64] = &[0.0, 0.001, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99, 0.9999];
for &ld in probes {
let display = DisplayLuminance::new(ld).unwrap();
let back = display.to_scene().to_display();
let denom = if ld > 0.0 { ld } else { 1.0 };
let rel = (back.get() - ld).abs() / denom;
assert!(
rel < 1e-9,
"round-trip lost precision at Ld={ld}: got {got}, rel err {rel}",
got = back.get(),
);
}
}
}