use indicatrix::geometry::meet_solver::MeetConstraint;
use indicatrix_cut_core::{ConstraintTier, TierTarget};
pub(in crate::gui::editor) fn parse_angle_only(angle: &str) -> Result<f64, String> {
let angle_deg: f64 = angle
.trim()
.parse()
.map_err(|_| format!("Angle '{}' is not a number.", angle.trim()))?;
if !angle_deg.is_finite() {
return Err("Angle must be a finite number.".to_string());
}
reject_angle_over_90(angle_deg)?;
Ok(angle_deg)
}
fn parse_colon_sequence(token: &str, gear_teeth_abs_f64: f64) -> Option<Vec<f64>> {
let parts: Vec<&str> = token.split(':').collect();
let [start, step, stop] = parts.as_slice() else {
return None;
};
let start: f64 = start.trim().parse().ok()?;
let step: f64 = step.trim().parse().ok()?;
let stop: f64 = stop.trim().parse().ok()?;
if step == 0.0 || !(start.is_finite() && step.is_finite() && stop.is_finite()) {
return None;
}
let mut values = Vec::new();
let mut current = start;
for _ in 0..10_000 {
if (step > 0.0 && current >= stop) || (step < 0.0 && current <= stop) {
break;
}
values.push(if gear_teeth_abs_f64 > 0.0 {
current.rem_euclid(gear_teeth_abs_f64)
} else {
current
});
current += step;
}
Some(values)
}
fn parse_orbit_suffix(suffix: &str) -> Option<u32> {
let digits = suffix.strip_prefix(['x', 'X'])?;
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
digits.parse::<u32>().ok().filter(|&n| n > 0)
}
fn expand_orbit_shorthand(base: f64, fold_count: u32, gear_teeth_abs_f64: f64) -> Vec<f64> {
if gear_teeth_abs_f64 <= 0.0 {
return vec![base];
}
let step = gear_teeth_abs_f64 / f64::from(fold_count);
(0..fold_count)
.map(|k| f64::mul_add(f64::from(k), step, base).rem_euclid(gear_teeth_abs_f64))
.collect()
}
fn reject_angle_over_90(angle_deg: f64) -> Result<(), String> {
if angle_deg.abs() > 90.0 {
return Err(format!(
"Angle {angle_deg:.2}\u{b0} exceeds 90\u{b0} -- angles are measured from the girdle \
plane, so no crown or pavilion facet can be steeper than that."
));
}
Ok(())
}
pub(in crate::gui::editor) struct TierFormFields<'a> {
pub(in crate::gui::editor) angle: &'a str,
pub(in crate::gui::editor) constraint_kind: i32,
pub(in crate::gui::editor) constraint_text: &'a str,
pub(in crate::gui::editor) name: &'a str,
pub(in crate::gui::editor) indices: &'a str,
pub(in crate::gui::editor) gear_teeth_abs: u32,
pub(in crate::gui::editor) imported_meet: Option<MeetConstraint>,
pub(in crate::gui::editor) original_notes: Option<String>,
pub(in crate::gui::editor) other_tier_names: Vec<String>,
}
pub(in crate::gui::editor) fn parse_index_list(
indices: &str,
gear_teeth_abs: u32,
) -> Result<Vec<f64>, String> {
let gear_teeth_abs_f64 = f64::from(gear_teeth_abs);
let mut parsed_indices: Vec<f64> = Vec::new();
let push_index =
|value: f64, label: &str, parsed_indices: &mut Vec<f64>| -> Result<(), String> {
if !value.is_finite() {
return Err(format!("Index '{label}' must be a finite number."));
}
if value < 0.0 || value >= gear_teeth_abs_f64 {
return Err(format!(
"Index '{label}' is outside this design's {gear_teeth_abs}-tooth gear."
));
}
if parsed_indices.contains(&value) {
return Err(format!("Index '{label}' is listed more than once."));
}
parsed_indices.push(value);
Ok(())
};
let raw_tokens: Vec<&str> = indices
.split([',', ' ', ';'])
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
let mut i = 0;
while i < raw_tokens.len() {
let token = raw_tokens[i];
if let Some(values) = parse_colon_sequence(token, gear_teeth_abs_f64) {
for value in values {
push_index(
value,
&format!("{value} (from '{token}')"),
&mut parsed_indices,
)?;
}
i += 1;
continue;
}
if let Ok(base) = token.parse::<f64>()
&& let Some(next) = raw_tokens.get(i + 1)
&& let Some(fold_count) = parse_orbit_suffix(next)
{
let shorthand = format!("{token} {next}");
for value in expand_orbit_shorthand(base, fold_count, gear_teeth_abs_f64) {
push_index(
value,
&format!("{value} (from '{shorthand}')"),
&mut parsed_indices,
)?;
}
i += 2;
continue;
}
let value: f64 = token
.parse()
.map_err(|_| format!("Index '{token}' is not a number."))?;
push_index(value, token, &mut parsed_indices)?;
i += 1;
}
Ok(parsed_indices)
}
pub(in crate::gui::editor) fn parse_tier_form(
form: TierFormFields<'_>,
) -> Result<ConstraintTier, String> {
let TierFormFields {
angle,
constraint_kind,
constraint_text,
name,
indices,
gear_teeth_abs,
imported_meet,
original_notes,
other_tier_names,
} = form;
let angle_deg: f64 = angle
.trim()
.parse()
.map_err(|_| format!("Angle '{}' is not a number.", angle.trim()))?;
if !angle_deg.is_finite() {
return Err("Angle must be a finite number.".to_string());
}
reject_angle_over_90(angle_deg)?;
let name = name.trim();
for token in name.split('/').map(str::trim).filter(|s| !s.is_empty()) {
if let Some(existing) = other_tier_names
.iter()
.find(|other| other.eq_ignore_ascii_case(token))
{
return Err(format!(
"Another tier is already named '{existing}' -- facet names must be unique so \
meet constraints resolve to the right tier."
));
}
}
let parsed_indices = parse_index_list(indices, gear_teeth_abs)?;
let constraint = match constraint_kind {
0 => MeetConstraint::MeetExisting,
1 => {
let names: Vec<String> = constraint_text
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
if names.is_empty() {
return Err("\"Meet named\" needs at least one facet name.".to_string());
}
MeetConstraint::MeetNamed(names)
}
2 => {
let value: f64 = constraint_text.trim().parse().map_err(|_| {
format!(
"Scale reference '{}' is not a number.",
constraint_text.trim()
)
})?;
if !value.is_finite() {
return Err("Scale reference must be a finite number.".to_string());
}
MeetConstraint::ScaleReference(value)
}
3..=5 => MeetConstraint::ScaleReference(0.0),
other => return Err(format!("Unknown constraint kind {other}.")),
};
Ok(ConstraintTier {
angle_deg,
name: name.to_string(),
indices: parsed_indices,
constraint,
imported_meet,
original_notes,
detached: Vec::new(),
})
}
pub(in crate::gui::editor) fn parse_tier_target(
constraint_kind: i32,
constraint_text: &str,
) -> Result<Option<TierTarget>, String> {
let (label, build): (&str, fn(f64) -> TierTarget) = match constraint_kind {
3 => ("Depth", TierTarget::DepthMm as fn(f64) -> TierTarget),
4 => (
"Girdle thickness",
TierTarget::GirdleThicknessMm as fn(f64) -> TierTarget,
),
5 => (
"Table width",
TierTarget::TableWidthMm as fn(f64) -> TierTarget,
),
_ => return Ok(None),
};
let text = constraint_text.trim();
let value: f64 = text
.parse()
.map_err(|_| format!("{label} '{text}' is not a number."))?;
if !value.is_finite() {
return Err(format!("{label} must be a finite number."));
}
if value <= 0.0 {
return Err(format!("{label} must be a positive number."));
}
Ok(Some(build(value)))
}
#[cfg(test)]
mod tests;