use crate::{
MainWindow, TiltModel,
bridge::render_thread::{RenderContext, hash_planes, resolve_material},
gui::optics::curve_path::full_axis_curve_path,
};
use indicatrix::{
color::metrics::{PROFILE_AZIMUTHS_DEG, evaluate_full_axis_profile_at_azimuth},
geometry::plane::GpuFacetPlane,
optics::materials::GemMaterial,
};
use slint::{ComponentHandle, ModelRc, SharedString, VecModel};
use std::sync::{
Arc, Mutex,
atomic::{AtomicU64, Ordering},
};
#[derive(Clone, PartialEq)]
struct AxesCacheKey {
light_yaw: f32,
light_pitch: f32,
material: GemMaterial,
planes_hash: u64,
}
#[must_use]
fn should_recompute_axes(current: Option<&AxesCacheKey>, requested: &AxesCacheKey) -> bool {
current != Some(requested)
}
type AxisCurveRows = Vec<[f32; 181]>;
fn sweep_all_axes(
planes: &[GpuFacetPlane],
material: &GemMaterial,
light_yaw: f32,
light_pitch: f32,
) -> (AxisCurveRows, AxisCurveRows, AxisCurveRows) {
let mut brilliance_rows: AxisCurveRows = Vec::with_capacity(PROFILE_AZIMUTHS_DEG.len());
let mut extinction_rows: AxisCurveRows = Vec::with_capacity(PROFILE_AZIMUTHS_DEG.len());
let mut windowing_rows: AxisCurveRows = Vec::with_capacity(PROFILE_AZIMUTHS_DEG.len());
for &azimuth_deg in &PROFILE_AZIMUTHS_DEG {
let (b, e, w) = evaluate_full_axis_profile_at_azimuth(
planes,
material,
azimuth_deg,
light_yaw,
light_pitch,
);
brilliance_rows.push(b);
extinction_rows.push(e);
windowing_rows.push(w);
}
(brilliance_rows, extinction_rows, windowing_rows)
}
fn handle_request_tilt_profile_axes(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
launched_key: &Arc<Mutex<Option<AxesCacheKey>>>,
generation: &Arc<AtomicU64>,
) {
let (planes, material_name, custom_materials, light_yaw, light_pitch) = {
let ctx = render_ctx.lock().unwrap();
(
ctx.active_planes.clone(),
ctx.material_name.clone(),
ctx.custom_materials.clone(),
ctx.light_yaw,
ctx.light_pitch,
)
};
let material = resolve_material(
&GemMaterial::all_materials(),
&custom_materials,
&material_name,
);
let key = AxesCacheKey {
light_yaw,
light_pitch,
material: material.clone(),
planes_hash: hash_planes(&planes),
};
{
let mut launched = launched_key.lock().unwrap();
if !should_recompute_axes(launched.as_ref(), &key) {
return;
}
*launched = Some(key);
}
let my_generation = generation.fetch_add(1, Ordering::SeqCst) + 1;
let generation = generation.clone();
ui.global::<TiltModel>().set_extra_axes_loading(true);
let ui_weak_bg = ui.as_weak();
spawn_tilt_profile_sweep(
planes,
material,
light_yaw,
light_pitch,
generation,
my_generation,
ui_weak_bg,
);
}
fn spawn_tilt_profile_sweep(
planes: Arc<Vec<GpuFacetPlane>>,
material: GemMaterial,
light_yaw: f32,
light_pitch: f32,
generation: Arc<AtomicU64>,
my_generation: u64,
ui_weak_bg: slint::Weak<MainWindow>,
) {
std::thread::spawn(move || {
let (brilliance_rows, extinction_rows, windowing_rows) =
sweep_all_axes(&planes, &material, light_yaw, light_pitch);
let brilliance_paths: Vec<SharedString> = brilliance_rows
.iter()
.map(|row| full_axis_curve_path(row).into())
.collect();
let extinction_paths: Vec<SharedString> = extinction_rows
.iter()
.map(|row| full_axis_curve_path(row).into())
.collect();
let windowing_paths: Vec<SharedString> = windowing_rows
.iter()
.map(|row| full_axis_curve_path(row).into())
.collect();
let to_model_rows = |rows: Vec<[f32; 181]>| -> ModelRc<ModelRc<f32>> {
ModelRc::new(VecModel::from(
rows.into_iter()
.map(|row| ModelRc::new(VecModel::from(row.to_vec())))
.collect::<Vec<_>>(),
))
};
let _ = ui_weak_bg.upgrade_in_event_loop(move |ui| {
if generation.load(Ordering::SeqCst) != my_generation {
return;
}
ui.global::<TiltModel>()
.set_graph_brilliance_extra_axes(to_model_rows(brilliance_rows));
ui.global::<TiltModel>()
.set_graph_extinction_extra_axes(to_model_rows(extinction_rows));
ui.global::<TiltModel>()
.set_graph_windowing_extra_axes(to_model_rows(windowing_rows));
ui.global::<TiltModel>()
.set_graph_brilliance_extra_paths(ModelRc::new(VecModel::from(brilliance_paths)));
ui.global::<TiltModel>()
.set_graph_extinction_extra_paths(ModelRc::new(VecModel::from(extinction_paths)));
ui.global::<TiltModel>()
.set_graph_windowing_extra_paths(ModelRc::new(VecModel::from(windowing_paths)));
ui.global::<TiltModel>().set_extra_axes_loading(false);
});
});
}
pub(in crate::gui) fn setup_tilt_profile_callback(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
) {
let launched_key: Arc<Mutex<Option<AxesCacheKey>>> = Arc::new(Mutex::new(None));
let launched_key_for_rerender = Arc::clone(&launched_key);
let generation = Arc::new(AtomicU64::new(0));
let ui_weak = ui.as_weak();
let render_ctx = render_ctx.clone();
ui.global::<TiltModel>()
.on_request_tilt_profile_axes(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
handle_request_tilt_profile_axes(&ui, &render_ctx, &launched_key, &generation);
});
ui.global::<TiltModel>().on_is_cached_curve_material_stale(
|cached: SharedString, current: SharedString| {
let cached = if cached.is_empty() {
None
} else {
Some(cached.as_str())
};
cached_curve_material_is_stale(cached, ¤t)
},
);
let ui_weak_rerender = ui.as_weak();
ui.global::<TiltModel>()
.on_rerender_curve_with_current_material(move || {
if let Some(ui) = ui_weak_rerender.upgrade() {
*launched_key_for_rerender.lock().unwrap() = None;
ui.global::<TiltModel>().invoke_request_tilt_profile_axes();
}
});
}
#[must_use]
pub fn cached_curve_material_is_stale(
cached_material_name: Option<&str>,
current_material_name: &str,
) -> bool {
cached_material_name.is_some_and(|cached| !cached.eq_ignore_ascii_case(current_material_name))
}
#[cfg(test)]
mod tests {
use super::{AxesCacheKey, cached_curve_material_is_stale, should_recompute_axes};
use indicatrix::optics::materials::GemMaterial;
#[test]
fn no_cached_material_is_never_stale() {
assert!(!cached_curve_material_is_stale(None, "Diamond"));
}
#[test]
fn matching_material_is_not_stale() {
assert!(!cached_curve_material_is_stale(Some("Diamond"), "Diamond"));
assert!(!cached_curve_material_is_stale(Some("diamond"), "Diamond"));
}
#[test]
fn differing_material_is_stale() {
assert!(cached_curve_material_is_stale(Some("Diamond"), "Sapphire"));
}
fn sample_key(material: GemMaterial) -> AxesCacheKey {
AxesCacheKey {
light_yaw: 48.0,
light_pitch: 54.0,
material,
planes_hash: 42,
}
}
#[test]
fn should_recompute_axes_is_true_the_first_time() {
let key = sample_key(GemMaterial::all_materials()[0].clone());
assert!(should_recompute_axes(None, &key));
}
#[test]
fn should_recompute_axes_is_false_once_the_exact_key_was_already_launched() {
let key = sample_key(GemMaterial::all_materials()[0].clone());
assert!(!should_recompute_axes(Some(&key), &key));
}
#[test]
fn should_recompute_axes_is_true_when_light_or_geometry_differ() {
let material = GemMaterial::all_materials()[0].clone();
let current = sample_key(material);
let mut moved_light = current.clone();
moved_light.light_yaw += 1.0;
assert!(should_recompute_axes(Some(¤t), &moved_light));
let mut moved_geometry = current.clone();
moved_geometry.planes_hash = 43;
assert!(should_recompute_axes(Some(¤t), &moved_geometry));
}
#[test]
fn should_recompute_axes_is_true_when_only_the_resolved_materials_optics_differ() {
let mut edited = GemMaterial::all_materials()[0].clone();
let current = sample_key(edited.clone());
edited.birefringence_delta += 0.01;
let requested = sample_key(edited);
assert_eq!(
current.material.name, requested.material.name,
"the display name must be unchanged -- that is the whole point of this case"
);
assert!(should_recompute_axes(Some(¤t), &requested));
}
}