use indicatrix::optics::{dispersion::DispersionModel, materials::GemMaterial};
use indicatrix_cut_core::material::{BuiltinMaterials, MaterialLookup, MaterialSelection};
pub(super) struct EditorMaterialLookup<'a> {
custom: &'a [GemMaterial],
}
impl<'a> EditorMaterialLookup<'a> {
pub(super) const fn new(custom: &'a [GemMaterial]) -> Self {
Self { custom }
}
}
impl MaterialLookup for EditorMaterialLookup<'_> {
fn lookup(&self, name: &str) -> Option<GemMaterial> {
self.custom
.iter()
.find(|m| m.name.eq_ignore_ascii_case(name))
.cloned()
.or_else(|| BuiltinMaterials.lookup(name))
}
}
#[must_use]
pub(super) fn resolved_gem_material(
selection: &MaterialSelection,
lookup: &dyn MaterialLookup,
) -> GemMaterial {
let resolved = selection.resolve(lookup);
if selection.refractive_index_override.is_none() {
return resolved.gem;
}
let mut gem = resolved.gem;
gem.dispersion = DispersionModel::Cauchy {
a: resolved.n_d as f32,
b: 0.0,
c: 0.0,
};
gem
}
#[must_use]
pub(super) fn nearest_built_in_material(n_d: f64, tolerance: f64) -> Option<(&'static str, f64)> {
super::state::MATERIAL_PRESET_NAMES
.iter()
.skip(1) .filter_map(|&name| {
indicatrix_cut_core::built_in_refractive_index(name).map(|ri| (name, ri))
})
.map(|(name, ri)| (name, ri, (ri - n_d).abs()))
.filter(|&(_, _, diff)| diff <= tolerance)
.min_by(|a, b| a.2.total_cmp(&b.2))
.map(|(name, ri, _)| (name, ri))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lookup_prefers_a_custom_material_over_a_same_named_built_in() {
let mut custom = GemMaterial::diamond();
custom.name = "Diamond".to_string();
custom.dispersion = DispersionModel::Cauchy {
a: 9.0,
b: 0.0,
c: 0.0,
};
let custom_list = [custom];
let lookup = EditorMaterialLookup::new(&custom_list);
let found = lookup.lookup("Diamond").unwrap();
assert!((f64::from(found.dispersion.evaluate(589.3)) - 9.0).abs() < 1e-6);
}
#[test]
fn lookup_falls_back_to_a_built_in_when_no_custom_material_matches() {
let custom_list: [GemMaterial; 0] = [];
let lookup = EditorMaterialLookup::new(&custom_list);
assert!(lookup.lookup("Quartz").is_some());
assert!(lookup.lookup("Not A Real Material").is_none());
}
#[test]
fn resolved_gem_material_uses_the_real_dispersion_curve_with_no_override() {
let selection = MaterialSelection {
name: Some("Quartz".to_string()),
specific_gravity_override: None,
refractive_index_override: None,
};
let custom_list: [GemMaterial; 0] = [];
let lookup = EditorMaterialLookup::new(&custom_list);
let gem = resolved_gem_material(&selection, &lookup);
let expected = GemMaterial::by_name("Quartz").unwrap();
assert_eq!(gem.dispersion, expected.dispersion);
}
#[test]
fn resolved_gem_material_flattens_dispersion_to_the_override_value() {
let selection = MaterialSelection {
name: Some("Diamond".to_string()),
specific_gravity_override: None,
refractive_index_override: Some(1.70),
};
let custom_list: [GemMaterial; 0] = [];
let lookup = EditorMaterialLookup::new(&custom_list);
let gem = resolved_gem_material(&selection, &lookup);
for lambda in [450.0f32, 589.3, 650.0] {
assert!((f64::from(gem.dispersion.evaluate(lambda)) - 1.70).abs() < 1e-4);
}
let base = GemMaterial::diamond();
assert_eq!(gem.crystal_system, base.crystal_system);
assert_eq!(gem.absorption, base.absorption);
}
#[test]
fn nearest_built_in_material_finds_quartz_for_its_own_ri() {
let quartz_n_d = indicatrix_cut_core::built_in_refractive_index("Quartz").unwrap();
let (name, ri) = nearest_built_in_material(quartz_n_d, 0.01).unwrap();
assert_eq!(name, "Quartz");
assert!((ri - quartz_n_d).abs() < 1e-9);
}
#[test]
fn nearest_built_in_material_returns_none_outside_tolerance() {
assert_eq!(nearest_built_in_material(1.9999, 0.001), None);
}
#[test]
fn nearest_built_in_material_picks_the_closest_when_two_are_within_tolerance() {
let diamond_n_d = indicatrix_cut_core::built_in_refractive_index("Diamond").unwrap();
let (name, _) = nearest_built_in_material(diamond_n_d - 0.02, 0.5).unwrap();
assert_eq!(name, "Diamond");
}
}