use super::{
auto_solve,
state::{
EditorState, apply_multi_selection, design_material_index_from_name,
design_material_options, design_to_gpu_planes, gear_index_from_teeth,
manufacturability_warning_lines, material_index_from_name, status_text_and_is_problem,
tier_items, tier_items_stale, yield_report_texts,
},
};
use crate::{
EditorModel, MainWindow, OptimizeResultRow, SolidPreviewModel, ViewportModel,
bridge::render_thread::RenderContext,
gui::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, VerifiedSolveReport};
use indicatrix_cut_core::{
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,
) {
let n_d = refresh_design_settings(ui, render_ctx, state);
let mut tiers = tier_items(&state.design, n_d);
apply_multi_selection(&mut tiers, &state.multi_selected);
ui.global::<EditorModel>()
.set_tiers(ModelRc::new(VecModel::from(tiers)));
ui.global::<EditorModel>()
.set_can_undo(state.history.can_undo());
ui.global::<EditorModel>()
.set_can_redo(state.history.can_redo());
let (status_text, is_problem) = status_text_and_is_problem(&state.design);
ui.global::<EditorModel>()
.set_status_text(status_text.into());
ui.global::<EditorModel>().set_status_is_problem(is_problem);
let warnings: Vec<SharedString> = manufacturability_warning_lines(&state.design)
.into_iter()
.map(SharedString::from)
.collect();
ui.global::<EditorModel>()
.set_manufacturability_warnings(ModelRc::new(VecModel::from(warnings)));
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_girdle_diameter_mm(
state
.design
.girdle_diameter_mm
.map_or_else(String::new, |mm| format!("{mm:.4}"))
.into(),
);
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(),
);
let (vol_yield_text, carat_text, sg_used_text, fit_text) = yield_report_texts(&state.design);
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());
refresh_deep_solve_availability(ui, state);
refresh_optimize_availability(ui, state);
}
fn refresh_design_settings(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &EditorState,
) -> f64 {
let design = &state.design;
let n_d = design.effective_refractive_index();
let mut ctx = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let options = design_material_options(&ctx.custom_materials);
ui.global::<EditorModel>()
.set_material_combo_index(design_material_index_from_name(
design.material.name.as_deref(),
&options,
));
ui.global::<EditorModel>()
.set_material_combo_options(ModelRc::new(VecModel::from(
options
.into_iter()
.map(SharedString::from)
.collect::<Vec<_>>(),
)));
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());
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());
ui.global::<EditorModel>()
.set_symmetry_order_text(design.meta.symmetry_order.to_string().into());
ui.global::<EditorModel>().set_mirror(design.meta.mirror);
if ui.get_render_view_tab() == 1 && ui.global::<ViewportModel>().get_viewport_material_linked()
{
let name = design
.material
.name
.clone()
.unwrap_or_else(|| "Diamond".to_string());
if ctx.material_name != name {
ctx.material_name = name;
ctx.dirty = true;
}
}
n_d
}
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,
) {
let n_d = refresh_design_settings(ui, render_ctx, state);
let mut tiers = tier_items_stale(&state.design, n_d);
apply_multi_selection(&mut tiers, &state.multi_selected);
ui.global::<EditorModel>()
.set_tiers(ModelRc::new(VecModel::from(tiers)));
ui.global::<EditorModel>()
.set_can_undo(state.history.can_undo());
ui.global::<EditorModel>()
.set_can_redo(state.history.can_redo());
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_manufacturability_warnings(ModelRc::new(VecModel::from(Vec::<SharedString>::new())));
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_girdle_diameter_mm(
state
.design
.girdle_diameter_mm
.map_or_else(String::new, |mm| format!("{mm:.4}"))
.into(),
);
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(),
);
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());
refresh_deep_solve_availability(ui, state);
refresh_optimize_availability(ui, state);
}
fn refresh_viewport(
ui: &MainWindow,
render_ctx: &Arc<Mutex<crate::bridge::render_thread::RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
state: &EditorState,
) {
let mut ctx = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
ctx.active_planes = std::sync::Arc::new(design_to_gpu_planes(&state.design));
ctx.dirty = true;
ctx.design_gear = Some((
state.design.meta.gear_teeth_abs(),
state.design.meta.gear_reference_angle as f32,
));
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 size = (
ui.global::<SolidPreviewModel>().get_viewport_width() as u32,
ui.global::<SolidPreviewModel>().get_viewport_height() as u32,
);
let view_mode = ui.global::<SolidPreviewModel>().get_view_mode() as u8;
preview_state.request_redraw_with_gear(
planes,
CameraPose {
yaw: ctx.yaw,
pitch: ctx.pitch,
distance: ctx.distance,
},
size,
view_mode,
design_gear,
);
drop(ctx);
if let Ok(solved) = state.design.solve() {
*solid_last_solved
.lock()
.unwrap_or_else(PoisonError::into_inner) = Some(solved);
}
}
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 = {
let ctx = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
CameraPose {
yaw: ctx.yaw,
pitch: ctx.pitch,
distance: ctx.distance,
}
};
let size = (
ui.global::<SolidPreviewModel>().get_viewport_width() as u32,
ui.global::<SolidPreviewModel>().get_viewport_height() as u32,
);
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;
preview_state.request_replan(ReplanRequest {
design: state.design.clone(),
dirty,
last_solved,
camera,
size,
selected_tier,
n_d,
view_mode,
});
}
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();
if state.design.tiers.len() <= auto_solve::SYNC_SOLVE_TIER_LIMIT {
let start = Instant::now();
refresh_editor_panel(ui, render_ctx, state);
refresh_viewport(ui, render_ctx, preview_state, solid_last_solved, state);
auto_solve::record_solve_duration(start.elapsed());
} 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 {
(
true,
"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 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) -- \
runs off the UI thread and can be cancelled.",
free.len()
),
)
}
}
pub(super) fn optimize_result_rows(outcome: &OptimizeOutcome) -> Vec<OptimizeResultRow> {
vec![
OptimizeResultRow {
label: "Windowing".into(),
before: format!("{:.2}%", outcome.before.windowing_pct).into(),
after: format!("{:.2}%", outcome.after.windowing_pct).into(),
},
OptimizeResultRow {
label: "Extinction".into(),
before: format!("{:.2}%", outcome.before.extinction_pct).into(),
after: format!("{:.2}%", outcome.after.extinction_pct).into(),
},
OptimizeResultRow {
label: "Tilt brilliance".into(),
before: format!("{:.2}%", outcome.before.tilt_brilliance_pct).into(),
after: format!("{:.2}%", outcome.after.tilt_brilliance_pct).into(),
},
OptimizeResultRow {
label: "Blended score".into(),
before: format!("{:.2}", outcome.before_score).into(),
after: format!("{:.2}", outcome.after_score).into(),
},
]
}
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 {
""
};
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}",
outcome.evaluations
)
} else {
format!(
"Optimize changed {} tier(s) in {} evaluation(s).{cancelled_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,
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_available_but_says_nothing_to_repair_when_every_tier_is_pinned() {
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,
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,
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%");
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%");
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%");
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");
}
#[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 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());
}
}