1use crate::*;
3use serde::{Serialize,Deserialize};
4use std::collections::BTreeSet;
5pub mod interchange;
6pub mod holders;
7
8#[derive(Clone,Copy,Debug,PartialEq,Eq)]
9pub enum ProfileSupport {
10 Exact,
11 Approximate,
14 Unavailable,
15}
16
17pub fn validate_tool(t:&Tool)->Result<ProfileSupport,String> {
21 if t.id.as_str().trim().is_empty() {return Err("tool id is empty".into());}
22 if t.number==0 {return Err("tool number must be positive".into());}
23 let g=&t.geometry;
24 for (name,value) in [("diameter",g.diameter),("cutting length",g.cutting_length),("shaft diameter",g.shaft_diameter()),("overall length",g.overall_length())] {
25 if !value.is_finite()||value<=0. {return Err(format!("{name} must be finite and positive"));}
26 }
27 if !g.shaft_length.is_finite()||g.shaft_length<0. {return Err("shaft length must be finite and nonnegative".into());}
28 if g.overall_length()<g.cutting_length+g.shaft_length {return Err("overall length is shorter than cutting plus shaft length".into());}
29 for (name,value,zero) in [("corner radius",g.corner_radius,true),("ball diameter",g.ball_diameter,false),("tip diameter",g.tip_diameter,true),("thread pitch",g.thread_pitch,false)] {
30 if let Some(v)=value {if !v.is_finite()||v<0.||(!zero&&v==0.) {return Err(format!("invalid {name}"));}}
31 }
32 if g.corner_radius.is_some_and(|v|v>g.diameter/2.)||g.ball_diameter.is_some_and(|v|v>g.diameter)||g.tip_diameter.is_some_and(|v|v>g.diameter) {return Err("end geometry exceeds the cutting diameter".into());}
33 if g.included_angle_deg.is_some_and(|v|!v.is_finite()||v<=0.||v>=180.) {return Err("included angle must be between 0 and 180 degrees".into());}
34 if g.flute_count==Some(0) {return Err("flute count must be positive".into());}
35 if matches!(t.kind,ToolKind::Tap|ToolKind::ThreadMill)&&g.thread_pitch.is_none() {return Err("thread tool requires thread pitch".into());}
36 if let Some(h)=&t.holder {h.validate()?;if h.segments.is_empty()||!h.height().is_finite() {return Err("holder profile is empty or its height overflows".into());}}
37 t.feeds.validate()?;
38 for v in [t.feeds.surface_speed,t.feeds.feed_per_tooth].into_iter().flatten() {if !v.is_finite()||v<=0. {return Err("feed metadata must be finite and positive".into());}}
39 match Cutter::from_tool(t) {
40 Ok(c)=>{c.validate().map_err(|e|e.to_string())?;Ok(ProfileSupport::Exact)},
41 Err(CutterError::UnsupportedToolKind {..})=>Ok(ProfileSupport::Unavailable),
42 Err(e)=>Err(e.to_string()),
43 }
44}
45
46#[derive(Clone,Copy,Debug,Default,PartialEq,Eq,Serialize,Deserialize)]
48#[serde(rename_all="kebab-case")]
49pub enum LibraryUnits {#[default] Millimeters, Inches}
50impl LibraryUnits {pub fn millimeters_per_unit(self)->f64 {match self {Self::Millimeters=>1.,Self::Inches=>25.4}}}
51#[derive(Clone,Debug,PartialEq,Serialize,Deserialize)]
52#[serde(rename_all="camelCase")]
53pub struct ToolLibrary {pub name:String,pub units:LibraryUnits,pub tools:Vec<Tool>}
54impl ToolLibrary {
55 pub fn new(name:impl Into<String>)->Self {Self {name:name.into(),units:LibraryUnits::Millimeters,tools:Vec::new()}}
56 pub fn validate(&self)->Result<(),String> {validate_program_tools(&self.tools)}
57 pub fn insert(&mut self,tool:Tool)->Result<(),String> {
58 validate_tool(&tool)?;
59 if self.tools.iter().any(|t|t.id==tool.id||t.number==tool.number) {return Err("duplicate tool id or number".into());}
60 self.tools.push(tool);Ok(())
61 }
62 pub fn replace(&mut self,tool:Tool)->Result<(),String> {
63 let i=self.tools.iter().position(|t|t.id==tool.id).ok_or("unknown tool id")?;
64 validate_tool(&tool)?;if self.tools.iter().enumerate().any(|(j,t)|j!=i&&t.number==tool.number) {return Err("duplicate tool number".into());}
65 self.tools[i]=tool;Ok(())
66 }
67 pub fn renumber(&mut self,first:u32,increment:u32)->Result<(),String> {
69 if first==0||increment==0 {return Err("numbering starts and increments must be positive".into());}
70 let numbers:Result<Vec<_>,_>=(0..self.tools.len()).map(|i|u32::try_from(i).ok().and_then(|i|i.checked_mul(increment)).and_then(|n|first.checked_add(n)).ok_or("tool numbering overflow")).collect();
71 for (tool,n) in self.tools.iter_mut().zip(numbers?) {tool.number=n;}Ok(())
72 }
73 pub fn search(&self,filter:&ToolFilter)->Result<Vec<&Tool>,String> {
74 for v in [filter.min_diameter,filter.max_diameter].into_iter().flatten() {if !v.is_finite()||v<0. {return Err("invalid diameter filter".into());}}
75 if matches!((filter.min_diameter,filter.max_diameter),(Some(a),Some(b)) if a>b) {return Err("reversed diameter range".into());}
76 Ok(self.tools.iter().filter(|t|filter.kind.map_or(true,|k|k==t.kind)&&filter.min_diameter.map_or(true,|v|t.geometry.diameter>=v)&&filter.max_diameter.map_or(true,|v|t.geometry.diameter<=v)&&filter.flutes.map_or(true,|v|t.geometry.flute_count==Some(v))&&filter.material.as_ref().map_or(true,|v|t.material.eq_ignore_ascii_case(v))&&filter.text.as_ref().map_or(true,|v|format!("{} {} {}",t.name,t.description,t.id).to_lowercase().contains(&v.to_lowercase()))).collect())
77 }
78}
79#[derive(Clone,Debug,Default)]
80pub struct ToolFilter {pub kind:Option<ToolKind>,pub min_diameter:Option<f64>,pub max_diameter:Option<f64>,pub flutes:Option<u32>,pub material:Option<String>,pub text:Option<String>}
81pub fn validate_program_tools(tools:&[Tool])->Result<(),String> {
83 let mut ids=BTreeSet::new();let mut numbers=BTreeSet::new();
84 for t in tools {validate_tool(t)?;if !ids.insert(&t.id)||!numbers.insert(t.number) {return Err("program contains duplicate tool ids or numbers".into());}}Ok(())
85}