use std::{
cell::RefCell,
collections::BTreeSet,
rc::Rc,
sync::{Arc, Mutex},
};
use indicatrix::geometry::meet_solver::MeetConstraint;
use indicatrix_cut_core::{ConstraintTier, Edit};
use slint::{ComponentHandle, SharedString};
use crate::{
EditorModel, MainWindow,
bridge::render_thread::RenderContext,
gui::{
editor::{
loading,
state::EditorState,
view::{SolidLastSolved, refresh_editor_panel_stale, submit_preview_replan},
},
show_toast,
solid_preview::preview_state::SolidPreviewState,
},
};
fn parse_step_series_form(
start_angle_text: &str,
angle_step_text: &str,
count: i32,
anchor_text: &str,
) -> Result<(f64, f64, usize, MeetConstraint), String> {
let start_angle: f64 = start_angle_text
.trim()
.parse()
.map_err(|_| format!("Start angle '{start_angle_text}' is not a number."))?;
let angle_step: f64 = angle_step_text
.trim()
.parse()
.map_err(|_| format!("Angle step '{angle_step_text}' is not a number."))?;
if !start_angle.is_finite() || !angle_step.is_finite() {
return Err("Start angle and angle step must be finite numbers.".to_string());
}
let count = usize::try_from(count)
.ok()
.filter(|&count| count >= 1)
.ok_or_else(|| "Tier count must be a positive whole number.".to_string())?;
let anchor_text = anchor_text.trim();
let first_constraint = if anchor_text.is_empty() {
MeetConstraint::MeetExisting
} else {
let anchor: f64 = anchor_text
.parse()
.map_err(|_| format!("Anchor '{anchor_text}' is not a number."))?;
if !anchor.is_finite() {
return Err("Anchor must be a finite number.".to_string());
}
MeetConstraint::ScaleReference(anchor)
};
Ok((start_angle, angle_step, count, first_constraint))
}
pub(super) fn setup_generate_step_series_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_generate_step_series(
move |name_prefix: SharedString,
start_angle_text: SharedString,
angle_step_text: SharedString,
count: i32,
indices_text: SharedString,
anchor_text: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let (start_angle, angle_step, count, first_constraint) = match parse_step_series_form(
&start_angle_text,
&angle_step_text,
count,
&anchor_text,
) {
Ok(parsed) => parsed,
Err(e) => {
show_toast(&ui, &e, "error");
return;
}
};
let mut st = state.borrow_mut();
let gear_teeth_abs = st.design.meta.gear_teeth_abs();
let indices = match loading::parse_index_list(&indices_text, gear_teeth_abs) {
Ok(indices) => indices,
Err(e) => {
show_toast(&ui, &e, "error");
return;
}
};
let tiers = ConstraintTier::step_series(
&name_prefix,
start_angle,
angle_step,
count,
&indices,
&first_constraint,
);
let start_index = st.design.tiers.len();
let edits: Vec<Edit> = tiers
.into_iter()
.enumerate()
.map(|(offset, tier)| Edit::AddTier {
index: start_index + offset,
tier,
})
.collect();
let added_count = edits.len();
match st.apply(Edit::Batch(edits)) {
Ok(()) => {
let dirty: BTreeSet<usize> = (start_index..start_index + added_count).collect();
refresh_editor_panel_stale(&ui, &render_ctx, &st, &dirty);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
dirty,
false,
);
drop(st);
show_toast(&ui, &format!("Generated {added_count} tier(s)."), "info");
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
},
);
}
pub(super) fn setup_mirror_tier_to_other_block_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_mirror_tier_to_other_block(
move |tier_index: i32, name_suffix: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let mut st = state.borrow_mut();
let Ok(tier_index) = usize::try_from(tier_index) else {
return;
};
let Some(source) = st.design.tiers.get(tier_index) else {
return;
};
let mirrored = source.mirrored_to_other_block(&name_suffix);
let mirrored_label = mirrored.name.clone();
let new_index = st.design.tiers.len();
match st.apply(Edit::AddTier {
index: new_index,
tier: mirrored,
}) {
Ok(()) => {
refresh_editor_panel_stale(&ui, &render_ctx, &st, &BTreeSet::from([new_index]));
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);
show_toast(&ui, &format!("Mirrored to {mirrored_label}"), "info");
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
},
);
}
pub(super) fn unique_duplicate_name(source_name: &str, existing_names: &[String]) -> String {
let (base, source_number) = split_duplicate_suffix(source_name.trim());
let base = if base.is_empty() { "Tier" } else { base };
let mut n: u32 = source_number
.and_then(|number| number.checked_add(1))
.unwrap_or(2);
loop {
let candidate = format!("{base} ({n})");
if !existing_names
.iter()
.any(|existing| existing.eq_ignore_ascii_case(&candidate))
{
return candidate;
}
n += 1;
}
}
pub(super) fn next_free_block_name(angle_deg: f64, existing_names: &[String]) -> String {
let letter = if angle_deg.is_sign_negative() {
'P'
} else {
'C'
};
let mut n: u32 = 1;
loop {
let candidate = format!("{letter}{n}");
if !existing_names
.iter()
.any(|existing| existing.eq_ignore_ascii_case(&candidate))
{
return candidate;
}
n += 1;
}
}
pub(super) fn split_duplicate_suffix(name: &str) -> (&str, Option<u32>) {
let Some((base, rest)) = name.rsplit_once(" (") else {
return (name, None);
};
let Some(digits) = rest.strip_suffix(')') else {
return (name, None);
};
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return (name, None);
}
digits
.parse()
.map_or((name, None), |number| (base, Some(number)))
}