use std::{
cell::{RefCell, RefMut},
collections::BTreeSet,
rc::Rc,
sync::{Arc, Mutex},
};
use indicatrix::geometry::meet_solver::MeetConstraint;
use indicatrix_cut_core::{Edit, TierTarget};
use indicatrix_editor::loading::non_integral_index_warning;
pub(super) use indicatrix_editor::loading::tier_form_error_field;
use slint::{ComponentHandle, SharedString};
use super::{nudge::tier_nudge_label, tier_generation::next_free_block_name};
use crate::{
EditorModel, MainWindow,
bridge::render_thread::RenderContext,
gui::{
editor::{
guide, loading,
state::{EditorState, first_unresolved_meet_name},
view::{SolidLastSolved, refresh_editor_panel_stale, submit_preview_replan},
},
show_toast,
solid_preview::preview_state::SolidPreviewState,
},
};
fn other_tier_names_excluding(st: &EditorState, excluded_index: i32) -> Vec<String> {
indicatrix_editor::tier_save::other_tier_names_excluding(&st.design, excluded_index)
}
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 parse_tier_target_reporting(
ui: &MainWindow,
constraint_kind: i32,
constraint_text: &str,
) -> Result<Option<TierTarget>, ()> {
loading::parse_tier_target(constraint_kind, constraint_text).map_err(|e| {
report_tier_form_error(ui, &e, tier_form_error_field(&e));
})
}
fn tier_save_edit_with_target(
ui: &MainWindow,
st: &EditorState,
index: i32,
tier: indicatrix_cut_core::ConstraintTier,
target: Option<TierTarget>,
) -> (usize, Edit) {
let selected = usize::try_from(ui.global::<EditorModel>().get_selected_tier_index()).ok();
indicatrix_editor::tier_save::tier_save_edit_with_target(
&st.design, index, tier, target, selected,
)
}
struct SaveTierFormContext {
imported_meet: Option<MeetConstraint>,
original_notes: Option<String>,
gear_teeth_abs: u32,
other_tier_names: Vec<String>,
}
fn save_tier_form_context(state: &Rc<RefCell<EditorState>>, index: i32) -> SaveTierFormContext {
let st = state.borrow();
let (imported_meet, original_notes) = (index >= 0)
.then(|| {
let tier = st.design.tiers.get(usize::try_from(index).ok()?)?;
Some((tier.imported_meet.clone(), tier.original_notes.clone()))
})
.flatten()
.unwrap_or_default();
SaveTierFormContext {
imported_meet,
original_notes,
gear_teeth_abs: st.design.meta.gear_teeth_abs(),
other_tier_names: other_tier_names_excluding(&st, index),
}
}
struct TierSaveOutcome {
index: i32,
dirty_index: usize,
non_integral_warning: Option<String>,
}
fn apply_tier_save_success(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
st: RefMut<'_, EditorState>,
outcome: TierSaveOutcome,
) {
let TierSaveOutcome {
index,
dirty_index,
non_integral_warning,
} = outcome;
let model = ui.global::<EditorModel>();
model.set_tier_form_error("".into());
model.set_tier_form_error_field("".into());
refresh_editor_panel_stale(ui, render_ctx, &st, &BTreeSet::from([dirty_index]));
guide::check_progress(ui, &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");
}
}
pub(in crate::gui::editor) fn setup_save_tier_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_save_tier(
move |index: i32,
angle: SharedString,
constraint_kind: i32,
constraint_text: SharedString,
name: SharedString,
indices: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let SaveTierFormContext {
imported_meet,
original_notes,
gear_teeth_abs,
other_tier_names,
} = save_tier_form_context(&state, index);
match loading::parse_tier_form(loading::TierFormFields {
angle: &angle,
constraint_kind,
constraint_text: &constraint_text,
name: &name,
indices: &indices,
gear_teeth_abs,
imported_meet,
original_notes,
other_tier_names: other_tier_names.clone(),
}) {
Ok(mut tier) => {
let Ok(target) =
parse_tier_target_reporting(&ui, constraint_kind, &constraint_text)
else {
return;
};
if index < 0 && tier.name.is_empty() {
tier.name = next_free_block_name(tier.angle_deg, &other_tier_names);
}
let non_integral_warning = non_integral_index_warning(&tier.indices);
let mut st = state.borrow_mut();
if let Some(current) = usize::try_from(index)
.ok()
.and_then(|i| st.design.tiers.get(i))
{
tier.detached.clone_from(¤t.detached);
}
if let MeetConstraint::MeetNamed(names) = &tier.constraint
&& let Some(bad_name) = first_unresolved_meet_name(&st.design, names)
{
report_tier_form_error(
&ui,
&format!("No facet named '{bad_name}' -- check the Meets field."),
"constraint",
);
return;
}
if let Some(message) = indicatrix_editor::tier_save::unnaming_blocked_message(
&st.design, index, &name,
) {
report_tier_form_error(&ui, &message, "name");
return;
}
let (dirty_index, edit) =
tier_save_edit_with_target(&ui, &st, index, tier, target);
match st.apply(edit) {
Ok(()) => {
apply_tier_save_success(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
st,
TierSaveOutcome {
index,
dirty_index,
non_integral_warning,
},
);
}
Err(e) => {
let message = e.to_string();
let field = tier_form_error_field(&message);
report_tier_form_error(&ui, &message, field);
}
}
}
Err(e) => report_tier_form_error(&ui, &e, tier_form_error_field(&e)),
}
},
);
}
fn report_tier_form_error(ui: &MainWindow, message: &str, field: &str) {
let model = ui.global::<EditorModel>();
model.set_tier_form_error(message.into());
model.set_tier_form_error_field(field.into());
show_toast(ui, message, "error");
}