use crate::gui::editor::material_lookup::{EditorMaterialLookup, resolved_gem_material};
use indicatrix::optics::{LightingPreset, materials::GemMaterial};
use indicatrix_cut_core::{
Design, MaterialSelection,
optimize::{CANONICAL_LIGHT_PITCH, CANONICAL_LIGHT_YAW},
};
use indicatrix_editor::sweep::{
SweepOptions, SweepOutcome, SweepPlan, SweepScene, sweep_tier_angle,
};
use std::{
sync::{
Arc, Mutex, PoisonError,
atomic::{AtomicBool, AtomicUsize, Ordering},
},
thread::{self, JoinHandle},
};
pub(super) struct Job {
pub design: Design,
pub plan: SweepPlan,
pub material: MaterialSelection,
pub custom_materials: Vec<GemMaterial>,
pub lighting: LightingPreset,
pub options: SweepOptions,
}
#[derive(Debug)]
pub(super) enum Poll {
Running { done: usize },
Finished(SweepOutcome),
Died,
}
pub(super) struct RunHandle {
cancel: Arc<AtomicBool>,
done: Arc<AtomicUsize>,
total: usize,
result: Arc<Mutex<Option<SweepOutcome>>>,
worker: Option<JoinHandle<()>>,
}
impl RunHandle {
pub(super) const fn total(&self) -> usize {
self.total
}
pub(super) fn cancel(&self) {
self.cancel.store(true, Ordering::Relaxed);
}
pub(super) fn poll(&mut self) -> Poll {
let ended = self.worker.as_ref().is_some_and(JoinHandle::is_finished);
let outcome = self
.result
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take();
match (outcome, ended) {
(Some(outcome), _) => {
self.reap();
Poll::Finished(outcome)
}
(None, true) => {
self.reap();
Poll::Died
}
(None, false) => Poll::Running {
done: self.done.load(Ordering::Relaxed),
},
}
}
fn reap(&mut self) {
if let Some(worker) = self.worker.take() {
let _ = worker.join();
}
}
}
impl Drop for RunHandle {
fn drop(&mut self) {
self.cancel.store(true, Ordering::Relaxed);
}
}
pub(super) fn start(job: Job) -> Result<RunHandle, String> {
let cancel = Arc::new(AtomicBool::new(false));
let done = Arc::new(AtomicUsize::new(0));
let result = Arc::new(Mutex::new(None));
let total = job.plan.angles.len();
let worker = {
let cancel = Arc::clone(&cancel);
let done = Arc::clone(&done);
let result = Arc::clone(&result);
thread::Builder::new()
.name("angle-sweep".to_owned())
.spawn(move || {
let lookup = EditorMaterialLookup::new(&job.custom_materials);
let material = resolved_gem_material(&job.material, &lookup);
let scene = SweepScene {
material: &material,
environment: job.lighting.studio(
1.0,
CANONICAL_LIGHT_YAW,
CANONICAL_LIGHT_PITCH,
),
};
let progress = |finished: usize, _total: usize| {
done.store(finished, Ordering::Relaxed);
};
let outcome = sweep_tier_angle(
&job.design,
&job.plan,
&scene,
&job.options,
&cancel,
&progress,
);
*result.lock().unwrap_or_else(PoisonError::into_inner) = Some(outcome);
})
.map_err(|error| format!("The sweep could not start: {error}."))?
};
Ok(RunHandle {
cancel,
done,
total,
result,
worker: Some(worker),
})
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix_cut_core::{ConstraintTier, PreformSpec, ScheduleMeta};
use indicatrix_editor::sweep::{SweepRange, plan_sweep};
use std::time::{Duration, Instant};
fn job(workers: usize) -> Job {
let mut design = Design::new(
PreformSpec::block(2.0, 1.0, 2.0),
ScheduleMeta::standard_round_brilliant(),
ConstraintTier::standard_round_brilliant(),
);
design.ensure_tier_ids();
let plan = plan_sweep(
&design,
5,
SweepRange {
from_deg: -42.0,
to_deg: -40.0,
step_deg: 1.0,
},
)
.expect("a pavilion sweep");
Job {
design,
plan,
material: MaterialSelection::none(),
custom_materials: Vec::new(),
lighting: LightingPreset::default(),
options: SweepOptions {
tilt_average: false,
workers,
},
}
}
fn wait_for(handle: &mut RunHandle) -> Poll {
let give_up = Instant::now() + Duration::from_secs(120);
loop {
match handle.poll() {
Poll::Running { .. } if Instant::now() < give_up => {
thread::sleep(Duration::from_millis(20));
}
other => return other,
}
}
}
#[test]
fn a_started_sweep_is_polled_to_its_outcome() {
let mut handle = start(job(2)).expect("the thread starts");
assert_eq!(handle.total(), 3);
let Poll::Finished(outcome) = wait_for(&mut handle) else {
panic!("the sweep should finish");
};
assert!(!outcome.cancelled);
assert_eq!(outcome.rows.len(), 3);
assert_eq!(outcome.tier, 5);
assert_eq!(outcome.rows.iter().filter(|row| row.is_current).count(), 1);
}
#[test]
fn a_cancelled_sweep_still_hands_back_what_it_finished() {
let mut handle = start(job(1)).expect("the thread starts");
handle.cancel();
let Poll::Finished(outcome) = wait_for(&mut handle) else {
panic!("a cancelled sweep still ends with an outcome");
};
assert!(outcome.rows.len() <= 3);
assert_eq!(outcome.cancelled, outcome.rows.len() < 3);
}
}