use super::super::{
auto_solve, deep_solve,
material_lookup::{EditorMaterialLookup, resolved_gem_material},
optimize_solve::{self, OptimizeSolveOutcome},
stall_guard::stall_guard,
state::EditorState,
view::{
SolidLastSolved, build_optimize_preview_design, configured_optimize_max_evaluations,
deep_solve_tier_rows, format_deep_solve_report, optimize_change_rows, optimize_result_rows,
optimize_status_text, parse_optimize_weights, refresh_all_now, refresh_editor_panel_stale,
submit_design_ghost_preview, submit_preview_replan,
},
};
use crate::{
EditorModel, MainWindow, OptimizeResultRow,
bridge::render_thread::RenderContext,
gui::{batch::tilt, show_toast, solid_preview::preview_state::SolidPreviewState},
};
use indicatrix::{
geometry::{
meet_solver::{MeetConstraint, SolvedTier},
stone_metrics::ExternalProportions,
},
optics::materials::GemMaterial,
};
use indicatrix_cut_core::{
Design, DesignSolveError, Edit, MaterialSelection, ObjectiveWeights, OptimizeConfig,
OptimizeOutcome, free_tier_indices, optimize::SearchStage,
};
use indicatrix_vault::{db::sqlite::Database, model::tilt_curves::TiltPerformanceCurves};
use slint::{ComponentHandle, ModelRc, SharedString, VecModel};
use std::{
cell::RefCell,
collections::BTreeSet,
rc::Rc,
sync::{
Arc, Mutex,
atomic::{AtomicBool, AtomicU64, Ordering as AtomicOrdering},
},
thread,
};
thread_local! {
static LAST_DEEP_SOLVE_DELTAS: RefCell<Vec<deep_solve::TierMastDelta>> =
const { RefCell::new(Vec::new()) };
static DEEP_SOLVE_ACTIVITY_ID: RefCell<Option<u64>> = const { RefCell::new(None) };
static OPTIMIZE_ACTIVITY_ID: RefCell<Option<u64>> = const { RefCell::new(None) };
}
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| {
stall_guard("on_adopt_meet", || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
if index < 0 {
return;
}
let index = index as usize;
let mut st = state.borrow_mut();
let Some(constraint) = st
.design
.tiers
.get(index)
.and_then(|t| t.imported_meet.clone())
else {
return;
};
match st.apply(Edit::SetConstraint { index, constraint }) {
Ok(()) => {
let dirty: BTreeSet<usize> = std::iter::once(index).collect();
refresh_editor_panel_stale(&ui, &render_ctx, &st, &dirty);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
dirty,
false,
);
}
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) {
if let Some(activity) = auto_solve::activity() {
if let Some(id) = DEEP_SOLVE_ACTIVITY_ID.with(RefCell::take) {
activity.finish(id);
}
if let Some(id) = OPTIMIZE_ACTIVITY_ID.with(RefCell::take) {
activity.finish(id);
}
}
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);
ui.global::<EditorModel>().set_deep_solve_stale(false);
ui.global::<EditorModel>().set_optimize_stale(false);
LAST_DEEP_SOLVE_DELTAS.with(|cell| cell.borrow_mut().clear());
}
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.filter(|solved| {
!needs_background_baseline(Some(solved.as_slice()), st.design.tiers.len())
});
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);
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;
}
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;
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,
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());
}
}
}
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();
}
if let Some(id) = DEEP_SOLVE_ACTIVITY_ID.with(RefCell::take)
&& let Some(activity) = auto_solve::activity()
{
activity.finish(id);
}
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);
});
}
#[must_use]
fn tier_index_from_row_number(text: &str) -> Option<usize> {
text.strip_prefix('#')?
.parse::<usize>()
.ok()?
.checked_sub(1)
}
#[must_use]
fn verified_mast_for(deltas: &[deep_solve::TierMastDelta], tier_index: usize) -> Option<f64> {
deltas
.iter()
.find(|d| d.tier_index == tier_index)
.map(|d| d.after_mast)
}
pub(in crate::gui::editor) fn setup_deep_solve_pin_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_pin_verified_mast(move |tier_number: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Some(index) = tier_index_from_row_number(&tier_number) else {
return;
};
let mast = LAST_DEEP_SOLVE_DELTAS.with(|cell| verified_mast_for(&cell.borrow(), index));
let Some(mast) = mast else {
show_toast(
&ui,
"No verified mast recorded for this tier any more -- re-run Deep \
Solve.",
"error",
);
return;
};
let mut st = state.borrow_mut();
match st.apply(Edit::SetConstraint {
index,
constraint: MeetConstraint::ScaleReference(mast),
}) {
Ok(()) => {
drop(st);
refresh_all_now(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&state,
false,
);
show_toast(
&ui,
&format!("Pinned tier #{} to Deep Solve's verified mast.", index + 1),
"success",
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
}
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,
yield_weight: f32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
stall_guard("on_optimize", || {
begin_tier_optimize_run(
&ui,
&state,
&render_ctx,
&run_epoch,
OptimizeWeightForm {
windowing: &windowing,
extinction: &extinction,
tilt_brilliance: &tilt_brilliance,
yield_weight,
},
);
});
},
);
}
struct OptimizeRunPrep {
material_selection: MaterialSelection,
custom_materials: Vec<GemMaterial>,
defaulted_ri: Option<f64>,
config: OptimizeConfig,
provenance: RunProvenance,
pending_optimize: Arc<Mutex<Option<(OptimizeOutcome, u64)>>>,
this_run: u64,
run_epoch_done: Arc<AtomicU64>,
}
fn prepare_optimize_run(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
st: &EditorState,
weights: ObjectiveWeights,
run_epoch: &Arc<AtomicU64>,
) -> OptimizeRunPrep {
let mut material_selection = st.design.material.clone();
let custom_materials = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.custom_materials
.as_ref()
.clone();
let defaulted_ri =
default_optimize_material_ri(&st.design, &mut material_selection, &custom_materials);
let seed = ui
.global::<EditorModel>()
.get_optimize_seed_text()
.trim()
.parse::<u64>()
.unwrap_or(0);
let max_evaluations = configured_optimize_max_evaluations(ui);
let mut config = OptimizeConfig {
weights,
seed,
max_evaluations,
..OptimizeConfig::default()
};
if !ui.global::<EditorModel>().get_optimize_polish_enabled() {
config.polish_start_step_deg = None;
}
let this_run = run_epoch.fetch_add(1, AtomicOrdering::Relaxed) + 1;
OptimizeRunPrep {
material_selection,
custom_materials,
defaulted_ri,
config,
provenance: RunProvenance::capture(st),
pending_optimize: Arc::clone(&st.pending_optimize),
this_run,
run_epoch_done: Arc::clone(run_epoch),
}
}
#[derive(Clone, Copy)]
struct OptimizeWeightForm<'a> {
windowing: &'a str,
extinction: &'a str,
tilt_brilliance: &'a str,
yield_weight: f32,
}
fn parse_weights_or_toast(
ui: &MainWindow,
weight_form: OptimizeWeightForm<'_>,
) -> Option<ObjectiveWeights> {
match parse_optimize_weights(
weight_form.windowing,
weight_form.extinction,
weight_form.tilt_brilliance,
weight_form.yield_weight,
) {
Ok(weights) => Some(weights),
Err(e) => {
show_toast(ui, &e, "error");
None
}
}
}
fn begin_tier_optimize_run(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
run_epoch: &Arc<AtomicU64>,
weight_form: OptimizeWeightForm<'_>,
) {
let model = ui.global::<EditorModel>();
if model.get_optimize_running() || model.get_solve_running() || model.get_deep_solve_running() {
return;
}
let Some(weights) = parse_weights_or_toast(ui, weight_form) else {
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_snapshot = st.design.clone();
let only_selected = ui.global::<EditorModel>().get_optimize_only_selected();
let Some(design) = optimize_target_design(ui, &st, only_selected) else {
return;
};
let OptimizeRunPrep {
material_selection,
custom_materials,
defaulted_ri,
config,
provenance,
pending_optimize,
this_run,
run_epoch_done,
} = prepare_optimize_run(ui, render_ctx, &st, weights, 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 activity = auto_solve::activity();
let activity_id = activity.as_ref().map(|a| {
a.start(
"optimize",
"Optimize",
Some({
let state = Rc::clone(state);
Box::new(move || {
if let Some(handle) = state.borrow().optimize.as_ref() {
handle.cancel();
}
})
}),
)
});
if activity_id.is_some() {
OPTIMIZE_ACTIVITY_ID.with(|cell| *cell.borrow_mut() = activity_id);
}
let ui_weak = ui.as_weak();
let handle = optimize_solve::spawn_optimize_solve(
ui_weak,
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 let (Some(activity), Some(id)) = (auto_solve::activity(), activity_id) {
activity.finish(id);
}
OPTIMIZE_ACTIVITY_ID.with(|cell| {
if *cell.borrow() == activity_id {
*cell.borrow_mut() = None;
}
});
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,
custom: &[GemMaterial],
) -> Option<f64> {
if selection.name.is_some() || selection.refractive_index_override.is_some() {
return None;
}
let n_d = design.effective_refractive_index_with(custom);
selection.refractive_index_override = Some(n_d);
Some(n_d)
}
fn optimize_target_design(
ui: &MainWindow,
st: &EditorState,
only_selected: bool,
) -> Option<Design> {
if !only_selected || st.multi_selected.is_empty() {
return Some(st.design.clone());
}
let Some(solved) = auto_solve::solid_last_solved()
.and_then(|cache| {
cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
})
.filter(|solved| solved.len() == st.design.tiers.len())
else {
show_toast(
ui,
"Solve first, then Optimize with \"Only selected tiers\".",
"error",
);
return None;
};
Some(pin_non_selected_free_tiers(
&st.design,
&solved,
&st.multi_selected,
))
}
fn pin_non_selected_free_tiers(
design: &Design,
solved: &[SolvedTier],
keep_free: &BTreeSet<usize>,
) -> Design {
let mut restricted = design.clone();
for index in free_tier_indices(design) {
if keep_free.contains(&index) {
continue;
}
let Some(mast) = solved.get(index).map(|s| s.mast) else {
continue;
};
if let Some(tier) = restricted.tiers.get_mut(index) {
tier.constraint = MeetConstraint::ScaleReference(mast);
}
}
restricted
}
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 } => {
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");
}
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);
ui.global::<EditorModel>().set_optimize_stale(stale);
*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,
"Result discarded: the design changed while Optimize was running. Re-run \
Optimize.",
"warning",
);
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);
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.to_string(), "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 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_preview_toggled(move |enabled: bool| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let st = state.borrow();
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, started_generation)) = pending
&& started_generation == current_generation
{
let candidate = build_optimize_preview_design(&st.design, &outcome);
if submit_design_ghost_preview(&ui, &render_ctx, &preview_state, &candidate) {
return;
}
}
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
false,
);
});
}
pub(in crate::gui::editor) fn setup_batch_tilt_for_open_design_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
db: &Arc<Mutex<Database>>,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let db = Arc::clone(db);
let ui_weak = ui.as_weak();
let run_epoch = Arc::new(AtomicU64::new(0));
ui.global::<EditorModel>()
.on_compute_tilt_curves_for_open_design(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
begin_batch_tilt_for_open_design(&ui, &state, &render_ctx, &db, &run_epoch);
});
}
struct BatchTiltRun {
design: Design,
custom_materials: Vec<GemMaterial>,
source_entry_id: Option<i64>,
provenance: RunProvenance,
run_epoch_done: Arc<AtomicU64>,
this_run: u64,
db: Arc<Mutex<Database>>,
ui_weak: slint::Weak<MainWindow>,
}
impl BatchTiltRun {
fn is_current(&self) -> bool {
self.run_epoch_done.load(AtomicOrdering::Relaxed) == self.this_run
}
}
fn begin_batch_tilt_for_open_design(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
db: &Arc<Mutex<Database>>,
run_epoch: &Arc<AtomicU64>,
) {
let st = state.borrow();
let design = st.design.clone();
let source_entry_id = st.source_entry_id;
let provenance = RunProvenance::capture(&st);
drop(st);
let custom_materials = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.custom_materials
.as_ref()
.clone();
let this_run = run_epoch.fetch_add(1, AtomicOrdering::Relaxed) + 1;
let run = BatchTiltRun {
design,
custom_materials,
source_entry_id,
provenance,
run_epoch_done: Arc::clone(run_epoch),
this_run,
db: Arc::clone(db),
ui_weak: ui.as_weak(),
};
show_toast(
ui,
"Computing tilt-performance curves for this design...",
"info",
);
thread::spawn(move || run_batch_tilt_for_open_design(run));
}
fn run_batch_tilt_for_open_design(run: BatchTiltRun) {
let solved = match run.design.solve() {
Ok(solved) => solved,
Err(e) => {
let ui_weak = run.ui_weak.clone();
let run_epoch_done = Arc::clone(&run.run_epoch_done);
let this_run = run.this_run;
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
if run_epoch_done.load(AtomicOrdering::Relaxed) == this_run {
show_toast(&ui, &format!("Cannot compute tilt curves: {e}"), "error");
}
});
return;
}
};
let planes = auto_solve::design_to_gpu_planes_from_solved(&run.design, &solved);
let lookup = EditorMaterialLookup::new(&run.custom_materials);
let material = resolved_gem_material(&run.design.material, &lookup);
let cancel = AtomicBool::new(false);
let curves = tilt::tilt_curves_for_planes(&planes, &material, None, &cancel);
let ui_weak = run.ui_weak.clone();
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
apply_batch_tilt_result(&ui, curves, &run);
});
}
fn apply_batch_tilt_result(
ui: &MainWindow,
curves: Option<TiltPerformanceCurves>,
run: &BatchTiltRun,
) {
if !run.is_current() {
return;
}
let Some(curves) = curves else {
show_toast(
ui,
"Tilt-curve computation failed for this design.",
"error",
);
return;
};
if run.provenance.is_stale() {
show_toast(
ui,
"Result discarded: the design changed while computing tilt curves. Re-run to \
save an up-to-date result.",
"warning",
);
return;
}
match run.source_entry_id {
Some(entry_id) => {
if tilt::save_tilt_curves_for_entry(&run.db, entry_id, &curves) {
show_toast(
ui,
"Saved tilt-performance curves for this design.",
"success",
);
} else {
show_toast(
ui,
"Could not save tilt-performance curves for this design.",
"error",
);
}
}
None => show_toast(
ui,
"Computed tilt-performance curves -- save this design to the \
catalogue to keep them.",
"info",
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use deep_solve::TierMastDelta;
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));
}
#[test]
fn tier_index_from_row_number_parses_the_1_based_label_back_to_a_0_based_index() {
assert_eq!(tier_index_from_row_number("#1"), Some(0));
assert_eq!(tier_index_from_row_number("#17"), Some(16));
}
#[test]
fn tier_index_from_row_number_rejects_anything_that_is_not_hash_digits() {
assert_eq!(tier_index_from_row_number("17"), None);
assert_eq!(tier_index_from_row_number("#"), None);
assert_eq!(tier_index_from_row_number("#abc"), None);
assert_eq!(tier_index_from_row_number(""), None);
}
#[test]
fn tier_index_from_row_number_rejects_hash_zero() {
assert_eq!(tier_index_from_row_number("#0"), None);
}
fn delta(tier_index: usize, before: f64, after: f64) -> TierMastDelta {
TierMastDelta {
tier_index,
before_mast: before,
after_mast: after,
}
}
#[test]
fn verified_mast_for_finds_the_matching_tiers_after_mast() {
let deltas = vec![delta(0, 1.0, 1.1), delta(3, 2.0, 2.5)];
assert_eq!(verified_mast_for(&deltas, 3), Some(2.5));
}
#[test]
fn verified_mast_for_is_none_when_the_tier_is_not_in_the_list() {
let deltas = vec![delta(0, 1.0, 1.1)];
assert_eq!(verified_mast_for(&deltas, 5), None);
}
fn fixture_design() -> Design {
let preform = indicatrix_cut_core::PreformSpec::cylinder(96, 1.5, 1.0, 1.5);
Design::fresh(preform, 96, 8, 1.54)
}
#[test]
fn default_optimize_material_ri_leaves_an_already_named_selection_alone() {
let design = fixture_design();
let mut selection = indicatrix_cut_core::MaterialSelection {
name: Some("Diamond".to_string()),
specific_gravity_override: None,
refractive_index_override: None,
};
assert_eq!(
default_optimize_material_ri(&design, &mut selection, &[]),
None
);
assert_eq!(selection.refractive_index_override, None);
}
#[test]
fn default_optimize_material_ri_defaults_to_the_designs_effective_ri() {
let mut design = fixture_design();
design.meta.refractive_index = 1.62;
let mut selection = indicatrix_cut_core::MaterialSelection::none();
let defaulted = default_optimize_material_ri(&design, &mut selection, &[]);
assert_eq!(defaulted, Some(1.62));
assert_eq!(selection.refractive_index_override, Some(1.62));
}
#[test]
fn default_optimize_material_ri_resolves_a_custom_materials_own_ri() {
let mut design = fixture_design();
design.material.name = Some("My Custom Garnet".to_string());
design.meta.refractive_index = 1.54;
let mut custom = GemMaterial::diamond();
custom.name = "My Custom Garnet".to_string();
custom.dispersion = indicatrix::optics::dispersion::DispersionModel::Cauchy {
a: 1.74,
b: 0.0,
c: 0.0,
};
let mut selection = indicatrix_cut_core::MaterialSelection::none();
let defaulted = default_optimize_material_ri(&design, &mut selection, &[custom]);
assert!(
(defaulted.unwrap() - 1.74).abs() < 1e-6,
"expected the custom material's own RI (1.74), got {defaulted:?}"
);
assert!((selection.refractive_index_override.unwrap() - 1.74).abs() < 1e-6);
}
fn tier(name: &str, constraint: MeetConstraint) -> indicatrix_cut_core::ConstraintTier {
indicatrix_cut_core::ConstraintTier {
angle_deg: -40.0,
name: name.to_string(),
indices: vec![0.0, 24.0],
constraint,
imported_meet: None,
original_notes: None,
detached: Vec::new(),
}
}
fn two_free_tier_design() -> Design {
Design::new(
indicatrix_cut_core::PreformSpec::block(2.0, 1.0, 2.0),
indicatrix_cut_core::ScheduleMeta::default(),
vec![
tier("Anchor", MeetConstraint::ScaleReference(0.5)),
tier("Free1", MeetConstraint::MeetExisting),
tier("Free2", MeetConstraint::MeetExisting),
],
)
}
#[test]
fn pin_non_selected_free_tiers_pins_every_free_tier_not_kept() {
let design = two_free_tier_design();
let solved = design.solve().expect("fixture must solve");
let keep = BTreeSet::from([1]);
let restricted = pin_non_selected_free_tiers(&design, &solved, &keep);
assert!(matches!(
restricted.tiers[1].constraint,
MeetConstraint::MeetExisting
));
assert!(matches!(
restricted.tiers[2].constraint,
MeetConstraint::ScaleReference(_)
));
assert!(matches!(
restricted.tiers[0].constraint,
MeetConstraint::ScaleReference(_)
));
}
#[test]
fn pin_non_selected_free_tiers_keeps_every_free_tier_when_keep_set_is_full() {
let design = two_free_tier_design();
let solved = design.solve().expect("fixture must solve");
let keep = BTreeSet::from([1, 2]);
let restricted = pin_non_selected_free_tiers(&design, &solved, &keep);
assert!(matches!(
restricted.tiers[1].constraint,
MeetConstraint::MeetExisting
));
assert!(matches!(
restricted.tiers[2].constraint,
MeetConstraint::MeetExisting
));
}
#[test]
fn pin_non_selected_free_tiers_never_reorders_or_removes_a_tier() {
let design = two_free_tier_design();
let solved = design.solve().expect("fixture must solve");
let keep = BTreeSet::from([1]);
let restricted = pin_non_selected_free_tiers(&design, &solved, &keep);
assert_eq!(restricted.tiers.len(), design.tiers.len());
for (a, b) in restricted.tiers.iter().zip(&design.tiers) {
assert_eq!(a.name, b.name);
}
}
}