use crate::constants::*;
use crate::error::Result;
use super::coordinate_transforms;
use super::hue_conversions;
use super::types::{MunsellSpecification, CieXyY};
use super::MathematicalMunsellConverter;
const THRESHOLD_INTEGER: f64 = 1e-3;
const MAX_OUTER_ITERATIONS: usize = 64;
const MAX_INNER_ITERATIONS: usize = 16;
impl MathematicalMunsellConverter {
pub fn xyy_to_munsell_specification(&self, xyy: CieXyY) -> Result<MunsellSpecification> {
let convergence_threshold = THRESHOLD_INTEGER / 1e4;
let mut value = self.luminance_to_munsell_value(xyy.y_luminance)?;
if (value - value.round()).abs() < 1e-10 {
value = value.round();
}
if value < MINIMUM_RENOTATION_VALUE {
return Ok(MunsellSpecification {
hue: 0.0, family: "N".to_string(), value, chroma: 0.0,
});
}
let dx_d65 = xyy.x - ILLUMINANT_D65_CHROMATICITY[0];
let dy_d65 = xyy.y - ILLUMINANT_D65_CHROMATICITY[1];
let rho_d65 = (dx_d65 * dx_d65 + dy_d65 * dy_d65).sqrt();
if rho_d65 < THRESHOLD_INTEGER {
return Ok(MunsellSpecification {
hue: 0.0, family: "N".to_string(), value, chroma: 0.0,
});
}
let (x_center, y_center) = self.achromatic_center(value)?;
let (rho_input, phi_input_rad, _) = coordinate_transforms::cartesian_to_cylindrical(
xyy.x - x_center, xyy.y - y_center, xyy.y_luminance,
);
let phi_input = phi_input_rad.to_degrees();
if rho_input < THRESHOLD_INTEGER || xyy.y_luminance < 1e-6 {
return Ok(MunsellSpecification {
hue: 0.0, family: "N".to_string(), value, chroma: 0.0,
});
}
let initial_spec = self.generate_initial_guess(xyy)?;
let mut hue_current = initial_spec.0;
let mut code_current = initial_spec.1;
let mut chroma_current = initial_spec.2;
for _outer_iteration in 0..MAX_OUTER_ITERATIONS {
let chroma_maximum = self.maximum_chroma_from_renotation(hue_current, value, code_current)?;
if chroma_current > chroma_maximum {
chroma_current = chroma_maximum;
}
let (hue_new, code_new) = self.run_hue_angle_inner_loop(
hue_current, code_current, value, chroma_current,
x_center, y_center, xyy.y_luminance, phi_input,
)?;
hue_current = hue_new;
code_current = code_new;
let chroma_maximum = self.maximum_chroma_from_renotation(hue_current, value, code_current)?;
if chroma_current > chroma_maximum {
chroma_current = chroma_maximum;
}
chroma_current = self.run_chroma_inner_loop(
hue_current, code_current, value, chroma_current,
x_center, y_center, xyy.y_luminance, rho_input,
)?;
let (x_final, y_final) = self.munsell_specification_to_xy(hue_current, value, chroma_current, code_current)?;
let difference = ((xyy.x - x_final).powi(2) + (xyy.y - y_final).powi(2)).sqrt();
if difference < convergence_threshold {
return self.build_converged_result(hue_current, code_current, value, chroma_current);
}
}
self.build_converged_result(hue_current, code_current, value, chroma_current)
}
fn achromatic_center(&self, value: f64) -> Result<(f64, f64)> {
let achromatic_spec = MunsellSpecification {
hue: 0.0, family: "N".to_string(), value, chroma: 0.0,
};
let achromatic_xyy = self.munsell_specification_to_xyy(&achromatic_spec)?;
Ok((achromatic_xyy.x, achromatic_xyy.y))
}
fn run_hue_angle_inner_loop(
&self,
hue_current: f64, code_current: u8,
value: f64, chroma_current: f64,
x_center: f64, y_center: f64,
y_luminance: f64, phi_input: f64,
) -> Result<(f64, u8)> {
let (x_current, y_current) = self.munsell_specification_to_xy(hue_current, value, chroma_current, code_current)?;
let phi_current_degrees = Self::compute_phi_degrees(x_current - x_center, y_current - y_center, y_luminance);
let phi_current_difference = Self::wrapped_phi_difference(phi_input, phi_current_degrees);
let mut phi_differences_data = if phi_current_difference.abs() < 1e-6 { vec![] } else { vec![phi_current_difference] };
let mut hue_angles_differences_data = if phi_current_difference.abs() < 1e-6 { vec![] } else { vec![0.0] };
let hue_angle_current = hue_conversions::hue_to_astm_hue(hue_current, code_current);
let mut extrapolate = false;
let mut iterations_inner = 0;
while (phi_differences_data.iter().all(|&d| d >= 0.0) ||
phi_differences_data.iter().all(|&d| d <= 0.0)) && !extrapolate {
iterations_inner += 1;
if iterations_inner > MAX_INNER_ITERATIONS { break; }
let (hue_angle_inner, hue_angle_diff) =
Self::compute_hue_step(hue_angle_current, phi_input, phi_current_degrees, iterations_inner);
let (hue_inner, code_inner) = hue_conversions::hue_angle_to_hue(hue_angle_inner);
let (x_inner, y_inner) = self.munsell_specification_to_xy(hue_inner, value, chroma_current, code_inner)?;
if phi_differences_data.len() >= 2 { extrapolate = true; }
if !extrapolate {
let phi_inner_degrees = Self::compute_phi_degrees(x_inner - x_center, y_inner - y_center, y_luminance);
let phi_inner_difference = Self::wrapped_phi_difference(phi_input, phi_inner_degrees);
phi_differences_data.push(phi_inner_difference);
hue_angles_differences_data.push(hue_angle_diff);
}
}
let hue_angle_difference_new = self.interpolate_hue_angle_difference(
&phi_differences_data, &hue_angles_differences_data,
)?;
let mut hue_angle_new = (hue_angle_current + hue_angle_difference_new) % 360.0;
if hue_angle_new < 0.0 { hue_angle_new += 360.0; }
Ok(hue_conversions::hue_angle_to_hue(hue_angle_new))
}
fn compute_phi_degrees(dx: f64, dy: f64, y_luminance: f64) -> f64 {
let (_, phi, _) = coordinate_transforms::cartesian_to_cylindrical(dx, dy, y_luminance);
let deg = phi.to_degrees();
if deg < 0.0 { deg + 360.0 } else { deg }
}
fn wrapped_phi_difference(phi_input: f64, phi_degrees: f64) -> f64 {
let d = (360.0 - phi_input + phi_degrees) % 360.0;
if d > 180.0 { d - 360.0 } else { d }
}
fn compute_hue_step(
hue_angle_current: f64, phi_input: f64, phi_current_degrees: f64, iteration: usize,
) -> (f64, f64) {
let step = iteration as f64 * (phi_input - phi_current_degrees);
let hue_angle_inner = if (hue_angle_current + step) < 0.0 {
((hue_angle_current + step) % 360.0 + 360.0) % 360.0
} else {
(hue_angle_current + step) % 360.0
};
let step_mod = if step < 0.0 {
((step % 360.0) + 360.0) % 360.0
} else {
step % 360.0
};
let hue_angle_diff = if step_mod > 180.0 { step_mod - 360.0 } else { step_mod };
(hue_angle_inner, hue_angle_diff)
}
fn interpolate_hue_angle_difference(
&self, phi_diffs: &[f64], hue_angle_diffs: &[f64],
) -> Result<f64> {
if phi_diffs.len() >= 2 {
let mut paired: Vec<_> = phi_diffs.iter()
.zip(hue_angle_diffs.iter())
.map(|(&p, &h)| (p, h))
.collect();
paired.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let sorted_phi: Vec<f64> = paired.iter().map(|&(p, _)| p).collect();
let sorted_hue: Vec<f64> = paired.iter().map(|&(_, h)| h).collect();
let result = self.linear_interpolate(&sorted_phi, &sorted_hue, 0.0)?;
Ok(if result < 0.0 { result % 360.0 + 360.0 } else { result % 360.0 })
} else {
Ok(0.0)
}
}
fn run_chroma_inner_loop(
&self,
hue_current: f64, code_current: u8,
value: f64, chroma_current: f64,
x_center: f64, y_center: f64,
y_luminance: f64, rho_input: f64,
) -> Result<f64> {
let (x_current_new, y_current_new) = self.munsell_specification_to_xy(hue_current, value, chroma_current, code_current)?;
let (rho_current_new, _, _) = coordinate_transforms::cartesian_to_cylindrical(
x_current_new - x_center, y_current_new - y_center, y_luminance,
);
let mut rho_bounds_data = vec![rho_current_new];
let mut chroma_bounds_data = vec![chroma_current];
let mut iterations_inner = 0;
for _ in 0..MAX_INNER_ITERATIONS {
let rho_min = *rho_bounds_data.iter().min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
let rho_max = *rho_bounds_data.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
if rho_min <= rho_input && rho_input <= rho_max {
break;
}
iterations_inner += 1;
if iterations_inner > MAX_INNER_ITERATIONS { break; }
let chroma_inner = if rho_current_new.abs() > 1e-10 {
let ratio = rho_input / rho_current_new;
let power = iterations_inner as f64;
ratio.powf(power) * chroma_current
} else {
chroma_current
};
let chroma_max = self.maximum_chroma_from_renotation(hue_current, value, code_current)?;
let chroma_bounded = chroma_inner.min(chroma_max).max(0.0);
let (x_inner, y_inner) = self.munsell_specification_to_xy(hue_current, value, chroma_bounded, code_current)?;
let (rho_inner, _, _) = coordinate_transforms::cartesian_to_cylindrical(
x_inner - x_center, y_inner - y_center, y_luminance,
);
rho_bounds_data.push(rho_inner);
chroma_bounds_data.push(chroma_bounded);
}
if rho_bounds_data.len() >= 2 {
let mut paired: Vec<_> = rho_bounds_data.iter()
.zip(chroma_bounds_data.iter())
.map(|(&r, &c)| (r, c))
.collect();
paired.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let sorted_rho: Vec<f64> = paired.iter().map(|&(r, _)| r).collect();
let sorted_chroma: Vec<f64> = paired.iter().map(|&(_, c)| c).collect();
let interpolated = self.linear_interpolate(&sorted_rho, &sorted_chroma, rho_input)?;
Ok(interpolated.max(0.0))
} else {
Ok(chroma_current)
}
}
fn build_converged_result(
&self, hue_current: f64, code_current: u8, value: f64, chroma_current: f64,
) -> Result<MunsellSpecification> {
if chroma_current < 1e-10 {
return Ok(MunsellSpecification {
hue: 0.0, family: "N".to_string(), value, chroma: 0.0,
});
}
let (normalized_hue, normalized_code) = Self::normalize_munsell_specification(hue_current, code_current);
let family = hue_conversions::code_to_family(normalized_code);
Ok(MunsellSpecification {
hue: normalized_hue,
family: family.to_string(),
value,
chroma: chroma_current,
})
}
pub(super) fn normalize_munsell_specification(hue: f64, code: u8) -> (f64, u8) {
if hue.abs() < 0.01 && hue < 5.0 {
let new_code = match code {
1 => 2, 2 => 3, 3 => 4, 4 => 5, 5 => 6, 6 => 7, 7 => 8, 8 => 9, 9 => 10, 10 => 1, _ => code
};
(10.0, new_code)
} else {
(hue, code)
}
}
pub(super) fn generate_initial_guess(&self, xyy: CieXyY) -> Result<(f64, u8, f64)> {
let dx = xyy.x - ILLUMINANT_C[0];
let dy = xyy.y - ILLUMINANT_C[1];
let (rho, phi_rad, _) = coordinate_transforms::cartesian_to_cylindrical(dx, dy, xyy.y_luminance);
let mut phi_deg = phi_rad.to_degrees();
if phi_deg < 0.0 {
phi_deg += 360.0;
}
let (hue_initial, code_initial) = hue_conversions::hue_angle_to_hue(phi_deg);
let chroma_initial = rho * 50.0;
Ok((hue_initial, code_initial, chroma_initial))
}
#[allow(dead_code)]
pub(super) fn lchab_to_munsell_specification(&self, _l: f64, _c: f64, hab: f64) -> (f64, u8) {
let code = if hab < 18.0 || hab >= 342.0 {
8 } else if hab < 54.0 {
7 } else if hab < 90.0 {
6 } else if hab < 126.0 {
5 } else if hab < 162.0 {
4 } else if hab < 198.0 {
3 } else if hab < 234.0 {
2 } else if hab < 270.0 {
1 } else if hab < 306.0 {
10 } else if hab < 342.0 {
9 } else {
8 };
let segment_start = ((hab / 36.0).floor() * 36.0) as f64;
let hab_in_segment = hab - segment_start;
let mut hue = (hab_in_segment / 36.0) * 10.0;
if hue == 0.0 && hab > 0.0 {
hue = 10.0;
}
(hue, code)
}
}