use super::{
state::{
EditorState, apply_multi_selection, design_to_gpu_planes, manufacturability_warning_lines,
status_text_and_is_problem, tier_items, yield_report_texts,
},
view::SolidLastSolved,
};
use crate::{
EditorModel, EditorTierItem, MainWindow, SolidPreviewModel,
bridge::render_thread::RenderContext,
gui::solid_preview::preview_state::{CameraPose, SolidPreviewState},
};
use indicatrix::geometry::{GpuFacetPlane, meet_solver::SolvedTier};
use indicatrix_cut_core::Design;
use slint::{ComponentHandle, ModelRc, SharedString, VecModel};
use std::{
cell::RefCell,
collections::BTreeSet,
sync::{
Arc, Mutex, PoisonError,
atomic::{AtomicBool, AtomicU64, Ordering},
},
thread,
time::{Duration, Instant},
};
pub(super) const SYNC_SOLVE_TIER_LIMIT: usize = 16;
const AUTO_SOLVE_DEBOUNCE: Duration = Duration::from_millis(150);
const TICK_INTERVAL: Duration = Duration::from_millis(250);
struct Runtime {
preview_state: Option<Arc<SolidPreviewState>>,
solid_last_solved: Option<SolidLastSolved>,
last_solve: Option<Duration>,
current_seq: u64,
debounce: Option<slint::Timer>,
}
impl Runtime {
const fn new() -> Self {
Self {
preview_state: None,
solid_last_solved: None,
last_solve: None,
current_seq: 0,
debounce: None,
}
}
}
thread_local! {
static RUNTIME: RefCell<Runtime> = const { RefCell::new(Runtime::new()) };
}
pub(super) fn init(preview_state: &Arc<SolidPreviewState>, solid_last_solved: &SolidLastSolved) {
RUNTIME.with(|cell| {
let mut rt = cell.borrow_mut();
rt.preview_state = Some(Arc::clone(preview_state));
rt.solid_last_solved = Some(Arc::clone(solid_last_solved));
});
}
pub(super) const fn should_schedule_auto_solve(
last_solve: Option<Duration>,
budget: Duration,
) -> bool {
if budget.is_zero() {
return false;
}
match last_solve {
Some(last) => last.as_millis() < budget.as_millis(),
None => true,
}
}
pub(super) fn auto_solve_off_note(last_solve: Duration) -> String {
format!(
"Auto-solve off for this design: last solve took {:.1}s.",
last_solve.as_secs_f32()
)
}
fn solving_banner(tier_count: usize, elapsed: Duration) -> String {
format!(
"Solving... ({tier_count} tier{}) -- {:.1}s elapsed",
if tier_count == 1 { "" } else { "s" },
elapsed.as_secs_f32()
)
}
pub(super) fn record_solve_duration(elapsed: Duration) {
RUNTIME.with(|cell| cell.borrow_mut().last_solve = Some(elapsed));
}
pub(super) fn reset_for_new_design() {
RUNTIME.with(|cell| cell.borrow_mut().last_solve = None);
}
fn is_current(seq: u64) -> bool {
RUNTIME.with(|cell| cell.borrow().current_seq == seq)
}
pub(super) fn on_edit(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &EditorState,
) {
let budget_ms =
u64::try_from(ui.global::<EditorModel>().get_auto_solve_budget_ms()).unwrap_or(0);
let budget = Duration::from_millis(budget_ms);
let last_solve = RUNTIME.with(|cell| cell.borrow().last_solve);
if !should_schedule_auto_solve(last_solve, budget) {
if !budget.is_zero()
&& let Some(last) = last_solve
{
ui.global::<EditorModel>()
.set_status_text(auto_solve_off_note(last).into());
ui.global::<EditorModel>().set_status_is_problem(true);
}
return;
}
let ui_weak = ui.as_weak();
let render_ctx = Arc::clone(render_ctx);
let design = state.design.clone();
let generation = Arc::clone(&state.generation);
let multi_selected = state.multi_selected.clone();
let timer = slint::Timer::default();
timer.start(
slint::TimerMode::SingleShot,
AUTO_SOLVE_DEBOUNCE,
move || {
if let Some(ui) = ui_weak.upgrade() {
dispatch_background_solve(
&ui,
&render_ctx,
design.clone(),
&generation,
multi_selected.clone(),
);
}
},
);
RUNTIME.with(|cell| cell.borrow_mut().debounce = Some(timer));
}
struct BackgroundSolveResult {
elapsed: Duration,
tiers: Vec<EditorTierItem>,
status_text: String,
status_is_problem: bool,
warnings: Vec<String>,
yield_texts: (String, String, String, String),
planes: Vec<GpuFacetPlane>,
solved: Option<Vec<SolvedTier>>,
gear: (u32, f32),
}
pub(super) fn dispatch_background_solve(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
design: Design,
generation: &Arc<AtomicU64>,
multi_selected: BTreeSet<usize>,
) {
let tier_count = design.tiers.len();
let started_generation = generation.load(Ordering::Relaxed);
let seq = RUNTIME.with(|cell| {
let mut rt = cell.borrow_mut();
rt.current_seq += 1;
rt.current_seq
});
ui.global::<EditorModel>().set_solve_running(true);
ui.global::<EditorModel>()
.set_status_text(solving_banner(tier_count, Duration::ZERO).into());
ui.global::<EditorModel>().set_status_is_problem(false);
let done_flag = Arc::new(AtomicBool::new(false));
let done_ticker = Arc::clone(&done_flag);
let ticker_ui = ui.as_weak();
thread::spawn(move || {
let start = Instant::now();
loop {
thread::sleep(TICK_INTERVAL);
if done_ticker.load(Ordering::Relaxed) {
break;
}
let elapsed = start.elapsed();
let _ = ticker_ui.upgrade_in_event_loop(move |ui| {
if is_current(seq) {
ui.global::<EditorModel>()
.set_status_text(solving_banner(tier_count, elapsed).into());
}
});
}
});
let ui_weak = ui.as_weak();
let render_ctx_for_apply = Arc::clone(render_ctx);
let generation = Arc::clone(generation);
thread::spawn(move || {
let start = Instant::now();
let n_d = design.effective_refractive_index();
let mut tiers = tier_items(&design, n_d);
apply_multi_selection(&mut tiers, &multi_selected);
let (status_text, status_is_problem) = status_text_and_is_problem(&design);
let warnings = manufacturability_warning_lines(&design);
let yield_texts = yield_report_texts(&design);
let planes = design_to_gpu_planes(&design);
let solved = design.solve().ok();
let gear = (
design.meta.gear_teeth_abs(),
design.meta.gear_reference_angle as f32,
);
let result = BackgroundSolveResult {
elapsed: start.elapsed(),
tiers,
status_text,
status_is_problem,
warnings,
yield_texts,
planes,
solved,
gear,
};
done_flag.store(true, Ordering::Relaxed);
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
apply_background_solve_result(
&ui,
&render_ctx_for_apply,
seq,
started_generation,
&generation,
result,
);
});
});
}
fn apply_background_solve_result(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
seq: u64,
started_generation: u64,
generation: &Arc<AtomicU64>,
result: BackgroundSolveResult,
) {
record_solve_duration(result.elapsed);
if !is_current(seq) {
return;
}
ui.global::<EditorModel>().set_solve_running(false);
if generation.load(Ordering::Relaxed) != started_generation {
return;
}
ui.global::<EditorModel>()
.set_tiers(ModelRc::new(VecModel::from(result.tiers)));
ui.global::<EditorModel>()
.set_status_text(result.status_text.into());
ui.global::<EditorModel>()
.set_status_is_problem(result.status_is_problem);
ui.global::<EditorModel>()
.set_manufacturability_warnings(ModelRc::new(VecModel::from(
result
.warnings
.into_iter()
.map(SharedString::from)
.collect::<Vec<_>>(),
)));
let (vol_yield_text, carat_text, sg_used_text, fit_text) = result.yield_texts;
ui.global::<EditorModel>()
.set_volumetric_yield_text(vol_yield_text.into());
ui.global::<EditorModel>()
.set_carat_weight_text(carat_text.into());
ui.global::<EditorModel>()
.set_specific_gravity_used_text(sg_used_text.into());
ui.global::<EditorModel>()
.set_preform_fit_warning(fit_text.into());
let planes = Arc::new(result.planes);
let mut ctx = render_ctx.lock().unwrap_or_else(PoisonError::into_inner);
ctx.active_planes = Arc::clone(&planes);
ctx.dirty = true;
ctx.design_gear = Some(result.gear);
let design_gear = ctx.design_gear;
let camera = CameraPose {
yaw: ctx.yaw,
pitch: ctx.pitch,
distance: ctx.distance,
};
drop(ctx);
let redraw_planes: Vec<(glam::Vec3, f32)> = planes
.iter()
.map(|p| (glam::Vec3::from(p.normal), -p.d))
.collect();
let size = (
ui.global::<SolidPreviewModel>().get_viewport_width() as u32,
ui.global::<SolidPreviewModel>().get_viewport_height() as u32,
);
let view_mode = ui.global::<SolidPreviewModel>().get_view_mode() as u8;
let (preview_state, solid_last_solved) = RUNTIME.with(|cell| {
let rt = cell.borrow();
(rt.preview_state.clone(), rt.solid_last_solved.clone())
});
if let Some(preview_state) = preview_state {
preview_state.request_redraw_with_gear(redraw_planes, camera, size, view_mode, design_gear);
}
if let (Some(solved), Some(cache)) = (result.solved, solid_last_solved) {
*cache.lock().unwrap_or_else(PoisonError::into_inner) = Some(solved);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_budget_disables_auto_solve_regardless_of_measurement() {
assert!(!should_schedule_auto_solve(None, Duration::ZERO));
assert!(!should_schedule_auto_solve(
Some(Duration::from_millis(1)),
Duration::ZERO
));
}
#[test]
fn no_measurement_yet_is_scheduled_optimistically() {
assert!(should_schedule_auto_solve(None, Duration::from_millis(300)));
}
#[test]
fn a_measurement_under_budget_is_scheduled() {
assert!(should_schedule_auto_solve(
Some(Duration::from_millis(120)),
Duration::from_millis(300)
));
}
#[test]
fn a_measurement_at_or_over_budget_is_not_scheduled() {
assert!(!should_schedule_auto_solve(
Some(Duration::from_millis(300)),
Duration::from_millis(300)
));
assert!(!should_schedule_auto_solve(
Some(Duration::from_secs(6)),
Duration::from_millis(300)
));
}
#[test]
fn a_freshly_recorded_sequence_number_is_current() {
let seq = RUNTIME.with(|cell| {
let mut rt = cell.borrow_mut();
rt.current_seq += 1;
rt.current_seq
});
assert!(is_current(seq));
}
#[test]
fn an_older_sequence_number_is_superseded_by_a_newer_dispatch() {
let old_seq = RUNTIME.with(|cell| {
let mut rt = cell.borrow_mut();
rt.current_seq += 1;
rt.current_seq
});
RUNTIME.with(|cell| cell.borrow_mut().current_seq += 1);
assert!(!is_current(old_seq));
}
#[test]
fn record_solve_duration_is_visible_to_the_next_schedule_decision() {
record_solve_duration(Duration::from_millis(42));
let last = RUNTIME.with(|cell| cell.borrow().last_solve);
assert_eq!(last, Some(Duration::from_millis(42)));
reset_for_new_design();
let last = RUNTIME.with(|cell| cell.borrow().last_solve);
assert_eq!(last, None);
}
#[test]
fn solving_banner_pluralizes_the_tier_count() {
assert!(solving_banner(1, Duration::ZERO).contains("1 tier)"));
assert!(solving_banner(2, Duration::ZERO).contains("2 tiers)"));
}
#[test]
fn auto_solve_off_note_reports_the_measured_seconds() {
let text = auto_solve_off_note(Duration::from_millis(5900));
assert!(text.contains("5.9s"));
}
}