use super::core::{EditorState, GEAR_PRESETS};
use crate::GearRemapRow;
use indicatrix::optics::materials::GemMaterial;
use indicatrix_cut_core::{Design, Edit, MaterialSelection, RemapRounding};
impl EditorState {
pub(in crate::gui::editor) fn material_combo_options(
&self,
custom: &[GemMaterial],
) -> Vec<String> {
let signature: Vec<String> = custom.iter().map(|m| m.name.clone()).collect();
let mut cache = self.material_combo_cache.borrow_mut();
if cache.signature.as_ref() != Some(&signature) {
cache.options = design_material_options(custom);
cache.signature = Some(signature);
}
cache.options.clone()
}
}
#[derive(Default)]
pub(in crate::gui::editor) struct MaterialComboCache {
signature: Option<Vec<String>>,
options: Vec<String>,
}
pub(in crate::gui::editor) fn builtin_preset_names() -> Vec<String> {
std::iter::once("(none)".to_string())
.chain(indicatrix_cut_core::MaterialCatalogue::build(&[]).names())
.collect()
}
pub(in crate::gui::editor) fn material_index_from_name(name: Option<&str>) -> i32 {
let names = builtin_preset_names();
name.and_then(|n| names.iter().position(|p| p == n))
.map_or(0, |i| i as i32)
}
pub(in crate::gui::editor) fn material_name_from_index(index: i32) -> Option<String> {
let names = builtin_preset_names();
usize::try_from(index)
.ok()
.and_then(|i| names.get(i).cloned())
.filter(|name| name != "(none)")
}
pub(in crate::gui::editor) fn parse_yield_form(
girdle_diameter_mm: &str,
_material_index: i32,
specific_gravity_override: &str,
current: &MaterialSelection,
) -> Result<(Option<f64>, MaterialSelection), String> {
let girdle_diameter_mm = if girdle_diameter_mm.trim().is_empty() {
None
} else {
let value: f64 = girdle_diameter_mm.trim().parse().map_err(|_| {
format!(
"Girdle diameter '{}' is not a number.",
girdle_diameter_mm.trim()
)
})?;
if !value.is_finite() || value <= 0.0 {
return Err("Girdle diameter must be a positive, finite number.".to_string());
}
Some(value)
};
let specific_gravity_override = if specific_gravity_override.trim().is_empty() {
None
} else {
let value: f64 = specific_gravity_override.trim().parse().map_err(|_| {
format!(
"Specific gravity '{}' is not a number.",
specific_gravity_override.trim()
)
})?;
if !value.is_finite() || value <= 0.0 {
return Err("Specific gravity override must be a positive, finite number.".to_string());
}
Some(value)
};
Ok((
girdle_diameter_mm,
current.with_specific_gravity_override(specific_gravity_override),
))
}
#[must_use]
pub(in crate::gui::editor) fn ri_source_text(
material: &MaterialSelection,
custom: &[GemMaterial],
) -> String {
if let Some(value) = material.refractive_index_override {
return format!("typed override ({value:.4})");
}
if let Some(name) = material.name.as_deref() {
if custom.iter().any(|gem| gem.name.eq_ignore_ascii_case(name)) {
return format!("custom catalogue material '{name}'");
}
if indicatrix_cut_core::built_in_refractive_index(name).is_some() {
return format!("built-in material '{name}'");
}
return format!(
"'{name}' is not a recognized material -- falling back to this design's legacy imported value"
);
}
"no material selected -- using this design's legacy imported value".to_string()
}
const CUSTOM_BUILTIN_COLLISION_SUFFIX: &str = " (custom)";
pub(in crate::gui::editor) fn design_material_options(custom: &[GemMaterial]) -> Vec<String> {
let mut options: Vec<String> = builtin_preset_names();
for material in custom {
if options
.iter()
.any(|name| name.eq_ignore_ascii_case(&material.name))
{
options.push(format!(
"{}{CUSTOM_BUILTIN_COLLISION_SUFFIX}",
material.name
));
} else {
options.push(material.name.clone());
}
}
options.push("Custom RI\u{2026}".to_string());
options
}
pub(in crate::gui::editor) fn design_material_index_from_name(
name: Option<&str>,
options: &[String],
) -> i32 {
name.and_then(|n| {
options.iter().position(|o| {
o.strip_suffix(CUSTOM_BUILTIN_COLLISION_SUFFIX)
.unwrap_or(o)
.eq_ignore_ascii_case(n)
})
})
.map_or(0, |i| i as i32)
}
pub(super) fn design_material_name_from_index(index: i32, options: &[String]) -> Option<String> {
usize::try_from(index)
.ok()
.and_then(|i| options.get(i))
.filter(|&name| name != "(none)" && name != "Custom RI\u{2026}")
.map(|name| {
name.strip_suffix(CUSTOM_BUILTIN_COLLISION_SUFFIX)
.map_or_else(|| name.clone(), str::to_string)
})
}
pub(in crate::gui::editor) fn parse_design_material_form(
combo_index: i32,
ri_override_text: &str,
options: &[String],
current: &MaterialSelection,
) -> Result<MaterialSelection, String> {
let refractive_index_override = if ri_override_text.trim().is_empty() {
None
} else {
let value: f64 = ri_override_text.trim().parse().map_err(|_| {
format!(
"Refractive index '{}' is not a number.",
ri_override_text.trim()
)
})?;
if !value.is_finite() || value <= 1.0 {
return Err(
"Refractive index override must be a finite number greater than 1.0.".to_string(),
);
}
Some(value)
};
Ok(MaterialSelection {
name: design_material_name_from_index(combo_index, options),
specific_gravity_override: current.specific_gravity_override,
refractive_index_override,
})
}
pub(in crate::gui::editor) fn gear_index_from_teeth(gear_teeth: i32) -> i32 {
GEAR_PRESETS
.iter()
.position(|&g| g == gear_teeth)
.map_or(GEAR_PRESETS.len() as i32, |i| i as i32)
}
pub(in crate::gui::editor) fn gear_choice_to_teeth(
preset_index: i32,
custom_text: &str,
) -> Result<i32, String> {
if let Ok(i) = usize::try_from(preset_index)
&& let Some(&teeth) = GEAR_PRESETS.get(i)
{
return Ok(teeth);
}
let teeth: i32 = custom_text.trim().parse().map_err(|_| {
format!(
"Gear tooth count '{}' is not a whole number.",
custom_text.trim()
)
})?;
if teeth <= 0 {
return Err("Gear tooth count must be a positive whole number.".to_string());
}
Ok(teeth)
}
pub(in crate::gui::editor) fn gear_remap_preview(
design: &Design,
from_gear: i32,
to_gear: i32,
rounding: RemapRounding,
) -> Vec<GearRemapRow> {
let format_indices = |v: &[f64]| v.iter().map(f64::to_string).collect::<Vec<_>>().join(", ");
let original: Vec<String> = design
.tiers
.iter()
.map(|t| format_indices(&t.indices))
.collect();
let ratio = indicatrix_cut_core::remap_ratio(from_gear, to_gear);
let non_integral: Vec<bool> = design
.tiers
.iter()
.map(|t| t.indices.iter().any(|&i| (i * ratio).fract() != 0.0))
.collect();
let mut remapped = design.clone();
if remapped
.apply_edit(Edit::RemapIndices {
from_gear,
to_gear,
rounding,
})
.is_err()
{
return Vec::new();
}
design
.tiers
.iter()
.zip(&remapped.tiers)
.enumerate()
.map(|(i, (before, after))| GearRemapRow {
name: before.name.clone().into(),
old_indices: original[i].clone().into(),
new_indices: format_indices(&after.indices).into(),
non_integral: non_integral[i],
})
.collect()
}