Skip to main content

brep_cam/feeds/
mod.rs

1//! Original starting values and metric cutting formulas; see the C12 design
2//! record for provenance, units and limitations. Not a vendor cutting database.
3use 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
14/// These deliberately simple starting values are original engineering defaults.
15/// Users can provide qualified grade/coating data to calculate_with_data instead.
16pub 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        // This UI/API has no work diameter. Preserve the selected fixed rpm;
30        // never mistake insert diameter for work diameter or silently enable CSS.
31        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}
37/// Surface speed in m/min; chip load in mm/tooth; engagement and machine feeds
38/// in the supplied model units. Informational surface_speed stays in m/min.
39pub 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/// Turning data is deliberately distinct from milling chip load: mm/revolution,
70/// with no flute multiplier. Original starting values, not vendor recommendations.
71#[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}
89/// Fixed-rpm calculation at an explicit work diameter in model units. The
90/// returned per-minute feed preserves f/rev at the achieved clamped rpm.
91pub 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    // No milling chip load is implied by a turning calculation.
114    Ok(FeedCalculation{feeds,requested_rpm,requested_chip_load_mm:0.,chip_thinning_factor:1.,adjustments})
115}