use super::{DEEP_SOLVE_ACTIVITY_ID, LAST_DEEP_SOLVE_DELTAS, RunProvenance};
use crate::{
EditorModel, MainWindow,
gui::{
editor::{
auto_solve, deep_solve,
stall_guard::stall_guard,
state::EditorState,
view::{deep_solve_tier_rows, format_deep_solve_report},
},
show_toast,
tutorial_events::raise,
},
};
use indicatrix::geometry::{meet_solver::SolvedTier, stone_metrics::ExternalProportions};
use indicatrix_cut_core::Design;
use indicatrix_editor::guide::solving_events::{DEEP_SOLVE_FINISHED, DEEP_SOLVE_STARTED};
use slint::{ComponentHandle, ModelRc, VecModel};
use std::{
cell::RefCell,
rc::Rc,
sync::{
Arc,
atomic::{AtomicU64, Ordering as AtomicOrdering},
},
};
pub(in crate::gui::editor) fn setup_deep_solve_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
) {
let state = Rc::clone(state);
auto_solve::stash_editor_state(&state);
let ui_weak = ui.as_weak();
let run_epoch = Arc::new(AtomicU64::new(0));
ui.global::<EditorModel>().on_deep_solve(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
stall_guard("on_deep_solve", || {
begin_deep_solve_run(&ui, &state, &run_epoch);
});
});
}
struct DeepSolveRunPrep {
design_for_run: Design,
current_masts: Option<Vec<SolvedTier>>,
design_snapshot: Design,
edited_since_load: bool,
source_label: Option<String>,
provenance: RunProvenance,
this_run: u64,
run_epoch_done: Arc<AtomicU64>,
}
#[must_use]
fn needs_background_baseline(cache: Option<&[SolvedTier]>, tier_count: usize) -> bool {
cache.is_none_or(|solved| solved.len() != tier_count)
}
fn capture_deep_solve_run_prep(st: &EditorState, run_epoch: &Arc<AtomicU64>) -> DeepSolveRunPrep {
let cached = auto_solve::solid_last_solved().and_then(|cache| {
cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
});
let current_masts = cached.and_then(|(_, solved)| {
(!needs_background_baseline(Some(solved.as_slice()), st.design.tiers.len()))
.then_some(solved)
});
let this_run = run_epoch.fetch_add(1, AtomicOrdering::Relaxed) + 1;
DeepSolveRunPrep {
design_for_run: st.design.clone(),
current_masts,
design_snapshot: st.design.clone(),
edited_since_load: st.history.can_undo(),
source_label: st.asc_filename.clone(),
provenance: RunProvenance::capture(st),
this_run,
run_epoch_done: Arc::clone(run_epoch),
}
}
fn begin_deep_solve_run(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
run_epoch: &Arc<AtomicU64>,
) {
let model = ui.global::<EditorModel>();
if model.get_deep_solve_running() || model.get_solve_running() || model.get_optimize_running() {
return;
}
let mut st = state.borrow_mut();
let Some(targets) = st.printed_proportions else {
show_toast(
ui,
"Deep Solve needs this design's printed proportions; none are available.",
"error",
);
return;
};
let DeepSolveRunPrep {
design_for_run,
current_masts,
design_snapshot,
edited_since_load,
source_label,
provenance,
this_run,
run_epoch_done,
} = capture_deep_solve_run_prep(&st, run_epoch);
let need_baseline = current_masts.is_none();
LAST_DEEP_SOLVE_DELTAS.with(|cell| cell.borrow_mut().clear());
ui.global::<EditorModel>().set_deep_solve_running(true);
ui.global::<EditorModel>()
.set_deep_solve_status("Deep solving... 0.0s elapsed".into());
ui.global::<EditorModel>()
.set_deep_solve_status_is_problem(false);
raise(ui, DEEP_SOLVE_STARTED);
let activity = auto_solve::activity();
let activity_id = activity.as_ref().map(|a| {
a.start(
"deep_solve",
"Deep Solve",
Some({
let state = Rc::clone(state);
Box::new(move || {
if let Some(handle) = state.borrow().deep_solve.as_ref() {
handle.cancel();
}
})
}),
)
});
if activity_id.is_some() {
DEEP_SOLVE_ACTIVITY_ID.with(|cell| *cell.borrow_mut() = activity_id);
}
let ui_weak = ui.as_weak();
let handle = deep_solve::spawn_deep_solve(
ui_weak,
design_for_run,
targets,
need_baseline,
|ui: &MainWindow, progress: deep_solve::DeepSolveProgress| {
ui.global::<EditorModel>().set_deep_solve_status(
format!(
"Deep solving... {:.1}s elapsed",
progress.elapsed.as_secs_f32()
)
.into(),
);
},
move |ui: &MainWindow, outcome: deep_solve::DeepSolveOutcome| {
if let (Some(activity), Some(id)) = (auto_solve::activity(), activity_id) {
activity.finish(id);
}
DEEP_SOLVE_ACTIVITY_ID.with(|cell| {
if *cell.borrow() == activity_id {
*cell.borrow_mut() = None;
}
});
if run_epoch_done.load(AtomicOrdering::Relaxed) != this_run {
return;
}
if provenance.design_replaced() {
return;
}
ui.global::<EditorModel>().set_deep_solve_running(false);
let Some(state_for_done) = auto_solve::editor_state() else {
tracing::warn!("deep solve finished before the editor state was stashed");
return;
};
apply_deep_solve_outcome(
ui,
outcome,
&DeepSolveRunContext {
provenance: &provenance,
targets,
source_label: source_label.as_deref(),
edited_since_load,
current_masts: current_masts.as_deref(),
design_snapshot: &design_snapshot,
state: &state_for_done,
},
);
},
);
st.deep_solve = Some(handle);
}
#[derive(Clone, Copy)]
struct DeepSolveRunContext<'a> {
provenance: &'a RunProvenance,
targets: ExternalProportions,
source_label: Option<&'a str>,
edited_since_load: bool,
current_masts: Option<&'a [SolvedTier]>,
design_snapshot: &'a Design,
state: &'a Rc<RefCell<EditorState>>,
}
fn apply_deep_solve_outcome(
ui: &MainWindow,
outcome: deep_solve::DeepSolveOutcome,
ctx: &DeepSolveRunContext<'_>,
) {
let DeepSolveRunContext {
provenance,
targets,
source_label,
edited_since_load,
current_masts,
design_snapshot,
state,
} = *ctx;
debug_assert!(
!provenance.design_replaced(),
"the caller must filter out a replaced design before calling this"
);
match outcome {
deep_solve::DeepSolveOutcome::Cancelled => {
ui.global::<EditorModel>()
.set_deep_solve_status("Deep Solve cancelled.".into());
ui.global::<EditorModel>()
.set_deep_solve_status_is_problem(false);
}
deep_solve::DeepSolveOutcome::Completed {
solved,
report,
baseline,
} => {
let mut status = format_deep_solve_report(&report, provenance.is_stale());
status.push_str(&deep_solve::format_verification_targets(
&targets,
source_label,
));
status.push_str(deep_solve::edited_since_load_caveat(edited_since_load));
let current_masts = current_masts.map(<[SolvedTier]>::to_vec).or(baseline);
if let Some(current) = current_masts.as_deref() {
let deltas = deep_solve::tier_mast_deltas(current, &solved);
status.push_str(&deep_solve::format_tier_mast_deltas(&deltas));
ui.global::<EditorModel>()
.set_deep_solve_tier_rows(ModelRc::new(VecModel::from(deep_solve_tier_rows(
&deltas,
design_snapshot,
))));
LAST_DEEP_SOLVE_DELTAS.with(|cell| cell.borrow_mut().clone_from(&deltas));
}
ui.global::<EditorModel>()
.set_deep_solve_status(status.into());
ui.global::<EditorModel>()
.set_deep_solve_status_is_problem(!report.accepted);
state.borrow_mut().deep_solve_result_generation = Some(provenance.started_generation);
ui.global::<EditorModel>()
.set_deep_solve_stale(provenance.is_stale());
raise(ui, DEEP_SOLVE_FINISHED);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_solved_tier(mast: f64) -> indicatrix::geometry::meet_solver::SolvedTier {
indicatrix::geometry::meet_solver::SolvedTier {
mast,
strategy: indicatrix::geometry::meet_solver::SolveStrategy::DependencyOrder,
detail: String::new(),
}
}
#[test]
fn needs_background_baseline_is_false_when_the_cache_matches_the_tier_count() {
let cache = vec![dummy_solved_tier(1.0), dummy_solved_tier(2.0)];
assert!(!needs_background_baseline(Some(&cache), 2));
}
#[test]
fn needs_background_baseline_is_true_when_the_cache_is_missing() {
assert!(needs_background_baseline(None, 2));
}
#[test]
fn needs_background_baseline_is_true_when_the_cache_is_the_wrong_tier_count() {
let cache = vec![dummy_solved_tier(1.0)];
assert!(needs_background_baseline(Some(&cache), 2));
}
}