use super::{
auto_solve,
deep_solve::TierMastDelta,
material_lookup::{
EditorMaterialLookup, MATERIAL_MATCH_TOLERANCE, material_for_refractive_index,
material_guess_candidates, nearest_built_in_material, traced_gem_material,
},
stale,
state::{
EditorState, ScratchDelta, apply_multi_selection, apply_proposed_angles,
builtin_preset_names, cutting_schedule_rows, design_label_text,
design_material_index_from_name, design_to_gpu_planes, gear_index_from_teeth,
girdle_and_ratio_texts, index_chip_items, manufacturability_warnings_tagged,
material_index_from_name, preform_mm_texts, preform_y_offset_mm_text, proportion_verdicts,
proportions_texts, push_multi_selected_count, push_rows, push_tiers, result_is_stale,
ri_source_text, should_open_anchor_explainer, status_text_and_is_problem,
status_text_and_is_problem_from_solved, tier_items, tier_items_from_solved,
tier_items_stale_with_last_solved, yield_report_texts, yield_report_texts_from_solved,
},
};
use crate::{
AngleItem, DeepSolveTierRow, EditorModel, EditorTierItem, IndexChipItem, MainWindow,
OptimizeChangeRow, OptimizeResultRow, SolidPreviewModel, TiltModel, UndoRedoLabels,
ViewportModel,
bridge::render_thread::{PlanesOwner, RenderContext},
gui::{
render::camera_lighting::contained_request_size,
solid_preview::preview_state::{CameraPose, ReplanRequest, SolidPreviewState},
},
};
pub(in crate::gui::editor) use crate::gui::solid_preview::preview_state::SolidLastSolved;
use indicatrix::geometry::meet_solver::{MeetConstraint, SolvedTier, VerifiedSolveReport};
use indicatrix_cut_core::{
Design, DesignSolveError, MaterialLookup, ObjectiveWeights, OptimizeOutcome, PreformShape,
critical_angle_deg, free_tier_indices,
};
use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel};
use std::{
cell::RefCell,
collections::BTreeSet,
rc::Rc,
sync::{Arc, Mutex, PoisonError, atomic::Ordering as AtomicOrdering},
time::{Duration, Instant},
};
pub(super) fn refresh_editor_panel(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &EditorState,
) -> Option<Vec<SolvedTier>> {
let solved_result = state.design.solve();
refresh_editor_panel_from_solve(ui, render_ctx, state, solved_result)
}
pub(super) fn refresh_editor_panel_from_solve(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &EditorState,
solved_result: Result<Vec<SolvedTier>, DesignSolveError>,
) -> Option<Vec<SolvedTier>> {
let delta = state.record_scratch_push();
let n_d = refresh_design_settings(ui, render_ctx, state, &delta);
let facet_count = solved_result.as_ref().ok().map_or(0, |solved| {
i32::try_from(facet_count_from_solved(&state.design, solved)).unwrap_or(i32::MAX)
});
ui.global::<EditorModel>().set_facet_count(facet_count);
let solved = solved_result.as_ref().ok();
let tiers = solved.map_or_else(
|| tier_items(&state.design, n_d),
|solved| tier_items_from_solved(&state.design, solved, n_d),
);
push_tier_list_and_undo_redo(ui, state, tiers);
let (status_text, is_problem) = solved.map_or_else(
|| status_text_and_is_problem(&state.design),
|solved| status_text_and_is_problem_from_solved(&state.design, solved),
);
ui.global::<EditorModel>()
.set_status_text(status_text.into());
ui.global::<EditorModel>().set_status_is_problem(is_problem);
ui.global::<EditorModel>()
.set_solve_state(if is_problem { "failed" } else { "solved" }.into());
if should_open_anchor_explainer(solved_result.is_err()) {
ui.global::<EditorModel>().set_anchor_explainer_open(true);
}
push_solve_dependent_panel_fields(ui, render_ctx, state, &delta, solved, n_d);
ui.global::<EditorModel>()
.set_design_label(design_label_text(state.asc_filename.as_deref()).into());
let rows: Vec<AngleItem> = solved.map_or_else(Vec::new, |solved| {
cutting_schedule_rows(&state.design, solved)
});
push_rows(
&ui.global::<EditorModel>().get_cutting_rows(),
rows,
|model| {
ui.global::<EditorModel>().set_cutting_rows(model);
},
);
refresh_deep_solve_availability(ui, state);
refresh_optimize_availability(ui, state);
solved_result.ok()
}
fn push_solve_dependent_panel_fields(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &EditorState,
delta: &ScratchDelta,
solved: Option<&Vec<SolvedTier>>,
n_d: f64,
) {
push_manufacturability_and_preform_scratch(ui, state, delta, solved);
push_yield_and_proportions(ui, render_ctx, state, delta, solved, n_d);
}
fn push_manufacturability_and_preform_scratch(
ui: &MainWindow,
state: &EditorState,
delta: &ScratchDelta,
solved: Option<&Vec<SolvedTier>>,
) {
let tagged_warnings =
manufacturability_warnings_tagged(&state.design, solved.map(Vec::as_slice));
let warning_tiers: Vec<i32> = tagged_warnings
.iter()
.map(|(index, _)| i32::try_from(*index).unwrap_or(i32::MAX))
.collect();
let warnings: Vec<SharedString> = tagged_warnings
.into_iter()
.map(|(_, text)| SharedString::from(text))
.collect();
push_rows(
&ui.global::<EditorModel>().get_manufacturability_warnings(),
warnings,
|model| {
ui.global::<EditorModel>()
.set_manufacturability_warnings(model);
},
);
push_rows(
&ui.global::<EditorModel>()
.get_manufacturability_warning_tiers(),
warning_tiers,
|model| {
ui.global::<EditorModel>()
.set_manufacturability_warning_tiers(model);
},
);
if delta.preform {
let preform = &state.design.preform;
ui.global::<EditorModel>()
.set_preform_shape_index(match preform.shape {
PreformShape::Block => 0,
PreformShape::Cylinder { .. } => 1,
});
ui.global::<EditorModel>()
.set_preform_half_width(format!("{:.4}", preform.half_width).into());
ui.global::<EditorModel>()
.set_preform_length_over_width(format!("{:.4}", preform.length_over_width).into());
ui.global::<EditorModel>()
.set_preform_depth(format!("{:.4}", preform.depth).into());
let (preform_half_width_mm, preform_depth_mm) = solved.map_or_else(
|| preform_mm_texts(&state.design),
|solved| preform_mm_texts_from_solved(&state.design, solved),
);
ui.global::<EditorModel>()
.set_preform_half_width_mm_text(preform_half_width_mm.into());
ui.global::<EditorModel>()
.set_preform_depth_mm_text(preform_depth_mm.into());
let mm_per_unit = solved.and_then(|solved| state.design.yield_report(solved).mm_per_unit);
ui.global::<EditorModel>().set_preform_y_offset_mm(
preform_y_offset_mm_text(state.design.preform_y_offset, mm_per_unit).into(),
);
}
}
fn push_yield_and_proportions(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &EditorState,
delta: &ScratchDelta,
solved: Option<&Vec<SolvedTier>>,
n_d: f64,
) {
push_yield_material_scratch(ui, state, delta);
let custom_sg = Arc::clone(
&render_ctx
.lock()
.unwrap_or_else(PoisonError::into_inner)
.custom_material_specific_gravity,
);
let (vol_yield_text, carat_text, sg_used_text, fit_text) = solved.map_or_else(
|| yield_report_texts(&state.design, &custom_sg),
|solved| yield_report_texts_from_solved(&state.design, solved, &custom_sg),
);
ui.global::<EditorModel>()
.set_volumetric_yield_text(vol_yield_text.into());
ui.global::<EditorModel>()
.set_carat_weight_text(carat_text.into());
ui.global::<EditorModel>()
.set_specific_gravity_used_text(sg_used_text.into());
ui.global::<EditorModel>()
.set_preform_fit_warning(fit_text.into());
let (table_pct, crown_height, pavilion_depth, total_depth, length_to_width) = solved
.map_or_else(
|| proportions_texts(&state.design),
|solved| proportions_texts_from_solved(&state.design, solved),
);
ui.global::<EditorModel>()
.set_proportions_table_pct(table_pct.into());
ui.global::<EditorModel>()
.set_proportions_crown_height(crown_height.into());
ui.global::<EditorModel>()
.set_proportions_pavilion_depth(pavilion_depth.into());
ui.global::<EditorModel>()
.set_proportions_total_depth(total_depth.into());
ui.global::<EditorModel>()
.set_proportions_length_to_width(length_to_width.into());
let (girdle_thickness, crown_to_width, pavilion_to_width, girdle_to_width) = solved
.map_or_else(
|| girdle_and_ratio_texts(&state.design),
|solved| girdle_and_ratio_texts_from_solved(&state.design, solved),
);
ui.global::<EditorModel>()
.set_girdle_thickness_text(girdle_thickness.into());
ui.global::<EditorModel>()
.set_crown_to_width_text(crown_to_width.into());
ui.global::<EditorModel>()
.set_pavilion_to_width_text(pavilion_to_width.into());
ui.global::<EditorModel>()
.set_girdle_to_width_text(girdle_to_width.into());
push_proportion_verdicts(ui, &state.design, solved, n_d);
}
fn push_proportion_verdicts(
ui: &MainWindow,
design: &Design,
solved: Option<&Vec<SolvedTier>>,
n_d: f64,
) {
let verdicts = solved
.and_then(|solved| design.stone_proportions(solved))
.map(|proportions| proportion_verdicts(design, &proportions, n_d));
let push = |level: i32,
reason: &str,
set_level: fn(&MainWindow, i32),
set_reason: fn(&MainWindow, SharedString)| {
set_level(ui, level);
set_reason(ui, reason.into());
};
let (level, reason) = verdicts.as_ref().map_or((-1, String::new()), |v| {
(v.table_pct.level, v.table_pct.reason.clone())
});
push(
level,
&reason,
|ui, v| {
ui.global::<EditorModel>()
.set_proportion_verdict_table_level(v);
},
|ui, v| {
ui.global::<EditorModel>()
.set_proportion_verdict_table_reason(v);
},
);
let (level, reason) = verdicts.as_ref().map_or((-1, String::new()), |v| {
(v.crown_angle.level, v.crown_angle.reason.clone())
});
push(
level,
&reason,
|ui, v| {
ui.global::<EditorModel>()
.set_proportion_verdict_crown_angle_level(v);
},
|ui, v| {
ui.global::<EditorModel>()
.set_proportion_verdict_crown_angle_reason(v);
},
);
let (level, reason) = verdicts.as_ref().map_or((-1, String::new()), |v| {
(v.pavilion_angle.level, v.pavilion_angle.reason.clone())
});
push(
level,
&reason,
|ui, v| {
ui.global::<EditorModel>()
.set_proportion_verdict_pavilion_angle_level(v);
},
|ui, v| {
ui.global::<EditorModel>()
.set_proportion_verdict_pavilion_angle_reason(v);
},
);
let (level, reason) = verdicts.as_ref().map_or((-1, String::new()), |v| {
(v.total_depth_pct.level, v.total_depth_pct.reason.clone())
});
push(
level,
&reason,
|ui, v| {
ui.global::<EditorModel>()
.set_proportion_verdict_total_depth_level(v);
},
|ui, v| {
ui.global::<EditorModel>()
.set_proportion_verdict_total_depth_reason(v);
},
);
let (level, reason) = verdicts.as_ref().map_or((-1, String::new()), |v| {
(v.girdle_pct.level, v.girdle_pct.reason.clone())
});
push(
level,
&reason,
|ui, v| {
ui.global::<EditorModel>()
.set_proportion_verdict_girdle_level(v);
},
|ui, v| {
ui.global::<EditorModel>()
.set_proportion_verdict_girdle_reason(v);
},
);
}
#[must_use]
pub(super) fn proportions_texts_from_solved(
design: &Design,
solved: &[SolvedTier],
) -> (String, String, String, String, String) {
let dash = || "-".to_string();
if design.tiers.is_empty() {
return (dash(), dash(), dash(), dash(), dash());
}
let Some(proportions) = design.stone_proportions(solved) else {
return (dash(), dash(), dash(), dash(), dash());
};
let mm_per_unit = design.yield_report(solved).mm_per_unit;
let proportions = mm_per_unit.map_or(proportions, |mm| proportions.to_mm(mm));
let unit = if mm_per_unit.is_some() { " mm" } else { "" };
let table_percent_text = proportions
.table_percent
.map_or_else(dash, |v| format!("{v:.1}%"));
let crown_height_text = proportions
.crown_height
.map_or_else(dash, |v| format!("{v:.3}{unit}"));
let pavilion_depth_text = proportions
.pavilion_depth
.map_or_else(dash, |v| format!("{v:.3}{unit}"));
let total_depth_text = format!("{:.3}{unit}", proportions.total_depth);
let length_to_width_text = proportions
.length_to_width
.map_or_else(dash, |v| format!("{v:.3}"));
(
table_percent_text,
crown_height_text,
pavilion_depth_text,
total_depth_text,
length_to_width_text,
)
}
#[must_use]
pub(super) fn girdle_and_ratio_texts_from_solved(
design: &Design,
solved: &[SolvedTier],
) -> (String, String, String, String) {
let dash = || "-".to_string();
if design.tiers.is_empty() {
return (dash(), dash(), dash(), dash());
}
let Some(proportions) = design.stone_proportions(solved) else {
return (dash(), dash(), dash(), dash());
};
let mm_per_unit = design.yield_report(solved).mm_per_unit;
let proportions = mm_per_unit.map_or(proportions, |mm| proportions.to_mm(mm));
let unit = if mm_per_unit.is_some() { " mm" } else { "" };
let girdle_thickness_text = proportions
.girdle_thickness
.map_or_else(dash, |v| format!("{v:.3}{unit}"));
let crown_to_width_percent_text = proportions
.crown_to_width_percent
.map_or_else(dash, |v| format!("{v:.1}%"));
let pavilion_to_width_percent_text = proportions
.pavilion_to_width_percent
.map_or_else(dash, |v| format!("{v:.1}%"));
let girdle_to_width_percent_text = proportions
.girdle_to_width_percent
.map_or_else(dash, |v| format!("{v:.1}%"));
(
girdle_thickness_text,
crown_to_width_percent_text,
pavilion_to_width_percent_text,
girdle_to_width_percent_text,
)
}
#[must_use]
pub(super) fn preform_mm_texts_from_solved(
design: &Design,
solved: &[SolvedTier],
) -> (String, String) {
let Some(mm_per_unit) = design.yield_report(solved).mm_per_unit else {
return (String::new(), String::new());
};
let preform = &design.preform;
(
format!("\u{2248} {:.3} mm", preform.half_width * mm_per_unit),
format!("\u{2248} {:.3} mm", preform.depth * mm_per_unit),
)
}
fn apply_pending_optimize_ghost(tiers: &mut [EditorTierItem], state: &EditorState) {
let current_generation = state.generation.load(AtomicOrdering::Relaxed);
let pending = state
.pending_optimize
.lock()
.unwrap_or_else(PoisonError::into_inner);
if let Some((outcome, started_generation)) = pending.as_ref()
&& *started_generation == current_generation
{
apply_proposed_angles(tiers, &outcome.changes);
}
}
fn push_tier_list_and_undo_redo(
ui: &MainWindow,
state: &EditorState,
mut tiers: Vec<EditorTierItem>,
) {
apply_multi_selection(&mut tiers, &state.multi_selected);
apply_pending_optimize_ghost(&mut tiers, state);
push_tiers(ui, tiers);
ui.global::<EditorModel>().set_is_dirty(state.is_dirty());
push_multi_selected_count(ui, state.multi_selected.len());
push_selected_tier_chips(ui, state);
ui.global::<EditorModel>()
.set_can_undo(state.history.can_undo());
ui.global::<EditorModel>()
.set_can_redo(state.history.can_redo());
ui.global::<UndoRedoLabels>().set_undo_label(
state
.history
.peek_undo()
.map_or_else(String::new, |e| e.describe(&state.design))
.into(),
);
ui.global::<UndoRedoLabels>().set_redo_label(
state
.history
.peek_redo()
.map_or_else(String::new, |e| e.describe(&state.design))
.into(),
);
}
fn push_yield_material_scratch(ui: &MainWindow, state: &EditorState, delta: &ScratchDelta) {
if delta.girdle {
ui.global::<EditorModel>().set_girdle_diameter_mm(
state
.design
.girdle_diameter_mm
.map_or_else(String::new, |mm| format!("{mm:.4}"))
.into(),
);
}
if delta.material {
ui.global::<EditorModel>()
.set_material_index(material_index_from_name(
state.design.material.name.as_deref(),
));
ui.global::<EditorModel>().set_specific_gravity_override(
state
.design
.material
.specific_gravity_override
.map_or_else(String::new, |sg| format!("{sg:.4}"))
.into(),
);
}
}
fn traced_material_for(
design: &Design,
custom: &[indicatrix::optics::materials::GemMaterial],
) -> (String, Option<String>) {
if let Some(name) = &design.material.name {
return if EditorMaterialLookup::new(custom).lookup(name).is_some() {
(name.clone(), None)
} else {
(
String::new(),
Some(format!(
"This design's material '{name}' is not a built-in preset or a \
saved custom material, so there is nothing to trace -- pick a \
material in Design Settings, or re-save the missing custom \
material, rather than rendering it as something else."
)),
)
};
}
let n_d = design.effective_refractive_index();
if let Some((name, _)) = nearest_built_in_material(n_d, MATERIAL_MATCH_TOLERANCE) {
return (name, None);
}
(
String::new(),
Some(format!(
"This design names no material, and its refractive index ({n_d:.4}) matches \
no built-in preset within {MATERIAL_MATCH_TOLERANCE:.2}. Pick a material in \
Design Settings -- rendering it as something else would give you the optics \
of a different stone."
)),
)
}
fn push_material_guess(ui: &MainWindow, design: &Design, n_d: f64) {
let clear = || {
ui.global::<EditorModel>()
.set_material_guess_text(SharedString::new());
ui.global::<EditorModel>()
.set_material_guess_name(SharedString::new());
ui.global::<EditorModel>()
.set_material_guess_other_candidates_text(SharedString::new());
};
if design.material.name.is_some() {
clear();
return;
}
let Some((name, _)) = material_for_refractive_index(n_d) else {
clear();
return;
};
ui.global::<EditorModel>()
.set_material_guess_text(format!("{name}? (from RI {n_d:.2})").into());
ui.global::<EditorModel>()
.set_material_guess_name(name.clone().into());
let others: Vec<String> = material_guess_candidates(n_d, MATERIAL_MATCH_TOLERANCE)
.into_iter()
.filter(|(candidate, _)| candidate != &name)
.map(|(candidate, ri)| format!("{candidate} ({ri:.3})"))
.collect();
ui.global::<EditorModel>()
.set_material_guess_other_candidates_text(if others.is_empty() {
SharedString::new()
} else {
format!("Also within tolerance: {}", others.join(", ")).into()
});
}
fn refresh_design_settings(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &EditorState,
delta: &ScratchDelta,
) -> f64 {
let design = &state.design;
let (n_d, options) = {
let ctx = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let n_d = design.effective_refractive_index_with(&ctx.custom_materials);
ui.global::<EditorModel>()
.set_ri_source_text(ri_source_text(&design.material, &ctx.custom_materials).into());
let options = state.material_combo_options(&ctx.custom_materials);
drop(ctx);
(n_d, options)
};
let selected_material_index = {
let mut ctx = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
sync_viewport_material_link(ui, &mut ctx, design)
};
if let Some(idx) = selected_material_index {
ui.global::<ViewportModel>()
.set_selected_material_index(idx);
}
let current_options = ui.global::<EditorModel>().get_material_combo_options();
let unchanged = current_options.row_count() == options.len()
&& current_options
.iter()
.zip(options.iter())
.all(|(current, new)| current == new.as_str());
if !unchanged {
ui.global::<EditorModel>()
.set_material_combo_options(ModelRc::new(VecModel::from(
options
.iter()
.cloned()
.map(SharedString::from)
.collect::<Vec<_>>(),
)));
}
ui.global::<EditorModel>()
.set_new_material_options(ModelRc::new(VecModel::from(
builtin_preset_names()
.into_iter()
.map(SharedString::from)
.collect::<Vec<_>>(),
)));
if delta.material {
ui.global::<EditorModel>()
.set_material_combo_index(design_material_index_from_name(
design.material.name.as_deref(),
&options,
));
ui.global::<EditorModel>().set_ri_override_text(
design
.material
.refractive_index_override
.map_or_else(String::new, |v| format!("{v:.4}"))
.into(),
);
}
ui.global::<EditorModel>()
.set_effective_ri_text(format!("{n_d:.4}").into());
ui.global::<EditorModel>()
.set_critical_angle_text(format!("{:.2}\u{b0}", critical_angle_deg(n_d)).into());
push_material_guess(ui, design, n_d);
if delta.gear {
ui.global::<EditorModel>()
.set_gear_index(gear_index_from_teeth(design.meta.gear_teeth));
ui.global::<EditorModel>()
.set_gear_custom_text(design.meta.gear_teeth.to_string().into());
}
if delta.symmetry {
ui.global::<EditorModel>()
.set_symmetry_order_text(design.meta.symmetry_order.to_string().into());
ui.global::<EditorModel>().set_mirror(design.meta.mirror);
ui.global::<EditorModel>()
.set_symmetry_preview_text("".into());
}
if delta.meta {
let (title, extra_headers) = design
.meta
.headers
.split_first()
.map_or((String::new(), String::new()), |(title, rest)| {
(title.clone(), rest.join(";"))
});
ui.global::<EditorModel>().set_design_title(title.into());
ui.global::<EditorModel>()
.set_design_extra_headers(extra_headers.into());
ui.global::<EditorModel>()
.set_design_footnotes(design.meta.footnotes.join(";").into());
ui.global::<EditorModel>().set_design_gear_reference_angle(
format!("{}", design.meta.gear_reference_angle).into(),
);
}
n_d
}
#[must_use]
pub(super) fn sync_viewport_material_link(
ui: &MainWindow,
ctx: &mut RenderContext,
design: &Design,
) -> Option<i32> {
if !ui.global::<ViewportModel>().get_viewport_material_linked() {
return None;
}
let (name, unresolved) = traced_material_for(design, &ctx.custom_materials);
if ctx.material_unresolved != unresolved {
ctx.material_unresolved.clone_from(&unresolved);
ctx.dirty = true;
}
let resolved = unresolved
.is_none()
.then(|| {
traced_gem_material(
&name,
&design.material,
&EditorMaterialLookup::new(&ctx.custom_materials),
)
})
.flatten();
if ctx.material_override != resolved {
ctx.material_override = resolved;
ctx.dirty = true;
}
if ctx.material_name != name {
ctx.material_name.clone_from(&name);
ctx.dirty = true;
}
let options = ui.global::<ViewportModel>().get_material_options();
let selected_material_index = crate::gui::startup_settings::find_option_index(&options, &name);
let stone_width_mm = design.girdle_diameter_mm.unwrap_or(0.0) as f32;
if (ctx.stone_width_mm - stone_width_mm).abs() > f32::EPSILON {
ctx.stone_width_mm = stone_width_mm;
ctx.dirty = true;
}
selected_material_index
}
#[must_use]
fn selected_tier_chips(design: &Design, selected_tier_index: i32) -> Vec<IndexChipItem> {
usize::try_from(selected_tier_index)
.ok()
.and_then(|idx| design.tiers.get(idx))
.map_or_else(Vec::new, |tier| {
index_chip_items(&tier.indices, &tier.detached)
})
}
pub(super) fn push_selected_tier_chips(ui: &MainWindow, state: &EditorState) {
let selected = ui.global::<EditorModel>().get_selected_tier_index();
let cheater_offset_text = usize::try_from(selected)
.ok()
.and_then(|index| state.design.cheater_offset_deg(index))
.map_or_else(String::new, |deg| format!("{deg:.2}"));
ui.global::<EditorModel>()
.set_selected_tier_cheater_offset_text(cheater_offset_text.into());
let note_text = usize::try_from(selected)
.ok()
.and_then(|index| state.design.tier_note(index))
.map_or_else(String::new, str::to_string);
ui.global::<EditorModel>()
.set_selected_tier_note_text(note_text.into());
let chips = selected_tier_chips(&state.design, selected);
push_rows(
&ui.global::<EditorModel>().get_selected_tier_chips(),
chips,
|model| ui.global::<EditorModel>().set_selected_tier_chips(model),
);
}
fn refresh_deep_solve_availability(ui: &MainWindow, state: &EditorState) {
let (available, hint) = deep_solve_hint(state);
ui.global::<EditorModel>()
.set_deep_solve_available(available);
ui.global::<EditorModel>().set_deep_solve_hint(hint.into());
}
#[must_use]
pub(super) fn configured_optimize_max_evaluations(ui: &MainWindow) -> usize {
ui.global::<EditorModel>()
.get_optimize_budget_text()
.parse::<usize>()
.ok()
.filter(|&v| v > 0)
.unwrap_or(200)
}
fn refresh_optimize_availability(ui: &MainWindow, state: &EditorState) {
let (available, hint) = optimize_hint(state, configured_optimize_max_evaluations(ui));
ui.global::<EditorModel>().set_optimize_available(available);
ui.global::<EditorModel>().set_optimize_hint(hint.into());
let current_generation = state.generation.load(AtomicOrdering::Relaxed);
let can_apply = state
.pending_optimize
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.is_some_and(|(_, started_generation)| *started_generation == current_generation);
ui.global::<EditorModel>().set_optimize_can_apply(can_apply);
}
pub(super) fn refresh_editor_panel_stale(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &EditorState,
dirty: &BTreeSet<usize>,
) {
push_stale_content(ui, render_ctx, state, dirty);
auto_solve::on_edit(ui, render_ctx, state);
}
fn push_stale_generation_badges(ui: &MainWindow, state: &EditorState, current_generation: u64) {
ui.global::<EditorModel>()
.set_deep_solve_stale(result_is_stale(
state.deep_solve_result_generation,
current_generation,
));
let optimize_result_generation = state
.pending_optimize
.lock()
.unwrap_or_else(PoisonError::into_inner)
.as_ref()
.map(|(_, generation)| *generation);
ui.global::<EditorModel>()
.set_optimize_stale(result_is_stale(
optimize_result_generation,
current_generation,
));
stale::refresh_badges(ui, current_generation);
}
fn push_stale_warnings(ui: &MainWindow, state: &EditorState) {
let tagged_warnings = manufacturability_warnings_tagged(&state.design, None);
let warning_tiers: Vec<i32> = tagged_warnings
.iter()
.map(|(index, _)| i32::try_from(*index).unwrap_or(i32::MAX))
.collect();
let warnings: Vec<SharedString> = tagged_warnings
.into_iter()
.map(|(_, text)| SharedString::from(text))
.collect();
push_rows(
&ui.global::<EditorModel>().get_manufacturability_warnings(),
warnings,
|model| {
ui.global::<EditorModel>()
.set_manufacturability_warnings(model);
},
);
push_rows(
&ui.global::<EditorModel>()
.get_manufacturability_warning_tiers(),
warning_tiers,
|model| {
ui.global::<EditorModel>()
.set_manufacturability_warning_tiers(model);
},
);
}
fn push_stale_preform_and_yield(ui: &MainWindow, state: &EditorState, delta: &ScratchDelta) {
if delta.preform {
let preform = &state.design.preform;
ui.global::<EditorModel>()
.set_preform_shape_index(match preform.shape {
PreformShape::Block => 0,
PreformShape::Cylinder { .. } => 1,
});
ui.global::<EditorModel>()
.set_preform_half_width(format!("{:.4}", preform.half_width).into());
ui.global::<EditorModel>()
.set_preform_length_over_width(format!("{:.4}", preform.length_over_width).into());
ui.global::<EditorModel>()
.set_preform_depth(format!("{:.4}", preform.depth).into());
}
ui.global::<EditorModel>()
.set_preform_half_width_mm_text("".into());
ui.global::<EditorModel>()
.set_preform_depth_mm_text("".into());
ui.global::<EditorModel>().set_preform_y_offset_mm(
preform_y_offset_mm_text(state.design.preform_y_offset, None).into(),
);
push_yield_material_scratch(ui, state, delta);
ui.global::<EditorModel>()
.set_volumetric_yield_text("".into());
ui.global::<EditorModel>().set_carat_weight_text("".into());
ui.global::<EditorModel>()
.set_specific_gravity_used_text("".into());
ui.global::<EditorModel>()
.set_preform_fit_warning("".into());
}
fn push_stale_proportions_reset(ui: &MainWindow) {
ui.global::<EditorModel>()
.set_proportions_table_pct("-".into());
ui.global::<EditorModel>()
.set_proportions_crown_height("-".into());
ui.global::<EditorModel>()
.set_proportions_pavilion_depth("-".into());
ui.global::<EditorModel>()
.set_proportions_total_depth("-".into());
ui.global::<EditorModel>()
.set_proportions_length_to_width("-".into());
ui.global::<EditorModel>()
.set_girdle_thickness_text("-".into());
ui.global::<EditorModel>()
.set_crown_to_width_text("-".into());
ui.global::<EditorModel>()
.set_pavilion_to_width_text("-".into());
ui.global::<EditorModel>()
.set_girdle_to_width_text("-".into());
ui.global::<EditorModel>()
.set_cutting_rows(ModelRc::new(VecModel::from(Vec::<AngleItem>::new())));
ui.global::<EditorModel>().set_facet_count(0);
}
fn push_stale_content(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &EditorState,
dirty: &BTreeSet<usize>,
) {
push_trace_staleness(ui, render_ctx, state);
let current_generation = state.generation.load(AtomicOrdering::Relaxed);
push_stale_generation_badges(ui, state, current_generation);
let delta = state.record_scratch_push();
let n_d = refresh_design_settings(ui, render_ctx, state, &delta);
let last_solved = auto_solve::solid_last_solved().and_then(|cache| {
cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
});
let tiers =
tier_items_stale_with_last_solved(&state.design, n_d, last_solved.as_deref(), dirty);
push_tier_list_and_undo_redo(ui, state, tiers);
ui.global::<EditorModel>().set_status_text(
"Not solved -- click Solve to compute masts and validate this design.".into(),
);
ui.global::<EditorModel>().set_status_is_problem(true);
ui.global::<EditorModel>().set_solve_state("stale".into());
push_stale_warnings(ui, state);
push_stale_preform_and_yield(ui, state, &delta);
push_stale_proportions_reset(ui);
ui.global::<EditorModel>()
.set_design_label(design_label_text(state.asc_filename.as_deref()).into());
refresh_deep_solve_availability(ui, state);
refresh_optimize_availability(ui, state);
}
pub(super) fn scaled_viewport_size(ui: &MainWindow) -> (u32, u32) {
let scale = ui.window().scale_factor();
(
(ui.global::<SolidPreviewModel>().get_viewport_width() * scale) as u32,
(ui.global::<SolidPreviewModel>().get_viewport_height() * scale) as u32,
)
}
fn push_trace_staleness(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &EditorState,
) {
let generation = state.generation.load(std::sync::atomic::Ordering::Relaxed);
let stale = {
let ctx = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
ctx.traced_planes_are_stale(generation)
};
ui.global::<ViewportModel>().set_trace_stale(stale);
}
fn refresh_viewport(
ui: &MainWindow,
render_ctx: &Arc<Mutex<crate::bridge::render_thread::RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
state: &EditorState,
solved: Option<&[SolvedTier]>,
) {
let mut ctx = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let planes_for_claim = solved.map_or_else(
|| design_to_gpu_planes(&state.design),
|solved| auto_solve::design_to_gpu_planes_from_solved(&state.design, solved),
);
ctx.claim_active_planes(
std::sync::Arc::new(planes_for_claim),
Some((
state.design.meta.gear_teeth_abs(),
state.design.meta.gear_reference_angle as f32,
)),
PlanesOwner::Editor {
generation: state.generation.load(std::sync::atomic::Ordering::Relaxed),
},
);
ctx.dirty = true;
let design_gear = ctx.design_gear;
let planes: Vec<(glam::Vec3, f32)> = ctx
.active_planes
.iter()
.map(|p| (glam::Vec3::from(p.normal), -p.d))
.collect();
let view_mode = ui.global::<SolidPreviewModel>().get_view_mode() as u8;
let size = contained_request_size(view_mode, scaled_viewport_size(ui), (ctx.width, ctx.height));
preview_state.request_redraw_with_gear(
planes,
CameraPose {
yaw: ctx.yaw,
pitch: ctx.pitch,
distance: ctx.distance,
},
size,
view_mode,
design_gear,
);
drop(ctx);
ui.global::<TiltModel>()
.set_cached_curve_material("".into());
if ui.global::<TiltModel>().get_dialog_open() {
ui.global::<TiltModel>().invoke_request_tilt_profile_axes();
}
if let Some(solved) = solved {
*solid_last_solved
.lock()
.unwrap_or_else(PoisonError::into_inner) = Some(solved.to_vec());
}
push_trace_staleness(ui, render_ctx, state);
}
pub(super) fn submit_preview_replan(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
state: &EditorState,
dirty: BTreeSet<usize>,
force_full_solve: bool,
) {
submit_preview_replan_for(
ui,
render_ctx,
preview_state,
solid_last_solved,
ReplanSource {
design: &state.design,
generation: state.generation.load(AtomicOrdering::Relaxed),
multi_selected: &state.multi_selected,
},
dirty,
force_full_solve,
);
}
#[derive(Clone, Copy)]
pub(super) struct ReplanSource<'a> {
pub(super) design: &'a Design,
pub(super) generation: u64,
pub(super) multi_selected: &'a BTreeSet<usize>,
}
pub(super) fn submit_preview_replan_for(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
source: ReplanSource<'_>,
dirty: BTreeSet<usize>,
force_full_solve: bool,
) {
let ReplanSource {
design,
generation,
multi_selected,
} = source;
let last_solved = if force_full_solve {
None
} else {
solid_last_solved
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
};
let (camera, render_size, n_d) = {
let ctx = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
(
CameraPose {
yaw: ctx.yaw,
pitch: ctx.pitch,
distance: ctx.distance,
},
(ctx.width, ctx.height),
design.effective_refractive_index_with(&ctx.custom_materials),
)
};
let selected_tier = usize::try_from(ui.global::<EditorModel>().get_selected_tier_index()).ok();
let view_mode = ui.global::<SolidPreviewModel>().get_view_mode() as u8;
let size = contained_request_size(view_mode, scaled_viewport_size(ui), render_size);
let show_preform = ui.global::<SolidPreviewModel>().get_show_preform_planes();
let enlarged_panel = ui
.global::<SolidPreviewModel>()
.get_diagram_enlarged_panel();
let design_snapshot = Arc::new(design.clone());
auto_solve::stash_current_design(
generation,
Arc::clone(&design_snapshot),
multi_selected.clone(),
);
let cutoff = ui.global::<SolidPreviewModel>().get_tier_cutoff();
preview_state.set_tier_cutoff(usize::try_from(cutoff).ok());
preview_state.request_replan(ReplanRequest {
design: Arc::clone(&design_snapshot),
dirty,
last_solved,
camera,
size,
selected_tier,
n_d,
view_mode,
generation,
show_preform,
enlarged_panel,
});
}
pub(super) fn push_solved_preview(
ui: &MainWindow,
design: &Design,
solved: &[SolvedTier],
multi_selected: &BTreeSet<usize>,
custom_materials: &[indicatrix::optics::materials::GemMaterial],
custom_sg: &[(String, f64)],
) {
let n_d = design.effective_refractive_index_with(custom_materials);
let mut tiers = tier_items_from_solved(design, solved, n_d);
apply_multi_selection(&mut tiers, multi_selected);
push_tiers(ui, tiers);
push_multi_selected_count(ui, multi_selected.len());
let (status_text, is_problem) = status_text_and_is_problem_from_solved(design, solved);
ui.global::<EditorModel>()
.set_status_text(status_text.into());
ui.global::<EditorModel>().set_status_is_problem(is_problem);
ui.global::<EditorModel>()
.set_solve_state(if is_problem { "failed" } else { "solved" }.into());
let tagged_warnings = manufacturability_warnings_tagged(design, Some(solved));
let warning_tiers: Vec<i32> = tagged_warnings
.iter()
.map(|(index, _)| i32::try_from(*index).unwrap_or(i32::MAX))
.collect();
let warnings: Vec<SharedString> = tagged_warnings
.into_iter()
.map(|(_, text)| SharedString::from(text))
.collect();
push_rows(
&ui.global::<EditorModel>().get_manufacturability_warnings(),
warnings,
|model| {
ui.global::<EditorModel>()
.set_manufacturability_warnings(model);
},
);
push_rows(
&ui.global::<EditorModel>()
.get_manufacturability_warning_tiers(),
warning_tiers,
|model| {
ui.global::<EditorModel>()
.set_manufacturability_warning_tiers(model);
},
);
let (vol_yield_text, carat_text, sg_used_text, fit_text) =
yield_report_texts_from_solved(design, solved, custom_sg);
ui.global::<EditorModel>()
.set_volumetric_yield_text(vol_yield_text.into());
ui.global::<EditorModel>()
.set_carat_weight_text(carat_text.into());
ui.global::<EditorModel>()
.set_specific_gravity_used_text(sg_used_text.into());
ui.global::<EditorModel>()
.set_preform_fit_warning(fit_text.into());
}
pub(super) fn refresh_all_now(
ui: &MainWindow,
render_ctx: &Arc<Mutex<crate::bridge::render_thread::RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
state: &Rc<RefCell<EditorState>>,
wholesale: bool,
) {
let (plane_estimate, last_solve) = {
let st = state.borrow();
let plane_estimate: usize = st.design.tiers.iter().map(|t| t.indices.len()).sum();
let last_solve = if wholesale {
None
} else {
auto_solve::last_solve()
};
(plane_estimate, last_solve)
};
if !auto_solve::should_solve_synchronously(plane_estimate, last_solve) {
let st = state.borrow();
refresh_all(
ui,
render_ctx,
preview_state,
solid_last_solved,
&st,
wholesale,
);
return;
}
ui.global::<EditorModel>().set_solve_running(true);
ui.global::<EditorModel>().set_solve_state("solving".into());
let ui_weak = ui.as_weak();
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let state = Rc::clone(state);
slint::Timer::single_shot(Duration::ZERO, move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let st = state.borrow();
refresh_all(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
wholesale,
);
});
}
pub(super) fn refresh_all(
ui: &MainWindow,
render_ctx: &Arc<Mutex<crate::bridge::render_thread::RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
state: &EditorState,
wholesale: bool,
) {
let plane_estimate: usize = state.design.tiers.iter().map(|t| t.indices.len()).sum();
let last_solve = if wholesale {
auto_solve::reset_for_new_design();
None
} else {
auto_solve::last_solve()
};
if auto_solve::should_solve_synchronously(plane_estimate, last_solve) {
ui.global::<EditorModel>().set_solve_running(true);
ui.global::<EditorModel>().set_solve_state("solving".into());
let start = Instant::now();
let solved_result = state.design.solve();
let elapsed = start.elapsed();
auto_solve::record_solve_duration(elapsed);
ui.global::<EditorModel>()
.set_last_solve_duration_ms(i32::try_from(elapsed.as_millis()).unwrap_or(i32::MAX));
let solved = refresh_editor_panel_from_solve(ui, render_ctx, state, solved_result);
refresh_viewport(
ui,
render_ctx,
preview_state,
solid_last_solved,
state,
solved.as_deref(),
);
ui.global::<EditorModel>().set_solve_running(false);
} else {
let all_dirty: BTreeSet<usize> = (0..state.design.tiers.len()).collect();
push_stale_content(ui, render_ctx, state, &all_dirty);
auto_solve::dispatch_background_solve(
ui,
render_ctx,
state.design.clone(),
&state.generation,
state.multi_selected.clone(),
);
}
}
fn deep_solve_hint(state: &EditorState) -> (bool, String) {
if state.printed_proportions.is_none() {
return (
false,
"Deep Solve needs this design's printed proportions (Vol/W^3, L/W, C/W, P/W, \
H/W) from the catalogue to verify against -- unavailable for a new or \
placeholder-reconstructed design."
.to_string(),
);
}
let has_repairable = state
.design
.tiers
.iter()
.any(|t| !matches!(t.constraint, MeetConstraint::ScaleReference(_)));
if has_repairable {
(
true,
"Slow (a mean of ~68 solves per design on the corpus -- minutes on a large \
design); runs off the UI thread and can be cancelled."
.to_string(),
)
} else {
(
false,
"Every tier is currently pinned to its recorded mast, exactly as imported -- \
Deep Solve has nothing to repair until you convert a tier to a meet \
constraint (the tier list's Adopt action)."
.to_string(),
)
}
}
pub(super) fn format_deep_solve_report(report: &VerifiedSolveReport, stale: bool) -> String {
let verdict = if report.accepted {
"ACCEPTED -- reproduces the printed figures to verification accuracy (not a \
correctness proof, only a strong external signal)"
} else {
"not accepted -- still deviates from the printed figures"
};
let scores = if report.initial_score.is_finite() {
format!(
"combined deviation {:.4} -> {:.4} ({} vertex-level repair(s), {} anchor \
calibration move(s), {} pipeline run(s))",
report.initial_score,
report.final_score,
report.overrides_applied,
report.anchor_moves_applied,
report.pipeline_runs
)
} else {
"none of this design's printed figures overlapped what could be measured -- \
unverifiable"
.to_string()
};
let stale_note = if stale {
" NOTE: the design changed while this ran -- re-run Deep Solve for a result that \
reflects the current schedule."
} else {
""
};
format!("Deep Solve: {verdict}. {scores}.{stale_note}")
}
fn tier_name_for_row(design: &Design, tier_index: usize) -> String {
design
.tiers
.get(tier_index)
.map(|tier| tier.name.clone())
.unwrap_or_default()
}
#[must_use]
pub(super) fn deep_solve_tier_rows(
deltas: &[TierMastDelta],
design: &Design,
) -> Vec<DeepSolveTierRow> {
deltas
.iter()
.map(|d| DeepSolveTierRow {
tier_number: format!("#{}", d.tier_index + 1).into(),
name: tier_name_for_row(design, d.tier_index).into(),
before_mast: format!("{:.4}", d.before_mast).into(),
after_mast: format!("{:.4}", d.after_mast).into(),
delta: format!("{:+.4}", d.delta()).into(),
})
.collect()
}
#[must_use]
pub(super) fn optimize_change_rows(
outcome: &OptimizeOutcome,
design: &Design,
) -> Vec<OptimizeChangeRow> {
outcome
.changes
.iter()
.map(|change| OptimizeChangeRow {
tier_number: format!("#{}", change.index + 1).into(),
name: tier_name_for_row(design, change.index).into(),
from_angle: format!("{:.2}\u{b0}", change.from_deg).into(),
to_angle: format!("{:.2}\u{b0}", change.to_deg).into(),
delta: format!("{:+.2}\u{b0}", change.to_deg - change.from_deg).into(),
})
.collect()
}
#[must_use]
pub(super) fn facet_count_from_solved(design: &Design, solved: &[SolvedTier]) -> usize {
design.planes_from_solved(solved).len()
}
fn optimize_hint(state: &EditorState, max_evaluations: usize) -> (bool, String) {
let free = free_tier_indices(&state.design);
if free.is_empty() {
(
false,
"Every tier is currently pinned as a scale reference -- a freshly \
imported design starts this way, and that is correct, not broken. \
Optimize has nothing free to move until you adopt at least one tier's \
real meet constraint (the tier list's Adopt action) or author one by \
hand."
.to_string(),
)
} else {
(
true,
format!(
"Coordinate search over {} free tier angle(s), scored on windowing, \
extinction, and tilt brilliance under a FIXED canonical light pose \
(not necessarily the light you see in the viewport right now -- drag \
the light and these figures can disagree with the trace/HUD until you \
re-run Optimize). Roughly 7 ms per evaluation on a small design, but \
up to several seconds each on a large, heavily meet-derived one (a \
{max_evaluations}-evaluation budget can then take minutes) -- plus two \
fixed full-fidelity scorings (one before, one after the search) that \
can each take over a second on their own, so even a fast run has some \
up-front and trailing wait beyond the quoted per-evaluation cost. Runs \
off the UI thread and can be cancelled.",
free.len()
),
)
}
}
#[must_use]
fn after_with_delta(before: f32, after: f32, higher_is_better: bool, unit: &str) -> (String, i32) {
let delta = after - before;
let direction = if delta.abs() < f32::EPSILON {
0
} else if (higher_is_better && delta > 0.0) || (!higher_is_better && delta < 0.0) {
1
} else {
-1
};
let verdict = match direction {
1 => "better",
-1 => "worse",
_ => "unchanged",
};
(
format!("{after:.2}{unit} ({delta:+.2}{unit}, {verdict})"),
direction,
)
}
pub(super) fn optimize_result_rows(outcome: &OptimizeOutcome) -> Vec<OptimizeResultRow> {
vec![
metric_row(
"Windowing",
outcome.before.windowing_pct,
outcome.after.windowing_pct,
false,
"%",
),
metric_row(
"Extinction",
outcome.before.extinction_pct,
outcome.after.extinction_pct,
false,
"%",
),
metric_row(
"Tilt brilliance",
outcome.before.tilt_brilliance_pct,
outcome.after.tilt_brilliance_pct,
true,
"%",
),
metric_row(
"Yield loss",
outcome.before_yield_loss_pct,
outcome.after_yield_loss_pct,
false,
"%",
),
metric_row(
"Blended score",
outcome.before_score,
outcome.after_score,
false,
"",
),
]
}
fn metric_row(
label: &str,
before: f32,
after: f32,
higher_is_better: bool,
unit: &str,
) -> OptimizeResultRow {
let (after_text, direction) = after_with_delta(before, after, higher_is_better, unit);
OptimizeResultRow {
label: label.into(),
before: format!("{before:.2}{unit}").into(),
after: after_text.into(),
direction,
}
}
pub(super) fn optimize_status_text(outcome: &OptimizeOutcome) -> String {
let cancelled_note = if outcome.cancelled {
" (cancelled -- showing the best partial result found before the checkpoint \
fired)"
} else {
""
};
let polish_note = if outcome.polish_evaluations > 0 {
format!(
" (polish: +{:.2} in {} evaluation(s))",
outcome.polish_improvement, outcome.polish_evaluations
)
} else {
String::new()
};
if outcome.changes.is_empty() {
format!(
"Optimize found no improving move in {} evaluation(s) -- this design's \
free tiers were already at (or very near) a local optimum for these \
weights.{cancelled_note}{polish_note}",
outcome.evaluations
)
} else {
format!(
"Optimize changed {} tier(s) in {} evaluation(s).{cancelled_note}{polish_note}",
outcome.changes.len(),
outcome.evaluations
)
}
}
#[must_use]
pub(super) fn build_optimize_preview_design(design: &Design, outcome: &OptimizeOutcome) -> Design {
let mut preview = design.clone();
for change in &outcome.changes {
if let Some(tier) = preview.tiers.get_mut(change.index) {
tier.angle_deg = change.to_deg;
}
}
preview
}
#[must_use]
pub(super) fn submit_design_ghost_preview(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
design: &Design,
) -> bool {
let Ok(solved) = design.solve() else {
return false;
};
let planes_gpu = auto_solve::design_to_gpu_planes_from_solved(design, &solved);
let planes: Vec<(glam::Vec3, f32)> = planes_gpu
.iter()
.map(|p| (glam::Vec3::from(p.normal), -p.d))
.collect();
let ctx = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let camera = CameraPose {
yaw: ctx.yaw,
pitch: ctx.pitch,
distance: ctx.distance,
};
let design_gear = ctx.design_gear;
let view_mode = ui.global::<SolidPreviewModel>().get_view_mode() as u8;
let size = contained_request_size(view_mode, scaled_viewport_size(ui), (ctx.width, ctx.height));
drop(ctx);
preview_state.request_redraw_with_gear(planes, camera, size, view_mode, design_gear);
true
}
pub(super) fn parse_optimize_weights(
windowing: &str,
extinction: &str,
tilt_brilliance: &str,
yield_weight: f32,
) -> Result<ObjectiveWeights, String> {
fn parse_weight(label: &str, text: &str) -> Result<f32, String> {
let value: f32 = text
.trim()
.parse()
.map_err(|_| format!("{label} weight must be a number."))?;
if !value.is_finite() || value < 0.0 {
return Err(format!(
"{label} weight must be a non-negative, finite number."
));
}
Ok(value)
}
Ok(ObjectiveWeights {
windowing: parse_weight("Windowing", windowing)?,
extinction: parse_weight("Extinction", extinction)?,
tilt_brilliance: parse_weight("Tilt brilliance", tilt_brilliance)?,
yield_weight,
})
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix::geometry::stone_metrics::ExternalProportions;
use indicatrix_cut_core::{AngleChange, ConstraintTier, ObjectiveComponents};
fn some_proportions() -> ExternalProportions {
ExternalProportions {
vol_w3: Some(1.2),
lw: Some(1.0),
cw: Some(0.2),
pw: Some(0.4),
hw: Some(0.6),
}
}
fn scale_reference_tier(value: f64) -> ConstraintTier {
ConstraintTier {
angle_deg: 0.0,
name: "T".to_string(),
indices: vec![],
constraint: MeetConstraint::ScaleReference(value),
imported_meet: None,
original_notes: None,
detached: Vec::new(),
}
}
#[test]
fn proportions_texts_from_solved_dashes_out_a_design_with_no_tiers() {
let design = EditorState::fresh().design;
let solved = design.solve().expect("a tierless design still solves");
assert_eq!(
proportions_texts_from_solved(&design, &solved),
(
"-".to_string(),
"-".to_string(),
"-".to_string(),
"-".to_string(),
"-".to_string(),
),
"a tierless design's proportions must never show the bare preform \
block's own numbers (table 100%, total depth = preform depth) as \
if they were the stone's"
);
}
#[test]
fn girdle_and_ratio_texts_from_solved_dashes_out_a_design_with_no_tiers() {
let design = EditorState::fresh().design;
let solved = design.solve().expect("a tierless design still solves");
assert_eq!(
girdle_and_ratio_texts_from_solved(&design, &solved),
(
"-".to_string(),
"-".to_string(),
"-".to_string(),
"-".to_string(),
)
);
}
#[test]
fn deep_solve_hint_is_unavailable_for_a_fresh_design_with_no_printed_proportions() {
let state = EditorState::fresh();
let (available, hint) = deep_solve_hint(&state);
assert!(!available);
assert!(hint.contains("printed proportions"));
}
#[test]
fn deep_solve_hint_is_unavailable_when_every_tier_is_pinned_with_nothing_to_repair() {
let mut state = EditorState::fresh();
state.printed_proportions = Some(some_proportions());
state.design.tiers.push(scale_reference_tier(0.5));
state.design.tiers.push(scale_reference_tier(0.8));
let (available, hint) = deep_solve_hint(&state);
assert!(!available);
assert!(hint.contains("pinned"));
assert!(hint.contains("Adopt"));
}
#[test]
fn deep_solve_hint_is_available_with_the_cost_caveat_when_a_tier_is_meet_derived() {
let mut state = EditorState::fresh();
state.printed_proportions = Some(some_proportions());
state.design.tiers.push(scale_reference_tier(0.5));
state.design.tiers.push(ConstraintTier {
angle_deg: -40.0,
name: "P1".to_string(),
indices: vec![0.0, 24.0],
constraint: MeetConstraint::MeetExisting,
imported_meet: None,
original_notes: None,
detached: Vec::new(),
});
let (available, hint) = deep_solve_hint(&state);
assert!(available);
assert!(!hint.contains("pinned"));
assert!(hint.to_lowercase().contains("cancel"));
}
#[test]
fn optimize_hint_is_unavailable_when_every_tier_is_pinned() {
let mut state = EditorState::fresh();
state.design.tiers.push(scale_reference_tier(0.5));
state.design.tiers.push(scale_reference_tier(0.8));
let (available, hint) = optimize_hint(&state, 200);
assert!(!available);
assert!(hint.contains("pinned"));
assert!(hint.contains("Adopt"));
}
#[test]
fn optimize_hint_is_available_once_a_tier_is_free_to_move() {
let mut state = EditorState::fresh();
state.design.tiers.push(scale_reference_tier(0.5));
state.design.tiers.push(ConstraintTier {
angle_deg: -40.0,
name: "P1".to_string(),
indices: vec![0.0, 24.0],
constraint: MeetConstraint::MeetExisting,
imported_meet: None,
original_notes: None,
detached: Vec::new(),
});
let (available, hint) = optimize_hint(&state, 200);
assert!(available);
assert!(
hint.contains('1'),
"expected the free-tier count in: {hint}"
);
assert!(hint.to_lowercase().contains("cancel"));
}
#[test]
fn optimize_hint_quotes_the_configured_budget_not_a_hardcoded_number() {
let mut state = EditorState::fresh();
state.design.tiers.push(scale_reference_tier(0.5));
state.design.tiers.push(ConstraintTier {
angle_deg: -40.0,
name: "P1".to_string(),
indices: vec![0.0, 24.0],
constraint: MeetConstraint::MeetExisting,
imported_meet: None,
original_notes: None,
detached: Vec::new(),
});
let (_, hint) = optimize_hint(&state, 750);
assert!(hint.contains("750"), "expected the budget in: {hint}");
assert!(!hint.contains("200-evaluation"));
}
#[test]
fn optimize_hint_names_the_canonical_light_pose() {
let mut state = EditorState::fresh();
state.design.tiers.push(scale_reference_tier(0.5));
state.design.tiers.push(ConstraintTier {
angle_deg: -40.0,
name: "P1".to_string(),
indices: vec![0.0, 24.0],
constraint: MeetConstraint::MeetExisting,
imported_meet: None,
original_notes: None,
detached: Vec::new(),
});
let (_, hint) = optimize_hint(&state, 200);
assert!(hint.to_lowercase().contains("canonical light pose"));
}
fn sample_outcome(changed: bool, cancelled: bool) -> OptimizeOutcome {
OptimizeOutcome {
before: ObjectiveComponents {
windowing_pct: 12.5,
extinction_pct: 8.0,
tilt_brilliance_pct: 60.0,
},
before_score: 20.0,
before_yield_loss_pct: 30.0,
after: ObjectiveComponents {
windowing_pct: 9.25,
extinction_pct: 11.0,
tilt_brilliance_pct: 65.0,
},
after_score: 15.0,
after_yield_loss_pct: 25.0,
evaluations: 42,
changes: if changed {
vec![AngleChange {
index: 3,
from_deg: -40.0,
to_deg: -41.5,
}]
} else {
Vec::new()
},
cancelled,
polish_evaluations: 0,
polish_improvement: 0.0,
}
}
#[test]
fn optimize_result_rows_reports_each_component_separately_never_collapsed() {
let outcome = sample_outcome(true, false);
let rows = optimize_result_rows(&outcome);
assert_eq!(rows.len(), 5);
assert_eq!(rows[0].label.as_str(), "Windowing");
assert_eq!(rows[0].before.as_str(), "12.50%");
assert_eq!(rows[0].after.as_str(), "9.25% (-3.25%, better)");
assert_eq!(rows[1].label.as_str(), "Extinction");
assert_eq!(rows[1].before.as_str(), "8.00%");
assert_eq!(rows[1].after.as_str(), "11.00% (+3.00%, worse)");
assert_eq!(rows[2].label.as_str(), "Tilt brilliance");
assert_eq!(rows[2].before.as_str(), "60.00%");
assert_eq!(rows[2].after.as_str(), "65.00% (+5.00%, better)");
assert_eq!(rows[3].label.as_str(), "Yield loss");
assert_eq!(rows[3].before.as_str(), "30.00%");
assert_eq!(rows[3].after.as_str(), "25.00% (-5.00%, better)");
assert_eq!(rows[4].label.as_str(), "Blended score");
assert_eq!(rows[4].before.as_str(), "20.00");
assert_eq!(rows[4].after.as_str(), "15.00 (-5.00, better)");
}
#[test]
fn build_optimize_preview_design_moves_only_the_changed_tiers_angle() {
let mut design = Design::new(
indicatrix_cut_core::PreformSpec::block(2.0, 1.0, 2.0),
indicatrix_cut_core::ScheduleMeta::default(),
vec![
scale_reference_tier(0.5),
scale_reference_tier(0.6),
scale_reference_tier(0.7),
ConstraintTier {
angle_deg: -40.0,
name: "P1".to_string(),
indices: vec![0.0, 24.0],
constraint: MeetConstraint::ScaleReference(0.8),
imported_meet: None,
original_notes: None,
detached: Vec::new(),
},
],
);
design.tiers[3].angle_deg = -40.0;
let outcome = sample_outcome(true, false); let preview = build_optimize_preview_design(&design, &outcome);
assert!((preview.tiers[3].angle_deg - (-41.5)).abs() < 1e-9);
for i in 0..3 {
assert!((preview.tiers[i].angle_deg - design.tiers[i].angle_deg).abs() < 1e-9);
}
assert!((design.tiers[3].angle_deg - (-40.0)).abs() < 1e-9);
}
#[test]
fn build_optimize_preview_design_ignores_an_out_of_range_change_index() {
let design = Design::new(
indicatrix_cut_core::PreformSpec::block(2.0, 1.0, 2.0),
indicatrix_cut_core::ScheduleMeta::default(),
vec![scale_reference_tier(0.5)],
);
let outcome = sample_outcome(true, false); let preview = build_optimize_preview_design(&design, &outcome);
assert_eq!(preview.tiers.len(), 1);
}
#[test]
fn after_with_delta_reports_unchanged_when_the_value_did_not_move() {
assert_eq!(
after_with_delta(5.0, 5.0, false, "%"),
("5.00% (+0.00%, unchanged)".to_string(), 0)
);
}
#[test]
fn optimize_status_text_reports_the_change_and_evaluation_count() {
let text = optimize_status_text(&sample_outcome(true, false));
assert!(text.contains("1 tier(s)"));
assert!(text.contains("42 evaluation(s)"));
assert!(!text.contains("cancelled"));
}
#[test]
fn optimize_status_text_reports_no_improving_move_when_nothing_changed() {
let text = optimize_status_text(&sample_outcome(false, false));
assert!(text.contains("no improving move"));
}
#[test]
fn optimize_status_text_notes_cancellation_without_hiding_the_partial_result() {
let text = optimize_status_text(&sample_outcome(true, true));
assert!(text.contains("cancelled"));
assert!(text.contains("1 tier(s)"));
}
#[test]
fn optimize_status_text_names_the_polish_stages_own_contribution_when_it_ran() {
let mut outcome = sample_outcome(true, false);
outcome.polish_evaluations = 31;
outcome.polish_improvement = 0.42;
let text = optimize_status_text(&outcome);
assert!(text.contains("polish: +0.42 in 31 evaluation(s)"));
}
#[test]
fn optimize_status_text_omits_the_polish_note_when_the_stage_never_ran() {
let text = optimize_status_text(&sample_outcome(true, false));
assert!(!text.contains("polish"));
}
#[test]
fn parse_optimize_weights_accepts_well_formed_input() {
let weights = parse_optimize_weights("1.0", "2.5", "0", 0.0).unwrap();
assert_eq!(weights.windowing, 1.0);
assert_eq!(weights.extinction, 2.5);
assert_eq!(weights.tilt_brilliance, 0.0);
assert_eq!(weights.yield_weight, 0.0);
}
#[test]
fn parse_optimize_weights_rejects_a_non_numeric_field() {
let err = parse_optimize_weights("not-a-number", "1.0", "1.0", 0.0).unwrap_err();
assert!(err.contains("Windowing"));
}
#[test]
fn parse_optimize_weights_rejects_a_negative_weight() {
let err = parse_optimize_weights("1.0", "-0.5", "1.0", 0.0).unwrap_err();
assert!(err.contains("Extinction"));
}
#[test]
fn parse_optimize_weights_rejects_non_finite_values() {
assert!(parse_optimize_weights("NaN", "1.0", "1.0", 0.0).is_err());
assert!(parse_optimize_weights("1.0", "inf", "1.0", 0.0).is_err());
}
#[test]
fn parse_optimize_weights_carries_the_yield_slider_value_through_untouched() {
let weights = parse_optimize_weights("1.0", "1.0", "1.0", 0.4).unwrap();
assert_eq!(weights.yield_weight, 0.4);
}
fn design_with_named_tiers(names: &[&str]) -> indicatrix_cut_core::Design {
let mut state = EditorState::fresh();
for &name in names {
state.design.tiers.push(ConstraintTier {
angle_deg: -40.0,
name: name.to_string(),
indices: vec![0.0],
constraint: MeetConstraint::MeetExisting,
imported_meet: None,
original_notes: None,
detached: Vec::new(),
});
}
state.design
}
#[test]
fn selected_tier_chips_is_empty_when_nothing_is_selected() {
let design = design_with_named_tiers(&["G1"]);
assert_eq!(selected_tier_chips(&design, -1).len(), 0);
}
#[test]
fn selected_tier_chips_is_empty_for_an_out_of_range_index() {
let design = design_with_named_tiers(&["G1"]);
assert_eq!(selected_tier_chips(&design, 5).len(), 0);
}
#[test]
fn selected_tier_chips_reads_the_selected_tiers_own_indices_and_detached_set() {
let mut design = design_with_named_tiers(&["G1", "P1"]);
design.tiers[1].indices = vec![0.0, 24.0];
design.tiers[1].detached = vec![24.0];
let chips = selected_tier_chips(&design, 1);
assert_eq!(chips.len(), 2);
assert!(!chips[0].detached);
assert!(chips[1].detached);
}
#[test]
fn deep_solve_tier_rows_formats_one_row_per_delta_with_the_tiers_own_name() {
let design = design_with_named_tiers(&["G1", "P1", "P2"]);
let deltas = [
TierMastDelta {
tier_index: 1,
before_mast: 1.0,
after_mast: 1.25,
},
TierMastDelta {
tier_index: 2,
before_mast: 2.0,
after_mast: 1.9,
},
];
let rows = deep_solve_tier_rows(&deltas, &design);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].tier_number.as_str(), "#2");
assert_eq!(rows[0].name.as_str(), "P1");
assert_eq!(rows[0].before_mast.as_str(), "1.0000");
assert_eq!(rows[0].after_mast.as_str(), "1.2500");
assert_eq!(rows[0].delta.as_str(), "+0.2500");
assert_eq!(rows[1].delta.as_str(), "-0.1000");
}
#[test]
fn deep_solve_tier_rows_is_empty_for_no_deltas() {
let design = design_with_named_tiers(&["G1"]);
assert_eq!(deep_solve_tier_rows(&[], &design).len(), 0);
}
#[test]
fn deep_solve_tier_rows_names_an_out_of_range_tier_blank_rather_than_panicking() {
let design = design_with_named_tiers(&["G1"]);
let deltas = [TierMastDelta {
tier_index: 5,
before_mast: 1.0,
after_mast: 1.1,
}];
let rows = deep_solve_tier_rows(&deltas, &design);
assert_eq!(rows[0].name.as_str(), "");
}
#[test]
fn optimize_change_rows_formats_one_row_per_angle_change_with_the_tiers_own_name() {
let design = design_with_named_tiers(&["G1", "P1"]);
let outcome = OptimizeOutcome {
before: ObjectiveComponents {
windowing_pct: 0.0,
extinction_pct: 0.0,
tilt_brilliance_pct: 0.0,
},
before_score: 0.0,
before_yield_loss_pct: 0.0,
after: ObjectiveComponents {
windowing_pct: 0.0,
extinction_pct: 0.0,
tilt_brilliance_pct: 0.0,
},
after_score: 0.0,
after_yield_loss_pct: 0.0,
evaluations: 1,
changes: vec![AngleChange {
index: 1,
from_deg: -40.0,
to_deg: -41.5,
}],
cancelled: false,
polish_evaluations: 0,
polish_improvement: 0.0,
};
let rows = optimize_change_rows(&outcome, &design);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].tier_number.as_str(), "#2");
assert_eq!(rows[0].name.as_str(), "P1");
assert_eq!(rows[0].from_angle.as_str(), "-40.00\u{b0}");
assert_eq!(rows[0].to_angle.as_str(), "-41.50\u{b0}");
assert_eq!(rows[0].delta.as_str(), "-1.50\u{b0}");
}
#[test]
fn facet_count_from_solved_is_the_length_of_the_designs_expanded_planes() {
let mut design = design_with_named_tiers(&["G1"]);
design.tiers[0].constraint = MeetConstraint::ScaleReference(1.0);
let solved = design
.solve()
.expect("a single scale-reference tier always solves");
assert_eq!(
facet_count_from_solved(&design, &solved),
design.planes_from_solved(&solved).len()
);
assert!(
facet_count_from_solved(&design, &solved) > 0,
"a solved design always has at least one facet plane"
);
}
#[test]
fn traced_material_for_names_a_resolvable_built_in_selection() {
let mut design = EditorState::fresh().design;
design.material.name = Some("Sapphire".to_string());
let (name, unresolved) = traced_material_for(&design, &[]);
assert_eq!(name, "Sapphire");
assert_eq!(unresolved, None);
}
#[test]
fn traced_material_for_names_a_resolvable_custom_selection() {
let mut design = EditorState::fresh().design;
design.material.name = Some("My Garnet".to_string());
let mut custom = indicatrix::optics::materials::GemMaterial::diamond();
custom.name = "My Garnet".to_string();
let (name, unresolved) = traced_material_for(&design, &[custom]);
assert_eq!(name, "My Garnet");
assert_eq!(unresolved, None);
}
#[test]
fn traced_material_for_refuses_a_name_no_catalogue_resolves() {
let mut design = EditorState::fresh().design;
design.material.name = Some("Deleted Custom Garnet".to_string());
let (name, unresolved) = traced_material_for(&design, &[]);
assert_eq!(name, "");
let reason = unresolved.expect("an unresolvable name must refuse, not substitute");
assert!(reason.contains("Deleted Custom Garnet"));
}
}