use std::{
cell::RefCell,
collections::BTreeSet,
rc::Rc,
sync::{Arc, Mutex},
};
use indicatrix_cut_core::Edit;
use indicatrix_editor::loading::eval_number;
use slint::{ComponentHandle, Model, SharedString};
use crate::{
EditorModel, MainWindow,
bridge::render_thread::RenderContext,
gui::{
editor::{
body_color_combo, guide,
state::{EditorState, tiers_incomplete_under_proposed_symmetry},
view::{SolidLastSolved, refresh_editor_panel_stale, submit_preview_replan},
},
show_toast,
solid_preview::preview_state::SolidPreviewState,
},
};
pub(super) use indicatrix_editor::loading::ri_override_for_material_pick;
fn symmetry_order_from_text(text: &str) -> Option<u32> {
let trimmed = text.trim();
if let Ok(order) = trimmed.parse::<u32>() {
return (order >= 1).then_some(order);
}
let value = eval_number(trimmed, None).ok()?;
let whole = value.round();
((value - whole).abs() < 1e-9 && (1.0..=f64::from(u32::MAX)).contains(&whole))
.then_some(whole as u32)
}
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, body_color_index: i32| {
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 body_color_combo::material_for_apply(
combo_index,
&ri_override_text,
&options,
&st.design.material,
body_color_index,
) {
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());
ui.global::<EditorModel>()
.set_material_suggestion_name("".into());
ui.global::<EditorModel>()
.set_material_suggestion_text("".into());
guide::check_progress(&ui, &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_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());
ui.global::<EditorModel>()
.set_material_suggestion_name("".into());
ui.global::<EditorModel>()
.set_material_suggestion_text("".into());
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 Some(symmetry_order) = symmetry_order_from_text(&symmetry_order_text) else {
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 Some(symmetry_order) = symmetry_order_from_text(&symmetry_order_text) else {
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());
},
);
}
#[cfg(test)]
mod tests {
use super::symmetry_order_from_text;
#[test]
fn a_plain_whole_number_reads_as_before() {
assert_eq!(symmetry_order_from_text("8"), Some(8));
assert_eq!(symmetry_order_from_text(" 4 "), Some(4));
assert_eq!(symmetry_order_from_text("1"), Some(1));
}
#[test]
fn arithmetic_that_works_out_to_a_whole_number_is_accepted() {
assert_eq!(symmetry_order_from_text("4*2"), Some(8));
assert_eq!(symmetry_order_from_text("(3 + 1) * 2"), Some(8));
assert_eq!(symmetry_order_from_text("24 / 4"), Some(6));
}
#[test]
fn zero_fractions_negatives_and_text_are_refused() {
for text in ["0", "-4", "4.5", "9 / 2", "0 * 5", "abc", "", "4 +", "1e12"] {
assert_eq!(symmetry_order_from_text(text), None, "{text:?}");
}
}
}