use super::{
super::{
auto_solve, loading,
material_lookup::nearest_built_in_material,
native_io::do_open_native,
state::{
ANGLE_NUDGE_COALESCE_WINDOW, EditorState, 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, tier_matches_filter,
tiers_incomplete_under_proposed_symmetry,
},
view::{
SolidLastSolved, push_selected_tier_chips, refresh_all, refresh_editor_panel_stale,
submit_preview_replan,
},
},
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, Edit, FreshDesignSpec, History, RemapRounding};
use indicatrix_vault::db::sqlite::Database;
use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel};
use std::{
cell::RefCell,
collections::BTreeSet,
rc::Rc,
sync::{Arc, Mutex, atomic::AtomicU64},
time::Duration,
};
const FIXED_CYLINDER_PREFORM_SIDES: usize = 96;
thread_local! {
static FACET_OVERLAY: RefCell<FacetOverlay> = RefCell::new(FacetOverlay::default());
}
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);
}
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 || {
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;
}
let st = state.borrow();
refresh_all(&ui, &render_ctx, &preview_state, &solid_last_solved, &st);
});
}
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| {
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 template_index == 1 {
st.design.tiers = ConstraintTier::standard_round_brilliant();
}
clear_analysis_results(ui);
refresh_all(ui, render_ctx, preview_state, solid_last_solved, &st);
drop(st);
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)),
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()),
});
clear_analysis_results(ui);
refresh_all(ui, render_ctx, preview_state, solid_last_solved, &st);
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(st.asc_filename.clone().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",
);
}
drop(st);
if let Some((name, ri)) = nearest_built_in_material(schedule_ri, 0.01) {
ui.global::<EditorModel>()
.set_material_suggestion_name(name.into());
ui.global::<EditorModel>()
.set_material_suggestion_text(format!("Set material to {name} (RI {ri:.4})?").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 || {
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);
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);
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);
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);
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);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
false,
);
}
Err(e) => show_toast(&ui, &e, "error"),
}
},
);
}
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 })
}
}
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 (imported_meet, original_notes) = (index >= 0)
.then(|| {
let st = state.borrow();
let tier = st.design.tiers.get(usize::try_from(index).ok()?)?;
Some((tier.imported_meet.clone(), tier.original_notes.clone()))
})
.flatten()
.unwrap_or_default();
let (gear_teeth_abs, other_tier_names) = {
let st = state.borrow();
(
st.design.meta.gear_teeth_abs(),
other_tier_names_excluding(&st, 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) => {
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."),
);
return;
}
let (dirty_index, edit) = tier_save_edit(&ui, &st, index, tier);
match st.apply(edit) {
Ok(()) => {
ui.global::<EditorModel>().set_tier_form_error("".into());
refresh_editor_panel_stale(&ui, &render_ctx, &st);
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");
}
}
Err(e) => report_tier_form_error(&ui, &e.to_string()),
}
}
Err(e) => report_tier_form_error(&ui, &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);
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);
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 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_nudge_angle(move |anchor_index: i32, delta_deg: f32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(anchor_index) = usize::try_from(anchor_index) else {
return;
};
let mut st = state.borrow_mut();
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]
};
let delta_deg = f64::from(delta_deg);
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(()) => {
refresh_editor_panel_stale(&ui, &render_ctx, &st);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
targets.into_iter().collect(),
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);
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 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 solved = auto_solve::solid_last_solved()
.and_then(|cache| {
cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
})
.unwrap_or_default();
let facet_map = FacetMap::from_design(&st.design, &solved);
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 solved = auto_solve::solid_last_solved()
.and_then(|cache| {
cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
})
.unwrap_or_default();
let facet_map = FacetMap::from_design(&st.design, &solved);
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);
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);
}
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 || {
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(()) => {
refresh_all(&ui, &render_ctx, &preview_state, &solid_last_solved, &st);
drop(st);
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 || {
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(()) => {
refresh_all(&ui, &render_ctx, &preview_state, &solid_last_solved, &st);
drop(st);
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| {
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(()) => {
refresh_all(&ui, &render_ctx, &preview_state, &solid_last_solved, &st);
drop(st);
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 solved = auto_solve::solid_last_solved()
.and_then(|cache| {
cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
})
.unwrap_or_default();
let facet_map = FacetMap::from_design(&st.design, &solved);
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);
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);
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);
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);
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);
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);
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);
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);
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"),
}
});
}
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(material) => match st.apply(Edit::SetMaterial { material }) {
Ok(()) => {
refresh_editor_panel_stale(&ui, &render_ctx, &st);
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_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);
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 = 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.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,
});
drop(st);
ui.global::<EditorModel>()
.set_gear_remap_rows(ModelRc::new(VecModel::from(rows)));
ui.global::<EditorModel>().set_gear_remap_open(true);
},
);
}
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 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);
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;
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 = Rc::clone(state);
let render_ctx = 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.borrow();
refresh_editor_panel_stale(&ui, &render_ctx, &st);
}
});
}
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 {
ui.global::<SolidPreviewModel>().set_hover_text("".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 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);
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);
}
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();
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) {
ui.global::<EditorModel>()
.set_tier_form_error(message.into());
show_toast(ui, message, "error");
}
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),
);
}
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, split_duplicate_suffix, unique_duplicate_name,
};
#[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 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));
}
}