use core::f64;
use std::sync::OnceLock;
use approx::{ulps_eq, AbsDiffEq, RelativeEq, UlpsEq};
use crate::{
error::Error, math::distance_to_line, observer::Observer, observer::Observer::Cie1931, xyz::XYZ,
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CCT(f64, f64);
impl AbsDiffEq for CCT {
type Epsilon = f64;
fn default_epsilon() -> Self::Epsilon {
f64::default_epsilon()
}
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
self.0.abs_diff_eq(&other.0, epsilon) && self.1.abs_diff_eq(&other.1, epsilon)
}
}
impl RelativeEq for CCT {
fn default_max_relative() -> Self::Epsilon {
f64::default_max_relative()
}
fn relative_eq(
&self,
other: &Self,
epsilon: Self::Epsilon,
max_relative: Self::Epsilon,
) -> bool {
self.0.relative_eq(&other.0, epsilon, max_relative)
&& self.1.relative_eq(&other.1, epsilon, max_relative)
}
}
impl UlpsEq for CCT {
fn default_max_ulps() -> u32 {
f64::default_max_ulps()
}
fn ulps_eq(&self, other: &Self, epsilon: Self::Epsilon, max_ulps: u32) -> bool {
self.0.ulps_eq(&other.0, epsilon, max_ulps) && self.1.ulps_eq(&other.1, epsilon, max_ulps)
}
}
impl CCT {
pub fn new(cct: f64, duv: f64) -> Result<Self, Error> {
match (cct, duv) {
(_, d) if d > 0.05 => Err(Error::CCTDuvHighError),
(_, d) if d < -0.05 => Err(Error::CCTDuvLowError),
(t, d) if ulps_eq!(t, im2t(0)) || ulps_eq!(t, im2t(N_STEPS - 1)) => Ok(Self(t, d)),
(t, _) if t > im2t(0) => Err(Error::CCTTemperatureTooHigh),
(t, _) if t < im2t(N_STEPS - 1) => Err(Error::CCTTemperatureTooLow),
(t, d) => Ok(Self(t, d)),
}
}
pub fn new_with_tint(cct: f64, tint: f64) -> Result<Self, Error> {
Self::new(cct, tint / 1000.0)
}
pub fn from_xyz(xyz: XYZ) -> Result<Self, Error> {
CCT::try_from(xyz)
}
pub fn t(&self) -> f64 {
self.0
}
pub fn d(&self) -> f64 {
self.1
}
pub fn tint(&self) -> f64 {
1000.0 * self.1
}
pub fn to_array(&self) -> [f64; 2] {
[self.0, self.1]
}
}
impl From<CCT> for [f64; 2] {
fn from(cct: CCT) -> Self {
[cct.0, cct.1]
}
}
pub const N_DEPTH: usize = 12;
pub const N_STEPS: usize = 2 << (N_DEPTH - 1);
const MIRED_MAX: usize = 1000;
impl TryFrom<XYZ> for CCT {
type Error = Error;
fn try_from(xyz: XYZ) -> Result<Self, Self::Error> {
if xyz.observer != Observer::Cie1931 {
return Err(Error::RequiresCIE1931XYZ);
}
let [u, v] = xyz.uv60();
let [mut imlow, mut imhigh] = [0usize, N_STEPS - 1];
let [mut dlow, mut dhigh] = [0.0, 0.0];
for _ in 0..N_DEPTH {
let im = (imhigh + imlow) / 2;
let &[ub, vb, m] = robertson_table(im);
let d = distance_to_line(u, v, ub, vb, m);
if d < 0.0 {
imlow = im;
dlow = d;
} else {
imhigh = im;
dhigh = d;
}
}
if imlow == 0 {
let &[ub, vb, m] = robertson_table(imlow);
match distance_to_line(u, v, ub, vb, m) {
d if ulps_eq!(d, 0.0, epsilon = 1E-10) => {
dlow = 0.0;
dhigh = 1.0;
imhigh = 1;
}
d if d > 0.0 => return Err(Error::CCTTemperatureTooHigh),
d => dlow = d,
};
};
if imhigh == N_STEPS - 1 {
let &[ub, vb, m] = robertson_table(imhigh);
match distance_to_line(u, v, ub, vb, m) {
d if ulps_eq!(d, 0.0, epsilon = 1E-10) => {
dhigh = -0.0;
imlow = N_STEPS - 2;
}
d if d < 0.0 => return Err(Error::CCTTemperatureTooLow),
d => dlow = d,
};
};
let t = robertson_interpolate(im2t(imlow), dlow, im2t(imhigh), dhigh);
let d = duv_interpolate(u, v, imlow, imhigh, dlow, dhigh);
CCT::new(t, d)
}
}
impl TryFrom<CCT> for XYZ {
type Error = Error;
fn try_from(cct: CCT) -> Result<Self, Self::Error> {
let CCT(t, d) = cct;
let [u0, v0, m] = iso_temp_line(t);
let du = m.signum() * d / (m * m + 1.0).sqrt();
let dv = m * du;
XYZ::from_luv60(u0 + du, v0 + dv, None, None)
}
}
pub fn iso_temp_line(t: f64) -> [f64; 3] {
let xyz = Cie1931.xyz_planckian_locus(t);
let [x, y, z] = xyz.to_array();
let [u, v] = xyz.uv60();
let [dx, dy, dz] = Cie1931.xyz_planckian_locus_slope(t).to_array();
let sigma = x + 15.0 * y + 3.0 * z;
let dsigma = dx + 15.0 * dy + 3.0 * dz;
let den = 6.0 * y * dsigma - 6.0 * dy * sigma; let m = if ulps_eq!(den, 0.0) {
f64::MAX
} else {
(4.0 * dx * sigma - 4.0 * x * dsigma) / den
};
[u, v, m]
}
pub fn robertson_table(im: usize) -> &'static [f64; 3] {
static ROBERTSON_TABLE: [OnceLock<[f64; 3]>; N_STEPS] = [const { OnceLock::new() }; N_STEPS];
ROBERTSON_TABLE[im].get_or_init(|| {
let cct = im2t(im);
iso_temp_line(cct)
})
}
fn im2t(im: usize) -> f64 {
1E6 / (1.0 + (((MIRED_MAX - 1) * im) as f64) / ((N_STEPS - 1) as f64))
}
#[test]
fn im2t_test() {
let i0 = im2t(0);
approx::assert_ulps_eq!(i0, 1E6);
let ins = im2t(N_STEPS - 1);
approx::assert_ulps_eq!(ins, 1000.0);
}
fn robertson_interpolate(tp: f64, dp: f64, tn: f64, dn: f64) -> f64 {
(tp.recip() + dp / (dp - dn) * (tn.recip() - tp.recip())).recip()
}
fn duv_interpolate(u: f64, v: f64, imp: usize, imn: usize, dp: f64, dh: f64) -> f64 {
let [ul, vl, ml] = robertson_table(imp);
let [uh, vh, mh] = robertson_table(imn);
let ux = (ml * ul - vl - mh * uh + vh) / (ml - mh);
let vx = ml * (ux - ul) + vl;
let ddl = (ul - ux).hypot(vl - vx);
let ddh = (uh - ux).hypot(vh - vx);
let dd = (u - ux).hypot(v - vx);
let d = ddl + dp / (dp - dh) * (ddh - ddl);
dd - d
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ends() {
use approx::assert_ulps_eq;
let cct0 = CCT::new(1000.0, 0.0).unwrap();
let xyz: XYZ = cct0.try_into().unwrap();
let cct: CCT = xyz.try_into().unwrap();
assert_ulps_eq!(cct, cct0);
let cct0 = CCT::new(1E6, 0.0).unwrap();
let xyz: XYZ = cct0.try_into().unwrap();
let cct: CCT = xyz.try_into().unwrap();
assert_ulps_eq!(cct, cct0);
let cct0 = CCT::new(1005.0, 0.0).unwrap();
let xyz: XYZ = cct0.try_into().unwrap();
let cct: CCT = xyz.try_into().unwrap();
assert_ulps_eq!(cct, cct0, epsilon = 1E-5);
let cct0 = CCT::new(1E6 - 100.0, 0.0).unwrap();
let xyz: XYZ = cct0.try_into().unwrap();
let cct: CCT = xyz.try_into().unwrap();
approx::assert_abs_diff_eq!(cct, cct0, epsilon = 0.2);
}
#[test]
fn test_cct() {
let xyz: XYZ = CCT(999.0, 0.0).try_into().unwrap();
let cct: Result<CCT, _> = xyz.try_into();
assert_eq!(cct, Err(Error::CCTTemperatureTooLow));
let xyz: XYZ = CCT(1E6 + 1.0, 0.0).try_into().unwrap();
let cct: Result<CCT, _> = xyz.try_into();
assert_eq!(cct, Err(Error::CCTTemperatureTooHigh));
let xyz: XYZ = CCT(6000.0, 0.051).try_into().unwrap();
let cct: Result<CCT, _> = xyz.try_into();
assert_eq!(cct, Err(Error::CCTDuvHighError));
let xyz: XYZ = CCT(6000.0, -0.051).try_into().unwrap();
let cct: Result<CCT, _> = xyz.try_into();
assert_eq!(cct, Err(Error::CCTDuvLowError));
}
fn compute_d_uv(mired: f64, d: f64) -> Option<f64> {
let t = 1E6 / mired;
let cct0 = CCT::new(t, d).unwrap();
let xyz0 = XYZ::try_from(cct0).ok()?;
let cct: CCT = CCT::try_from(xyz0).unwrap();
let xyz = XYZ::try_from(cct).unwrap();
let d_uv = xyz.uv_prime_distance(&xyz0);
Some(d_uv)
}
#[test]
#[ignore]
fn test_cct_exhaustive() {
const MIRED_START: f64 = 1.0;
const MIRED_END: f64 = 1000.0;
const MIRED_STEP: f64 = 0.025;
const D_START: f64 = -0.0499;
const D_END: f64 = 0.0499;
const D_STEP: f64 = 0.001;
let mut mired = MIRED_START;
while mired < MIRED_END {
let mut d = D_START;
while d <= D_END {
if let Some(d_uv) = compute_d_uv(mired, d) {
approx::assert_ulps_eq!(d_uv, 0.0, epsilon = 5.8E-5);
}
d += D_STEP;
}
mired += MIRED_STEP;
}
}
#[test]
fn test_cct_at_max_error() {
let mired = 999.757;
let d = 0.0005;
let d_uv = compute_d_uv(mired, d).unwrap();
approx::assert_ulps_eq!(d_uv, 0.0, epsilon = 5.8E-5);
}
#[test]
#[cfg(feature = "cie-illuminants")]
fn f1_test() {
let xyz_f1 =
Cie1931.xyz_cie_table(&crate::illuminant::cie_illuminant::CieIlluminant::F1, None);
approx::assert_ulps_eq!(xyz_f1.cct().unwrap().t(), 6430.0, epsilon = 0.5);
}
#[test]
#[cfg(feature = "cie-illuminants")]
fn f3_1_test() {
let xyz_f3_1 = Cie1931.xyz_cie_table(
&crate::illuminant::cie_illuminant::CieIlluminant::F3_1,
None,
);
approx::assert_ulps_eq!(xyz_f3_1.cct().unwrap().t(), 2932.0, epsilon = 0.5);
}
}