use super::super::{
deep_solve,
optimize_solve::{self, OptimizeSolveOutcome},
state::EditorState,
view::{
SolidLastSolved, deep_solve_tier_rows, format_deep_solve_report, optimize_change_rows,
optimize_result_rows, optimize_status_text, parse_optimize_weights, refresh_all,
},
};
use crate::{
EditorModel, MainWindow, OptimizeResultRow,
bridge::render_thread::RenderContext,
gui::{show_toast, solid_preview::preview_state::SolidPreviewState},
};
use indicatrix_cut_core::{
Edit, OptimizeConfig, OptimizeOutcome, free_tier_indices, optimize::SearchStage,
};
use slint::{ComponentHandle, ModelRc, SharedString, VecModel};
use std::{
cell::RefCell,
rc::Rc,
sync::{
Arc, Mutex,
atomic::{AtomicU64, Ordering as AtomicOrdering},
},
};
pub(in crate::gui::editor) fn setup_adopt_meet_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_adopt_meet(move |index: i32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
if index < 0 {
return;
}
let mut st = state.borrow_mut();
let Some(constraint) = st
.design
.tiers
.get(index as usize)
.and_then(|t| t.imported_meet.clone())
else {
return;
};
match st.apply(Edit::SetConstraint {
index: index as usize,
constraint,
}) {
Ok(()) => refresh_all(&ui, &render_ctx, &preview_state, &solid_last_solved, &st),
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
}
struct RunProvenance {
generation: Arc<AtomicU64>,
started_generation: u64,
design_epoch: Arc<AtomicU64>,
started_epoch: u64,
}
impl RunProvenance {
fn capture(state: &EditorState) -> Self {
Self {
generation: Arc::clone(&state.generation),
started_generation: state.generation.load(AtomicOrdering::Relaxed),
design_epoch: Arc::clone(&state.design_epoch),
started_epoch: state.design_epoch.load(AtomicOrdering::Relaxed),
}
}
fn is_stale(&self) -> bool {
self.generation.load(AtomicOrdering::Relaxed) != self.started_generation
}
fn design_replaced(&self) -> bool {
self.design_epoch.load(AtomicOrdering::Relaxed) != self.started_epoch
}
}
pub(in crate::gui::editor) fn clear_analysis_results(ui: &MainWindow) {
ui.global::<EditorModel>().set_deep_solve_status("".into());
ui.global::<EditorModel>()
.set_deep_solve_status_is_problem(false);
ui.global::<EditorModel>()
.set_deep_solve_tier_rows(ModelRc::new(VecModel::from(
Vec::<crate::DeepSolveTierRow>::new(),
)));
ui.global::<EditorModel>().set_optimize_status("".into());
ui.global::<EditorModel>()
.set_optimize_status_is_problem(false);
ui.global::<EditorModel>()
.set_optimize_result_rows(ModelRc::new(
VecModel::from(Vec::<OptimizeResultRow>::new()),
));
ui.global::<EditorModel>()
.set_optimize_change_rows(ModelRc::new(VecModel::from(
Vec::<crate::OptimizeChangeRow>::new(),
)));
ui.global::<EditorModel>().set_optimize_can_apply(false);
}
pub(in crate::gui::editor) fn setup_deep_solve_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
) {
let state = Rc::clone(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;
};
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 tiers = st.design.meet_tier_inputs();
let gear_teeth_abs = st.design.meta.gear_teeth_abs();
let current_masts = st.design.solve().ok();
let design_snapshot = st.design.clone();
let edited_since_load = st.history.can_undo();
let provenance = RunProvenance::capture(&st);
let this_run = run_epoch.fetch_add(1, AtomicOrdering::Relaxed) + 1;
let run_epoch_done = Arc::clone(&run_epoch);
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);
let handle = deep_solve::spawn_deep_solve(
ui_weak.clone(),
gear_teeth_abs,
tiers,
targets,
|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 run_epoch_done.load(AtomicOrdering::Relaxed) != this_run {
return;
}
ui.global::<EditorModel>().set_deep_solve_running(false);
if provenance.design_replaced() {
clear_analysis_results(ui);
return;
}
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 } => {
let mut status = format_deep_solve_report(&report, provenance.is_stale());
status.push_str(&deep_solve::format_verification_targets(&targets));
status.push_str(deep_solve::edited_since_load_caveat(edited_since_load));
if let Some(current) = ¤t_masts {
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),
)));
}
ui.global::<EditorModel>()
.set_deep_solve_status(status.into());
ui.global::<EditorModel>()
.set_deep_solve_status_is_problem(!report.accepted);
}
}
},
);
st.deep_solve = Some(handle);
});
}
pub(in crate::gui::editor) fn setup_deep_solve_cancel_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
) {
let state = Rc::clone(state);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_deep_solve_cancel(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
if let Some(handle) = state.borrow().deep_solve.as_ref() {
handle.cancel();
}
ui.global::<EditorModel>().set_deep_solve_running(false);
ui.global::<EditorModel>()
.set_deep_solve_status("Deep Solve cancelled.".into());
ui.global::<EditorModel>()
.set_deep_solve_status_is_problem(false);
});
}
pub(in crate::gui::editor) fn setup_optimize_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let ui_weak = ui.as_weak();
let run_epoch = Arc::new(AtomicU64::new(0));
ui.global::<EditorModel>().on_optimize(
move |windowing: SharedString, extinction: SharedString, tilt_brilliance: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let model = ui.global::<EditorModel>();
if model.get_optimize_running()
|| model.get_solve_running()
|| model.get_deep_solve_running()
{
return;
}
let weights = match parse_optimize_weights(&windowing, &extinction, &tilt_brilliance) {
Ok(weights) => weights,
Err(e) => {
show_toast(&ui, &e, "error");
return;
}
};
let mut st = state.borrow_mut();
if free_tier_indices(&st.design).is_empty() {
show_toast(
&ui,
"Optimize has nothing free to move on this design right now.",
"error",
);
return;
}
let design = st.design.clone();
let design_snapshot = st.design.clone();
let mut material_selection = st.design.material.clone();
let defaulted_ri = default_optimize_material_ri(&st.design, &mut material_selection);
let custom_materials = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.custom_materials
.as_ref()
.clone();
let config = OptimizeConfig {
weights,
..OptimizeConfig::default()
};
let provenance = RunProvenance::capture(&st);
let pending_optimize = Arc::clone(&st.pending_optimize);
let this_run = run_epoch.fetch_add(1, AtomicOrdering::Relaxed) + 1;
let run_epoch_done = Arc::clone(&run_epoch);
ui.global::<EditorModel>().set_optimize_running(true);
ui.global::<EditorModel>()
.set_optimize_status(optimize_start_status(defaulted_ri).into());
ui.global::<EditorModel>()
.set_optimize_status_is_problem(false);
ui.global::<EditorModel>().set_optimize_can_apply(false);
ui.global::<EditorModel>()
.set_optimize_result_rows(ModelRc::new(VecModel::from(
Vec::<OptimizeResultRow>::new(),
)));
ui.global::<EditorModel>()
.set_optimize_change_rows(ModelRc::new(VecModel::from(Vec::<
crate::OptimizeChangeRow,
>::new())));
let handle = optimize_solve::spawn_optimize_solve(
ui_weak.clone(),
design,
material_selection,
custom_materials,
config,
|ui: &MainWindow, progress: optimize_solve::OptimizeSolveProgress| {
ui.global::<EditorModel>()
.set_optimize_status(optimize_progress_status(&progress).into());
},
move |ui: &MainWindow, outcome: OptimizeSolveOutcome| {
if run_epoch_done.load(AtomicOrdering::Relaxed) != this_run {
return;
}
handle_optimize_outcome(
ui,
outcome,
&provenance,
&pending_optimize,
&design_snapshot,
);
},
);
st.optimize = Some(handle);
},
);
}
fn default_optimize_material_ri(
design: &indicatrix_cut_core::Design,
selection: &mut indicatrix_cut_core::MaterialSelection,
) -> Option<f64> {
if selection.name.is_some() || selection.refractive_index_override.is_some() {
return None;
}
let n_d = design.effective_refractive_index();
selection.refractive_index_override = Some(n_d);
Some(n_d)
}
fn optimize_start_status(defaulted_ri: Option<f64>) -> String {
defaulted_ri.map_or_else(
|| "Optimizing... 0 evaluations, 0.0s elapsed".to_string(),
|n_d| {
format!(
"Optimizing (no material set -- scored for n_d={n_d:.4})... 0 \
evaluations, 0.0s elapsed"
)
},
)
}
fn optimize_progress_status(progress: &optimize_solve::OptimizeSolveProgress) -> String {
let elapsed = progress.elapsed.as_secs_f32();
match progress.stage {
SearchStage::BaselineFull => {
format!(
"Optimizing... scoring the starting point at full fidelity, {elapsed:.1}s elapsed"
)
}
SearchStage::Coordinate => format!(
"Optimizing... {} of ~{} evaluations, {elapsed:.1}s elapsed",
progress.evaluations, progress.max_evaluations
),
SearchStage::Polish => format!(
"Optimizing (polish)... {} of ~{} evaluations, {elapsed:.1}s elapsed",
progress.evaluations, progress.max_evaluations
),
SearchStage::FinalFull => {
format!("Optimizing... scoring the result at full fidelity, {elapsed:.1}s elapsed")
}
}
}
fn handle_optimize_outcome(
ui: &MainWindow,
outcome: OptimizeSolveOutcome,
provenance: &RunProvenance,
pending_optimize: &Arc<Mutex<Option<(OptimizeOutcome, u64)>>>,
design: &indicatrix_cut_core::Design,
) {
ui.global::<EditorModel>().set_optimize_running(false);
if provenance.design_replaced() {
clear_analysis_results(ui);
return;
}
match outcome {
OptimizeSolveOutcome::Failed { error } => {
ui.global::<EditorModel>().set_optimize_status(
format!(
"Optimize failed: {error} -- add an Exact scale value tier there \
and Solve first."
)
.into(),
);
ui.global::<EditorModel>()
.set_optimize_status_is_problem(true);
ui.global::<EditorModel>().set_optimize_can_apply(false);
show_toast(ui, &format!("Optimize failed: {error}"), "error");
}
OptimizeSolveOutcome::Completed { outcome }
| OptimizeSolveOutcome::Cancelled { outcome } => {
let stale = provenance.is_stale();
let no_net_improvement =
!outcome.changes.is_empty() && outcome.after_score >= outcome.before_score;
let status = if no_net_improvement {
format!(
"No net improvement at full tilt fidelity -- not recommended \
({})",
optimize_status_text(&outcome)
)
} else {
optimize_status_text(&outcome)
};
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_result_rows(ModelRc::new(VecModel::from(optimize_result_rows(
&outcome,
))));
ui.global::<EditorModel>()
.set_optimize_change_rows(ModelRc::new(VecModel::from(optimize_change_rows(
&outcome, design,
))));
let can_apply = !outcome.changes.is_empty() && !stale;
ui.global::<EditorModel>().set_optimize_can_apply(can_apply);
*pending_optimize
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
can_apply.then_some((outcome, provenance.started_generation));
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);
}
}
}
}
}
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,
"The design changed since this Optimize result was computed; re-run \
Optimize.",
"error",
);
return;
}
match st.apply_optimize_outcome(&outcome) {
Ok(applied) => {
*st.pending_optimize
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
ui.global::<EditorModel>().set_optimize_can_apply(false);
refresh_all(&ui, &render_ctx, &preview_state, &solid_last_solved, &st);
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.to_string(), "error");
}
}
});
}