use super::{
RedrawGate, SolidPreviewState,
controller::{PlanFindings, SharedWarnings},
live_update,
request::{PlanJob, PlannedFrame, RedrawRequest},
sink::LateFindings,
};
use indicatrix::geometry::meet_solver::SolvedTier;
use indicatrix_cut_core::{Design, ManufacturabilityWarning};
use std::sync::{
Arc, PoisonError,
atomic::Ordering,
mpsc::{self, Sender},
};
const KEPT_FINDINGS: usize = 4;
#[must_use]
const fn findings_wanted(generation: u64, stale: bool, unsolvable: bool) -> bool {
!stale && !unsolvable && crate::gui::editor::frame_updates_mast_cache(generation)
}
#[must_use]
pub(super) fn newer_plan_will_run_a_pass(queued: Option<u64>) -> bool {
queued.is_some_and(crate::gui::editor::frame_updates_mast_cache)
}
pub(super) struct FindingsJob {
generation: u64,
design: Arc<Design>,
masts: Vec<SolvedTier>,
}
impl FindingsJob {
pub(super) fn run(&self) -> Vec<ManufacturabilityWarning> {
indicatrix_editor::view_model::rows::solved_manufacturability_warnings(
&self.design,
&self.masts,
)
}
}
pub(super) fn findings_job(
generation: u64,
design: &Arc<Design>,
solved: Option<&[SolvedTier]>,
stale: bool,
unsolvable: bool,
) -> Option<FindingsJob> {
if !findings_wanted(generation, stale, unsolvable) {
return None;
}
let masts = solved.filter(|masts| masts.len() == design.tiers.len())?;
Some(FindingsJob {
generation,
design: Arc::clone(design),
masts: masts.to_vec(),
})
}
pub(super) fn finish_findings(
state: &SolidPreviewState,
warnings: &SharedWarnings,
plan_gate: &RedrawGate<PlanJob>,
owed: Option<FindingsJob>,
) {
let Some(job) = owed else {
return;
};
if newer_plan_will_run_a_pass(plan_gate.peek(|queued| queued.generation)) {
return;
}
deliver_findings(state, warnings, job);
}
fn deliver_findings(state: &SolidPreviewState, warnings: &SharedWarnings, job: FindingsJob) {
let Some(findings) = survive_panic("a manufacturability check", || job.run()) else {
return;
};
store_findings(warnings, &job.design, findings.clone());
state.sink.apply_findings(LateFindings {
generation: job.generation,
design: job.design,
masts: job.masts,
warnings: Arc::new(findings),
});
}
pub(super) fn store_findings(
slot: &SharedWarnings,
design: &Arc<Design>,
warnings: Vec<ManufacturabilityWarning>,
) {
let mut kept = slot.lock().unwrap_or_else(PoisonError::into_inner);
kept.push(PlanFindings {
design: Arc::clone(design),
warnings: Arc::new(warnings),
});
let surplus = kept.len().saturating_sub(KEPT_FINDINGS);
kept.drain(..surplus);
}
pub(super) fn findings_of_plan(
slot: &SharedWarnings,
design: &Arc<Design>,
) -> Option<Arc<Vec<ManufacturabilityWarning>>> {
slot.lock()
.unwrap_or_else(PoisonError::into_inner)
.iter()
.rev()
.find(|entry| Arc::ptr_eq(&entry.design, design))
.map(|entry| Arc::clone(&entry.warnings))
}
#[derive(Debug, Clone, Copy, Default)]
struct InstantClock;
impl live_update::Clock for InstantClock {
fn now_ms(&self) -> f64 {
static EPOCH: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
EPOCH
.get_or_init(std::time::Instant::now)
.elapsed()
.as_secs_f64()
* 1000.0
}
}
pub fn build_planned_frame(job: PlanJob, budget: std::time::Duration) -> PlannedFrame {
indicatrix_solid::preview::build_planned_frame(job, budget, &InstantClock)
}
impl SolidPreviewState {
pub(super) fn submit_plan(&self, job: PlanJob) {
let tx = {
let mut guard = self
.plan_wake
.lock()
.unwrap_or_else(PoisonError::into_inner);
if guard.is_none() {
*guard = Some(self.spawn_plan_worker());
}
guard
.clone()
.expect("just initialized above if it was empty")
};
if self.plan_gate.submit(job).is_some() {
let _ = tx.send(());
}
}
fn spawn_plan_worker(&self) -> Sender<()> {
let (tx, rx) = mpsc::channel::<()>();
let plan_gate = Arc::clone(&self.plan_gate);
let generation_floor = Arc::clone(&self.generation_floor);
let warnings = Arc::clone(&self.warnings);
let self_weak = self
.self_weak
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone();
std::thread::spawn(move || {
for () in rx {
let Some(job) = plan_gate.take() else {
continue;
};
if job.generation < generation_floor.load(Ordering::Relaxed) {
continue;
}
let Some(frame) = survive_panic("a replan", || {
build_planned_frame(job, live_update::DEFAULT_PREVIEW_BUDGET)
}) else {
continue;
};
let owed = findings_job(
frame.generation,
&frame.design,
frame.solved.as_deref(),
frame.stale,
frame.unsolvable_status.is_some(),
);
if let Some(state) = self_weak.upgrade() {
state.submit(RedrawRequest::Planned(Box::new(frame)));
finish_findings(&state, &warnings, &plan_gate, owed);
}
}
});
tx
}
}
pub(super) fn survive_panic<T>(what: &str, work: impl FnOnce() -> T) -> Option<T> {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(work))
.map_err(|_| {
tracing::error!("The solid-preview worker panicked on {what}; it keeps running.");
})
.ok()
}