use indicatrix::optics::materials::{CrystalSystem, GemMaterial, OpticalCharacter};
use indicatrix_vault::{
db::sqlite::{CustomMaterialParams, Database},
model::material::CustomMaterialRow,
};
const CRYSTAL_SYSTEMS: [CrystalSystem; 7] = [
CrystalSystem::Cubic,
CrystalSystem::Tetragonal,
CrystalSystem::Hexagonal,
CrystalSystem::Trigonal,
CrystalSystem::Orthorhombic,
CrystalSystem::Monoclinic,
CrystalSystem::Triclinic,
];
const OPTICAL_CHARACTERS: [OpticalCharacter; 5] = [
OpticalCharacter::Isotropic,
OpticalCharacter::UniaxialPositive,
OpticalCharacter::UniaxialNegative,
OpticalCharacter::BiaxialPositive,
OpticalCharacter::BiaxialNegative,
];
#[must_use]
pub const fn crystal_system_to_str(cs: CrystalSystem) -> &'static str {
match cs {
CrystalSystem::Cubic => "Cubic",
CrystalSystem::Tetragonal => "Tetragonal",
CrystalSystem::Hexagonal => "Hexagonal",
CrystalSystem::Trigonal => "Trigonal",
CrystalSystem::Orthorhombic => "Orthorhombic",
CrystalSystem::Monoclinic => "Monoclinic",
CrystalSystem::Triclinic => "Triclinic",
}
}
#[must_use]
pub fn crystal_system_from_str(s: &str) -> Option<CrystalSystem> {
CRYSTAL_SYSTEMS
.iter()
.copied()
.find(|cs| crystal_system_to_str(*cs) == s)
}
#[must_use]
pub const fn optical_character_to_str(oc: OpticalCharacter) -> &'static str {
match oc {
OpticalCharacter::Isotropic => "Isotropic",
OpticalCharacter::UniaxialPositive => "UniaxialPositive",
OpticalCharacter::UniaxialNegative => "UniaxialNegative",
OpticalCharacter::BiaxialPositive => "BiaxialPositive",
OpticalCharacter::BiaxialNegative => "BiaxialNegative",
}
}
#[must_use]
pub fn optical_character_from_str(s: &str) -> Option<OpticalCharacter> {
OPTICAL_CHARACTERS
.iter()
.copied()
.find(|oc| optical_character_to_str(*oc) == s)
}
#[must_use]
pub fn crystal_system_from_index(idx: i32) -> Option<CrystalSystem> {
usize::try_from(idx)
.ok()
.and_then(|idx| CRYSTAL_SYSTEMS.get(idx).copied())
}
#[must_use]
pub fn optical_character_from_index(idx: i32) -> Option<OpticalCharacter> {
usize::try_from(idx)
.ok()
.and_then(|idx| OPTICAL_CHARACTERS.get(idx).copied())
}
#[must_use]
pub const fn is_biaxial(oc: OpticalCharacter) -> bool {
matches!(
oc,
OpticalCharacter::BiaxialPositive | OpticalCharacter::BiaxialNegative
)
}
#[must_use]
pub fn gem_material_from_row(row: &CustomMaterialRow) -> GemMaterial {
let mut material = GemMaterial::new_custom(
&row.name,
row.refractive_index,
row.dispersion,
row.birefringence,
row.absorption_rgb,
);
if let Some(cs) = row
.crystal_system
.as_deref()
.and_then(crystal_system_from_str)
{
material.crystal_system = cs;
}
if let Some(oc) = row
.optical_character
.as_deref()
.and_then(optical_character_from_str)
{
material.optical_character = oc;
}
if let Some(delta) = row.biaxial_delta_beta_alpha {
material.biaxial_delta_beta_alpha = Some(delta);
}
material
}
pub fn save_gem_material(
db: &Database,
material: &GemMaterial,
ri: f32,
dispersion: f32,
birefringence: f32,
absorption_rgb: [f32; 3],
specific_gravity: Option<f32>,
) -> anyhow::Result<()> {
db.save_custom_material(&CustomMaterialParams {
name: &material.name,
refractive_index: ri,
dispersion,
birefringence,
absorption_rgb,
crystal_system: Some(crystal_system_to_str(material.crystal_system)),
optical_character: Some(optical_character_to_str(material.optical_character)),
biaxial_delta_beta_alpha: material.biaxial_delta_beta_alpha,
per_axis_dispersion_json: None,
specific_gravity,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crystal_system_str_round_trips_all_seven_variants() {
for cs in CRYSTAL_SYSTEMS {
let s = crystal_system_to_str(cs);
assert_eq!(crystal_system_from_str(s), Some(cs), "variant={cs:?}");
}
}
#[test]
fn optical_character_str_round_trips_all_five_variants() {
for oc in OPTICAL_CHARACTERS {
let s = optical_character_to_str(oc);
assert_eq!(optical_character_from_str(s), Some(oc), "variant={oc:?}");
}
}
#[test]
fn crystal_system_index_matches_the_declared_enum_order() {
for (idx, cs) in CRYSTAL_SYSTEMS.iter().enumerate() {
assert_eq!(
crystal_system_from_index(i32::try_from(idx).unwrap()),
Some(*cs),
"idx={idx}"
);
}
}
#[test]
fn optical_character_index_matches_the_declared_enum_order() {
for (idx, oc) in OPTICAL_CHARACTERS.iter().enumerate() {
assert_eq!(
optical_character_from_index(i32::try_from(idx).unwrap()),
Some(*oc),
"idx={idx}"
);
}
}
#[test]
fn unrecognized_strings_and_out_of_range_indices_return_none() {
assert_eq!(crystal_system_from_str("Amorphous"), None);
assert_eq!(optical_character_from_str(""), None);
assert_eq!(crystal_system_from_index(-1), None);
assert_eq!(crystal_system_from_index(99), None);
assert_eq!(optical_character_from_index(-1), None);
assert_eq!(optical_character_from_index(99), None);
}
#[test]
fn is_biaxial_is_true_only_for_the_two_biaxial_variants() {
assert!(is_biaxial(OpticalCharacter::BiaxialPositive));
assert!(is_biaxial(OpticalCharacter::BiaxialNegative));
assert!(!is_biaxial(OpticalCharacter::Isotropic));
assert!(!is_biaxial(OpticalCharacter::UniaxialPositive));
assert!(!is_biaxial(OpticalCharacter::UniaxialNegative));
}
#[test]
fn row_with_no_stored_crystal_optics_falls_back_to_new_custom_inference() {
let row = CustomMaterialRow {
name: "Legacy Custom Sapphire".to_string(),
refractive_index: 1.768,
dispersion: 0.018,
birefringence: -0.008,
absorption_rgb: [2.8, 1.2, 0.1],
crystal_system: None,
optical_character: None,
biaxial_delta_beta_alpha: None,
per_axis_dispersion_json: None,
specific_gravity: None,
};
let from_row = gem_material_from_row(&row);
let inferred = GemMaterial::new_custom(
&row.name,
row.refractive_index,
row.dispersion,
row.birefringence,
row.absorption_rgb,
);
assert_eq!(from_row.crystal_system, inferred.crystal_system);
assert_eq!(from_row.optical_character, inferred.optical_character);
assert_eq!(
from_row.biaxial_delta_beta_alpha,
inferred.biaxial_delta_beta_alpha
);
assert_eq!(from_row.crystal_system, CrystalSystem::Trigonal);
assert_eq!(
from_row.optical_character,
OpticalCharacter::UniaxialNegative
);
}
#[test]
fn row_with_stored_crystal_optics_overrides_new_custom_inference() {
let row = CustomMaterialRow {
name: "Custom Tanzanite".to_string(),
refractive_index: 1.691,
dispersion: 0.030,
birefringence: 0.0130,
absorption_rgb: [1.8, 1.6, 0.2],
crystal_system: Some("Orthorhombic".to_string()),
optical_character: Some("BiaxialPositive".to_string()),
biaxial_delta_beta_alpha: Some(0.0070),
per_axis_dispersion_json: None,
specific_gravity: None,
};
let material = gem_material_from_row(&row);
assert_eq!(material.crystal_system, CrystalSystem::Orthorhombic);
assert_eq!(
material.optical_character,
OpticalCharacter::BiaxialPositive
);
assert_eq!(material.biaxial_delta_beta_alpha, Some(0.0070));
assert!(material.gpu_supported());
}
}