use super::material_lookup::{EditorMaterialLookup, resolved_gem_material};
use indicatrix::optics::materials::GemMaterial;
use indicatrix_cut_core::{
Design, MaterialSelection, MissingAnchor, OptimizeConfig, OptimizeOutcome, SearchHooks,
free_tier_indices,
optimize::{SearchStage, inclusive_max_evaluations},
optimize_design,
};
use slint::{ComponentHandle, Weak};
use std::{
sync::{
Arc,
atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering},
},
thread,
time::{Duration, Instant},
};
const TICK_INTERVAL: Duration = Duration::from_millis(250);
pub struct OptimizeSolveHandle {
cancel: Arc<AtomicBool>,
}
impl OptimizeSolveHandle {
pub fn cancel(&self) {
self.cancel.store(true, Ordering::Relaxed);
}
}
#[derive(Debug, Clone, Copy)]
pub struct OptimizeSolveProgress {
pub evaluations: usize,
pub max_evaluations: usize,
pub stage: SearchStage,
pub elapsed: Duration,
}
pub enum OptimizeSolveOutcome {
Completed {
outcome: OptimizeOutcome,
},
Cancelled {
outcome: OptimizeOutcome,
},
Failed {
error: MissingAnchor,
},
}
struct OptimizeJob {
design: Design,
material_selection: MaterialSelection,
custom_materials: Vec<GemMaterial>,
config: OptimizeConfig,
}
pub fn spawn_optimize_solve<T, P, D>(
ui_weak: Weak<T>,
design: Design,
material_selection: MaterialSelection,
custom_materials: Vec<GemMaterial>,
config: OptimizeConfig,
on_progress: P,
on_done: D,
) -> OptimizeSolveHandle
where
T: ComponentHandle + 'static,
P: Fn(&T, OptimizeSolveProgress) + Send + 'static + Clone,
D: FnOnce(&T, OptimizeSolveOutcome) + Send + 'static,
{
let cancel = Arc::new(AtomicBool::new(false));
let cancel_ticker = Arc::clone(&cancel);
let cancel_worker = Arc::clone(&cancel);
let done_flag = Arc::new(AtomicBool::new(false));
let done_ticker = Arc::clone(&done_flag);
let evaluations_done = Arc::new(AtomicUsize::new(0));
let evaluations_ticker = Arc::clone(&evaluations_done);
let stage_done = Arc::new(AtomicU8::new(SearchStage::BaselineFull.to_code()));
let stage_ticker = Arc::clone(&stage_done);
let ticker_ui = ui_weak.clone();
let max_evaluations = inclusive_max_evaluations(&config, free_tier_indices(&design).len());
thread::spawn(move || {
let start = Instant::now();
loop {
thread::sleep(TICK_INTERVAL);
if done_ticker.load(Ordering::Relaxed) || cancel_ticker.load(Ordering::Relaxed) {
break;
}
let progress = OptimizeSolveProgress {
evaluations: evaluations_ticker.load(Ordering::Relaxed),
max_evaluations,
stage: SearchStage::from_code(stage_ticker.load(Ordering::Relaxed)),
elapsed: start.elapsed(),
};
let on_progress = on_progress.clone();
let _ = ticker_ui.upgrade_in_event_loop(move |ui| {
on_progress(&ui, progress);
});
}
});
let job = OptimizeJob {
design,
material_selection,
custom_materials,
config,
};
thread::spawn(move || {
let lookup = EditorMaterialLookup::new(&job.custom_materials);
let material = resolved_gem_material(&job.material_selection, &lookup);
let hooks = SearchHooks {
cancel: Some(&cancel_worker),
on_progress: Some(&|evaluations: usize, stage: SearchStage| {
evaluations_done.store(evaluations, Ordering::Relaxed);
stage_done.store(stage.to_code(), Ordering::Relaxed);
}),
};
let result = optimize_design(&job.design, &material, &job.config, &hooks);
done_flag.store(true, Ordering::Relaxed);
let cancelled = cancel_worker.load(Ordering::Relaxed);
let outcome = outcome_for(cancelled, result);
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
on_done(&ui, outcome);
});
});
OptimizeSolveHandle { cancel }
}
fn outcome_for(
cancelled: bool,
result: Result<OptimizeOutcome, MissingAnchor>,
) -> OptimizeSolveOutcome {
match result {
Err(error) => OptimizeSolveOutcome::Failed { error },
Ok(outcome) if cancelled => OptimizeSolveOutcome::Cancelled { outcome },
Ok(outcome) => OptimizeSolveOutcome::Completed { outcome },
}
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix_cut_core::{AngleChange, ObjectiveComponents};
fn dummy_outcome(changed: bool) -> OptimizeOutcome {
let components = ObjectiveComponents {
windowing_pct: 10.0,
extinction_pct: 10.0,
tilt_brilliance_pct: 80.0,
};
OptimizeOutcome {
before: components,
before_score: 10.0,
after: components,
after_score: 10.0,
evaluations: 4,
changes: if changed {
vec![AngleChange {
index: 0,
from_deg: 30.0,
to_deg: 31.0,
}]
} else {
Vec::new()
},
cancelled: false,
polish_evaluations: 0,
polish_improvement: 0.0,
}
}
#[test]
fn cancel_sets_the_flag_the_worker_and_ticker_threads_poll() {
let flag = Arc::new(AtomicBool::new(false));
let handle = OptimizeSolveHandle {
cancel: Arc::clone(&flag),
};
assert!(!flag.load(Ordering::Relaxed));
handle.cancel();
assert!(flag.load(Ordering::Relaxed));
}
#[test]
fn outcome_for_reports_failed_regardless_of_cancelled_when_the_design_does_not_solve() {
let error = MissingAnchor {
blocks: vec![indicatrix::geometry::meet_solver::Block::Crown],
};
for cancelled in [true, false] {
let outcome = outcome_for(cancelled, Err(error.clone()));
assert!(matches!(outcome, OptimizeSolveOutcome::Failed { .. }));
}
}
#[test]
fn outcome_for_preserves_the_real_partial_outcome_when_cancelled() {
let outcome = outcome_for(true, Ok(dummy_outcome(true)));
match outcome {
OptimizeSolveOutcome::Cancelled { outcome } => {
assert_eq!(outcome.changes.len(), 1);
assert_eq!(outcome.evaluations, 4);
}
_ => panic!("expected Cancelled"),
}
}
#[test]
fn outcome_for_reports_completed_when_not_cancelled() {
let outcome = outcome_for(false, Ok(dummy_outcome(false)));
assert!(matches!(outcome, OptimizeSolveOutcome::Completed { .. }));
}
#[test]
fn progress_is_copy_not_just_clone() {
let progress = OptimizeSolveProgress {
evaluations: 12,
max_evaluations: 200,
stage: SearchStage::Coordinate,
elapsed: Duration::from_secs(1),
};
let copied = progress;
assert_eq!(copied.evaluations, progress.evaluations);
}
#[test]
fn optimize_job_resolves_no_material_selection_to_diamond() {
let lookup = EditorMaterialLookup::new(&[]);
let material = resolved_gem_material(&MaterialSelection::none(), &lookup);
assert_eq!(material.name, GemMaterial::diamond().name);
}
}