use super::{
super::{
auto_solve, edit_intent, loading,
material_lookup::nearest_built_in_material,
native_io::do_open_native,
stall_guard::stall_guard,
state::{
ANGLE_NUDGE_COALESCE_WINDOW, EditorState, MaterialComboCache, PendingGearRemap,
PendingUnsavedAction, PushedScratch, angle_nudge_coalesce_key, apply_multi_selection,
first_unresolved_meet_name, gear_choice_to_teeth, gear_remap_preview,
parse_design_material_form, push_multi_selected_count, push_tiers,
representative_crown_and_pavilion_angles_deg, tier_matches_filter,
tiers_incomplete_under_proposed_symmetry,
},
view::{
SolidLastSolved, push_selected_tier_chips, refresh_all_now, refresh_editor_panel_stale,
submit_preview_replan, sync_viewport_material_link,
},
},
solve_actions::clear_analysis_results,
};
use crate::{
EditorModel, EditorTierItem, GearRemapRow, LibraryModel, MainWindow, SolidPreviewModel,
ViewportModel,
bridge::{library::source::LibrarySource, render_thread::RenderContext},
gui::{
library::remote::fetch_remote_design_source,
show_toast,
solid_preview::{
facet_map::FacetMap,
preview_state::{FacetOverlay, SolidPickState, SolidPreviewState},
},
},
};
use indicatrix::geometry::meet_solver::MeetConstraint;
use indicatrix_cut_core::{
ConstraintTier, Design, Edit, FreshDesignSpec, History, RemapRounding, TierTarget,
};
use indicatrix_vault::db::sqlite::Database;
use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel};
use std::{
cell::{Cell, RefCell, RefMut},
collections::BTreeSet,
rc::Rc,
sync::{
Arc, Mutex,
atomic::{AtomicU64, Ordering},
},
time::Duration,
};
const FIXED_CYLINDER_PREFORM_SIDES: usize = 96;
thread_local! {
static FACET_OVERLAY: RefCell<FacetOverlay> = const {
RefCell::new(FacetOverlay {
hovered: None,
selected_facet: None,
multi_selected: Vec::new(),
})
};
static SELECTED_FACET_LABEL: RefCell<String> = const { RefCell::new(String::new()) };
static PENDING_GEAR_REMAP_GENERATION: Cell<Option<u64>> = const { Cell::new(None) };
}
fn resubmit_facet_overlay(
preview_state: &SolidPreviewState,
mutate: impl FnOnce(&mut FacetOverlay),
) {
let overlay = FACET_OVERLAY.with(|cell| {
let mut overlay = cell.borrow_mut();
mutate(&mut overlay);
overlay.clone()
});
preview_state.request_facet_overlay(overlay);
}
fn facet_map_from_aligned_solve(design: &Design) -> FacetMap {
let solved = auto_solve::solid_last_solved()
.and_then(|cache| {
cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
})
.filter(|solved| solved.len() == design.tiers.len())
.unwrap_or_default();
FacetMap::from_design(design, &solved)
}
pub(in crate::gui::editor) fn setup_solve_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_solve(move || {
stall_guard("on_solve", || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let model = ui.global::<EditorModel>();
if model.get_solve_running()
|| model.get_deep_solve_running()
|| model.get_optimize_running()
{
return;
}
refresh_all_now(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&state,
false,
);
});
});
}
pub(in crate::gui::editor) fn setup_new_design_create_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_new_design_create(
move |shape_index: i32,
half_width: SharedString,
length_over_width: SharedString,
depth: SharedString,
gear_preset_index: i32,
gear_custom_text: SharedString,
symmetry_order_text: SharedString,
mirror: bool,
material_index: i32,
template_index: i32| {
stall_guard("on_new_design_create", || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let gear_teeth = match gear_choice_to_teeth(gear_preset_index, &gear_custom_text) {
Ok(t) => t,
Err(e) => {
show_toast(&ui, &e, "error");
return;
}
};
let preform = match loading::parse_preform_form(
shape_index,
&half_width,
&length_over_width,
&depth,
FIXED_CYLINDER_PREFORM_SIDES,
) {
Ok(p) => p,
Err(e) => {
show_toast(&ui, &e, "error");
return;
}
};
match loading::parse_new_design_form(
gear_teeth,
preform,
&symmetry_order_text,
mirror,
material_index,
) {
Ok(spec) => {
if state.borrow().is_dirty() {
state.borrow_mut().pending_unsaved_action =
Some(PendingUnsavedAction::New {
spec,
template_index,
});
ui.global::<EditorModel>().set_unsaved_dialog_message(
"Starting a new design will discard the current one's unsaved \
changes."
.into(),
);
ui.global::<EditorModel>().set_unsaved_dialog_open(true);
} else {
do_new_design_create(
&ui,
&state,
&render_ctx,
&preview_state,
&solid_last_solved,
spec,
template_index,
);
}
}
Err(e) => show_toast(&ui, &e, "error"),
}
});
},
);
}
fn do_new_design_create(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
spec: FreshDesignSpec,
template_index: i32,
) {
let mut st = state.borrow_mut();
st.replace_wholesale(EditorState::fresh_from_spec(spec));
if let Ok(table_index) = usize::try_from(template_index - 1)
&& let Some(spec) = indicatrix_cut_core::templates::TEMPLATES.get(table_index)
{
st.design.tiers = spec.tiers();
}
clear_analysis_results(ui);
drop(st);
refresh_all_now(
ui,
render_ctx,
preview_state,
solid_last_solved,
state,
true,
);
ui.global::<EditorModel>().set_selected_tier_index(-1);
bump_form_reset_pulse(ui);
ui.global::<EditorModel>().set_new_dialog_open(false);
ui.global::<EditorModel>().set_is_dirty(false);
ui.set_loaded_design_name(SharedString::new());
}
thread_local! {
static REMOTE_LOAD_TARGET: RefCell<Option<Rc<RefCell<EditorState>>>> = const { RefCell::new(None) };
}
struct LoadedDesignOutcome<'a> {
loaded: loading::LoadedDesign,
printed_proportions: Option<indicatrix::geometry::stone_metrics::ExternalProportions>,
label: &'a str,
source_entry_id: Option<i64>,
}
fn apply_loaded_design(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
outcome: LoadedDesignOutcome<'_>,
) {
let LoadedDesignOutcome {
loaded,
printed_proportions,
label: loaded_label,
source_entry_id,
} = outcome;
let schedule_ri = loaded.design.meta.refractive_index;
let mut st = state.borrow_mut();
st.replace_wholesale(EditorState {
used_placeholder: loaded.used_placeholder,
source_entry_id,
design: loaded.design,
history: History::with_coalesce_window(ANGLE_NUDGE_COALESCE_WINDOW),
printed_proportions,
generation: Arc::new(AtomicU64::new(0)),
design_epoch: Arc::new(AtomicU64::new(0)),
saved_generation: 0,
pending_unsaved_action: None,
deep_solve: None,
optimize: None,
pending_optimize: Arc::new(Mutex::new(None)),
deep_solve_result_generation: None,
asc_filename: loaded.asc_filename,
original_asc_text: loaded.original_asc_text,
pending_gear_remap: None,
pending_retarget: None,
multi_selected: BTreeSet::new(),
last_pushed_scratch: RefCell::new(PushedScratch::default()),
material_combo_cache: RefCell::new(MaterialComboCache::default()),
});
clear_analysis_results(ui);
let loaded_asc_filename = st.asc_filename.clone();
drop(st);
refresh_all_now(
ui,
render_ctx,
preview_state,
solid_last_solved,
state,
true,
);
ui.global::<EditorModel>().set_selected_tier_index(-1);
bump_form_reset_pulse(ui);
ui.global::<EditorModel>().set_is_dirty(false);
ui.set_loaded_design_name(loaded_asc_filename.unwrap_or_default().into());
if loaded.used_placeholder {
show_toast(
ui,
"Loaded a reconstructed schedule -- mast distances are \
placeholders (no attached .asc file was found); adjust \
masts before exporting.",
"warning",
);
} else {
show_toast(
ui,
&format!("Loaded '{loaded_label}' into the editor."),
"success",
);
}
if let Some((name, ri)) = nearest_built_in_material(schedule_ri, 0.01) {
let suggestion_text = format!("Set material to {name} (RI {ri:.4})?");
ui.global::<EditorModel>()
.set_material_suggestion_name(name.into());
ui.global::<EditorModel>()
.set_material_suggestion_text(suggestion_text.into());
} else {
ui.global::<EditorModel>()
.set_material_suggestion_name("".into());
ui.global::<EditorModel>()
.set_material_suggestion_text("".into());
}
}
pub(in crate::gui::editor) fn setup_load_selected_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) {
let state = Rc::clone(state);
REMOTE_LOAD_TARGET.with(|cell| *cell.borrow_mut() = Some(Rc::clone(&state)));
setup_unsaved_guard_dispatch(
ui,
&state,
render_ctx,
preview_state,
solid_last_solved,
db,
source,
);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let db = Arc::clone(db);
let source = Arc::clone(source);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_load_selected(move || {
stall_guard("on_load_selected", || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let entry_id = ui.global::<LibraryModel>().get_selected_entry_id();
if entry_id < 0 {
show_toast(&ui, "No diagram selected to load.", "error");
return;
}
if state.borrow().is_dirty() {
state.borrow_mut().pending_unsaved_action =
Some(PendingUnsavedAction::LoadSelected);
ui.global::<EditorModel>().set_unsaved_dialog_message(
"Loading another design will discard the current one's unsaved changes.".into(),
);
ui.global::<EditorModel>().set_unsaved_dialog_open(true);
return;
}
do_load_selected(
&ui,
&state,
&render_ctx,
&preview_state,
&solid_last_solved,
&db,
&source,
);
});
});
}
fn do_load_selected(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) {
let entry_id = ui.global::<LibraryModel>().get_selected_entry_id();
if entry_id < 0 {
show_toast(ui, "No diagram selected to load.", "error");
return;
}
let current_source = source
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
if let Some(worker) = current_source.worker().cloned() {
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
fetch_remote_design_source(
ui.as_weak(),
worker,
i64::from(entry_id),
move |ui, result| match result {
Ok(remote) => {
match loading::design_from_asc_text(&remote.file_name, &remote.asc_text, None) {
Ok(loaded) => REMOTE_LOAD_TARGET.with(|cell| {
let target = cell.borrow().clone();
if let Some(state) = target {
apply_loaded_design(
ui,
&state,
&render_ctx,
&preview_state,
&solid_last_solved,
LoadedDesignOutcome {
loaded,
printed_proportions: None,
label: &remote.file_name,
source_entry_id: None,
},
);
}
}),
Err(e) => show_toast(
ui,
&format!(
"'{}' failed to parse as a .asc cutting schedule: {e}",
remote.file_name
),
"error",
),
}
}
Err(e) => show_toast(ui, &e, "error"),
},
);
return;
}
let full = {
let db = db.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
db.get_diagram_full(i64::from(entry_id))
};
let Ok(Some(full)) = full else {
show_toast(ui, "Diagram detail not found.", "error");
return;
};
match loading::design_from_full_record(&full) {
Ok(loaded) => {
let printed_proportions = loading::external_proportions_from_full_record(&full);
apply_loaded_design(
ui,
state,
render_ctx,
preview_state,
solid_last_solved,
LoadedDesignOutcome {
loaded,
printed_proportions,
label: &full.title,
source_entry_id: Some(full.entry_id),
},
);
}
Err(e) => show_toast(ui, &e, "error"),
}
}
fn resume_pending_unsaved_action(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) {
let pending = state.borrow_mut().pending_unsaved_action.take();
match pending {
Some(PendingUnsavedAction::New {
spec,
template_index,
}) => {
do_new_design_create(
ui,
state,
render_ctx,
preview_state,
solid_last_solved,
spec,
template_index,
);
}
Some(PendingUnsavedAction::LoadSelected) => {
do_load_selected(
ui,
state,
render_ctx,
preview_state,
solid_last_solved,
db,
source,
);
}
Some(PendingUnsavedAction::OpenNative) => {
do_open_native(ui, state, render_ctx, preview_state, solid_last_solved);
}
None => {}
}
}
fn setup_unsaved_guard_dispatch(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) {
let state_save = Rc::clone(state);
let render_ctx_save = Arc::clone(render_ctx);
let preview_state_save = Arc::clone(preview_state);
let solid_last_solved_save = Arc::clone(solid_last_solved);
let db_save = Arc::clone(db);
let source_save = Arc::clone(source);
let ui_weak_save = ui.as_weak();
ui.global::<EditorModel>().on_unsaved_dialog_save(move || {
let Some(ui) = ui_weak_save.upgrade() else {
return;
};
ui.global::<EditorModel>().set_unsaved_dialog_open(false);
ui.global::<EditorModel>().invoke_save_native();
if state_save.borrow().is_dirty() {
state_save.borrow_mut().pending_unsaved_action = None;
return;
}
resume_pending_unsaved_action(
&ui,
&state_save,
&render_ctx_save,
&preview_state_save,
&solid_last_solved_save,
&db_save,
&source_save,
);
});
let state_discard = Rc::clone(state);
let render_ctx_discard = Arc::clone(render_ctx);
let preview_state_discard = Arc::clone(preview_state);
let solid_last_solved_discard = Arc::clone(solid_last_solved);
let db_discard = Arc::clone(db);
let source_discard = Arc::clone(source);
let ui_weak_discard = ui.as_weak();
ui.global::<EditorModel>()
.on_unsaved_dialog_discard(move || {
let Some(ui) = ui_weak_discard.upgrade() else {
return;
};
ui.global::<EditorModel>().set_unsaved_dialog_open(false);
resume_pending_unsaved_action(
&ui,
&state_discard,
&render_ctx_discard,
&preview_state_discard,
&solid_last_solved_discard,
&db_discard,
&source_discard,
);
});
let state_cancel = Rc::clone(state);
ui.global::<EditorModel>()
.on_unsaved_dialog_cancel(move || {
state_cancel.borrow_mut().pending_unsaved_action = None;
});
}
pub(in crate::gui::editor) fn setup_material_suggestion_accept_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_material_suggestion_accept(move |name: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let mut st = state.borrow_mut();
let schedule_ri = st.design.meta.refractive_index;
let material = loading::material_selection_for_accepted_suggestion(
&name,
schedule_ri,
&st.design.material,
);
match st.apply(Edit::SetMaterial { material }) {
Ok(()) => {
refresh_editor_panel_stale(&ui, &render_ctx, &st, &BTreeSet::new());
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
false,
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
drop(st);
ui.global::<EditorModel>()
.set_material_suggestion_name("".into());
ui.global::<EditorModel>()
.set_material_suggestion_text("".into());
});
}
pub(in crate::gui::editor) fn setup_material_suggestion_dismiss_callback(ui: &MainWindow) {
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_material_suggestion_dismiss(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
ui.global::<EditorModel>()
.set_material_suggestion_name("".into());
ui.global::<EditorModel>()
.set_material_suggestion_text("".into());
});
}
pub(in crate::gui::editor) fn setup_undo_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_undo(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let mut st = state.borrow_mut();
match st.undo() {
Ok(true) => {
refresh_editor_panel_stale(
&ui,
&render_ctx,
&st,
&(0..st.design.tiers.len()).collect(),
);
clamp_selection_to_tier_count(&ui, st.design.tiers.len());
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
true,
);
}
Ok(false) => {}
Err(e) => show_toast(&ui, &format!("Undo failed: {e}"), "error"),
}
});
}
pub(in crate::gui::editor) fn setup_redo_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_redo(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let mut st = state.borrow_mut();
match st.redo() {
Ok(true) => {
refresh_editor_panel_stale(
&ui,
&render_ctx,
&st,
&(0..st.design.tiers.len()).collect(),
);
clamp_selection_to_tier_count(&ui, st.design.tiers.len());
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
true,
);
}
Ok(false) => {}
Err(e) => show_toast(&ui, &format!("Redo failed: {e}"), "error"),
}
});
}
pub(in crate::gui::editor) fn setup_apply_preform_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_apply_preform(
move |shape_index: i32,
half_width: SharedString,
length_over_width: SharedString,
depth: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let mut st = state.borrow_mut();
match loading::parse_preform_form(
shape_index,
&half_width,
&length_over_width,
&depth,
FIXED_CYLINDER_PREFORM_SIDES,
) {
Ok(preform) => {
let _ = st.apply(Edit::SetPreform { preform });
refresh_editor_panel_stale(&ui, &render_ctx, &st, &BTreeSet::new());
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
false,
);
}
Err(e) => show_toast(&ui, &e, "error"),
}
},
);
}
pub(in crate::gui::editor) fn setup_apply_yield_inputs_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_apply_yield_inputs(
move |girdle_diameter_mm: SharedString,
material_index: i32,
specific_gravity_override: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let mut st = state.borrow_mut();
match super::super::state::parse_yield_form(
&girdle_diameter_mm,
material_index,
&specific_gravity_override,
&st.design.material,
) {
Ok((girdle_diameter_mm, material)) => {
let _ = st.apply(Edit::Batch(vec![
Edit::SetGirdleDiameterMm { girdle_diameter_mm },
Edit::SetMaterial { material },
]));
refresh_editor_panel_stale(&ui, &render_ctx, &st, &BTreeSet::new());
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
false,
);
}
Err(e) => show_toast(&ui, &e, "error"),
}
},
);
}
pub(in crate::gui::editor) fn setup_apply_preform_y_offset_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_apply_preform_y_offset(move |mm_text: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
stall_guard("on_apply_preform_y_offset", || {
let trimmed = mm_text.trim();
let Ok(mm) = trimmed.parse::<f64>() else {
show_toast(
&ui,
&format!("Y-offset '{trimmed}' is not a number."),
"error",
);
return;
};
if !mm.is_finite() {
show_toast(&ui, "Y-offset must be a finite number.", "error");
return;
}
let mut st = state.borrow_mut();
let Some(solved) = auto_solve::solid_last_solved()
.and_then(|cache| {
cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
})
.filter(|solved| solved.len() == st.design.tiers.len())
else {
show_toast(&ui, "Solve first, then set a Y-offset.", "error");
return;
};
let Some(mm_per_unit) = st.design.yield_report(&solved).mm_per_unit else {
show_toast(
&ui,
"Set a girdle diameter in the Yield tab before setting a Y-offset in \
millimetres.",
"error",
);
return;
};
let y_offset = mm / mm_per_unit;
if let Err(e) = st.apply(Edit::SetPreformYOffset { y_offset }) {
show_toast(&ui, &e.to_string(), "error");
return;
}
refresh_editor_panel_stale(&ui, &render_ctx, &st, &BTreeSet::new());
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
false,
);
});
});
}
pub(in crate::gui::editor) fn setup_apply_cheater_offset_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_apply_cheater_offset(move |index: i32, text: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(index) = usize::try_from(index) else {
return;
};
let trimmed = text.trim();
let offset_deg = if trimmed.is_empty() {
None
} else {
match trimmed.parse::<f64>() {
Ok(value) if value.is_finite() => Some(value),
_ => {
show_toast(
&ui,
&format!("Cheater offset '{trimmed}' is not a number."),
"error",
);
return;
}
}
};
let mut st = state.borrow_mut();
if let Err(e) = st.apply(Edit::SetCheaterOffset { index, offset_deg }) {
show_toast(&ui, &e.to_string(), "error");
return;
}
refresh_editor_panel_stale(&ui, &render_ctx, &st, &BTreeSet::new());
});
}
pub(in crate::gui::editor) fn setup_apply_tier_note_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_apply_tier_note(move |index: i32, text: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(index) = usize::try_from(index) else {
return;
};
let trimmed = text.trim();
let note = if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
};
let mut st = state.borrow_mut();
if let Err(e) = st.apply(Edit::SetTierNote { index, note }) {
show_toast(&ui, &e.to_string(), "error");
return;
}
refresh_editor_panel_stale(&ui, &render_ctx, &st, &BTreeSet::new());
});
}
pub(in crate::gui::editor) fn setup_apply_design_meta_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_apply_design_meta(
move |title: SharedString,
extra_headers: SharedString,
footnotes: SharedString,
gear_ref: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let split_lines = |text: &str| -> Vec<String> {
text.split(';')
.map(str::trim)
.filter(|line| !line.is_empty())
.map(str::to_string)
.collect()
};
let trimmed_title = title.trim();
let mut headers = Vec::new();
if !trimmed_title.is_empty() {
headers.push(trimmed_title.to_string());
}
headers.extend(split_lines(&extra_headers));
let footnotes = split_lines(&footnotes);
let gear_ref_trimmed = gear_ref.trim();
let Ok(gear_reference_angle) = gear_ref_trimmed.parse::<f64>() else {
show_toast(
&ui,
&format!("Gear reference angle '{gear_ref_trimmed}' is not a number."),
"error",
);
return;
};
if !gear_reference_angle.is_finite() {
show_toast(
&ui,
"Gear reference angle must be a finite number.",
"error",
);
return;
}
let mut st = state.borrow_mut();
let _ = st.apply(Edit::SetMeta {
headers,
footnotes,
gear_reference_angle,
});
refresh_editor_panel_stale(&ui, &render_ctx, &st, &BTreeSet::new());
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
false,
);
},
);
}
fn other_tier_names_excluding(st: &EditorState, excluded_index: i32) -> Vec<String> {
st.design
.tiers
.iter()
.enumerate()
.filter(|&(i, _)| excluded_index < 0 || i != excluded_index as usize)
.flat_map(|(_, tier)| tier.names().into_iter().map(str::to_string))
.collect()
}
fn added_tier_label(st: &EditorState, dirty_index: usize) -> Option<String> {
st.design
.tiers
.get(dirty_index)
.map(|tier| tier_nudge_label(tier, dirty_index))
}
fn select_and_announce_added_tier(ui: &MainWindow, dirty_index: usize, label: Option<String>) {
ui.global::<EditorModel>()
.set_selected_tier_index(dirty_index as i32);
if let Some(label) = label {
show_toast(ui, &format!("Added {label}"), "info");
}
}
fn tier_save_edit(
ui: &MainWindow,
st: &EditorState,
index: i32,
tier: indicatrix_cut_core::ConstraintTier,
) -> (usize, Edit) {
if index < 0 {
let append_index = st.design.tiers.len();
let insert_after_selected =
usize::try_from(ui.global::<EditorModel>().get_selected_tier_index())
.ok()
.filter(|&i| i < append_index)
.map_or(append_index, |i| i + 1);
(
insert_after_selected,
Edit::AddTier {
index: insert_after_selected,
tier,
},
)
} else {
let index = index as usize;
(index, Edit::ModifyTier { index, tier })
}
}
fn parse_tier_target_reporting(
ui: &MainWindow,
constraint_kind: i32,
constraint_text: &str,
) -> Result<Option<TierTarget>, ()> {
loading::parse_tier_target(constraint_kind, constraint_text).map_err(|e| {
report_tier_form_error(ui, &e, tier_form_error_field(&e));
})
}
fn tier_save_edit_with_target(
ui: &MainWindow,
st: &EditorState,
index: i32,
tier: indicatrix_cut_core::ConstraintTier,
target: Option<TierTarget>,
) -> (usize, Edit) {
let had_target = usize::try_from(index)
.ok()
.and_then(|i| st.design.tier_target(i))
.is_some();
let (dirty_index, edit) = tier_save_edit(ui, st, index, tier);
let edit = if target.is_some() || had_target {
Edit::Batch(vec![
edit,
Edit::SetTierTarget {
index: dirty_index,
target,
},
])
} else {
edit
};
(dirty_index, edit)
}
struct SaveTierFormContext {
imported_meet: Option<MeetConstraint>,
original_notes: Option<String>,
gear_teeth_abs: u32,
other_tier_names: Vec<String>,
}
fn save_tier_form_context(state: &Rc<RefCell<EditorState>>, index: i32) -> SaveTierFormContext {
let st = state.borrow();
let (imported_meet, original_notes) = (index >= 0)
.then(|| {
let tier = st.design.tiers.get(usize::try_from(index).ok()?)?;
Some((tier.imported_meet.clone(), tier.original_notes.clone()))
})
.flatten()
.unwrap_or_default();
SaveTierFormContext {
imported_meet,
original_notes,
gear_teeth_abs: st.design.meta.gear_teeth_abs(),
other_tier_names: other_tier_names_excluding(&st, index),
}
}
struct TierSaveOutcome {
index: i32,
dirty_index: usize,
non_integral_warning: Option<String>,
}
fn apply_tier_save_success(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
st: RefMut<'_, EditorState>,
outcome: TierSaveOutcome,
) {
let TierSaveOutcome {
index,
dirty_index,
non_integral_warning,
} = outcome;
let model = ui.global::<EditorModel>();
model.set_tier_form_error("".into());
model.set_tier_form_error_field("".into());
refresh_editor_panel_stale(ui, render_ctx, &st, &BTreeSet::from([dirty_index]));
submit_preview_replan(
ui,
render_ctx,
preview_state,
solid_last_solved,
&st,
BTreeSet::from([dirty_index]),
false,
);
if index < 0 {
let label = added_tier_label(&st, dirty_index);
drop(st);
select_and_announce_added_tier(ui, dirty_index, label);
}
if let Some(warning) = non_integral_warning {
show_toast(ui, &warning, "info");
}
}
pub(in crate::gui::editor) fn setup_save_tier_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_save_tier(
move |index: i32,
angle: SharedString,
constraint_kind: i32,
constraint_text: SharedString,
name: SharedString,
indices: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let SaveTierFormContext {
imported_meet,
original_notes,
gear_teeth_abs,
other_tier_names,
} = save_tier_form_context(&state, index);
match loading::parse_tier_form(loading::TierFormFields {
angle: &angle,
constraint_kind,
constraint_text: &constraint_text,
name: &name,
indices: &indices,
gear_teeth_abs,
imported_meet,
original_notes,
other_tier_names: other_tier_names.clone(),
}) {
Ok(mut tier) => {
let Ok(target) =
parse_tier_target_reporting(&ui, constraint_kind, &constraint_text)
else {
return;
};
if index < 0 && tier.name.is_empty() {
tier.name = next_free_block_name(tier.angle_deg, &other_tier_names);
}
let non_integral_warning = non_integral_index_warning(&tier.indices);
let mut st = state.borrow_mut();
if let Some(current) = usize::try_from(index)
.ok()
.and_then(|i| st.design.tiers.get(i))
{
tier.detached.clone_from(¤t.detached);
}
if let MeetConstraint::MeetNamed(names) = &tier.constraint
&& let Some(bad_name) = first_unresolved_meet_name(&st.design, names)
{
report_tier_form_error(
&ui,
&format!("No facet named '{bad_name}' -- check the Meets field."),
"constraint",
);
return;
}
let (dirty_index, edit) =
tier_save_edit_with_target(&ui, &st, index, tier, target);
match st.apply(edit) {
Ok(()) => {
apply_tier_save_success(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
st,
TierSaveOutcome {
index,
dirty_index,
non_integral_warning,
},
);
}
Err(e) => {
let message = e.to_string();
let field = tier_form_error_field(&message);
report_tier_form_error(&ui, &message, field);
}
}
}
Err(e) => report_tier_form_error(&ui, &e, tier_form_error_field(&e)),
}
},
);
}
pub(in crate::gui::editor) fn setup_remove_tier_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_remove_tier(move |index: i32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
if index < 0 {
return;
}
let mut st = state.borrow_mut();
let removed_summary = st.design.tiers.get(index as usize).map(|tier| {
let name = if tier.name.is_empty() {
"(unnamed)".to_string()
} else {
tier.name.clone()
};
(name, tier.indices.len())
});
match st.apply(Edit::RemoveTier {
index: index as usize,
}) {
Ok(()) => {
refresh_editor_panel_stale(&ui, &render_ctx, &st, &BTreeSet::new());
adjust_selection_after_remove(&ui, index);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
false,
);
drop(st);
if let Some((name, facet_count)) = removed_summary {
let plural = if facet_count == 1 { "" } else { "s" };
show_toast(
&ui,
&format!("Removed {name} ({facet_count} facet{plural}), Undo"),
"info",
);
}
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
}
pub(in crate::gui::editor) fn setup_inline_set_angle_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_inline_set_angle(move |index: i32, text: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(index) = usize::try_from(index) else {
return;
};
let mut st = state.borrow_mut();
let Some(current) = st.design.tiers.get(index) else {
return;
};
match loading::parse_angle_only(&text) {
Ok(angle_deg) => {
if angle_deg.to_bits() == current.angle_deg.to_bits() {
st.history.end_coalesce_run();
show_toast(&ui, "No change.", "info");
return;
}
let mut tier = current.clone();
tier.angle_deg = angle_deg;
match st.apply(Edit::ModifyTier { index, tier }) {
Ok(()) => {
refresh_editor_panel_stale(
&ui,
&render_ctx,
&st,
&BTreeSet::from([index]),
);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::from([index]),
false,
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
}
Err(e) => show_toast(&ui, &e, "error"),
}
});
}
const fn clamp_nudge_to_side(current: f64, nudged: f64) -> f64 {
if current.is_sign_negative() == nudged.is_sign_negative() {
return nudged;
}
if current.is_sign_negative() {
-0.0
} else {
0.0
}
}
fn tier_nudge_label(tier: &indicatrix_cut_core::ConstraintTier, index: usize) -> String {
if tier.name.is_empty() {
format!("tier {}", index + 1)
} else {
format!("tier {} ({})", index + 1, tier.name)
}
}
pub(in crate::gui::editor) fn setup_nudge_angle_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let intent_queue = {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
edit_intent::EditIntentQueue::new(move |intent| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let edit_intent::EditIntent::NudgeAngle { targets, delta_deg } = intent else {
return;
};
apply_nudge_intent(
&ui,
&state,
&render_ctx,
&preview_state,
&solid_last_solved,
&targets,
delta_deg,
);
})
};
let state = Rc::clone(state);
ui.global::<EditorModel>()
.on_nudge_angle(move |anchor_index: i32, delta_deg: f32| {
stall_guard("on_nudge_angle", || {
let Ok(anchor_index) = usize::try_from(anchor_index) else {
return;
};
let st = state.borrow();
let is_multi_target =
st.multi_selected.len() > 1 && st.multi_selected.contains(&anchor_index);
let targets: Vec<usize> = if is_multi_target {
st.multi_selected.iter().copied().collect()
} else {
vec![anchor_index]
};
drop(st);
intent_queue.post(edit_intent::EditIntent::NudgeAngle {
targets,
delta_deg: f64::from(delta_deg),
});
});
});
}
fn apply_nudge_intent(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
targets: &[usize],
delta_deg: f64,
) {
let mut st = state.borrow_mut();
let mut clamped_labels: Vec<String> = Vec::new();
let changes: Option<Vec<(usize, f64, f64)>> = targets
.iter()
.map(|&index| {
st.design.tiers.get(index).map(|tier| {
let wanted = tier.angle_deg + delta_deg;
let nudged = clamp_nudge_to_side(tier.angle_deg, wanted);
if nudged != wanted {
clamped_labels.push(tier_nudge_label(tier, index));
}
(index, tier.angle_deg, nudged)
})
})
.collect();
let Some(changes) = changes else {
return;
};
if changes.is_empty() {
return;
}
let key = angle_nudge_coalesce_key(targets);
match st.apply_coalescing(Edit::RetargetAngles { changes }, key) {
Ok(()) => {
let dirty: BTreeSet<usize> = targets.iter().copied().collect();
refresh_editor_panel_stale(ui, render_ctx, &st, &dirty);
submit_preview_replan(
ui,
render_ctx,
preview_state,
solid_last_solved,
&st,
dirty,
false,
);
if !clamped_labels.is_empty() {
show_toast(
ui,
&format!(
"{} stopped at 0° -- nudging further would move it into the \
other block. Type the angle directly (e.g. \"-0\") to cross \
blocks on purpose.",
clamped_labels.join(", ")
),
"info",
);
}
}
Err(e) => show_toast(ui, &e.to_string(), "error"),
}
}
pub(in crate::gui::editor) fn setup_duplicate_tier_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_duplicate_tier(move |index: i32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(index) = usize::try_from(index) else {
return;
};
let mut st = state.borrow_mut();
let Some(source) = st.design.tiers.get(index) else {
return;
};
let mut duplicate = source.clone();
let source_label = if source.name.is_empty() {
"(unnamed)".to_string()
} else {
source.name.clone()
};
let existing_names: Vec<String> =
st.design.tiers.iter().map(|t| t.name.clone()).collect();
duplicate.name = unique_duplicate_name(&source.name, &existing_names);
let duplicate_label = duplicate.name.clone();
duplicate.imported_meet = None;
let new_index = index + 1;
match st.apply(Edit::AddTier {
index: new_index,
tier: duplicate,
}) {
Ok(()) => {
refresh_editor_panel_stale(&ui, &render_ctx, &st, &BTreeSet::from([new_index]));
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::from([new_index]),
false,
);
drop(st);
ui.global::<EditorModel>()
.set_selected_tier_index(new_index as i32);
show_toast(
&ui,
&format!("Duplicated {source_label} as {duplicate_label}"),
"info",
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
}
fn parse_step_series_form(
start_angle_text: &str,
angle_step_text: &str,
count: i32,
anchor_text: &str,
) -> Result<(f64, f64, usize, MeetConstraint), String> {
let start_angle: f64 = start_angle_text
.trim()
.parse()
.map_err(|_| format!("Start angle '{start_angle_text}' is not a number."))?;
let angle_step: f64 = angle_step_text
.trim()
.parse()
.map_err(|_| format!("Angle step '{angle_step_text}' is not a number."))?;
if !start_angle.is_finite() || !angle_step.is_finite() {
return Err("Start angle and angle step must be finite numbers.".to_string());
}
let count = usize::try_from(count)
.ok()
.filter(|&count| count >= 1)
.ok_or_else(|| "Tier count must be a positive whole number.".to_string())?;
let anchor_text = anchor_text.trim();
let first_constraint = if anchor_text.is_empty() {
MeetConstraint::MeetExisting
} else {
let anchor: f64 = anchor_text
.parse()
.map_err(|_| format!("Anchor '{anchor_text}' is not a number."))?;
if !anchor.is_finite() {
return Err("Anchor must be a finite number.".to_string());
}
MeetConstraint::ScaleReference(anchor)
};
Ok((start_angle, angle_step, count, first_constraint))
}
pub(in crate::gui::editor) fn setup_generate_step_series_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_generate_step_series(
move |name_prefix: SharedString,
start_angle_text: SharedString,
angle_step_text: SharedString,
count: i32,
indices_text: SharedString,
anchor_text: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let (start_angle, angle_step, count, first_constraint) = match parse_step_series_form(
&start_angle_text,
&angle_step_text,
count,
&anchor_text,
) {
Ok(parsed) => parsed,
Err(e) => {
show_toast(&ui, &e, "error");
return;
}
};
let mut st = state.borrow_mut();
let gear_teeth_abs = st.design.meta.gear_teeth_abs();
let indices = match loading::parse_index_list(&indices_text, gear_teeth_abs) {
Ok(indices) => indices,
Err(e) => {
show_toast(&ui, &e, "error");
return;
}
};
let tiers = ConstraintTier::step_series(
&name_prefix,
start_angle,
angle_step,
count,
&indices,
&first_constraint,
);
let start_index = st.design.tiers.len();
let edits: Vec<Edit> = tiers
.into_iter()
.enumerate()
.map(|(offset, tier)| Edit::AddTier {
index: start_index + offset,
tier,
})
.collect();
let added_count = edits.len();
match st.apply(Edit::Batch(edits)) {
Ok(()) => {
let dirty: BTreeSet<usize> = (start_index..start_index + added_count).collect();
refresh_editor_panel_stale(&ui, &render_ctx, &st, &dirty);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
dirty,
false,
);
drop(st);
show_toast(&ui, &format!("Generated {added_count} tier(s)."), "info");
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
},
);
}
pub(in crate::gui::editor) fn setup_mirror_tier_to_other_block_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_mirror_tier_to_other_block(
move |tier_index: i32, name_suffix: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let mut st = state.borrow_mut();
let Ok(tier_index) = usize::try_from(tier_index) else {
return;
};
let Some(source) = st.design.tiers.get(tier_index) else {
return;
};
let mirrored = source.mirrored_to_other_block(&name_suffix);
let mirrored_label = mirrored.name.clone();
let new_index = st.design.tiers.len();
match st.apply(Edit::AddTier {
index: new_index,
tier: mirrored,
}) {
Ok(()) => {
refresh_editor_panel_stale(&ui, &render_ctx, &st, &BTreeSet::from([new_index]));
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::from([new_index]),
false,
);
drop(st);
ui.global::<EditorModel>()
.set_selected_tier_index(new_index as i32);
show_toast(&ui, &format!("Mirrored to {mirrored_label}"), "info");
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
},
);
}
fn unique_duplicate_name(source_name: &str, existing_names: &[String]) -> String {
let (base, source_number) = split_duplicate_suffix(source_name.trim());
let base = if base.is_empty() { "Tier" } else { base };
let mut n: u32 = source_number
.and_then(|number| number.checked_add(1))
.unwrap_or(2);
loop {
let candidate = format!("{base} ({n})");
if !existing_names
.iter()
.any(|existing| existing.eq_ignore_ascii_case(&candidate))
{
return candidate;
}
n += 1;
}
}
fn next_free_block_name(angle_deg: f64, existing_names: &[String]) -> String {
let letter = if angle_deg.is_sign_negative() {
'P'
} else {
'C'
};
let mut n: u32 = 1;
loop {
let candidate = format!("{letter}{n}");
if !existing_names
.iter()
.any(|existing| existing.eq_ignore_ascii_case(&candidate))
{
return candidate;
}
n += 1;
}
}
fn split_duplicate_suffix(name: &str) -> (&str, Option<u32>) {
let Some((base, rest)) = name.rsplit_once(" (") else {
return (name, None);
};
let Some(digits) = rest.strip_suffix(')') else {
return (name, None);
};
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return (name, None);
}
digits
.parse()
.map_or((name, None), |number| (base, Some(number)))
}
pub(in crate::gui::editor) fn setup_toggle_multi_select_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
) {
setup_select_tier_range_callback(ui, state);
let state = Rc::clone(state);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_toggle_multi_select(move |index: i32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(index) = usize::try_from(index) else {
return;
};
let mut st = state.borrow_mut();
if !st.multi_selected.remove(&index) {
st.multi_selected.insert(index);
}
let multi_selected = st.multi_selected.clone();
let mut rows: Vec<EditorTierItem> =
ui.global::<EditorModel>().get_tiers().iter().collect();
apply_multi_selection(&mut rows, &multi_selected);
push_tiers(&ui, rows);
push_multi_selected_count(&ui, multi_selected.len());
if let Some(preview_state) = auto_solve::preview_state() {
let facet_map = facet_map_from_aligned_solve(&st.design);
let facet_ids: Vec<u32> = multi_selected
.iter()
.flat_map(|&tier_index| facet_map.facets_of_tier(tier_index).iter().copied())
.collect();
resubmit_facet_overlay(&preview_state, |overlay| {
overlay.multi_selected = facet_ids;
});
}
});
}
pub(in crate::gui::editor) fn setup_select_tier_range_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
) {
let state = Rc::clone(state);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_select_tier_range(move |index: i32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(index) = usize::try_from(index) else {
return;
};
let anchor = usize::try_from(ui.global::<EditorModel>().get_selected_tier_index())
.unwrap_or(index);
let (lo, hi) = if anchor <= index {
(anchor, index)
} else {
(index, anchor)
};
let mut st = state.borrow_mut();
st.multi_selected = (lo..=hi).collect();
let multi_selected = st.multi_selected.clone();
let mut rows: Vec<EditorTierItem> =
ui.global::<EditorModel>().get_tiers().iter().collect();
apply_multi_selection(&mut rows, &multi_selected);
push_tiers(&ui, rows);
push_multi_selected_count(&ui, multi_selected.len());
if let Some(preview_state) = auto_solve::preview_state() {
let facet_map = facet_map_from_aligned_solve(&st.design);
let facet_ids: Vec<u32> = multi_selected
.iter()
.flat_map(|&tier_index| facet_map.facets_of_tier(tier_index).iter().copied())
.collect();
resubmit_facet_overlay(&preview_state, |overlay| {
overlay.multi_selected = facet_ids;
});
}
});
}
pub(in crate::gui::editor) fn setup_toggle_detach_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
auto_solve::stash_render_ctx(render_ctx);
let state_toggle = Rc::clone(state);
let render_ctx_toggle = Arc::clone(render_ctx);
let preview_state_toggle = Arc::clone(preview_state);
let solid_last_solved_toggle = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_toggle_detach(move |index: i32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
if index < 0 {
return;
}
let mut st = state_toggle.borrow_mut();
let index = index as usize;
let Some(tier) = st.design.tiers.get(index) else {
return;
};
let edit_result = if tier.detached.is_empty() {
st.design.detach_all_in_tier(index)
} else {
st.design.reattach_all_in_tier(index)
};
match edit_result.and_then(|edit| st.apply(edit)) {
Ok(()) => {
refresh_editor_panel_stale(
&ui,
&render_ctx_toggle,
&st,
&BTreeSet::from([index]),
);
submit_preview_replan(
&ui,
&render_ctx_toggle,
&preview_state_toggle,
&solid_last_solved_toggle,
&st,
BTreeSet::from([index]),
false,
);
let now_detached = st
.design
.tiers
.get(index)
.is_some_and(|tier| !tier.detached.is_empty());
let label = st
.design
.tiers
.get(index)
.map(|tier| tier_nudge_label(tier, index));
drop(st);
if let Some(label) = label {
let verb = if now_detached {
"Detached"
} else {
"Reattached"
};
show_toast(&ui, &format!("{verb} {label}"), "info");
}
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
setup_move_tier_callback(ui, state, render_ctx, preview_state, solid_last_solved);
setup_complete_orbit_callback(ui, state, render_ctx, preview_state, solid_last_solved);
setup_clear_multi_select_callback(ui, state);
setup_remove_multi_selected_callback(ui, state, render_ctx, preview_state, solid_last_solved);
setup_facet_remove_callback(ui, state, render_ctx, preview_state, solid_last_solved);
setup_facet_toggle_detach_callback(ui, state, render_ctx, preview_state, solid_last_solved);
setup_facet_add_callback(ui, state, render_ctx, preview_state, solid_last_solved);
setup_tier_rotate_indices_callback(ui, state, render_ctx, preview_state, solid_last_solved);
setup_tier_mirror_indices_callback(ui, state, render_ctx, preview_state, solid_last_solved);
setup_adopt_all_callback(ui, state, render_ctx, preview_state, solid_last_solved);
setup_adopt_selected_callback(ui, state, render_ctx, preview_state, solid_last_solved);
setup_pin_to_mast_callback(ui, state, render_ctx, preview_state, solid_last_solved);
setup_highlight_tooth_callback(ui, state);
setup_generate_step_series_callback(ui, state, render_ctx, preview_state, solid_last_solved);
setup_mirror_tier_to_other_block_callback(
ui,
state,
render_ctx,
preview_state,
solid_last_solved,
);
}
pub(in crate::gui::editor) fn setup_adopt_all_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_adopt_all(move || {
stall_guard("on_adopt_all", || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let mut st = state.borrow_mut();
let edits: Vec<Edit> = st
.design
.tiers
.iter()
.enumerate()
.filter_map(|(index, tier)| {
tier.imported_meet
.clone()
.map(|constraint| Edit::SetConstraint { index, constraint })
})
.collect();
if edits.is_empty() {
return;
}
let count = edits.len();
match st.apply(Edit::Batch(edits)) {
Ok(()) => {
drop(st);
refresh_all_now(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&state,
false,
);
let plural = if count == 1 { "" } else { "s" };
show_toast(
&ui,
&format!("Adopted {count} imported meet{plural}"),
"info",
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
});
}
pub(in crate::gui::editor) fn setup_adopt_selected_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_adopt_selected(move || {
stall_guard("on_adopt_selected", || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let mut st = state.borrow_mut();
let edits: Vec<Edit> = st
.design
.tiers
.iter()
.enumerate()
.filter(|(index, _)| st.multi_selected.contains(index))
.filter_map(|(index, tier)| {
tier.imported_meet
.clone()
.map(|constraint| Edit::SetConstraint { index, constraint })
})
.collect();
if edits.is_empty() {
return;
}
let count = edits.len();
match st.apply(Edit::Batch(edits)) {
Ok(()) => {
drop(st);
refresh_all_now(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&state,
false,
);
let plural = if count == 1 { "" } else { "s" };
show_toast(
&ui,
&format!("Adopted {count} imported meet{plural} from the selection"),
"info",
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
});
}
pub(in crate::gui::editor) fn setup_pin_to_mast_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_pin_to_mast(move |index: i32| {
stall_guard("on_pin_to_mast", || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(index) = usize::try_from(index) else {
return;
};
let Some(mast) = solid_last_solved
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.and_then(|solved| solved.get(index))
.map(|tier| tier.mast)
else {
return;
};
let mut st = state.borrow_mut();
match st.apply(Edit::SetConstraint {
index,
constraint: MeetConstraint::ScaleReference(mast),
}) {
Ok(()) => {
let dirty: BTreeSet<usize> = std::iter::once(index).collect();
refresh_editor_panel_stale(&ui, &render_ctx, &st, &dirty);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
dirty,
false,
);
show_toast(
&ui,
&format!("Pinned tier {} at {mast:.4}", index + 1),
"info",
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
});
}
pub(in crate::gui::editor) fn setup_highlight_tooth_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
) {
let state = Rc::clone(state);
ui.global::<EditorModel>()
.on_highlight_tooth(move |tooth: i32| {
let Some(preview_state) = auto_solve::preview_state() else {
return;
};
if tooth < 0 {
resubmit_facet_overlay(&preview_state, |overlay| overlay.multi_selected.clear());
return;
}
let st = state.borrow();
let facet_map = facet_map_from_aligned_solve(&st.design);
let tooth = tooth as u32;
let facet_ids: Vec<u32> = (0..facet_map.facet_count() as u32)
.filter(|&facet_id| facet_map.index_on_gear(facet_id as usize) == tooth)
.collect();
resubmit_facet_overlay(&preview_state, |overlay| {
overlay.multi_selected = facet_ids;
});
});
}
pub(in crate::gui::editor) fn setup_facet_remove_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_facet_remove(move |tier_index: i32, position: f32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(tier_index) = usize::try_from(tier_index) else {
return;
};
let mut st = state.borrow_mut();
match st
.design
.remove_orbit_member(tier_index, f64::from(position))
.and_then(|edit| st.apply(edit))
{
Ok(()) => {
refresh_editor_panel_stale(
&ui,
&render_ctx,
&st,
&BTreeSet::from([tier_index]),
);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::from([tier_index]),
false,
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
}
pub(in crate::gui::editor) fn setup_facet_toggle_detach_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_facet_toggle_detach(move |tier_index: i32, position: f32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(tier_index) = usize::try_from(tier_index) else {
return;
};
let position = f64::from(position);
let mut st = state.borrow_mut();
let Some(tier) = st.design.tiers.get(tier_index) else {
return;
};
let edit_result = if tier.detached.contains(&position) {
st.design.reattach_orbit_member(tier_index, position)
} else {
st.design.detach_orbit_member(tier_index, position)
};
match edit_result.and_then(|edit| st.apply(edit)) {
Ok(()) => {
refresh_editor_panel_stale(
&ui,
&render_ctx,
&st,
&BTreeSet::from([tier_index]),
);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::from([tier_index]),
false,
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
}
pub(in crate::gui::editor) fn setup_facet_add_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_facet_add(move |tier_index: i32, position: f32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(tier_index) = usize::try_from(tier_index) else {
return;
};
let mut st = state.borrow_mut();
match st
.design
.add_orbit_member(tier_index, f64::from(position))
.and_then(|edit| st.apply(edit))
{
Ok(()) => {
refresh_editor_panel_stale(
&ui,
&render_ctx,
&st,
&BTreeSet::from([tier_index]),
);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::from([tier_index]),
false,
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
}
pub(in crate::gui::editor) fn setup_tier_rotate_indices_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_tier_rotate_indices(move |tier_index: i32, k_teeth: f32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(tier_index) = usize::try_from(tier_index) else {
return;
};
let mut st = state.borrow_mut();
match st
.design
.rotate_indices(tier_index, f64::from(k_teeth))
.and_then(|edit| st.apply(edit))
{
Ok(()) => {
refresh_editor_panel_stale(
&ui,
&render_ctx,
&st,
&BTreeSet::from([tier_index]),
);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::from([tier_index]),
false,
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
}
pub(in crate::gui::editor) fn setup_tier_mirror_indices_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_tier_mirror_indices(move |tier_index: i32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(tier_index) = usize::try_from(tier_index) else {
return;
};
let mut st = state.borrow_mut();
match st
.design
.mirror_indices(tier_index)
.and_then(|edit| st.apply(edit))
{
Ok(()) => {
refresh_editor_panel_stale(
&ui,
&render_ctx,
&st,
&BTreeSet::from([tier_index]),
);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::from([tier_index]),
false,
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
}
pub(in crate::gui::editor) fn setup_move_tier_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_move_tier(move |index: i32, direction: i32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(index) = usize::try_from(index) else {
return;
};
let mut st = state.borrow_mut();
let tier_count = st.design.tiers.len();
let target = if direction < 0 {
index.checked_sub(1)
} else {
index.checked_add(1).filter(|&t| t < tier_count)
};
let Some(target) = target else {
return;
};
match st.apply(Edit::MoveTier {
from: index,
to: target,
}) {
Ok(()) => {
refresh_editor_panel_stale(
&ui,
&render_ctx,
&st,
&(0..st.design.tiers.len()).collect(),
);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
true,
);
drop(st);
ui.global::<EditorModel>()
.set_selected_tier_index(target as i32);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
}
pub(in crate::gui::editor) fn setup_complete_orbit_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_complete_orbit(move |index: i32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(index) = usize::try_from(index) else {
return;
};
let mut st = state.borrow_mut();
let Ok(units) = st.design.orbit_units(index) else {
return;
};
let anchors: Vec<f64> = units
.iter()
.filter(|unit| !unit.is_complete())
.filter_map(|unit| unit.members.first().copied())
.collect();
if anchors.is_empty() {
return;
}
let mut last_err = None;
for position in anchors {
let outcome = st
.design
.add_orbit_member(index, position)
.and_then(|edit| st.apply(edit));
if let Err(e) = outcome {
last_err = Some(e);
}
}
match last_err {
None => {
refresh_editor_panel_stale(&ui, &render_ctx, &st, &BTreeSet::from([index]));
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::from([index]),
false,
);
}
Some(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
}
pub(in crate::gui::editor) fn setup_clear_multi_select_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
) {
let state = Rc::clone(state);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_clear_multi_select(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let mut st = state.borrow_mut();
if st.multi_selected.is_empty() {
return;
}
st.multi_selected.clear();
let mut rows: Vec<EditorTierItem> = ui.global::<EditorModel>().get_tiers().iter().collect();
apply_multi_selection(&mut rows, &st.multi_selected);
push_tiers(&ui, rows);
push_multi_selected_count(&ui, 0);
if let Some(preview_state) = auto_solve::preview_state() {
resubmit_facet_overlay(&preview_state, |overlay| overlay.multi_selected.clear());
}
});
}
pub(in crate::gui::editor) fn setup_remove_multi_selected_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_remove_multi_selected(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let mut st = state.borrow_mut();
let targets: Vec<usize> = st.multi_selected.iter().rev().copied().collect();
if targets.is_empty() {
return;
}
let removed_count = targets.len();
let mut last_err = None;
for index in targets {
if let Err(e) = st.apply(Edit::RemoveTier { index }) {
last_err = Some(e);
}
}
match last_err {
None => {
refresh_editor_panel_stale(&ui, &render_ctx, &st, &BTreeSet::new());
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
false,
);
drop(st);
ui.global::<EditorModel>().set_selected_tier_index(-1);
bump_form_reset_pulse(&ui);
show_toast(&ui, &format!("Removed {removed_count} tier(s)."), "info");
}
Some(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
}
fn ri_override_for_material_pick(
picked_name: Option<&str>,
previous_material_name: Option<&str>,
legacy_ri: f64,
) -> Option<f64> {
if previous_material_name.is_some() {
return None;
}
loading::ri_override_to_preserve(picked_name?, legacy_ri)
}
pub(in crate::gui::editor) fn setup_apply_design_material_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_apply_design_material(
move |combo_index: i32, ri_override_text: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let options: Vec<String> = ui
.global::<EditorModel>()
.get_material_combo_options()
.iter()
.map(|s| s.to_string())
.collect();
let mut st = state.borrow_mut();
match parse_design_material_form(
combo_index,
&ri_override_text,
&options,
&st.design.material,
) {
Ok(mut material) => {
if material.refractive_index_override.is_none() {
material.refractive_index_override = ri_override_for_material_pick(
material.name.as_deref(),
st.design.material.name.as_deref(),
st.design.meta.refractive_index,
);
}
match st.apply(Edit::SetMaterial { material }) {
Ok(()) => {
refresh_editor_panel_stale(&ui, &render_ctx, &st, &BTreeSet::new());
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
false,
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
}
Err(e) => show_toast(&ui, &e, "error"),
}
},
);
}
pub(in crate::gui::editor) fn setup_material_guess_set_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_set_material_from_guess(move |name: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let mut st = state.borrow_mut();
let mut material = st.design.material.clone();
material.name = Some(name.to_string());
match st.apply(Edit::SetMaterial { material }) {
Ok(()) => {
refresh_editor_panel_stale(&ui, &render_ctx, &st, &BTreeSet::new());
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
false,
);
show_toast(&ui, &format!("Material set to {name}."), "success");
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
}
pub(in crate::gui::editor) fn setup_apply_symmetry_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
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_preview = Rc::clone(&state);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_apply_symmetry(
move |symmetry_order_text: SharedString, mirror: bool| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let symmetry_order: u32 = match symmetry_order_text.trim().parse() {
Ok(v) if v >= 1 => v,
_ => {
show_toast(
&ui,
"Symmetry order must be a positive whole number.",
"error",
);
return;
}
};
let mut st = state.borrow_mut();
let gear_teeth = st.design.meta.gear_teeth;
match st.apply(Edit::SetSchedule {
gear_teeth,
symmetry_order,
mirror,
}) {
Ok(()) => {
refresh_editor_panel_stale(
&ui,
&render_ctx,
&st,
&(0..st.design.tiers.len()).collect(),
);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
true,
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
},
);
let ui_weak_preview = ui.as_weak();
ui.global::<EditorModel>().on_request_symmetry_preview(
move |symmetry_order_text: SharedString, mirror: bool| {
let Some(ui) = ui_weak_preview.upgrade() else {
return;
};
let Ok(symmetry_order) = symmetry_order_text.trim().parse::<u32>() else {
ui.global::<EditorModel>()
.set_symmetry_preview_text(String::new().into());
return;
};
if symmetry_order == 0 {
ui.global::<EditorModel>()
.set_symmetry_preview_text(String::new().into());
return;
}
let st = state_preview.borrow();
let incomplete =
tiers_incomplete_under_proposed_symmetry(&st.design, symmetry_order, mirror);
let text = if incomplete == 0 {
"No tiers would become incomplete orbits.".to_string()
} else {
let plural = if incomplete == 1 { "" } else { "s" };
format!("{incomplete} tier{plural} would become incomplete orbits.")
};
ui.global::<EditorModel>()
.set_symmetry_preview_text(text.into());
},
);
}
pub(in crate::gui::editor) fn setup_gear_apply_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
) {
let state_apply = Rc::clone(state);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_gear_apply(
move |gear_preset_index: i32, gear_custom_text: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let to_gear = match gear_choice_to_teeth(gear_preset_index, &gear_custom_text) {
Ok(t) => t,
Err(e) => {
show_toast(&ui, &e, "error");
return;
}
};
let mut st = state_apply.borrow_mut();
let from_gear = st.design.meta.gear_teeth;
if to_gear == from_gear {
show_toast(&ui, "Already using this gear.", "info");
return;
}
let rounding = RemapRounding::Nearest;
let rows: Vec<GearRemapRow> =
gear_remap_preview(&st.design, from_gear, to_gear, rounding);
st.pending_gear_remap = Some(PendingGearRemap {
from_gear,
to_gear,
symmetry_order: st.design.meta.symmetry_order,
mirror: st.design.meta.mirror,
rounding,
});
PENDING_GEAR_REMAP_GENERATION
.with(|cell| cell.set(Some(st.generation.load(Ordering::Relaxed))));
drop(st);
ui.global::<EditorModel>()
.set_gear_remap_rows(ModelRc::new(VecModel::from(rows)));
ui.global::<EditorModel>().set_gear_remap_open(true);
},
);
let state_rounding = Rc::clone(state);
let ui_weak_rounding = ui.as_weak();
ui.global::<EditorModel>()
.on_gear_remap_set_rounding(move |rounding_index: i32| {
let Some(ui) = ui_weak_rounding.upgrade() else {
return;
};
let rounding = match rounding_index {
1 => RemapRounding::Floor,
2 => RemapRounding::Ceil,
_ => RemapRounding::Nearest,
};
let mut st = state_rounding.borrow_mut();
let Some(pending) = st.pending_gear_remap.as_mut() else {
return;
};
pending.rounding = rounding;
let from_gear = pending.from_gear;
let to_gear = pending.to_gear;
let rows: Vec<GearRemapRow> =
gear_remap_preview(&st.design, from_gear, to_gear, rounding);
drop(st);
ui.global::<EditorModel>()
.set_gear_remap_rows(ModelRc::new(VecModel::from(rows)));
});
}
pub(in crate::gui::editor) fn setup_gear_remap_confirm_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_gear_remap_confirm(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let mut st = state.borrow_mut();
let Some(pending) = st.pending_gear_remap.take() else {
ui.global::<EditorModel>().set_gear_remap_open(false);
return;
};
let started_generation = PENDING_GEAR_REMAP_GENERATION.with(Cell::take);
if started_generation.is_some_and(|g| g != st.generation.load(Ordering::Relaxed)) {
ui.global::<EditorModel>().set_gear_remap_open(false);
show_toast(
&ui,
"The design changed while Apply Gear was open, so this preview no \
longer matches it. Re-open Apply Gear to remap against the current \
design.",
"warning",
);
return;
}
let batch_result = st.apply(Edit::Batch(vec![
Edit::RemapIndices {
from_gear: pending.from_gear,
to_gear: pending.to_gear,
rounding: pending.rounding,
},
Edit::SetSchedule {
gear_teeth: pending.to_gear,
symmetry_order: pending.symmetry_order,
mirror: pending.mirror,
},
]));
ui.global::<EditorModel>().set_gear_remap_open(false);
match batch_result {
Ok(()) => {
refresh_editor_panel_stale(
&ui,
&render_ctx,
&st,
&(0..st.design.tiers.len()).collect(),
);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
true,
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
}
pub(in crate::gui::editor) fn setup_gear_remap_cancel_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
) {
let state = Rc::clone(state);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_gear_remap_cancel(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
state.borrow_mut().pending_gear_remap = None;
PENDING_GEAR_REMAP_GENERATION.with(|cell| cell.set(None));
ui.global::<EditorModel>().set_gear_remap_open(false);
});
}
pub(in crate::gui::editor) fn setup_viewport_material_linked_changed_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
) {
let state_linked = Rc::clone(state);
let render_ctx_linked = Arc::clone(render_ctx);
let ui_weak = ui.as_weak();
ui.global::<ViewportModel>()
.on_viewport_material_linked_changed(move |linked: bool| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
if linked {
let st = state_linked.borrow();
let selected_material_index = {
let mut ctx = render_ctx_linked
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
sync_viewport_material_link(&ui, &mut ctx, &st.design)
};
drop(st);
if let Some(idx) = selected_material_index {
ui.global::<ViewportModel>()
.set_selected_material_index(idx);
}
}
});
let state_props = Rc::clone(state);
let render_ctx_props = Arc::clone(render_ctx);
let ui_weak_props = ui.as_weak();
ui.global::<EditorModel>().on_apply_printed_proportions(
move |vol_w3: SharedString,
lw: SharedString,
cw: SharedString,
pw: SharedString,
hw: SharedString| {
let Some(ui) = ui_weak_props.upgrade() else {
return;
};
let props = match parse_printed_proportions_form(&vol_w3, &lw, &cw, &pw, &hw) {
Ok(props) => props,
Err(e) => {
show_toast(&ui, &e, "error");
return;
}
};
let has_any = props.vol_w3.is_some()
|| props.lw.is_some()
|| props.cw.is_some()
|| props.pw.is_some()
|| props.hw.is_some();
let mut st = state_props.borrow_mut();
st.printed_proportions = has_any.then_some(props);
refresh_editor_panel_stale(&ui, &render_ctx_props, &st, &BTreeSet::new());
drop(st);
show_toast(
&ui,
if has_any {
"Printed proportions saved -- Deep Solve can now verify against them."
} else {
"Printed proportions cleared."
},
"success",
);
},
);
}
fn parse_printed_proportions_field(label: &str, text: &str) -> Result<Option<f64>, String> {
let trimmed = text.trim();
if trimmed.is_empty() {
return Ok(None);
}
let value: f64 = trimmed
.parse()
.map_err(|_| format!("{label} '{trimmed}' is not a number."))?;
if !value.is_finite() || value <= 0.0 {
return Err(format!("{label} must be a positive number."));
}
Ok(Some(value))
}
fn parse_printed_proportions_form(
vol_w3: &str,
lw: &str,
cw: &str,
pw: &str,
hw: &str,
) -> Result<indicatrix::geometry::stone_metrics::ExternalProportions, String> {
Ok(indicatrix::geometry::stone_metrics::ExternalProportions {
vol_w3: parse_printed_proportions_field("Vol/W\u{b3}", vol_w3)?,
lw: parse_printed_proportions_field("L/W", lw)?,
cw: parse_printed_proportions_field("C/W", cw)?,
pw: parse_printed_proportions_field("P/W", pw)?,
hw: parse_printed_proportions_field("H/W", hw)?,
})
}
fn map_to_pick_coordinates(ui: &MainWindow, x: f32, y: f32) -> (f32, f32) {
let scale = ui.window().scale_factor();
let physical = (x * scale, y * scale);
let Some(render_ctx) = auto_solve::render_ctx() else {
return physical;
};
let view_mode = ui.global::<SolidPreviewModel>().get_view_mode() as u8;
let viewport_physical = (
(ui.global::<SolidPreviewModel>().get_viewport_width() * scale) as u32,
(ui.global::<SolidPreviewModel>().get_viewport_height() * scale) as u32,
);
let render_size = {
let ctx = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
(ctx.width, ctx.height)
};
let contained = crate::gui::render::camera_lighting::contained_request_size(
view_mode,
viewport_physical,
render_size,
);
let (margin_x, margin_y) = letterbox_margin(viewport_physical, contained);
(physical.0 - margin_x, physical.1 - margin_y)
}
fn letterbox_margin(viewport_physical: (u32, u32), contained_physical: (u32, u32)) -> (f32, f32) {
(
viewport_physical.0.saturating_sub(contained_physical.0) as f32 / 2.0,
viewport_physical.1.saturating_sub(contained_physical.1) as f32 / 2.0,
)
}
pub(in crate::gui::editor) fn setup_solid_facet_hover_callback(
ui: &MainWindow,
solid_pick_state: &SolidPickState,
) {
let solid_pick = Arc::clone(&solid_pick_state.pick);
let solid_hover_text = Arc::clone(&solid_pick_state.hover_text);
let ui_weak = ui.as_weak();
ui.global::<SolidPreviewModel>()
.on_facet_hover(move |x: f32, y: f32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let (px, py) = map_to_pick_coordinates(&ui, x, y);
let facet_id = solid_pick
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.and_then(|pick| pick.facet_at(px.max(0.0) as u32, py.max(0.0) as u32));
let Some(facet_id) = facet_id else {
let selected_label = SELECTED_FACET_LABEL.with(|cell| cell.borrow().clone());
ui.global::<SolidPreviewModel>()
.set_hover_text(selected_label.into());
if let Some(preview_state) = auto_solve::preview_state() {
resubmit_facet_overlay(&preview_state, |overlay| overlay.hovered = None);
}
return;
};
let text = solid_hover_text
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(facet_id as usize)
.cloned()
.unwrap_or_default();
ui.global::<SolidPreviewModel>().set_hover_text(text.into());
if let Some(preview_state) = auto_solve::preview_state() {
resubmit_facet_overlay(&preview_state, |overlay| overlay.hovered = Some(facet_id));
}
});
}
pub(in crate::gui::editor) fn setup_solid_facet_click_callback(
ui: &MainWindow,
solid_pick_state: &SolidPickState,
) {
let solid_pick = Arc::clone(&solid_pick_state.pick);
let solid_facet_tier = Arc::clone(&solid_pick_state.facet_tier);
let solid_hover_text = Arc::clone(&solid_pick_state.hover_text);
let ui_weak = ui.as_weak();
ui.global::<SolidPreviewModel>()
.on_facet_click(move |x: f32, y: f32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let (px, py) = map_to_pick_coordinates(&ui, x, y);
let Some(facet_id) = solid_pick
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.and_then(|pick| pick.facet_at(px.max(0.0) as u32, py.max(0.0) as u32))
else {
ui.global::<EditorModel>().set_selected_tier_index(-1);
SELECTED_FACET_LABEL.with(|cell| cell.borrow_mut().clear());
ui.global::<SolidPreviewModel>().set_hover_text("".into());
return;
};
let tier_index = solid_facet_tier
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(facet_id as usize)
.copied()
.flatten();
if let Some(tier_index) = tier_index {
ui.global::<EditorModel>()
.set_selected_tier_index(tier_index as i32);
}
let label = solid_hover_text
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(facet_id as usize)
.cloned()
.unwrap_or_default();
SELECTED_FACET_LABEL.with(|cell| cell.borrow_mut().clone_from(&label));
ui.global::<SolidPreviewModel>()
.set_hover_text(label.into());
if let Some(preview_state) = auto_solve::preview_state() {
resubmit_facet_overlay(&preview_state, |overlay| {
overlay.selected_facet = Some(facet_id);
});
}
});
}
pub(in crate::gui::editor) fn setup_solid_selected_tier_changed_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_selected_tier_changed(move |_index: i32| {
let busy = state.try_borrow_mut().is_err();
if !busy {
if let Some(ui) = ui_weak.upgrade() {
apply_selected_tier_change(
&ui,
&state,
&render_ctx,
&preview_state,
&solid_last_solved,
);
}
return;
}
let ui_weak = ui_weak.clone();
let state = Rc::clone(&state);
let render_ctx = Arc::clone(&render_ctx);
let preview_state = Arc::clone(&preview_state);
let solid_last_solved = Arc::clone(&solid_last_solved);
slint::Timer::single_shot(Duration::ZERO, move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
apply_selected_tier_change(
&ui,
&state,
&render_ctx,
&preview_state,
&solid_last_solved,
);
});
});
}
fn apply_selected_tier_change(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let mut st = state.borrow_mut();
st.history.end_coalesce_run();
if !st.multi_selected.is_empty() {
st.multi_selected.clear();
let mut rows: Vec<EditorTierItem> = ui.global::<EditorModel>().get_tiers().iter().collect();
apply_multi_selection(&mut rows, &st.multi_selected);
push_tiers(ui, rows);
push_multi_selected_count(ui, 0);
resubmit_facet_overlay(preview_state, |overlay| overlay.multi_selected.clear());
}
push_selected_tier_chips(ui, &st);
submit_preview_replan(
ui,
render_ctx,
preview_state,
solid_last_solved,
&st,
BTreeSet::new(),
false,
);
}
fn report_tier_form_error(ui: &MainWindow, message: &str, field: &str) {
let model = ui.global::<EditorModel>();
model.set_tier_form_error(message.into());
model.set_tier_form_error_field(field.into());
show_toast(ui, message, "error");
}
fn tier_form_error_field(message: &str) -> &'static str {
if message.starts_with("Angle") {
"angle"
} else if message.starts_with("Another tier is already named") {
"name"
} else if message.starts_with("Index '") {
"indices"
} else if message.starts_with("\"Meet named\"")
|| message.starts_with("Scale reference")
|| message.starts_with("No facet named")
|| message.starts_with("Unknown constraint kind")
|| message.starts_with("Depth")
|| message.starts_with("Girdle thickness")
|| message.starts_with("Table width")
{
"constraint"
} else {
""
}
}
fn non_integral_index_warning(indices: &[f64]) -> Option<String> {
let mut fractional: Vec<String> = indices
.iter()
.filter(|v| (**v - v.round()).abs() > 1e-3)
.map(|v| format!("{v:.3}"))
.collect();
fractional.dedup();
if fractional.is_empty() {
return None;
}
Some(format!(
"Note: non-integral index position(s) {} -- check this was intentional.",
fractional.join(", ")
))
}
pub(in crate::gui::editor) fn setup_tier_filter_callback(ui: &MainWindow) {
ui.global::<EditorModel>().on_tier_matches_filter(
|haystack: SharedString, filter: SharedString| tier_matches_filter(&haystack, &filter),
);
}
pub(in crate::gui::editor) fn setup_angle_live_preview_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
) {
let state = Rc::clone(state);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_angle_live_preview(move |text: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let model = ui.global::<EditorModel>();
let clear = || {
model.set_angle_live_margin_text(SharedString::new());
model.set_angle_live_margin_level(-1);
model.set_angle_live_margin_is_estimate(false);
};
let Ok(angle_deg) = text.trim().parse::<f64>() else {
clear();
return;
};
let st = state.borrow();
let n_d = st.design.effective_refractive_index();
if angle_deg < 0.0 {
let margin = indicatrix_cut_core::tier_margin_deg(angle_deg, n_d);
let level = match indicatrix_cut_core::windowing_risk(angle_deg, n_d) {
indicatrix_cut_core::Risk::Safe => 0,
indicatrix_cut_core::Risk::Marginal => 1,
indicatrix_cut_core::Risk::Windows => 2,
};
model.set_angle_live_margin_text(format!("{margin:+.1}\u{b0}").into());
model.set_angle_live_margin_level(level);
model.set_angle_live_margin_is_estimate(false);
} else if angle_deg > 0.0 {
let (_, pavilion_deg) = representative_crown_and_pavilion_angles_deg(&st.design);
let Some(pavilion_deg) = pavilion_deg else {
clear();
return;
};
let margin =
indicatrix_cut_core::crown_window_margin_deg(pavilion_deg, angle_deg, n_d);
let level =
match indicatrix_cut_core::crown_windowing_risk(pavilion_deg, angle_deg, n_d) {
indicatrix_cut_core::Risk::Safe => 0,
indicatrix_cut_core::Risk::Marginal => 1,
indicatrix_cut_core::Risk::Windows => 2,
};
model.set_angle_live_margin_text(format!("{margin:+.1}\u{b0}").into());
model.set_angle_live_margin_level(level);
model.set_angle_live_margin_is_estimate(true);
} else {
clear();
}
});
}
pub(in crate::gui::editor) fn setup_anchor_explainer_dismiss_callback(ui: &MainWindow) {
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_anchor_explainer_dismiss(move |dont_show_again: bool| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
ui.global::<EditorModel>().set_anchor_explainer_open(false);
if dont_show_again {
super::super::state::anchor_explainer_suppress_permanently();
}
});
}
fn bump_form_reset_pulse(ui: &MainWindow) {
let pulse = ui.global::<EditorModel>().get_form_reset_pulse();
ui.global::<EditorModel>()
.set_form_reset_pulse(pulse.wrapping_add(1));
}
fn adjust_selection_after_remove(ui: &MainWindow, removed_index: i32) {
let selected = ui.global::<EditorModel>().get_selected_tier_index();
if selected == removed_index {
ui.global::<EditorModel>().set_selected_tier_index(-1);
bump_form_reset_pulse(ui);
} else if selected > removed_index {
ui.global::<EditorModel>()
.set_selected_tier_index(selected - 1);
}
}
fn clamp_selection_to_tier_count(ui: &MainWindow, tier_count: usize) {
let selected = ui.global::<EditorModel>().get_selected_tier_index();
let out_of_range = usize::try_from(selected).is_ok_and(|index| index >= tier_count);
if out_of_range {
ui.global::<EditorModel>().set_selected_tier_index(-1);
bump_form_reset_pulse(ui);
}
}
#[cfg(test)]
mod tests {
use super::{
letterbox_margin, next_free_block_name, parse_printed_proportions_field,
parse_printed_proportions_form, ri_override_for_material_pick, split_duplicate_suffix,
tier_form_error_field, unique_duplicate_name,
};
#[test]
fn ri_override_for_material_pick_does_not_pin_when_the_design_already_had_a_material() {
assert_eq!(
ri_override_for_material_pick(Some("Quartz"), Some("Diamond"), 2.417),
None
);
}
#[test]
fn ri_override_for_material_pick_pins_the_legacy_ri_on_a_first_pick_that_drifts() {
assert_eq!(
ri_override_for_material_pick(Some("Diamond"), None, 1.54),
Some(1.54)
);
}
#[test]
fn ri_override_for_material_pick_does_nothing_for_a_non_built_in_name() {
assert_eq!(
ri_override_for_material_pick(Some("Not A Real Material"), None, 1.54),
None
);
}
#[test]
fn next_free_block_name_starts_at_1_for_a_crown_angle() {
assert_eq!(next_free_block_name(45.0, &[]), "C1");
}
#[test]
fn next_free_block_name_uses_p_for_a_negative_angle() {
assert_eq!(next_free_block_name(-40.0, &[]), "P1");
}
#[test]
fn next_free_block_name_treats_non_negative_zero_as_crown() {
assert_eq!(next_free_block_name(0.0, &[]), "C1");
}
#[test]
fn next_free_block_name_skips_names_already_in_use() {
let existing = vec!["C1".to_string(), "C2".to_string()];
assert_eq!(next_free_block_name(45.0, &existing), "C3");
}
#[test]
fn next_free_block_name_collision_check_is_case_insensitive() {
let existing = vec!["c1".to_string()];
assert_eq!(next_free_block_name(45.0, &existing), "C2");
}
#[test]
fn unique_duplicate_name_starts_at_2_when_nothing_collides() {
assert_eq!(unique_duplicate_name("P1", &[]), "P1 (2)");
}
#[test]
fn unique_duplicate_name_skips_names_already_in_use() {
let existing = vec!["P1".to_string(), "P1 (2)".to_string(), "P1 (3)".to_string()];
assert_eq!(unique_duplicate_name("P1", &existing), "P1 (4)");
}
#[test]
fn unique_duplicate_name_collision_check_is_case_insensitive() {
let existing = vec!["p1 (2)".to_string()];
assert_eq!(unique_duplicate_name("P1", &existing), "P1 (3)");
}
#[test]
fn unique_duplicate_name_counts_up_instead_of_nesting() {
assert_eq!(unique_duplicate_name("P1 (2)", &[]), "P1 (3)");
}
#[test]
fn unique_duplicate_name_continues_from_a_high_source_number() {
assert_eq!(unique_duplicate_name("P1 (9)", &[]), "P1 (10)");
}
#[test]
fn unique_duplicate_name_treats_an_unreadable_suffix_as_part_of_the_name() {
let huge = format!("P1 ({})", u128::from(u32::MAX) + 1);
assert_eq!(unique_duplicate_name(&huge, &[]), format!("{huge} (2)"));
}
#[test]
fn unique_duplicate_name_falls_back_to_tier_for_an_unnamed_source() {
assert_eq!(unique_duplicate_name("", &[]), "Tier (2)");
}
#[test]
fn split_duplicate_suffix_reads_a_trailing_parenthesized_number() {
assert_eq!(split_duplicate_suffix("P1 (2)"), ("P1", Some(2)));
assert_eq!(split_duplicate_suffix("P1 (12)"), ("P1", Some(12)));
}
#[test]
fn split_duplicate_suffix_leaves_unrelated_text_alone() {
assert_eq!(split_duplicate_suffix("P1"), ("P1", None));
assert_eq!(split_duplicate_suffix("P1 (a)"), ("P1 (a)", None));
assert_eq!(split_duplicate_suffix("P1 (2"), ("P1 (2", None));
assert_eq!(split_duplicate_suffix("P1/P2 (2)"), ("P1/P2", Some(2)));
}
#[test]
fn parse_printed_proportions_field_treats_blank_text_as_none() {
assert_eq!(parse_printed_proportions_field("L/W", ""), Ok(None));
assert_eq!(parse_printed_proportions_field("L/W", " "), Ok(None));
}
#[test]
fn parse_printed_proportions_field_parses_a_positive_number() {
assert_eq!(
parse_printed_proportions_field("L/W", "1.05"),
Ok(Some(1.05))
);
}
#[test]
fn parse_printed_proportions_field_rejects_unparsable_text() {
assert!(parse_printed_proportions_field("L/W", "abc").is_err());
}
#[test]
fn parse_printed_proportions_field_rejects_zero_and_negative() {
assert!(parse_printed_proportions_field("L/W", "0").is_err());
assert!(parse_printed_proportions_field("L/W", "-1.0").is_err());
}
#[test]
fn parse_printed_proportions_form_builds_every_field() {
let props = parse_printed_proportions_form("1.30", "1.05", "0.61", "0.43", "0.60").unwrap();
assert_eq!(props.vol_w3, Some(1.30));
assert_eq!(props.lw, Some(1.05));
assert_eq!(props.cw, Some(0.61));
assert_eq!(props.pw, Some(0.43));
assert_eq!(props.hw, Some(0.60));
}
#[test]
fn parse_printed_proportions_form_allows_every_field_blank() {
let props = parse_printed_proportions_form("", "", "", "", "").unwrap();
assert_eq!(props.vol_w3, None);
assert_eq!(props.lw, None);
assert_eq!(props.cw, None);
assert_eq!(props.pw, None);
assert_eq!(props.hw, None);
}
#[test]
fn parse_printed_proportions_form_names_the_failing_field() {
let err = parse_printed_proportions_form("1.30", "not a number", "0.61", "0.43", "0.60")
.unwrap_err();
assert!(err.contains("L/W"), "error should name the field: {err}");
}
#[test]
fn letterbox_margin_is_zero_when_the_pick_buffer_fills_the_viewport() {
assert_eq!(letterbox_margin((800, 600), (800, 600)), (0.0, 0.0));
}
#[test]
fn letterbox_margin_is_half_the_leftover_on_each_side() {
assert_eq!(letterbox_margin((800, 600), (800, 450)), (0.0, 75.0));
}
#[test]
fn letterbox_margin_never_goes_negative_if_the_pick_buffer_is_larger() {
assert_eq!(letterbox_margin((800, 600), (900, 700)), (0.0, 0.0));
}
#[test]
fn tier_form_error_field_classifies_every_documented_message_shape() {
assert_eq!(
tier_form_error_field("Angle '41x' is not a number."),
"angle"
);
assert_eq!(
tier_form_error_field(
"Angle 91.00\u{b0} exceeds 90\u{b0} -- angles are measured from the girdle \
plane, so no crown or pavilion facet can be steeper than that."
),
"angle"
);
assert_eq!(
tier_form_error_field(
"Another tier is already named 'P1' -- facet names must be unique so meet \
constraints resolve to the right tier."
),
"name"
);
assert_eq!(
tier_form_error_field("Index '99' is outside this design's 96-tooth gear."),
"indices"
);
assert_eq!(
tier_form_error_field("\"Meet named\" needs at least one facet name."),
"constraint"
);
assert_eq!(
tier_form_error_field("Scale reference 'x' is not a number."),
"constraint"
);
assert_eq!(
tier_form_error_field("No facet named 'C1' -- check the Meets field."),
"constraint"
);
assert_eq!(
tier_form_error_field("Unknown constraint kind 7."),
"constraint"
);
}
#[test]
fn tier_form_error_field_is_empty_for_an_unrecognized_message() {
assert_eq!(
tier_form_error_field("Failed to remap this design's indices to the new gear."),
""
);
}
}