use libm::sqrtf;
use super::{LABS_A, LABS_B, LABS_C, LABS_L};
#[allow(dead_code)]
#[inline]
pub fn delta_e_94_sq(lab_ref: [f32; 3], lab_sample: [f32; 3]) -> f32 {
let [l1, a1, b1] = lab_ref;
let [l2, a2, b2] = lab_sample;
let dl = l1 - l2;
let da = a1 - a2;
let db = b1 - b2;
let c1_sq = a1 * a1 + b1 * b1;
let c1 = sqrtf(c1_sq);
let c2 = sqrtf(a2 * a2 + b2 * b2);
let dc = c1 - c2;
let dh_sq = (da * da + db * db - dc * dc).max(0.0);
let sl = 1.0;
let sc = 1.0 + 0.045 * c1;
let sh = 1.0 + 0.015 * c1;
let dl_term = dl / sl;
let dc_term = dc / sc;
let dh_term_sq = dh_sq / (sh * sh);
dl_term * dl_term + dc_term * dc_term + dh_term_sq
}
pub fn nearest_idx(query: [f32; 3]) -> usize {
let n = LABS_L.len();
let q_a = query[1];
let q_b = query[2];
let c2 = sqrtf(q_a * q_a + q_b * q_b);
let mut best_idx = 0usize;
let mut best_d2 = f32::INFINITY;
for i in 0..n {
let l1 = LABS_L[i];
let a1 = LABS_A[i];
let b1 = LABS_B[i];
let c1 = LABS_C[i]; let dl = l1 - query[0];
let da = a1 - q_a;
let db = b1 - q_b;
let dc = c1 - c2;
let dh_sq = (da * da + db * db - dc * dc).max(0.0);
let sc = 1.0 + 0.045 * c1;
let sh = 1.0 + 0.015 * c1;
let dl_term = dl;
let dc_term = dc / sc;
let dh_term_sq = dh_sq / (sh * sh);
let d2 = dl_term * dl_term + dc_term * dc_term + dh_term_sq;
if d2 < best_d2 {
best_d2 = d2;
best_idx = i;
}
}
best_idx
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn self_distance_is_zero() {
for lab in [[50.0, 0.0, 0.0], [80.0, 20.0, -30.0], [0.0, 0.0, 0.0]] {
let d2 = delta_e_94_sq(lab, lab);
assert_eq!(d2, 0.0, "self-distance non-zero for {lab:?}: {d2}");
}
}
#[test]
fn black_vs_black_handled() {
let d2 = delta_e_94_sq([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
assert_eq!(d2, 0.0);
assert!(d2.is_finite());
}
#[test]
fn asymmetric_in_arguments() {
let high_chroma = [50.0, 60.0, 40.0];
let low_chroma = [50.0, 5.0, 5.0];
let de_ab = delta_e_94_sq(high_chroma, low_chroma);
let de_ba = delta_e_94_sq(low_chroma, high_chroma);
assert!(
(de_ab - de_ba).abs() > 1.0,
"expected meaningful asymmetry, got ΔE²(A→B)={de_ab}, ΔE²(B→A)={de_ba}"
);
}
#[test]
#[cfg_attr(
miri,
ignore = "125-query grid × 949-entry palette too slow under miri"
)]
fn lookup_returns_valid_index_across_grid() {
for r in (0..=255u32).step_by(64) {
for g in (0..=255u32).step_by(64) {
for b in (0..=255u32).step_by(64) {
let q = crate::rgb_to_lab([r as u8, g as u8, b as u8]);
let idx = nearest_idx(q);
assert!(idx < LABS_L.len());
let entry = [LABS_L[idx], LABS_A[idx], LABS_B[idx]];
let d2 = delta_e_94_sq(entry, q);
assert!(
d2.is_finite() && d2 >= 0.0,
"non-finite/negative ΔE94² at rgb=[{r},{g},{b}]: d2={d2}"
);
}
}
}
}
}