use super::{
panel::{push_tier_list_and_undo_redo, push_yield_material_scratch},
solve_results::{deep_solve_hint, optimize_hint},
state::{
EditorState, ScratchDelta, design_label_text, manufacturability_warnings_tagged,
preform_y_offset_mm_text, result_is_stale, tier_items_stale_with_last_solved,
},
};
use crate::{
AngleItem, EditorModel,
bridge::render_thread::RenderContext,
gui::editor::{auto_solve, stale},
};
use indicatrix_cut_core::PreformShape;
use slint::{ComponentHandle, ModelRc, SharedString, VecModel};
use std::{
cell::Cell,
collections::BTreeSet,
sync::{Arc, Mutex, PoisonError, atomic::Ordering as AtomicOrdering},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct FormSyncMark {
generation: u64,
tier_count: usize,
}
thread_local! {
static LAST_FORM_SYNC: Cell<Option<FormSyncMark>> = const { Cell::new(None) };
}
fn form_reseed_request(
last: Option<FormSyncMark>,
now: FormSyncMark,
selection_survives: bool,
) -> Option<bool> {
if !selection_survives {
return None;
}
match last {
Some(previous) if previous == now => None,
Some(previous) => Some(previous.tier_count != now.tier_count),
None => Some(false),
}
}
fn push_form_reseed_pulse(ui: &crate::MainWindow, tier_count_changed: bool) {
let model = ui.global::<EditorModel>();
model.set_form_reseed_tier_count_changed(tier_count_changed);
let pulse = model.get_form_reseed_pulse();
model.set_form_reseed_pulse(pulse.wrapping_add(1));
}
pub(super) fn sync_tier_form_with_design(ui: &crate::MainWindow, state: &EditorState) {
let now = FormSyncMark {
generation: state.generation.load(AtomicOrdering::Relaxed),
tier_count: state.design.tiers.len(),
};
let previous = LAST_FORM_SYNC.with(|cell| cell.replace(Some(now)));
let selected = ui.global::<EditorModel>().get_selected_tier_index();
let selection_survives = usize::try_from(selected).is_ok_and(|index| index < now.tier_count);
if let Some(tier_count_changed) = form_reseed_request(previous, now, selection_survives) {
push_form_reseed_pulse(ui, tier_count_changed);
}
}
pub(super) fn refresh_deep_solve_availability(ui: &crate::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());
}
pub(super) fn refresh_optimize_availability(ui: &crate::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);
}
#[must_use]
pub(in crate::gui::editor) fn configured_optimize_max_evaluations(ui: &crate::MainWindow) -> usize {
ui.global::<EditorModel>()
.get_optimize_budget_text()
.parse::<usize>()
.ok()
.filter(|&v| v > 0)
.unwrap_or(200)
}
pub(in crate::gui::editor) fn refresh_editor_panel_stale(
ui: &crate::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);
crate::gui::editor::guide::check_progress(ui, state);
}
fn push_stale_generation_badges(
ui: &crate::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: &crate::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();
super::state::push_rows(
&ui.global::<EditorModel>().get_manufacturability_warnings(),
warnings,
|model| {
ui.global::<EditorModel>()
.set_manufacturability_warnings(model);
},
);
super::state::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: &crate::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: &crate::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);
ui.global::<EditorModel>()
.set_proportion_verdict_table_level(-1);
ui.global::<EditorModel>()
.set_proportion_verdict_table_reason("".into());
ui.global::<EditorModel>()
.set_proportion_verdict_crown_angle_level(-1);
ui.global::<EditorModel>()
.set_proportion_verdict_crown_angle_reason("".into());
ui.global::<EditorModel>()
.set_proportion_verdict_pavilion_angle_level(-1);
ui.global::<EditorModel>()
.set_proportion_verdict_pavilion_angle_reason("".into());
ui.global::<EditorModel>()
.set_proportion_verdict_total_depth_level(-1);
ui.global::<EditorModel>()
.set_proportion_verdict_total_depth_reason("".into());
ui.global::<EditorModel>()
.set_proportion_verdict_girdle_level(-1);
ui.global::<EditorModel>()
.set_proportion_verdict_girdle_reason("".into());
}
pub(super) fn push_stale_content(
ui: &crate::MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &EditorState,
dirty: &BTreeSet<usize>,
) {
super::viewport::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 = super::inspector::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()
.map(|(_, solved)| solved)
});
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);
sync_tier_form_with_design(ui, state);
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);
}
#[cfg(test)]
mod tests {
use super::{FormSyncMark, form_reseed_request};
const fn mark(generation: u64, tier_count: usize) -> FormSyncMark {
FormSyncMark {
generation,
tier_count,
}
}
#[test]
fn a_new_generation_with_a_surviving_selection_requests_a_reseed() {
assert_eq!(
form_reseed_request(Some(mark(4, 6)), mark(5, 6), true),
Some(false),
"an in-place edit re-seeds without claiming the tier count moved"
);
}
#[test]
fn a_tier_count_change_is_reported_in_the_request() {
assert_eq!(
form_reseed_request(Some(mark(4, 6)), mark(5, 7), true),
Some(true)
);
assert_eq!(
form_reseed_request(Some(mark(4, 6)), mark(5, 5), true),
Some(true)
);
}
#[test]
fn an_unchanged_design_never_requests_a_reseed() {
assert_eq!(
form_reseed_request(Some(mark(4, 6)), mark(4, 6), true),
None,
"a refresh with no edit must not disturb a form being typed into"
);
}
#[test]
fn a_vanished_selection_leaves_the_form_to_the_reset_pulse() {
assert_eq!(
form_reseed_request(Some(mark(4, 6)), mark(5, 5), false),
None
);
assert_eq!(form_reseed_request(None, mark(1, 0), false), None);
}
#[test]
fn the_first_refresh_reseeds_once() {
assert_eq!(form_reseed_request(None, mark(1, 3), true), Some(false));
}
}