mod apply;
mod present;
#[cfg(test)]
mod tests;
mod work;
use self::work::Job;
use super::state::EditorState;
use crate::{
EditorModel, MainWindow, VerdictModel, ViewportModel,
bridge::render_thread::RenderContext,
gui::solid_preview::preview_state::{SolidLastSolved, SolidPreviewState},
};
use indicatrix::{geometry::meet_solver::SolvedTier, optics::LightingPreset};
use indicatrix_cut_core::{ConstraintTier, Design};
use indicatrix_editor::verdict::{FixAction, SolveFacts, Verdict, VerdictInputs, evaluate};
use slint::{ComponentHandle, Timer, TimerMode};
use std::{
cell::RefCell,
rc::Rc,
sync::{Arc, Mutex, TryLockError},
time::Duration,
};
const REQUEST_DEBOUNCE: Duration = Duration::from_millis(150);
const OUT_OF_DATE: &str =
"The design changed since this was checked. The verdict updates when the solve finishes.";
const FIX_RUNNING: &str = "Another fix is still running.";
struct Services {
editor: Rc<RefCell<EditorState>>,
render_ctx: Arc<Mutex<RenderContext>>,
preview_state: Arc<SolidPreviewState>,
solid_last_solved: SolidLastSolved,
}
#[derive(Default)]
struct Slot {
serial: u64,
verdict: Option<Verdict>,
tiers: Vec<ConstraintTier>,
n_d: f64,
current: bool,
busy: bool,
}
thread_local! {
static SERVICES: RefCell<Option<Rc<Services>>> = const { RefCell::new(None) };
static SLOT: RefCell<Slot> = RefCell::new(Slot::default());
static DEBOUNCE: Timer = Timer::default();
}
fn services() -> Option<Rc<Services>> {
SERVICES.with(|cell| cell.borrow().clone())
}
pub(super) fn setup_verdict(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
SERVICES.with(|cell| {
*cell.borrow_mut() = Some(Rc::new(Services {
editor: Rc::clone(state),
render_ctx: Arc::clone(render_ctx),
preview_state: Arc::clone(preview_state),
solid_last_solved: Arc::clone(solid_last_solved),
}));
});
let ui_weak = ui.as_weak();
ui.global::<VerdictModel>().on_fix_reason(move |index| {
if let Some(ui) = ui_weak.upgrade() {
apply::fix_reason(&ui, index);
}
});
}
pub(super) fn on_solved(ui: &MainWindow, design: &Design, solved: Option<&[SolvedTier]>, n_d: f64) {
let Some(services) = services() else {
return;
};
if design.tiers.is_empty() {
reset(ui);
return;
}
let serial = SLOT.with(|cell| {
let mut slot = cell.borrow_mut();
slot.serial += 1;
slot.current = false;
slot.tiers.clone_from(&design.tiers);
slot.n_d = n_d;
slot.serial
});
present::set_stale(ui, true);
let custom = match services.render_ctx.try_lock() {
Ok(ctx) => Some(Arc::clone(&ctx.custom_materials)),
Err(TryLockError::Poisoned(poisoned)) => {
Some(Arc::clone(&poisoned.into_inner().custom_materials))
}
Err(TryLockError::WouldBlock) => None,
};
let job = RefCell::new(Some(Job {
design: design.clone(),
solved: solved.map(<[SolvedTier]>::to_vec),
n_d,
lighting: LightingPreset::from_index(
ui.global::<ViewportModel>().get_selected_lighting_index(),
),
custom,
serial,
ui_weak: ui.as_weak(),
}));
DEBOUNCE.with(|timer| {
timer.start(TimerMode::SingleShot, REQUEST_DEBOUNCE, move || {
let pending = job.borrow_mut().take();
if let Some(job) = pending {
work::spawn(job);
}
});
});
}
pub(super) fn mark_stale(ui: &MainWindow) {
SLOT.with(|cell| {
let mut slot = cell.borrow_mut();
slot.serial += 1;
slot.current = false;
});
DEBOUNCE.with(Timer::stop);
present::set_stale(ui, true);
}
fn reset(ui: &MainWindow) {
SLOT.with(|cell| {
let mut slot = cell.borrow_mut();
slot.serial += 1;
slot.verdict = None;
slot.tiers.clear();
slot.current = false;
});
DEBOUNCE.with(Timer::stop);
present::push_hidden(ui);
}
fn failure_text(solve_state: &str, status_text: &str) -> Option<String> {
let text = status_text.trim();
(solve_state == "failed" && !text.is_empty()).then(|| text.to_string())
}
fn finish(ui: &MainWindow, serial: u64, had_solution: bool, inputs: Option<VerdictInputs>) {
if SLOT.with(|cell| cell.borrow().serial) != serial {
return;
}
let Some(mut inputs) = inputs else {
return;
};
if inputs.is_empty() {
reset(ui);
return;
}
if !had_solution && let SolveFacts::DoesNotSolve(why) = &mut inputs.solve {
let model = ui.global::<EditorModel>();
if let Some(text) = failure_text(
model.get_solve_state().as_str(),
model.get_status_text().as_str(),
) {
*why = text;
}
}
let verdict = evaluate(&inputs);
SLOT.with(|cell| {
let mut slot = cell.borrow_mut();
slot.verdict = Some(verdict.clone());
slot.current = true;
});
present::push_verdict(ui, &verdict);
}
fn ensure_current(tiers: &[ConstraintTier]) -> Result<(), String> {
SLOT.with(|cell| {
let slot = cell.borrow();
if slot.busy {
Err(FIX_RUNNING.to_string())
} else if !slot.current || slot.tiers != tiers {
Err(OUT_OF_DATE.to_string())
} else {
Ok(())
}
})
}
fn pending_fix(
index: usize,
tiers: &[ConstraintTier],
) -> Result<(FixAction, Option<String>), String> {
ensure_current(tiers)?;
SLOT.with(|cell| {
cell.borrow()
.verdict
.as_ref()
.and_then(|verdict| verdict.reasons.get(index))
.and_then(|reason| reason.fix.clone().map(|fix| (fix, reason.confirm.clone())))
.ok_or_else(|| "There is no automatic fix for this one.".to_string())
})
}