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 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> {
st.design
.tiers
.iter()
.enumerate()
.filter(|&(i, _)| excluded_index < 0 || i != excluded_index as usize)
.flat_map(|(_, tier)| tier.names().into_iter().map(str::to_string))
.collect()
}
fn added_tier_label(st: &EditorState, dirty_index: usize) -> Option<String> {
st.design
.tiers
.get(dirty_index)
.map(|tier| tier_nudge_label(tier, dirty_index))
}
fn select_and_announce_added_tier(ui: &MainWindow, dirty_index: usize, label: Option<String>) {
ui.global::<EditorModel>()
.set_selected_tier_index(dirty_index as i32);
if let Some(label) = label {
show_toast(ui, &format!("Added {label}"), "info");
}
}
fn tier_save_edit(
ui: &MainWindow,
st: &EditorState,
index: i32,
tier: indicatrix_cut_core::ConstraintTier,
) -> (usize, Edit) {
if index < 0 {
let append_index = st.design.tiers.len();
let insert_after_selected =
usize::try_from(ui.global::<EditorModel>().get_selected_tier_index())
.ok()
.filter(|&i| i < append_index)
.map_or(append_index, |i| i + 1);
(
insert_after_selected,
Edit::AddTier {
index: insert_after_selected,
tier,
},
)
} else {
let index = index as usize;
(index, Edit::ModifyTier { index, tier })
}
}
fn parse_tier_target_reporting(
ui: &MainWindow,
constraint_kind: i32,
constraint_text: &str,
) -> Result<Option<TierTarget>, ()> {
loading::parse_tier_target(constraint_kind, constraint_text).map_err(|e| {
report_tier_form_error(ui, &e, tier_form_error_field(&e));
})
}
fn tier_save_edit_with_target(
ui: &MainWindow,
st: &EditorState,
index: i32,
tier: indicatrix_cut_core::ConstraintTier,
target: Option<TierTarget>,
) -> (usize, Edit) {
let had_target = usize::try_from(index)
.ok()
.and_then(|i| st.design.tier_target(i))
.is_some();
let (dirty_index, edit) = tier_save_edit(ui, st, index, tier);
let edit = if target.is_some() || had_target {
Edit::Batch(vec![
edit,
Edit::SetTierTarget {
index: dirty_index,
target,
},
])
} else {
edit
};
(dirty_index, edit)
}
struct SaveTierFormContext {
imported_meet: Option<MeetConstraint>,
original_notes: Option<String>,
gear_teeth_abs: u32,
other_tier_names: Vec<String>,
}
fn save_tier_form_context(state: &Rc<RefCell<EditorState>>, index: i32) -> SaveTierFormContext {
let st = state.borrow();
let (imported_meet, original_notes) = (index >= 0)
.then(|| {
let tier = st.design.tiers.get(usize::try_from(index).ok()?)?;
Some((tier.imported_meet.clone(), tier.original_notes.clone()))
})
.flatten()
.unwrap_or_default();
SaveTierFormContext {
imported_meet,
original_notes,
gear_teeth_abs: st.design.meta.gear_teeth_abs(),
other_tier_names: other_tier_names_excluding(&st, index),
}
}
struct TierSaveOutcome {
index: i32,
dirty_index: usize,
non_integral_warning: Option<String>,
}
fn apply_tier_save_success(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
st: RefMut<'_, EditorState>,
outcome: TierSaveOutcome,
) {
let TierSaveOutcome {
index,
dirty_index,
non_integral_warning,
} = outcome;
let model = ui.global::<EditorModel>();
model.set_tier_form_error("".into());
model.set_tier_form_error_field("".into());
refresh_editor_panel_stale(ui, render_ctx, &st, &BTreeSet::from([dirty_index]));
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;
}
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");
}
pub(super) fn tier_form_error_field(message: &str) -> &'static str {
if message.starts_with("Angle") {
"angle"
} else if message.starts_with("Another tier is already named") {
"name"
} else if message.starts_with("Index '") {
"indices"
} else if message.starts_with("\"Meet named\"")
|| message.starts_with("Scale reference")
|| message.starts_with("No facet named")
|| message.starts_with("Unknown constraint kind")
|| message.starts_with("Depth")
|| message.starts_with("Girdle thickness")
|| message.starts_with("Table width")
{
"constraint"
} else {
""
}
}
fn non_integral_index_warning(indices: &[f64]) -> Option<String> {
let mut fractional: Vec<String> = indices
.iter()
.filter(|v| (**v - v.round()).abs() > 1e-3)
.map(|v| format!("{v:.3}"))
.collect();
fractional.dedup();
if fractional.is_empty() {
return None;
}
Some(format!(
"Note: non-integral index position(s) {} -- check this was intentional.",
fractional.join(", ")
))
}