use super::{OPTIMIZE_ACTIVITY_ID, RunProvenance, optimize_outcome::handle_optimize_outcome};
use crate::{
EditorModel, MainWindow, OptimizeChangeRow, OptimizeModel, OptimizeResultRow,
bridge::render_thread::RenderContext,
gui::{
editor::{
auto_solve,
optimize_panel::{self, WeightBoxes},
optimize_solve::{self, OptimizeRunRequest},
stall_guard::stall_guard,
state::EditorState,
},
show_toast,
},
};
use indicatrix::optics::materials::GemMaterial;
use indicatrix_cut_core::{Design, MaterialSelection, OptimizeOutcome};
use indicatrix_editor::optimize_view::{
default_optimize_material_ri, optimize_start_status, pin_non_selected_free_tiers,
};
use slint::{ComponentHandle, ModelRc, SharedString, VecModel};
use std::{
cell::RefCell,
rc::Rc,
sync::{
Arc, Mutex,
atomic::{AtomicU64, Ordering as AtomicOrdering},
},
time::Instant,
};
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,
tone: f32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
stall_guard("on_optimize", || {
begin_tier_optimize_run(
&ui,
&state,
&render_ctx,
&run_epoch,
WeightBoxes {
windowing: &windowing,
extinction: &extinction,
tilt_brilliance: &tilt_brilliance,
yield_weight,
tone,
},
);
});
},
);
}
struct OptimizeRunPrep {
material_selection: MaterialSelection,
custom_materials: Vec<GemMaterial>,
defaulted_ri: Option<f64>,
provenance: RunProvenance,
pending_optimize: Arc<Mutex<Option<(OptimizeOutcome, u64)>>>,
this_run: u64,
run_epoch_done: Arc<AtomicU64>,
}
fn prepare_optimize_run(
render_ctx: &Arc<Mutex<RenderContext>>,
st: &EditorState,
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 this_run = run_epoch.fetch_add(1, AtomicOrdering::Relaxed) + 1;
OptimizeRunPrep {
material_selection,
custom_materials,
defaulted_ri,
provenance: RunProvenance::capture(st),
pending_optimize: Arc::clone(&st.pending_optimize),
this_run,
run_epoch_done: Arc::clone(run_epoch),
}
}
fn progress_fraction(progress: &optimize_solve::OptimizeSolveProgress) -> f32 {
(progress.evaluations as f32 / progress.max_evaluations.max(1) as f32).clamp(0.0, 1.0)
}
fn mark_run_started(ui: &MainWindow, st: &EditorState, defaulted_ri: Option<f64>) {
let editor = ui.global::<EditorModel>();
editor.set_optimize_running(true);
editor.set_optimize_status(optimize_start_status(defaulted_ri).into());
editor.set_optimize_status_is_problem(false);
editor.set_optimize_can_apply(false);
editor.set_optimize_result_rows(ModelRc::new(
VecModel::from(Vec::<OptimizeResultRow>::new()),
));
editor.set_optimize_change_rows(ModelRc::new(
VecModel::from(Vec::<OptimizeChangeRow>::new()),
));
optimize_panel::clear_results(ui);
*st.pending_optimize
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
}
fn begin_tier_optimize_run(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
run_epoch: &Arc<AtomicU64>,
boxes: WeightBoxes<'_>,
) {
let model = ui.global::<EditorModel>();
if model.get_optimize_running() || model.get_solve_running() || model.get_deep_solve_running() {
return;
}
let mut st = state.borrow_mut();
let plan = match optimize_panel::plan_from_ui(ui, &st.design, boxes) {
Ok(plan) => plan,
Err(message) => {
show_toast(ui, &message, "error");
return;
}
};
if plan.movable_tiers == 0 {
show_toast(
ui,
"Optimize has no tier it may change right now. Tick \"Vary anchored tiers\" in the \
Optimize tab, or adopt a tier first.",
"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 only_tiers =
(only_selected && !st.multi_selected.is_empty()).then(|| st.multi_selected.clone());
let OptimizeRunPrep {
material_selection,
custom_materials,
defaulted_ri,
provenance,
pending_optimize,
this_run,
run_epoch_done,
} = prepare_optimize_run(render_ctx, &st, run_epoch);
mark_run_started(ui, &st, defaulted_ri);
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 started = Instant::now();
let ui_weak = ui.as_weak();
let handle = optimize_solve::spawn_optimize_run(
ui_weak,
OptimizeRunRequest {
design,
material_selection,
custom_materials,
config: plan.config,
options: plan.options,
only_tiers,
},
|ui: &MainWindow, progress: optimize_solve::OptimizeSolveProgress| {
ui.global::<EditorModel>()
.set_optimize_status(optimize_progress_status(&progress).into());
ui.global::<OptimizeModel>()
.set_progress(progress_fraction(&progress));
},
move |ui: &MainWindow, outcome: optimize_solve::OptimizeRunOutcome| {
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,
started.elapsed().as_secs_f32(),
);
},
);
st.optimize = Some(handle);
}
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())
.map(|(_, solved)| solved)
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 optimize_progress_status(progress: &optimize_solve::OptimizeSolveProgress) -> String {
indicatrix_editor::optimize_view::optimize_progress_status(
progress.stage,
progress.evaluations,
progress.max_evaluations,
progress.start,
progress.elapsed.as_secs_f32(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix::geometry::meet_solver::MeetConstraint;
use indicatrix_cut_core::optimize::SearchStage;
use std::{collections::BTreeSet, time::Duration};
fn progress(
evaluations: usize,
max_evaluations: usize,
) -> optimize_solve::OptimizeSolveProgress {
optimize_solve::OptimizeSolveProgress {
evaluations,
max_evaluations,
stage: SearchStage::Coordinate,
start: None,
elapsed: Duration::from_secs(1),
}
}
#[test]
fn the_progress_bar_is_the_share_of_the_budget_done() {
assert!((progress_fraction(&progress(50, 200)) - 0.25).abs() < 1e-6);
assert_eq!(progress_fraction(&progress(0, 200)), 0.0);
}
#[test]
fn the_progress_bar_never_leaves_zero_to_one() {
assert_eq!(progress_fraction(&progress(300, 200)), 1.0);
assert_eq!(
progress_fraction(&progress(5, 0)),
1.0,
"no budget known yet"
);
assert_eq!(progress_fraction(&progress(0, 0)), 0.0);
}
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,
body_color_override: None,
body_color_bands_override: None,
absorption_path_scale_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);
}
}
}