use super::super::{
loading,
material_lookup::nearest_built_in_material,
state::{
EditorState, PendingGearRemap, angle_nudge_coalesce_key, apply_multi_selection,
gear_choice_to_teeth, gear_remap_preview, parse_design_material_form,
},
view::{SolidLastSolved, refresh_all, refresh_editor_panel_stale, submit_preview_replan},
};
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::{PickBuffer, SolidPreviewState},
},
},
};
use indicatrix_cut_core::{Edit, 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},
};
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 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| {
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,
gear_teeth.unsigned_abs() as usize,
) {
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) => {
let mut st = state.borrow_mut();
*st = EditorState::fresh_from_spec(spec);
refresh_all(&ui, &render_ctx, &preview_state, &solid_last_solved, &st);
ui.global::<EditorModel>().set_new_dialog_open(false);
}
Err(e) => show_toast(&ui, &e, "error"),
}
},
);
}
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,
}
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,
} = outcome;
let schedule_ri = loaded.design.meta.refractive_index;
let mut st = state.borrow_mut();
*st = EditorState {
design: loaded.design,
history: History::new(),
printed_proportions,
generation: Arc::new(AtomicU64::new(0)),
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(),
};
refresh_all(ui, render_ctx, preview_state, solid_last_solved, &st);
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.",
"info",
);
} 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)));
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;
}
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,
},
);
}
}),
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,
},
);
}
Err(e) => show_toast(&ui, &e, "error"),
}
});
}
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) {
ui.global::<EditorModel>()
.on_material_suggestion_dismiss(move || {});
}
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);
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);
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();
let cylinder_sides = st.design.meta.gear_teeth_abs() as usize;
match loading::parse_preform_form(
shape_index,
&half_width,
&length_over_width,
&depth,
cylinder_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,
) {
Ok((girdle_diameter_mm, material)) => {
let _ = st.apply(Edit::SetGirdleDiameterMm { girdle_diameter_mm });
let _ = st.apply(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"),
}
},
);
}
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 = (index >= 0)
.then(|| {
state
.borrow()
.design
.tiers
.get(index as usize)?
.imported_meet
.clone()
})
.flatten();
match loading::parse_tier_form(
&angle,
constraint_kind,
&constraint_text,
&name,
&indices,
imported_meet,
) {
Ok(tier) => {
let mut st = state.borrow_mut();
let dirty_index = if index < 0 {
st.design.tiers.len()
} else {
index as usize
};
let edit = if index < 0 {
Edit::AddTier {
index: st.design.tiers.len(),
tier,
}
} else {
Edit::ModifyTier {
index: index as usize,
tier,
}
};
match 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([dirty_index]),
false,
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
}
Err(e) => show_toast(&ui, &e, "error"),
}
},
);
}
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();
match st.apply(Edit::RemoveTier {
index: index as usize,
}) {
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"),
}
});
}
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() {
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"),
}
});
}
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 changes: Option<Vec<(usize, f64, f64)>> = targets
.iter()
.map(|&index| {
st.design
.tiers
.get(index)
.map(|tier| (index, tier.angle_deg, tier.angle_deg + delta_deg))
})
.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,
);
}
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();
duplicate.name = format!("{}'", duplicate.name);
duplicate.imported_meet = None;
let new_index = st.design.tiers.len();
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);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
});
}
pub(in crate::gui::editor) fn setup_toggle_multi_select_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
) {
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();
drop(st);
let mut rows: Vec<EditorTierItem> =
ui.global::<EditorModel>().get_tiers().iter().collect();
apply_multi_selection(&mut rows, &multi_selected);
ui.global::<EditorModel>()
.set_tiers(ModelRc::new(VecModel::from(rows)));
});
}
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,
) {
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_toggle_detach(move |index: i32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
if index < 0 {
return;
}
let mut st = state.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, &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"),
}
});
}
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 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"),
}
},
);
}
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 remap_result = st.apply(Edit::RemapIndices {
from_gear: pending.from_gear,
to_gear: pending.to_gear,
rounding: pending.rounding,
});
let schedule_result = remap_result.and_then(|()| {
st.apply(Edit::SetSchedule {
gear_teeth: pending.to_gear,
symmetry_order: pending.symmetry_order,
mirror: pending.mirror,
})
});
ui.global::<EditorModel>().set_gear_remap_open(false);
match schedule_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);
}
});
}
pub(in crate::gui::editor) fn setup_solid_facet_hover_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
solid_pick: &Arc<Mutex<Option<PickBuffer>>>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let solid_pick = Arc::clone(solid_pick);
let solid_last_solved = Arc::clone(solid_last_solved);
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 st = state.borrow();
let Some(facet_id) = solid_pick
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.and_then(|pick| pick.facet_at(x.max(0.0) as u32, y.max(0.0) as u32))
else {
ui.global::<SolidPreviewModel>().set_hover_text("".into());
return;
};
let solved = solid_last_solved
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
.unwrap_or_default();
let facet_map = FacetMap::from_design(&st.design, &solved);
let n_d = st.design.effective_refractive_index();
ui.global::<SolidPreviewModel>()
.set_hover_text(facet_map.hover_text(facet_id as usize, n_d).into());
});
}
pub(in crate::gui::editor) fn setup_solid_facet_click_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
solid_pick: &Arc<Mutex<Option<PickBuffer>>>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let solid_pick = Arc::clone(solid_pick);
let solid_last_solved = Arc::clone(solid_last_solved);
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 st = state.borrow();
let Some(facet_id) = solid_pick
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.and_then(|pick| pick.facet_at(x.max(0.0) as u32, y.max(0.0) as u32))
else {
return;
};
let solved = solid_last_solved
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
.unwrap_or_default();
let facet_map = FacetMap::from_design(&st.design, &solved);
if let Some(tier_index) = facet_map.tier_of(facet_id as usize) {
ui.global::<EditorModel>()
.set_selected_tier_index(tier_index as i32);
}
});
}
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 Some(ui) = ui_weak.upgrade() else {
return;
};
let st = state.borrow();
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
false,
);
});
}