use std::{
cell::{RefCell, RefMut},
collections::BTreeSet,
rc::Rc,
sync::{Arc, Mutex},
};
use indicatrix::geometry::meet_solver::MeetConstraint;
use indicatrix_cut_core::TierTarget;
pub(super) use indicatrix_editor::loading::tier_form_error_field;
use indicatrix_editor::{
loading::{non_integral_index_warning, parse_tier_form_with_relation},
slider_ranges::{
AnglePreset, AngleSide, SliderSpecData, angle_presets_for, angle_spec_data,
format_angle_text, margin_preview_text, stepped_angle_text,
},
};
use slint::{ComponentHandle, ModelRc, SharedString, VecModel};
use super::{nudge::tier_nudge_label, tier_generation::next_free_block_name};
use crate::{
EditorModel, MainWindow, SliderModel, SliderPreset, SliderSpec,
bridge::render_thread::RenderContext,
gui::{
editor::{
guide, loading,
relation_ui::{
is_relation_text, plan_tier_save, relation_placeholder_angle_deg,
save_error_text_and_field, with_followers,
},
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 selected_row(ui: &MainWindow) -> Option<usize> {
usize::try_from(ui.global::<EditorModel>().get_selected_tier_index()).ok()
}
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,
dirty: BTreeSet<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,
dirty,
non_integral_warning,
} = outcome;
clear_tier_form_error(ui);
refresh_editor_panel_stale(ui, render_ctx, &st, &dirty);
guide::check_progress(ui, &st);
submit_preview_replan(
ui,
render_ctx,
preview_state,
solid_last_solved,
&st,
dirty,
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");
}
}
fn preserve_saved_tier_side_and_detached(
tier: &mut indicatrix_cut_core::ConstraintTier,
design: &indicatrix_cut_core::Design,
index: i32,
) {
if let Some(current) = usize::try_from(index)
.ok()
.and_then(|i| design.tiers.get(i))
{
tier.detached.clone_from(¤t.detached);
if current.angle_deg.is_sign_negative() {
tier.angle_deg = -tier.angle_deg.abs();
}
} else if index < 0
&& indicatrix_cut_core::design::labelling::name_indicates_pavilion(&tier.name)
{
tier.angle_deg = -tier.angle_deg.abs();
}
}
fn settle_saved_tier(
tier: &mut indicatrix_cut_core::ConstraintTier,
design: &indicatrix_cut_core::Design,
index: i32,
other_tier_names: &[String],
) {
preserve_saved_tier_side_and_detached(tier, design, index);
if index < 0 && tier.name.is_empty() {
tier.name = next_free_block_name(tier.angle_deg, other_tier_names);
}
}
struct SaveServices<'a> {
state: &'a Rc<RefCell<EditorState>>,
render_ctx: &'a Arc<Mutex<RenderContext>>,
preview_state: &'a Arc<SolidPreviewState>,
solid_last_solved: &'a SolidLastSolved,
}
struct TierFormInput<'a> {
index: i32,
angle: &'a str,
constraint_kind: i32,
constraint_text: &'a str,
name: &'a str,
indices: &'a str,
}
fn parse_save_form(
st: &Rc<RefCell<EditorState>>,
form: &TierFormInput<'_>,
context: SaveTierFormContext,
) -> Result<(indicatrix_cut_core::ConstraintTier, Option<String>), String> {
let SaveTierFormContext {
imported_meet,
original_notes,
gear_teeth_abs,
other_tier_names,
} = context;
let fields = loading::TierFormFields {
angle: form.angle,
constraint_kind: form.constraint_kind,
constraint_text: form.constraint_text,
name: form.name,
indices: form.indices,
gear_teeth_abs,
imported_meet,
original_notes,
other_tier_names,
};
if is_relation_text(form.angle) {
let placeholder =
relation_placeholder_angle_deg(&st.borrow().design, form.index, form.angle, form.name);
parse_tier_form_with_relation(fields, placeholder)
} else {
loading::parse_tier_form(fields).map(|tier| (tier, None))
}
}
fn save_tier_now(ui: &MainWindow, services: &SaveServices<'_>, form: &TierFormInput<'_>) {
let SaveServices {
state,
render_ctx,
preview_state,
solid_last_solved,
} = services;
let index = form.index;
let context = save_tier_form_context(state, index);
let other_tier_names = context.other_tier_names.clone();
let (mut tier, relation_text) = match parse_save_form(state, form, context) {
Ok(parsed) => parsed,
Err(e) => {
report_tier_form_error(ui, &e, tier_form_error_field(&e));
return;
}
};
let Ok(target) = parse_tier_target_reporting(ui, form.constraint_kind, form.constraint_text)
else {
return;
};
let non_integral_warning = non_integral_index_warning(&tier.indices);
let mut st = state.borrow_mut();
settle_saved_tier(&mut tier, &st.design, index, &other_tier_names);
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, form.name)
{
report_tier_form_error(ui, &message, "name");
return;
}
let applied = plan_tier_save(
&st.design,
index,
tier,
relation_text.as_deref(),
target,
selected_row(ui),
)
.and_then(|(dirty_index, edit)| st.try_apply(edit).map(|_| dirty_index));
match applied {
Ok(dirty_index) => {
let dirty = with_followers(&st.design, [dirty_index]);
apply_tier_save_success(
ui,
render_ctx,
preview_state,
solid_last_solved,
st,
TierSaveOutcome {
index,
dirty_index,
dirty,
non_integral_warning,
},
);
}
Err(error) => {
let (message, field) = save_error_text_and_field(&error);
report_tier_form_error(ui, &message, field);
}
}
}
pub(super) fn slider_spec_from(data: SliderSpecData) -> SliderSpec {
let (band_from, band_to) = data.band.unwrap_or((0.0, 0.0));
SliderSpec {
visible: data.visible,
usable: data.usable,
side: data.side.into(),
value: data.value as f32,
minimum: data.range.min as f32,
maximum: data.range.max as f32,
step: data.range.step as f32,
fine_step: data.range.fine_step as f32,
mark: data.range.mark as f32,
band_from: band_from as f32,
band_to: band_to as f32,
note: data.note.into(),
value_text: data.value_text.into(),
}
}
fn preset_rows(presets: Vec<AnglePreset>) -> ModelRc<SliderPreset> {
let rows: Vec<SliderPreset> = presets
.into_iter()
.map(|preset| SliderPreset {
label: preset.label.into(),
value: preset.value_deg as f32,
value_text: preset.value_text.into(),
reason: preset.reason.into(),
})
.collect();
ModelRc::new(VecModel::from(rows))
}
fn setup_angle_slider_callbacks(ui: &MainWindow) {
let model = ui.global::<SliderModel>();
model.on_angle_spec(|kind, text, name, ri_text| {
slider_spec_from(angle_spec_data(&kind, &text, &name, &ri_text))
});
model.on_angle_presets(|side, ri_text, material| {
preset_rows(angle_presets_for(&side, &ri_text, &material))
});
model.on_angle_text(|value, current| format_angle_text(f64::from(value), ¤t).into());
model.on_stepped_angle_text(|current, delta| {
stepped_angle_text(¤t, f64::from(delta))
.unwrap_or_default()
.into()
});
model.on_margin_preview_text(|side, text| {
margin_preview_text(AngleSide::from_name(&side), &text).into()
});
}
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,
) {
super::concave_tier::setup_concave_tier_callbacks(
ui,
state,
render_ctx,
preview_state,
solid_last_solved,
);
setup_angle_slider_callbacks(ui);
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;
};
save_tier_now(
&ui,
&SaveServices {
state: &state,
render_ctx: &render_ctx,
preview_state: &preview_state,
solid_last_solved: &solid_last_solved,
},
&TierFormInput {
index,
angle: &angle,
constraint_kind,
constraint_text: &constraint_text,
name: &name,
indices: &indices,
},
);
},
);
}
pub(super) fn clear_tier_form_error(ui: &MainWindow) {
let model = ui.global::<EditorModel>();
model.set_tier_form_error("".into());
model.set_tier_form_error_field("".into());
}
pub(super) 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");
}
#[cfg(test)]
mod side_tests;
#[cfg(test)]
mod slider_tests;