1use crate::*;
4use crate::library::{LibraryUnits,validate_tool};
5use serde::{Serialize,Deserialize};
6#[derive(Clone,Copy,Debug,PartialEq,Eq,Serialize,Deserialize)]
7#[serde(rename_all="kebab-case")]
8pub enum WorkMaterial {Aluminium,MildSteel,AlloySteel,StainlessSteel,CastIron,Brass,Titanium,Plastics,Wood,Mdf}
9#[derive(Clone,Copy,Debug,PartialEq,Serialize,Deserialize)]
10pub struct CuttingData {pub surface_speed_m_min:f64,pub chip_load_mm:f64}
11#[derive(Clone,Debug,PartialEq)]
12pub struct FeedCalculation {pub feeds:FeedsSpeeds,pub requested_rpm:f64,pub requested_chip_load_mm:f64,pub chip_thinning_factor:f64,pub adjustments:Vec<String>}
13
14pub fn default_data(tool:&Tool,units:LibraryUnits,material:WorkMaterial)->Result<CuttingData,String> {
17 validate_tool(tool)?;
18 let (speed,chip)=match material {
19 WorkMaterial::Aluminium=>(60.,0.030),WorkMaterial::MildSteel=>(25.,0.020),WorkMaterial::AlloySteel=>(20.,0.015),WorkMaterial::StainlessSteel=>(15.,0.012),WorkMaterial::CastIron=>(20.,0.020),WorkMaterial::Brass=>(50.,0.025),WorkMaterial::Titanium=>(10.,0.010),WorkMaterial::Plastics=>(60.,0.040),WorkMaterial::Wood=>(100.,0.060),WorkMaterial::Mdf=>(80.,0.040),
20 };
21 let (speed_factor,chip_factor)=match tool.material.to_ascii_lowercase().as_str() {"carbide"|"solid carbide"=>(2.,1.25),"hss"|"high speed steel"=>(1.,1.),_=>return Err("default cutting data requires carbide or HSS tool material".into())};
22 let diameter=tool.geometry.diameter*units.millimeters_per_unit();let size=if diameter<=3. {0.5}else if diameter<=10. {1.}else{1.5};
23 let kind=match tool.kind {ToolKind::Drill|ToolKind::SpotDrill|ToolKind::CenterDrill|ToolKind::Reamer|ToolKind::BoringBar=>(0.7,0.5),ToolKind::Tap=>(0.3,1.),ToolKind::LatheInsert=>return Err("turning cutting data requires a turning feed-per-revolution contract".into()),ToolKind::Chamfer|ToolKind::VBit|ToolKind::Engraver=>(0.8,0.5),ToolKind::ThreadMill=>(0.7,0.5),_ =>(1.,1.)};
24 Ok(CuttingData {surface_speed_m_min:speed*speed_factor*kind.0,chip_load_mm:chip*chip_factor*size*kind.1})
25}
26pub fn calculate(tool:&Tool,machine:&Machine,units:LibraryUnits,material:WorkMaterial,radial_engagement:Option<f64>)->Result<FeedCalculation,String> {
27 if tool.kind==ToolKind::LatheInsert {
28 if radial_engagement.is_some() {return Err("radial chip thinning does not apply to turning".into());}
29 let mut out=turning_at_rpm(tool,machine,units,default_turning_data(tool,material)?,tool.feeds.spindle_rpm)?;
32 out.adjustments.push("turning uses selected fixed rpm; supply work diameter to calculate_turning_with_data for surface-speed-derived rpm".into());
33 return Ok(out);
34 }
35 calculate_with_data(tool,machine,units,default_data(tool,units,material)?,radial_engagement)
36}
37pub fn calculate_with_data(tool:&Tool,machine:&Machine,units:LibraryUnits,data:CuttingData,radial_engagement:Option<f64>)->Result<FeedCalculation,String> {
40 validate_tool(tool)?;machine.validate()?;
41 for v in [machine.spindle.min_rpm,machine.spindle.max_rpm,machine.feed.max_cutting,machine.feed.rapid,data.surface_speed_m_min,data.chip_load_mm] {if !v.is_finite()||v<0. {return Err("cutting data and machine limits must be finite and nonnegative".into());}}
42 if data.surface_speed_m_min==0.||data.chip_load_mm==0.||machine.spindle.max_rpm==0. {return Err("cutting speed, chip load and maximum rpm must be positive".into());}
43 if tool.kind==ToolKind::LatheInsert {return Err("turning requires feed per revolution".into());}
44 let scale=units.millimeters_per_unit();let diameter=tool.geometry.diameter*scale;
45 let requested_rpm=1000.*data.surface_speed_m_min/(std::f64::consts::PI*diameter);
46 if !requested_rpm.is_finite()||requested_rpm<=0. {return Err("cutting speed calculation overflow".into());}
47 let mut rpm=requested_rpm.clamp(machine.spindle.min_rpm,machine.spindle.max_rpm);let mut adjustments=Vec::new();
48 if rpm!=requested_rpm {adjustments.push("spindle speed clamped to machine limits".into());}
49 let mut factor=1.;
50 if let Some(a)=radial_engagement {
51 if !a.is_finite()||a<=0.||a>tool.geometry.diameter {return Err("radial engagement must be in (0, diameter]".into());}
52 if tool.kind.is_hole_tool() {return Err("radial chip thinning does not apply to hole-making cycles".into());}
53 let ratio=a/tool.geometry.diameter;
54 if ratio<0.5 {factor=1./(2.*(ratio*(1.-ratio)).sqrt());if factor>4. {factor=4.;adjustments.push("radial chip thinning capped at four times base chip load".into());}}
55 }
56 let teeth=tool.geometry.flute_count.ok_or("feed calculation requires flute count")? as f64;
57 let mut feed=if tool.kind==ToolKind::Tap {rpm*tool.geometry.thread_pitch.unwrap()}else{rpm*teeth*data.chip_load_mm/scale*factor};
58 if !feed.is_finite()||feed<=0. {return Err("feed calculation overflow".into());}
59 if feed>machine.feed.max_cutting {
60 if tool.kind==ToolKind::Tap {
61 rpm=machine.feed.max_cutting/tool.geometry.thread_pitch.unwrap();
62 if rpm<machine.spindle.min_rpm||rpm<=0. {return Err("machine cannot maintain tapping pitch within spindle/feed limits".into());}
63 feed=rpm*tool.geometry.thread_pitch.unwrap();adjustments.push("tapping rpm reduced to preserve pitch at machine feed limit".into());
64 }else {feed=machine.feed.max_cutting;adjustments.push("cutting feed clamped to machine limit; achieved chip load reduced".into());}
65 }
66 let cycle=tool.kind.is_hole_tool();let feeds=FeedsSpeeds {spindle_rpm:rpm,spindle_direction:if tool.geometry.left_hand {SpindleDirection::Ccw}else{SpindleDirection::Cw},cutting_feed:feed,plunge_feed:if cycle {feed}else{feed*0.25},lead_feed:Some(if cycle {feed}else{feed*0.5}),ramp_feed:Some(if cycle {feed}else{feed*0.3}),retract_feed:None,surface_speed:Some(std::f64::consts::PI*diameter*rpm/1000.),feed_per_tooth:Some(feed/(rpm*teeth)),surface_speed_limit_rpm:None,css:false};
67 feeds.validate()?;Ok(FeedCalculation {feeds,requested_rpm,requested_chip_load_mm:data.chip_load_mm,chip_thinning_factor:factor,adjustments})
68}
69#[derive(Clone,Copy,Debug,PartialEq,Serialize,Deserialize)]
72pub struct TurningCuttingData {pub surface_speed_m_min:f64,pub feed_per_revolution_mm:f64}
73pub fn default_turning_data(tool:&Tool,material:WorkMaterial)->Result<TurningCuttingData,String> {
74 validate_tool(tool)?;
75 if tool.kind!=ToolKind::LatheInsert {return Err("turning cutting data requires a lathe insert".into());}
76 let (vc,feed)=match material {
77 WorkMaterial::Aluminium=>(120.,0.12),WorkMaterial::MildSteel=>(80.,0.10),
78 WorkMaterial::AlloySteel=>(60.,0.08),WorkMaterial::StainlessSteel=>(45.,0.06),
79 WorkMaterial::CastIron=>(70.,0.10),WorkMaterial::Brass=>(100.,0.10),
80 WorkMaterial::Titanium=>(30.,0.05),WorkMaterial::Plastics=>(80.,0.08),
81 WorkMaterial::Wood=>(100.,0.10),WorkMaterial::Mdf=>(80.,0.08),
82 };
83 let factor=match tool.material.to_ascii_lowercase().as_str() {
84 "carbide"|"solid carbide"=>1.,"hss"|"high speed steel"=>0.5,
85 _=>return Err("default turning data requires carbide or HSS insert material".into()),
86 };
87 Ok(TurningCuttingData{surface_speed_m_min:vc*factor,feed_per_revolution_mm:feed*factor})
88}
89pub fn calculate_turning_with_data(tool:&Tool,machine:&Machine,units:LibraryUnits,work_diameter:f64,data:TurningCuttingData)->Result<FeedCalculation,String> {
92 if !work_diameter.is_finite() || work_diameter<=0. {return Err("turning work diameter must be finite and positive".into());}
93 let diameter=work_diameter*units.millimeters_per_unit();
94 let rpm=1000.*data.surface_speed_m_min/(std::f64::consts::PI*diameter);
95 let mut out=turning_at_rpm(tool,machine,units,data,rpm)?;
96 out.feeds.surface_speed=Some(std::f64::consts::PI*diameter*out.feeds.spindle_rpm/1000.);
97 Ok(out)
98}
99fn turning_at_rpm(tool:&Tool,machine:&Machine,units:LibraryUnits,data:TurningCuttingData,requested_rpm:f64)->Result<FeedCalculation,String> {
100 validate_tool(tool)?;machine.validate()?;
101 if tool.kind!=ToolKind::LatheInsert {return Err("turning feed calculation requires a lathe insert".into());}
102 for v in [data.surface_speed_m_min,data.feed_per_revolution_mm,requested_rpm,machine.spindle.max_rpm,machine.feed.max_cutting] {
103 if !v.is_finite() || v<=0. {return Err("turning cutting data, rpm and limits must be finite and positive".into());}
104 }
105 let per_rev=data.feed_per_revolution_mm/units.millimeters_per_unit();
106 let max_rpm=machine.spindle.max_rpm.min(machine.feed.max_cutting/per_rev);
107 if max_rpm<machine.spindle.min_rpm || max_rpm<=0. {return Err("machine cannot maintain turning feed per revolution within spindle/feed limits".into());}
108 let rpm=requested_rpm.clamp(machine.spindle.min_rpm,max_rpm);let feed=rpm*per_rev;
109 if !feed.is_finite() || feed<=0. {return Err("turning feed calculation overflow".into());}
110 let mut adjustments=Vec::new();if rpm!=requested_rpm {adjustments.push("turning rpm clamped to spindle/feed limits while preserving feed per revolution".into());}
111 let feeds=FeedsSpeeds{spindle_rpm:rpm,spindle_direction:tool.feeds.spindle_direction,cutting_feed:feed,plunge_feed:feed,lead_feed:Some(feed),ramp_feed:Some(feed),retract_feed:None,surface_speed:None,feed_per_tooth:None,surface_speed_limit_rpm:None,css:false};
112 feeds.validate()?;
113 Ok(FeedCalculation{feeds,requested_rpm,requested_chip_load_mm:0.,chip_thinning_factor:1.,adjustments})
115}