use super::{RunProvenance, clear_analysis_results};
use crate::{
EditorModel, MainWindow, OptimizeModel,
bridge::render_thread::RenderContext,
gui::{
editor::{
optimize_panel::{self, FinishedRun},
optimize_solve::OptimizeRunOutcome,
state::EditorState,
view::{
SolidLastSolved, build_optimize_preview_design, refresh_all_now,
submit_design_ghost_preview, submit_preview_replan,
},
},
show_toast,
solid_preview::{cut_slider, preview_state::SolidPreviewState},
tutorial_events::raise,
},
};
use indicatrix_cut_core::{Design, DesignSolveError, OptimizeOutcome};
use indicatrix_editor::{
edit_intent::DRAIN_INTERVAL, guide::solving_events::OPTIMIZE_FINISHED,
optimize_view::optimize_run_status,
};
use slint::ComponentHandle;
use std::{
cell::{Cell, RefCell},
collections::BTreeSet,
rc::Rc,
sync::{Arc, Mutex, atomic::Ordering as AtomicOrdering},
};
pub(super) fn handle_optimize_outcome(
ui: &MainWindow,
outcome: OptimizeRunOutcome,
provenance: &RunProvenance,
pending_optimize: &Arc<Mutex<Option<(OptimizeOutcome, u64)>>>,
design: &Design,
elapsed_secs: f32,
) {
ui.global::<EditorModel>().set_optimize_running(false);
ui.global::<OptimizeModel>().set_progress(0.0);
if provenance.design_replaced() {
clear_analysis_results(ui);
return;
}
match outcome {
OptimizeRunOutcome::Failed { error } => {
let status = if matches!(error, DesignSolveError::MissingAnchor(_)) {
format!(
"Optimize failed: {error} -- add an Exact scale value tier there \
and Solve first."
)
} else {
format!("Optimize failed: {error}.")
};
ui.global::<EditorModel>()
.set_optimize_status(status.clone().into());
ui.global::<EditorModel>()
.set_optimize_status_is_problem(true);
ui.global::<EditorModel>().set_optimize_can_apply(false);
show_toast(ui, &status, "error");
}
OptimizeRunOutcome::Completed { result } | OptimizeRunOutcome::Cancelled { result } => {
let stale = provenance.is_stale();
let no_net_improvement = result
.candidates
.first()
.is_some_and(|best| best.score >= result.outcome.before_score);
let summary = optimize_run_status(&result, Some(elapsed_secs));
let status = if no_net_improvement {
format!(
"No net improvement at full tilt fidelity -- not recommended \
({summary})"
)
} else {
summary
};
ui.global::<EditorModel>()
.set_optimize_status(status.clone().into());
ui.global::<EditorModel>()
.set_optimize_status_is_problem(no_net_improvement);
ui.global::<EditorModel>().set_optimize_stale(stale);
let picked = optimize_panel::show_run(
ui,
pending_optimize,
FinishedRun {
result,
design: design.clone(),
generation: provenance.started_generation,
stale,
elapsed_secs,
},
);
let can_apply = picked.is_some() && !stale;
if stale {
show_toast(
ui,
"The design changed while Optimize was running -- its result is \
shown for reference but can no longer be applied. Re-run \
Optimize on the current design.",
"info",
);
} else {
show_toast(
ui,
&format!("Optimize finished: {status}"),
if no_net_improvement {
"info"
} else {
"success"
},
);
if can_apply {
ui.global::<EditorModel>().set_inspector_tab(2);
ui.global::<EditorModel>().set_inspector_collapsed(false);
}
raise(ui, OPTIMIZE_FINISHED);
}
}
}
}
pub(in crate::gui::editor) fn setup_optimize_cancel_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
) {
let state = Rc::clone(state);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_optimize_cancel(move || {
if let Some(handle) = state.borrow().optimize.as_ref() {
handle.cancel();
}
if let Some(ui) = ui_weak.upgrade() {
ui.global::<EditorModel>()
.set_optimize_status("Cancelling -- finishing the current evaluation...".into());
ui.global::<EditorModel>()
.set_optimize_status_is_problem(false);
}
});
}
pub(in crate::gui::editor) fn setup_optimize_apply_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_optimize_apply(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let mut st = state.borrow_mut();
let snapshot = st
.pending_optimize
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
let Some((outcome, started_generation)) = snapshot else {
return;
};
if st.generation.load(AtomicOrdering::Relaxed) != started_generation {
ui.global::<EditorModel>().set_optimize_can_apply(false);
show_toast(
&ui,
"Result discarded: the design changed while Optimize was running. Re-run \
Optimize.",
"warning",
);
return;
}
match optimize_panel::apply_pending(&mut st, &outcome) {
Ok(applied) => {
*st.pending_optimize
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
ui.global::<EditorModel>().set_optimize_can_apply(false);
optimize_panel::clear_results(&ui);
drop(st);
refresh_all_now(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&state,
false,
);
let applied_status = format!(
"Applied: {}",
ui.global::<EditorModel>().get_optimize_status()
);
ui.global::<EditorModel>()
.set_optimize_status(applied_status.into());
show_toast(
&ui,
&format!("Applied {applied} tier change(s) from Optimize."),
"success",
);
}
Err(e) => {
ui.global::<EditorModel>().set_optimize_can_apply(false);
show_toast(&ui, &e, "error");
}
}
});
}
pub(in crate::gui::editor) fn setup_optimize_preview_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let preview = Rc::new(OptimizePreview {
ui_weak: ui.as_weak(),
state: Rc::clone(state),
render_ctx: Arc::clone(render_ctx),
preview_state: Arc::clone(preview_state),
solid_last_solved: Arc::clone(solid_last_solved),
latest_toggle: LatestToggle::default(),
});
ui.global::<EditorModel>()
.on_optimize_preview_toggled(move |enabled: bool| {
preview.latest_toggle.set(enabled);
run_optimize_preview(&preview, cut_slider::REPLAN_RETRIES);
});
}
struct OptimizePreview {
ui_weak: slint::Weak<MainWindow>,
state: Rc<RefCell<EditorState>>,
render_ctx: Arc<Mutex<RenderContext>>,
preview_state: Arc<SolidPreviewState>,
solid_last_solved: SolidLastSolved,
latest_toggle: LatestToggle,
}
#[derive(Default)]
struct LatestToggle(Cell<bool>);
impl LatestToggle {
fn set(&self, enabled: bool) {
self.0.set(enabled);
}
const fn get(&self) -> bool {
self.0.get()
}
}
#[must_use]
fn preview_candidate<T>(
enabled: bool,
pending: Option<(T, u64)>,
current_generation: u64,
) -> Option<T> {
pending
.filter(|_| enabled)
.and_then(|(outcome, started_generation)| {
(started_generation == current_generation).then_some(outcome)
})
}
fn run_optimize_preview(preview: &Rc<OptimizePreview>, retries_left: u8) {
let Some(ui) = preview.ui_weak.upgrade() else {
return;
};
let Ok(st) = preview.state.try_borrow() else {
if let Some(next) = cut_slider::retries_after_busy(retries_left) {
let again = Rc::clone(preview);
slint::Timer::single_shot(DRAIN_INTERVAL, move || {
run_optimize_preview(&again, next);
});
}
return;
};
let enabled = preview.latest_toggle.get();
let pending = enabled
.then(|| {
st.pending_optimize
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
})
.flatten();
let current_generation = st.generation.load(AtomicOrdering::Relaxed);
if let Some(outcome) = preview_candidate(enabled, pending, current_generation) {
let candidate = build_optimize_preview_design(&st.design, &outcome);
if submit_design_ghost_preview(&ui, &preview.render_ctx, &preview.preview_state, &candidate)
{
return;
}
}
submit_preview_replan(
&ui,
&preview.render_ctx,
&preview.preview_state,
&preview.solid_last_solved,
&st,
BTreeSet::new(),
false,
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_a_switched_on_preview_of_a_current_result_shows_the_ghost() {
assert_eq!(
preview_candidate(true, Some(("result", 7)), 7),
Some("result")
);
assert_eq!(preview_candidate(false, Some(("result", 7)), 7), None);
assert_eq!(preview_candidate::<&str>(true, None, 7), None);
assert_eq!(preview_candidate(true, Some(("result", 6)), 7), None);
assert_eq!(preview_candidate(true, Some(("result", 8)), 7), None);
}
#[test]
fn a_retry_follows_the_checkbox_not_the_click_it_was_scheduled_for() {
let toggle = LatestToggle::default();
let pending = || Some(("ghost", 7));
let attempt = |toggle: &LatestToggle| preview_candidate(toggle.get(), pending(), 7);
toggle.set(true);
assert_eq!(attempt(&toggle), Some("ghost"), "the click itself");
toggle.set(false);
assert_eq!(
attempt(&toggle),
None,
"the retry after the box was unticked"
);
toggle.set(true);
assert_eq!(attempt(&toggle), Some("ghost"), "ticked again");
}
}