use indicatrix::{
color::metrics::{GemOpticalMetrics, evaluate_angular_profile, evaluate_gem_optical_metrics},
geometry::plane::GpuFacetPlane,
optics::materials::GemMaterial,
};
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
pub fn hash_planes(planes: &[GpuFacetPlane]) -> u64 {
let bytes: &[u8] = bytemuck::cast_slice(planes);
let mut hasher = DefaultHasher::new();
planes.len().hash(&mut hasher);
bytes.hash(&mut hasher);
hasher.finish()
}
#[derive(Clone, PartialEq, Debug)]
pub(super) struct MetricsCacheKey {
yaw: f32,
pitch: f32,
light_yaw: f32,
light_pitch: f32,
material: GemMaterial,
planes_hash: u64,
}
pub(super) struct MetricsCache {
key: MetricsCacheKey,
metrics: GemOpticalMetrics,
graph_brilliance: [f32; 19],
graph_extinction: [f32; 19],
graph_windowing: [f32; 19],
}
pub(super) fn compute_or_reuse_metrics(
metrics_cache: &mut Option<MetricsCache>,
active_planes: &[GpuFacetPlane],
current_mat: &GemMaterial,
yaw: f32,
pitch: f32,
light_yaw: f32,
light_pitch: f32,
) -> (GemOpticalMetrics, [f32; 19], [f32; 19], [f32; 19]) {
let cache_key = MetricsCacheKey {
yaw,
pitch,
light_yaw,
light_pitch,
material: current_mat.clone(),
planes_hash: hash_planes(active_planes),
};
match metrics_cache {
Some(cache) if cache.key == cache_key => (
cache.metrics,
cache.graph_brilliance,
cache.graph_extinction,
cache.graph_windowing,
),
_ => {
let metrics = evaluate_gem_optical_metrics(
active_planes,
current_mat,
yaw,
pitch,
light_yaw,
light_pitch,
);
let (graph_brilliance, graph_extinction, graph_windowing) =
evaluate_angular_profile(active_planes, current_mat, light_yaw, light_pitch);
*metrics_cache = Some(MetricsCache {
key: cache_key,
metrics,
graph_brilliance,
graph_extinction,
graph_windowing,
});
(metrics, graph_brilliance, graph_extinction, graph_windowing)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix::geometry::cuts::StandardGemCuts;
fn key_for(material: GemMaterial, planes: &[GpuFacetPlane]) -> MetricsCacheKey {
MetricsCacheKey {
yaw: 0.60,
pitch: 0.45,
light_yaw: 0.85,
light_pitch: 0.95,
material,
planes_hash: hash_planes(planes),
}
}
#[test]
fn metrics_cache_key_distinguishes_same_named_material_with_different_optics() {
let planes = StandardGemCuts::standard_round_brilliant();
let low = GemMaterial::new_custom("MyGem", 1.50, 0.010, 0.0, [0.0, 0.0, 0.0]);
let high = GemMaterial::new_custom("MyGem", 2.42, 0.044, 0.0, [0.0, 0.0, 0.0]);
assert_eq!(
low.name, high.name,
"test premise: the names must be identical"
);
assert_ne!(
key_for(low, &planes),
key_for(high, &planes),
"editing a custom material's refractive index under the same name must invalidate the metrics cache"
);
}
#[test]
fn metrics_cache_key_is_stable_for_identical_inputs() {
let planes = StandardGemCuts::standard_round_brilliant();
let a = key_for(GemMaterial::diamond(), &planes);
let b = key_for(GemMaterial::diamond(), &planes);
assert_eq!(a, b, "identical inputs must produce an identical cache key");
}
#[test]
fn metrics_cache_key_changes_with_the_facet_geometry() {
let srb = StandardGemCuts::standard_round_brilliant();
let emerald = StandardGemCuts::emerald_cut();
assert_ne!(
key_for(GemMaterial::diamond(), &srb),
key_for(GemMaterial::diamond(), &emerald),
"a different cutting schedule must invalidate the metrics cache"
);
}
}