use super::{
auto_solve,
deep_solve::TierMastDelta,
material_lookup::{EditorMaterialLookup, nearest_built_in_material, traced_gem_material},
state::{
EditorState, ScratchDelta, apply_multi_selection, cutting_schedule_rows, design_label_text,
design_material_index_from_name, design_material_options, design_to_gpu_planes,
gear_index_from_teeth, index_chip_items, manufacturability_warning_lines,
manufacturability_warning_lines_from_solved, manufacturability_warnings_tagged,
material_index_from_name, preform_mm_texts, proportions_texts, push_multi_selected_count,
push_tiers, ri_source_text, status_text_and_is_problem,
status_text_and_is_problem_from_solved, tier_items, tier_items_from_solved,
tier_items_stale, 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, MissingAnchor, ObjectiveWeights, OptimizeOutcome, PreformShape, critical_angle_deg,
free_tier_indices,
};
use slint::{ComponentHandle, ModelRc, SharedString, VecModel};
use std::{
collections::BTreeSet,
sync::{Arc, Mutex, PoisonError, atomic::Ordering as AtomicOrdering},
time::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>, MissingAnchor>,
) -> 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());
let warnings: Vec<SharedString> = solved
.map_or_else(
|| manufacturability_warning_lines(&state.design),
|solved| manufacturability_warning_lines_from_solved(&state.design, solved),
)
.into_iter()
.map(SharedString::from)
.collect();
ui.global::<EditorModel>()
.set_manufacturability_warnings(ModelRc::new(VecModel::from(warnings)));
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());
}
push_yield_material_scratch(ui, state, &delta);
let (vol_yield_text, carat_text, sg_used_text, fit_text) = solved.map_or_else(
|| yield_report_texts(&state.design),
|solved| yield_report_texts_from_solved(&state.design, solved),
);
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());
ui.global::<EditorModel>()
.set_design_label(design_label_text(state.asc_filename.as_deref()).into());
if let Some(solved) = solved {
let rows: Vec<AngleItem> = cutting_schedule_rows(&state.design, solved);
ui.global::<EditorModel>()
.set_cutting_rows(ModelRc::new(VecModel::from(rows)));
} else {
ui.global::<EditorModel>()
.set_cutting_rows(ModelRc::new(VecModel::from(Vec::<AngleItem>::new())));
}
refresh_deep_solve_availability(ui, state);
refresh_optimize_availability(ui, state);
solved_result.ok()
}
#[must_use]
fn proportions_texts_from_solved(
design: &Design,
solved: &[SolvedTier],
) -> (String, String, String, String, String) {
let dash = || "-".to_string();
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]
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 push_tier_list_and_undo_redo(
ui: &MainWindow,
state: &EditorState,
mut tiers: Vec<EditorTierItem>,
) {
apply_multi_selection(&mut tiers, &state.multi_selected);
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(),
);
}
}
const MATERIAL_MATCH_TOLERANCE: f64 = 0.02;
fn traced_material_for(design: &Design) -> (String, Option<String>) {
if let Some(name) = &design.material.name {
return (name.clone(), None);
}
let n_d = design.effective_refractive_index();
if let Some((name, _)) = nearest_built_in_material(n_d, MATERIAL_MATCH_TOLERANCE) {
return (name.to_string(), 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 refresh_design_settings(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &EditorState,
delta: &ScratchDelta,
) -> f64 {
let design = &state.design;
let mut 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 = design_material_options(&ctx.custom_materials);
ui.global::<EditorModel>()
.set_material_combo_options(ModelRc::new(VecModel::from(
options
.iter()
.cloned()
.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());
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 ui.global::<ViewportModel>().get_viewport_material_linked() {
let (name, unresolved) = traced_material_for(design);
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();
if let Some(idx) = crate::gui::startup_settings::find_option_index(&options, &name) {
ui.global::<ViewportModel>()
.set_selected_material_index(idx);
}
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;
}
}
n_d
}
#[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 chips = selected_tier_chips(&state.design, selected);
ui.global::<EditorModel>()
.set_selected_tier_chips(ModelRc::new(VecModel::from(chips)));
}
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());
}
fn refresh_optimize_availability(ui: &MainWindow, state: &EditorState) {
let (available, hint) = optimize_hint(state);
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,
) {
push_stale_content(ui, render_ctx, state);
auto_solve::on_edit(ui, render_ctx, state);
}
fn push_stale_content(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &EditorState,
) {
push_trace_staleness(ui, render_ctx, state);
let delta = state.record_scratch_push();
let n_d = refresh_design_settings(ui, render_ctx, state, &delta);
let tiers = tier_items_stale(&state.design, n_d);
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());
let warnings: Vec<SharedString> = manufacturability_warnings_tagged(&state.design, None)
.into_iter()
.map(|(_, text)| SharedString::from(text))
.collect();
ui.global::<EditorModel>()
.set_manufacturability_warnings(ModelRc::new(VecModel::from(warnings)));
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());
}
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());
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_cutting_rows(ModelRc::new(VecModel::from(Vec::<AngleItem>::new())));
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,
) {
let last_solved = if force_full_solve {
None
} else {
solid_last_solved
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
};
let (camera, render_size) = {
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),
)
};
let selected_tier = usize::try_from(ui.global::<EditorModel>().get_selected_tier_index()).ok();
let n_d = state.design.effective_refractive_index();
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 generation = state.generation.load(AtomicOrdering::Relaxed);
auto_solve::stash_current_design(
generation,
state.design.clone(),
state.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: state.design.clone(),
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>,
) {
let n_d = design.effective_refractive_index();
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 warnings: Vec<SharedString> = manufacturability_warning_lines_from_solved(design, solved)
.into_iter()
.map(SharedString::from)
.collect();
ui.global::<EditorModel>()
.set_manufacturability_warnings(ModelRc::new(VecModel::from(warnings)));
let (vol_yield_text, carat_text, sg_used_text, fit_text) =
yield_report_texts_from_solved(design, solved);
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(
ui: &MainWindow,
render_ctx: &Arc<Mutex<crate::bridge::render_thread::RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
state: &EditorState,
) {
auto_solve::reset_for_new_design();
let plane_estimate: usize = state.design.tiers.iter().map(|t| t.indices.len()).sum();
if auto_solve::should_solve_synchronously(plane_estimate, None) {
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 {
push_stale_content(ui, render_ctx, state);
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) -> (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. Roughly 7 ms per evaluation on a \
small design, but up to several seconds each on a large, heavily \
meet-derived one (a 200-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(
"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
)
}
}
pub(super) fn parse_optimize_weights(
windowing: &str,
extinction: &str,
tilt_brilliance: &str,
) -> 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)?,
})
}
#[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 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);
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);
assert!(available);
assert!(
hint.contains('1'),
"expected the free-tier count in: {hint}"
);
assert!(hint.to_lowercase().contains("cancel"));
}
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,
after: ObjectiveComponents {
windowing_pct: 9.25,
extinction_pct: 11.0,
tilt_brilliance_pct: 65.0,
},
after_score: 15.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(), 4);
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(), "Blended score");
assert_eq!(rows[3].before.as_str(), "20.00");
assert_eq!(rows[3].after.as_str(), "15.00 (-5.00, better)");
}
#[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").unwrap();
assert_eq!(weights.windowing, 1.0);
assert_eq!(weights.extinction, 2.5);
assert_eq!(weights.tilt_brilliance, 0.0);
}
#[test]
fn parse_optimize_weights_rejects_a_non_numeric_field() {
let err = parse_optimize_weights("not-a-number", "1.0", "1.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").unwrap_err();
assert!(err.contains("Extinction"));
}
#[test]
fn parse_optimize_weights_rejects_non_finite_values() {
assert!(parse_optimize_weights("NaN", "1.0", "1.0").is_err());
assert!(parse_optimize_weights("1.0", "inf", "1.0").is_err());
}
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,
after: ObjectiveComponents {
windowing_pct: 0.0,
extinction_pct: 0.0,
tilt_brilliance_pct: 0.0,
},
after_score: 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"
);
}
}