use super::{
proposal_view::{CheckView, push_check_view},
sync_embedded_comparison,
};
use crate::{MainWindow, RetargetModel};
use indicatrix::{
geometry::meet_solver::SolvedTier,
optics::{LightingPreset, materials::GemMaterial},
};
use indicatrix_cut_core::{CANONICAL_LIGHTING_PRESET, Design};
use indicatrix_editor::{
material_lookup::{EditorMaterialLookup, resolved_gem_material},
retarget::{
AnchorChange, GirdleAllowance, RetargetPlan,
check::{AnalysisResult, CheckInputs, RetargetCheck, run_check},
metrics::RetargetMetrics,
validity::{InvalidReason, RetargetValidity},
},
};
use slint::{ComponentHandle, Timer, TimerMode};
use std::{
cell::RefCell,
panic::{AssertUnwindSafe, catch_unwind},
sync::{
Arc,
atomic::{AtomicU64, Ordering as AtomicOrdering},
},
time::Duration,
};
static CHECK_SERIAL: AtomicU64 = AtomicU64::new(0);
#[derive(Default)]
enum Progress {
#[default]
Idle,
Checking,
Done(Box<RetargetCheck>),
}
#[derive(Default)]
struct CheckSlot {
progress: Progress,
serial: u64,
generation: u64,
analysis: Option<(u64, Arc<AnalysisResult>)>,
}
thread_local! {
static CHECK: RefCell<CheckSlot> = RefCell::new(CheckSlot::default());
static DEBOUNCE: Timer = Timer::default();
}
const CHECK_DEBOUNCE: Duration = Duration::from_millis(120);
pub(super) enum CheckStatus {
Idle,
Checking,
Done {
anchors: Vec<AnchorChange>,
allows_apply: bool,
headline: String,
},
}
pub(super) fn status_for(generation: u64) -> CheckStatus {
CHECK.with(|cell| {
let slot = cell.borrow();
match &slot.progress {
Progress::Checking if slot.generation == generation => CheckStatus::Checking,
Progress::Done(check) if slot.generation == generation => CheckStatus::Done {
anchors: check.anchors.clone(),
allows_apply: check.allows_apply(),
headline: check.validity.headline(),
},
Progress::Idle | Progress::Checking | Progress::Done(_) => CheckStatus::Idle,
}
})
}
pub(super) fn cached_original_solved(generation: u64) -> Option<Vec<SolvedTier>> {
CHECK.with(|cell| {
cell.borrow()
.analysis
.as_ref()
.filter(|(cached_generation, _)| *cached_generation == generation)
.and_then(|(_, analysis)| Result::as_ref(analysis).ok().map(|a| a.solved.clone()))
})
}
pub(super) enum ApplyGate {
Open(Vec<AnchorChange>),
Checking,
Blocked(String),
}
pub(super) fn apply_gate(generation: u64) -> ApplyGate {
match status_for(generation) {
CheckStatus::Idle => ApplyGate::Open(Vec::new()),
CheckStatus::Checking => ApplyGate::Checking,
CheckStatus::Done {
anchors,
allows_apply,
headline,
} => {
if allows_apply {
ApplyGate::Open(anchors)
} else {
ApplyGate::Blocked(headline)
}
}
}
}
pub(super) fn candidate_anchors(generation: u64) -> Result<Vec<AnchorChange>, String> {
match status_for(generation) {
CheckStatus::Idle => Ok(Vec::new()),
CheckStatus::Checking => {
Err("Checking this change -- the comparison appears when it is done.".to_string())
}
CheckStatus::Done { anchors, .. } => Ok(anchors),
}
}
pub(super) fn is_invalid(generation: u64) -> bool {
matches!(
status_for(generation),
CheckStatus::Done {
allows_apply: false,
..
}
)
}
pub(super) fn latest_anchors() -> Vec<AnchorChange> {
CHECK.with(|cell| match &cell.borrow().progress {
Progress::Done(check) => check.anchors.clone(),
Progress::Idle | Progress::Checking => Vec::new(),
})
}
pub(super) fn reset_check(ui: &MainWindow) {
CHECK_SERIAL.fetch_add(1, AtomicOrdering::SeqCst);
DEBOUNCE.with(Timer::stop);
CHECK.with(|cell| {
let mut slot = cell.borrow_mut();
slot.progress = Progress::Idle;
slot.serial = 0;
});
push_check_view(ui, &CheckView::default());
}
pub(super) fn set_external_check(ui: &MainWindow, generation: u64, check: RetargetCheck) {
let serial = CHECK_SERIAL.fetch_add(1, AtomicOrdering::SeqCst) + 1;
DEBOUNCE.with(Timer::stop);
let view = CheckView::from_check(&check);
CHECK.with(|cell| {
let mut slot = cell.borrow_mut();
slot.progress = Progress::Done(Box::new(check));
slot.serial = serial;
slot.generation = generation;
});
push_check_view(ui, &view);
}
pub(super) fn current_gem(design: &Design, custom: &[GemMaterial]) -> Option<GemMaterial> {
let material = &design.material;
(material.name.is_some() || material.refractive_index_override.is_some())
.then(|| resolved_gem_material(material, &EditorMaterialLookup::new(custom)))
}
fn failed_check() -> RetargetCheck {
RetargetCheck {
validity: RetargetValidity::unchecked(&InvalidReason::DoesNotSolve(
"the check stopped unexpectedly".to_string(),
)),
anchors: Vec::new(),
metrics: RetargetMetrics::default(),
}
}
struct CheckJob {
design: Design,
plan: RetargetPlan,
custom: Vec<GemMaterial>,
lighting: LightingPreset,
girdle_allowance: bool,
serial: u64,
generation: u64,
ui_weak: slint::Weak<MainWindow>,
}
pub(super) fn begin_check(
ui: &MainWindow,
design: &Design,
plan: RetargetPlan,
custom: &[GemMaterial],
generation: u64,
) {
let serial = CHECK_SERIAL.fetch_add(1, AtomicOrdering::SeqCst) + 1;
CHECK.with(|cell| {
let mut slot = cell.borrow_mut();
slot.progress = Progress::Checking;
slot.serial = serial;
slot.generation = generation;
});
push_check_view(ui, &CheckView::checking());
let job = RefCell::new(Some(CheckJob {
design: design.clone(),
plan,
custom: custom.to_vec(),
lighting: CANONICAL_LIGHTING_PRESET,
girdle_allowance: ui.global::<RetargetModel>().get_girdle_allowance(),
serial,
generation,
ui_weak: ui.as_weak(),
}));
DEBOUNCE.with(|timer| {
timer.start(TimerMode::SingleShot, CHECK_DEBOUNCE, move || {
let pending = job.borrow_mut().take();
if let Some(job) = pending {
spawn_worker(job);
}
});
});
}
fn spawn_worker(job: CheckJob) {
let cached = CHECK.with(|cell| {
cell.borrow()
.analysis
.as_ref()
.filter(|(cached_generation, _)| *cached_generation == job.generation)
.map(|(_, analysis)| Arc::clone(analysis))
});
std::thread::spawn(move || {
let CheckJob {
design,
plan,
custom,
lighting,
girdle_allowance,
serial,
generation,
ui_weak,
} = job;
let stop = move || CHECK_SERIAL.load(AtomicOrdering::SeqCst) != serial;
let current = current_gem(&design, &custom);
let inputs = CheckInputs {
design: &design,
plan: &plan,
current_gem: current.as_ref(),
lighting,
girdle: girdle_allowance.then(GirdleAllowance::standard),
};
let outcome = catch_unwind(AssertUnwindSafe(|| run_check(&inputs, cached, &stop)));
let (analysis, check) = match outcome {
Ok(Some((analysis, check))) => (Some(analysis), check),
Ok(None) => return,
Err(_) => (None, failed_check()),
};
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
finish_check(&ui, serial, generation, analysis, check);
});
});
}
fn finish_check(
ui: &MainWindow,
serial: u64,
generation: u64,
analysis: Option<Arc<AnalysisResult>>,
check: RetargetCheck,
) {
let view = CheckView::from_check(&check).with_optimize_hint();
let current = CHECK.with(|cell| {
let mut slot = cell.borrow_mut();
if slot.serial != serial {
return false;
}
if let Some(analysis) = analysis {
slot.analysis = Some((generation, analysis));
}
slot.progress = Progress::Done(Box::new(check));
true
});
if !current {
return;
}
push_check_view(ui, &view);
sync_embedded_comparison(ui, true);
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix_editor::retarget::validity::ValidityStatus;
fn ready(valid: bool) -> RetargetCheck {
let validity = if valid {
let mut validity = RetargetValidity::unchecked(&InvalidReason::NotClosed);
validity.status = ValidityStatus::Valid;
validity.reasons.clear();
validity
} else {
let mut validity = RetargetValidity::unchecked(&InvalidReason::GirdleGone);
validity.status = ValidityStatus::Invalid;
validity
};
RetargetCheck {
validity,
anchors: vec![AnchorChange {
tier_index: 3,
old_mast: 0.5,
new_mast: 0.6,
}],
metrics: RetargetMetrics::default(),
}
}
fn set(progress: Progress, generation: u64) {
CHECK.with(|cell| {
let mut slot = cell.borrow_mut();
slot.progress = progress;
slot.generation = generation;
});
}
#[test]
fn an_idle_slot_lets_apply_through_without_masts() {
set(Progress::Idle, 7);
assert!(matches!(apply_gate(7), ApplyGate::Open(anchors) if anchors.is_empty()));
assert_eq!(candidate_anchors(7), Ok(Vec::new()));
}
#[test]
fn a_running_check_holds_apply_and_the_comparison() {
set(Progress::Checking, 7);
assert!(matches!(apply_gate(7), ApplyGate::Checking));
assert!(candidate_anchors(7).is_err());
}
#[test]
fn a_valid_check_opens_apply_with_its_masts() {
set(Progress::Done(Box::new(ready(true))), 7);
match apply_gate(7) {
ApplyGate::Open(anchors) => assert_eq!(anchors.len(), 1),
_ => panic!("a valid check must open Apply"),
}
assert!(!is_invalid(7));
}
#[test]
fn an_invalid_check_blocks_apply_but_still_allows_the_comparison() {
set(Progress::Done(Box::new(ready(false))), 7);
match apply_gate(7) {
ApplyGate::Blocked(headline) => {
assert!(headline.starts_with("Not valid"), "{headline}");
}
_ => panic!("an invalid check must block Apply"),
}
assert!(is_invalid(7));
assert_eq!(candidate_anchors(7).expect("not checking").len(), 1);
}
#[test]
fn a_result_for_another_generation_never_leaks_its_masts() {
set(Progress::Done(Box::new(ready(true))), 7);
assert!(matches!(apply_gate(8), ApplyGate::Open(anchors) if anchors.is_empty()));
assert_eq!(candidate_anchors(8), Ok(Vec::new()));
}
#[test]
fn the_latest_masts_are_empty_until_a_check_finishes() {
set(Progress::Checking, 7);
assert_eq!(latest_anchors(), Vec::new());
set(Progress::Done(Box::new(ready(true))), 7);
assert_eq!(latest_anchors().len(), 1);
}
}