use super::{
finished::FinishedStone,
pose_batch::{AxisCurves, sweep_axes, sweep_axes_gated},
profile::{EVALUATIONS_PER_AXIS, PROFILE_AZIMUTHS_DEG},
};
use crate::{
geometry::{plane::GpuFacetPlane, tool::StoneGeometry},
optics::{materials::GemMaterial, raytracer::EnvironmentSource},
};
#[derive(Debug, Clone, PartialEq)]
pub struct AxisProfile {
pub brilliance: [f32; 181],
pub extinction: [f32; 181],
pub windowing: [f32; 181],
}
impl From<([f32; 181], [f32; 181], [f32; 181])> for AxisProfile {
fn from((brilliance, extinction, windowing): ([f32; 181], [f32; 181], [f32; 181])) -> Self {
Self {
brilliance,
extinction,
windowing,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SweepProgress {
pub axis: usize,
pub done: usize,
pub total: usize,
}
impl From<AxisCurves> for AxisProfile {
fn from([brilliance, extinction, windowing]: AxisCurves) -> Self {
Self {
brilliance,
extinction,
windowing,
}
}
}
#[must_use]
pub const fn total_evaluations() -> usize {
PROFILE_AZIMUTHS_DEG.len() * EVALUATIONS_PER_AXIS
}
#[must_use]
pub fn evaluate_all_axes_profiles(
planes: &[GpuFacetPlane],
material: &GemMaterial,
environment: EnvironmentSource<'_>,
) -> Vec<AxisProfile> {
evaluate_all_axes_profiles_geom(StoneGeometry::planes_only(planes), material, environment)
}
#[must_use]
pub fn evaluate_all_axes_profiles_geom(
geom: StoneGeometry<'_>,
material: &GemMaterial,
environment: EnvironmentSource<'_>,
) -> Vec<AxisProfile> {
evaluate_all_axes_profiles_geom_threaded(geom, material, environment, None)
}
#[must_use]
pub fn evaluate_all_axes_profiles_geom_threaded(
geom: StoneGeometry<'_>,
material: &GemMaterial,
environment: EnvironmentSource<'_>,
max_threads: Option<usize>,
) -> Vec<AxisProfile> {
evaluate_all_axes_profiles_finished_threaded(
FinishedStone::polished(geom),
material,
environment,
max_threads,
)
}
#[must_use]
pub fn evaluate_all_axes_profiles_finished_threaded(
stone: FinishedStone<'_>,
material: &GemMaterial,
environment: EnvironmentSource<'_>,
max_threads: Option<usize>,
) -> Vec<AxisProfile> {
sweep_axes(
stone,
material,
environment,
&PROFILE_AZIMUTHS_DEG,
max_threads,
)
.into_iter()
.map(AxisProfile::from)
.collect()
}
#[must_use]
pub fn evaluate_all_axes_profiles_stepped(
planes: &[GpuFacetPlane],
material: &GemMaterial,
environment: EnvironmentSource<'_>,
step: &mut dyn FnMut(SweepProgress) -> bool,
) -> Option<Vec<AxisProfile>> {
evaluate_all_axes_profiles_stepped_geom(
StoneGeometry::planes_only(planes),
material,
environment,
step,
)
}
#[must_use]
pub fn evaluate_all_axes_profiles_stepped_geom(
geom: StoneGeometry<'_>,
material: &GemMaterial,
environment: EnvironmentSource<'_>,
step: &mut dyn FnMut(SweepProgress) -> bool,
) -> Option<Vec<AxisProfile>> {
evaluate_all_axes_profiles_stepped_geom_threaded(geom, material, environment, None, step)
}
#[must_use]
pub fn evaluate_all_axes_profiles_stepped_geom_threaded(
geom: StoneGeometry<'_>,
material: &GemMaterial,
environment: EnvironmentSource<'_>,
max_threads: Option<usize>,
step: &mut dyn FnMut(SweepProgress) -> bool,
) -> Option<Vec<AxisProfile>> {
evaluate_all_axes_profiles_stepped_finished_threaded(
FinishedStone::polished(geom),
material,
environment,
max_threads,
step,
)
}
#[must_use]
pub fn evaluate_all_axes_profiles_stepped_finished_threaded(
stone: FinishedStone<'_>,
material: &GemMaterial,
environment: EnvironmentSource<'_>,
max_threads: Option<usize>,
step: &mut dyn FnMut(SweepProgress) -> bool,
) -> Option<Vec<AxisProfile>> {
let total = total_evaluations();
let mut done = 0usize;
let axes = sweep_axes_gated(
stone,
material,
environment,
&PROFILE_AZIMUTHS_DEG,
max_threads,
&mut |axis| {
let go = step(SweepProgress { axis, done, total });
done += 1;
go
},
)?;
Some(axes.into_iter().map(AxisProfile::from).collect())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
color::metrics::{
TILT_ANGLES_DEG, evaluate_full_axis_profile_at_azimuth,
evaluate_full_axis_profile_at_azimuth_stepped, evaluate_gem_optical_metrics,
},
geometry::cuts::StandardGemCuts,
optics::raytracer::LightingPreset,
};
const fn studio() -> EnvironmentSource<'static> {
LightingPreset::RingLights.studio(1.0, 0.85, 0.95)
}
fn bits(curves: &([f32; 181], [f32; 181], [f32; 181])) -> Vec<u32> {
curves
.0
.iter()
.chain(&curves.1)
.chain(&curves.2)
.map(|v| v.to_bits())
.collect()
}
#[test]
fn a_full_axis_profile_is_the_metric_at_each_tilt_pose_and_the_stepped_one_matches() {
let planes = StandardGemCuts::standard_round_brilliant();
let material = GemMaterial::diamond();
let plain = evaluate_full_axis_profile_at_azimuth(&planes, &material, 45.0, studio());
for &index in &[0usize, 37, 90, 101, 180] {
let tilt = TILT_ANGLES_DEG[index];
let azimuth = if tilt >= 0.0 { 45.0f32 } else { 225.0 };
let m = evaluate_gem_optical_metrics(
&planes,
&material,
azimuth.to_radians(),
(90.0 - tilt.abs()).to_radians(),
studio(),
);
assert_eq!(plain.0[index].to_bits(), m.brilliance_pct.to_bits());
assert_eq!(plain.1[index].to_bits(), m.extinction_pct.to_bits());
assert_eq!(plain.2[index].to_bits(), m.windowing_pct.to_bits());
}
let mut gate_calls = 0usize;
let stepped = evaluate_full_axis_profile_at_azimuth_stepped(
&planes,
&material,
45.0,
studio(),
&mut || {
gate_calls += 1;
true
},
)
.expect("never stopped");
assert_eq!(bits(&stepped), bits(&plain));
assert_eq!(gate_calls, EVALUATIONS_PER_AXIS);
}
#[test]
fn the_stepped_sweep_reports_every_evaluation_and_stops_when_told() {
let planes = StandardGemCuts::standard_round_brilliant();
let material = GemMaterial::diamond();
let mut seen = Vec::new();
let stopped = evaluate_all_axes_profiles_stepped(&planes, &material, studio(), &mut |p| {
seen.push(p);
seen.len() < 190
});
assert!(stopped.is_none());
assert_eq!(seen.len(), 190, "nothing is asked for after the refusal");
let first = SweepProgress {
axis: 0,
done: 0,
total: 724,
};
assert_eq!(seen[0], first);
assert_eq!(seen[180].axis, 0);
assert_eq!(
seen[181].axis, 1,
"the second axis starts after 181 evaluations"
);
assert_eq!(seen[181].done, 181);
assert_eq!(total_evaluations(), 724);
}
}