use crate::{
AngleItem, DiagramDetailData, LibraryModel, MainWindow,
gui::editor::material_lookup::material_for_refractive_index,
};
use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel};
const GIRDLE_ANGLE_DEG: f64 = 90.0;
const GIRDLE_ANGLE_TOLERANCE_DEG: f64 = 0.01;
pub(super) fn parse_catalogue_angle_deg(angle: &str) -> Option<f64> {
angle
.trim()
.trim_end_matches(['\u{b0}', '\u{fffd}'])
.trim()
.parse()
.ok()
}
fn eq_ignore_case_trim(text: &str, target: &str) -> bool {
text.trim().eq_ignore_ascii_case(target)
}
fn is_table_facet(facet: &str) -> bool {
let f = facet.trim();
let core = f.strip_suffix('.').unwrap_or(f);
core.eq_ignore_ascii_case("t") || core.eq_ignore_ascii_case("table")
}
fn is_table_row(facet: &str, index_val: &str) -> bool {
eq_ignore_case_trim(index_val, "table") || is_table_facet(facet)
}
fn is_pavilion_prefixed(facet: &str) -> bool {
let mut chars = facet.trim().chars();
let Some(first) = chars.next() else {
return false;
};
if !first.eq_ignore_ascii_case(&'p') {
return false;
}
let mut next = chars.next();
if next.is_some_and(|c| c.eq_ignore_ascii_case(&'f')) {
next = chars.next();
}
next.is_some_and(|c| c.is_ascii_digit())
}
fn is_crown_prefixed(facet: &str) -> bool {
let mut chars = facet.trim().chars();
chars.next().is_some_and(|c| c.eq_ignore_ascii_case(&'c'))
&& chars.next().is_some_and(|c| c.is_ascii_digit())
}
fn is_digit_with_trailing_girdle_letter(facet: &str) -> bool {
let f = facet.trim();
match f.chars().next_back() {
Some(last) if last.eq_ignore_ascii_case(&'g') => {
let digits = &f[..f.len() - last.len_utf8()];
!digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
}
_ => false,
}
}
fn starts_with_girdle_letter(facet: &str) -> bool {
facet
.trim()
.chars()
.next()
.is_some_and(|c| c.eq_ignore_ascii_case(&'g'))
}
fn is_digits_with_optional_trailing_letter(facet: &str) -> bool {
let f = facet.trim();
let digits = match f.chars().next_back() {
Some(c) if c.is_ascii_alphabetic() => &f[..f.len() - c.len_utf8()],
_ => f,
};
!digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
}
fn is_single_crown_letter(facet: &str) -> bool {
let f = facet.trim();
let mut chars = f.chars();
let Some(c) = chars.next() else {
return false;
};
chars.next().is_none()
&& c.is_ascii_alphabetic()
&& !c.eq_ignore_ascii_case(&'g')
&& !c.eq_ignore_ascii_case(&'t')
}
fn label_side(facet: &str, angle_deg: Option<f64>, index_val: &str) -> Option<i32> {
if angle_deg.is_some_and(|d| (d - GIRDLE_ANGLE_DEG).abs() < GIRDLE_ANGLE_TOLERANCE_DEG)
|| eq_ignore_case_trim(index_val, "girdle")
{
return Some(0);
}
if is_table_row(facet, index_val) {
return Some(1);
}
if is_pavilion_prefixed(facet) {
return Some(-1);
}
if is_crown_prefixed(facet) {
return Some(1);
}
if is_digit_with_trailing_girdle_letter(facet) {
return Some(0);
}
if starts_with_girdle_letter(facet) {
return Some(0);
}
if is_digits_with_optional_trailing_letter(facet) {
return Some(-1);
}
if is_single_crown_letter(facet) {
return Some(1);
}
None
}
const fn positional_fallback(
index: usize,
girdle_positions: &[usize],
table_position: Option<usize>,
row_count: usize,
) -> i32 {
let (Some(&first), Some(&last)) = (girdle_positions.first(), girdle_positions.last()) else {
return 0;
};
if index >= first && index <= last {
return 0;
}
let row_after_girdle = index > last;
match table_position {
Some(table_idx) if table_idx < first || table_idx > last => {
let table_after_girdle = table_idx > last;
if row_after_girdle == table_after_girdle {
1
} else {
-1
}
}
Some(_) => 0,
None => {
let girdle_is_first_or_last = first == 0 || last == row_count - 1;
if girdle_is_first_or_last {
0
} else if row_after_girdle {
-1
} else {
1
}
}
}
}
pub(super) fn sides_from_rows<'a>(
rows: impl Iterator<Item = (&'a str, &'a str, &'a str)>,
) -> Vec<i32> {
let rows: Vec<(&str, &str, &str)> = rows.collect();
let provisional: Vec<Option<i32>> = rows
.iter()
.map(|&(facet, angle, index_val)| {
label_side(facet, parse_catalogue_angle_deg(angle), index_val)
})
.collect();
let girdle_positions: Vec<usize> = provisional
.iter()
.enumerate()
.filter(|&(_, side)| *side == Some(0))
.map(|(i, _)| i)
.collect();
let table_position =
rows.iter()
.zip(provisional.iter())
.position(|(&(facet, _, index_val), side)| {
*side == Some(1) && is_table_row(facet, index_val)
});
let row_count = rows.len();
provisional
.iter()
.enumerate()
.map(|(i, side)| {
side.unwrap_or_else(|| {
positional_fallback(i, &girdle_positions, table_position, row_count)
})
})
.collect()
}
#[must_use]
pub(super) fn count_angles_for_mode(angles: &ModelRc<AngleItem>, mode: i32) -> i32 {
let count = angles
.iter()
.filter(|a| match mode {
1 => a.side < 0,
2 => a.side > 0,
_ => true,
})
.count();
i32::try_from(count).unwrap_or(i32::MAX)
}
pub fn setup_filtered_row_count_callback(ui: &MainWindow) {
let ui_weak = ui.as_weak();
ui.global::<LibraryModel>()
.on_filtered_row_count(move |mode: i32| {
let Some(ui) = ui_weak.upgrade() else {
return 0;
};
let angles = ui.global::<LibraryModel>().get_current_angles();
count_angles_for_mode(&angles, mode)
});
}
pub fn clear_current_detail_display(ui: &MainWindow) {
ui.global::<LibraryModel>()
.set_current_detail(DiagramDetailData {
id: -1,
title: SharedString::default(),
url: SharedString::default(),
designer: SharedString::default(),
shape: SharedString::default(),
gear: SharedString::default(),
facets: SharedString::default(),
lw_ratio: SharedString::default(),
ri: SharedString::default(),
material_guess: SharedString::default(),
volume: SharedString::default(),
competition: SharedString::default(),
image_name: SharedString::default(),
has_image: false,
is_local: false,
hw_ratio: SharedString::default(),
cw_ratio: SharedString::default(),
pw_ratio: SharedString::default(),
symmetry_order: SharedString::default(),
mirror_symmetry: false,
});
ui.global::<LibraryModel>().set_selected_entry_id(-1);
ui.global::<LibraryModel>()
.set_current_angles(ModelRc::new(VecModel::from(Vec::new())));
ui.global::<LibraryModel>()
.set_current_files(ModelRc::new(VecModel::from(Vec::new())));
}
fn format_proportion(raw: &str) -> String {
match raw.trim().parse::<f64>() {
Ok(n) if n.is_finite() => format!("{n:.3}"),
_ => raw.to_string(),
}
}
pub(super) fn format_optional_proportion(raw: Option<&str>) -> String {
raw.map(format_proportion).unwrap_or_default()
}
pub(super) fn non_empty(text: &str) -> Option<String> {
let trimmed = text.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
pub(super) fn catalogue_material_guess(ri_text: Option<&str>) -> String {
let Some(n_d) = ri_text.and_then(|s| s.trim().parse::<f64>().ok()) else {
return String::new();
};
material_for_refractive_index(n_d).map_or_else(String::new, |(name, _)| {
format!("{name}? (from RI {n_d:.2})")
})
}