use crate::error::{Error, Result};
use std::f64::consts::FRAC_PI_2;
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Tool {
pub cutter: Cutter,
pub shank: Option<Shank>,
pub holder: Option<Holder>,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Cutter {
FlatEndMill {
diameter: f64,
flute_length: f64,
},
BallEndMill {
diameter: f64,
flute_length: f64,
},
BullNoseEndMill {
diameter: f64,
corner_radius: f64,
flute_length: f64,
},
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Shank {
pub diameter: f64,
pub length: f64,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Holder {
pub diameter: f64,
pub length: f64,
}
fn positive(name: &str, v: f64) -> Result<()> {
if v > 0.0 && v.is_finite() {
Ok(())
} else {
Err(Error::InvalidInput(format!("{name} must be positive: {v}")))
}
}
impl Tool {
pub fn flat_end_mill(diameter: f64, flute_length: f64) -> Tool {
Tool {
cutter: Cutter::FlatEndMill { diameter, flute_length },
shank: None,
holder: None,
}
}
pub fn ball_end_mill(diameter: f64, flute_length: f64) -> Tool {
Tool {
cutter: Cutter::BallEndMill { diameter, flute_length },
shank: None,
holder: None,
}
}
pub fn bull_nose_end_mill(diameter: f64, corner_radius: f64, flute_length: f64) -> Tool {
Tool {
cutter: Cutter::BullNoseEndMill {
diameter,
corner_radius,
flute_length,
},
shank: None,
holder: None,
}
}
pub fn with_shank(mut self, shank: Shank) -> Tool {
self.shank = Some(shank);
self
}
pub fn with_holder(mut self, holder: Holder) -> Tool {
self.holder = Some(holder);
self
}
pub fn radius(&self) -> f64 {
match self.cutter {
Cutter::FlatEndMill { diameter, .. }
| Cutter::BallEndMill { diameter, .. }
| Cutter::BullNoseEndMill { diameter, .. } => diameter * 0.5,
}
}
pub fn flute_length(&self) -> f64 {
match self.cutter {
Cutter::FlatEndMill { flute_length, .. }
| Cutter::BallEndMill { flute_length, .. }
| Cutter::BullNoseEndMill { flute_length, .. } => flute_length,
}
}
pub fn corner_radius(&self) -> f64 {
match self.cutter {
Cutter::FlatEndMill { .. } => 0.0,
Cutter::BallEndMill { diameter, .. } => diameter * 0.5,
Cutter::BullNoseEndMill { corner_radius, .. } => corner_radius,
}
}
pub fn reach(&self) -> f64 {
(self.flute_length().powi(2) + self.radius().powi(2)).sqrt()
}
pub fn non_cutting_cylinders(&self) -> Vec<(f64, f64, f64)> {
let mut out = Vec::new();
let mut h = self.flute_length();
if let Some(s) = self.shank {
out.push((s.diameter * 0.5, h, h + s.length));
h += s.length;
}
if let Some(hd) = self.holder {
out.push((hd.diameter * 0.5, h, h + hd.length));
}
out
}
pub fn cutting_profile(&self, chord_tolerance: f64) -> Vec<(f64, f64)> {
let radius = self.radius();
let length = self.flute_length();
let rc = self.corner_radius();
if rc <= 0.0 {
return vec![(radius, 0.0), (radius, length)];
}
let rings = corner_rings(rc, chord_tolerance);
let flat = radius - rc;
let mut profile = Vec::with_capacity(rings + 2);
for k in 0..=rings {
let phi = FRAC_PI_2 * k as f64 / rings as f64;
profile.push((flat + rc * phi.sin(), rc * (1.0 - phi.cos())));
}
if length > rc {
profile.push((radius, length));
}
profile
}
pub fn contains(&self, radial: f64, height: f64) -> bool {
let radius = self.radius();
if height < 0.0 || height > self.flute_length() || radial > radius {
return false;
}
let rc = self.corner_radius();
if rc <= 0.0 || height >= rc {
return true;
}
let flat = radius - rc;
let dz = rc - height;
radial <= flat + (rc * rc - dz * dz).max(0.0).sqrt()
}
pub fn validate(&self) -> Result<()> {
match self.cutter {
Cutter::FlatEndMill { diameter, flute_length } => {
positive("tool diameter", diameter)?;
positive("tool flute length", flute_length)?;
}
Cutter::BallEndMill { diameter, flute_length } => {
positive("tool diameter", diameter)?;
positive("tool flute length", flute_length)?;
if flute_length < diameter * 0.5 - 1e-9 {
return Err(Error::InvalidInput(
"ball end mill flute length must be at least the radius".into(),
));
}
}
Cutter::BullNoseEndMill {
diameter,
corner_radius,
flute_length,
} => {
positive("tool diameter", diameter)?;
positive("corner radius", corner_radius)?;
positive("tool flute length", flute_length)?;
if corner_radius > diameter * 0.5 + 1e-9 {
return Err(Error::InvalidInput(
"corner radius must not exceed the tool radius".into(),
));
}
if flute_length < corner_radius - 1e-9 {
return Err(Error::InvalidInput(
"flute length must be at least the corner radius".into(),
));
}
}
}
if let Some(s) = self.shank {
positive("shank diameter", s.diameter)?;
positive("shank length", s.length)?;
}
if let Some(h) = self.holder {
positive("holder diameter", h.diameter)?;
positive("holder length", h.length)?;
}
Ok(())
}
}
fn corner_rings(corner_radius: f64, chord_tolerance: f64) -> usize {
let tol = chord_tolerance.max(1e-9).min(corner_radius * 0.5);
let step = 2.0 * (1.0 - tol / corner_radius).acos();
let n = (FRAC_PI_2 / step).ceil();
if n.is_finite() {
(n as usize).clamp(2, 64)
} else {
64
}
}