1use crate::{CamError,Point3,Tool,ToolKind};
2use serde::{Serialize,Deserialize};
3use std::f64::consts::TAU;
4
5#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
6#[derive(Clone,Copy,Debug,PartialEq,Eq,Serialize,Deserialize)]
7pub enum InsertShape { C,D,V,T,W,R,S }
8
9#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12#[derive(Clone,Debug,PartialEq,Serialize,Deserialize)]
13pub struct Insert {
14 pub shape:InsertShape,
15 pub edge_length:f64,
16 pub nose_radius:f64,
17 pub clearance_deg:f64,
18 pub lead_angle_deg:f64,
20 pub holder_angle_deg:f64,
21}
22#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
23#[derive(Clone,Debug,PartialEq,Serialize,Deserialize)]
24pub struct Blade {pub width:f64,pub reach:f64,pub corner_radius:f64,pub holder_angle_deg:f64}
25#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
26#[derive(Clone,Debug,PartialEq,Serialize,Deserialize)]
27pub struct ThreadInsert {pub pitch_min:f64,pub pitch_max:f64,pub angle_deg:f64,pub reach:f64,pub nose_radius:f64,pub holder_angle_deg:f64}
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
29#[derive(Clone,Debug,PartialEq,Serialize,Deserialize)]
30pub struct BoringBar {pub diameter:f64,pub reach:f64,pub insert:Insert,pub insert_offset:Point3}
31
32#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34#[derive(Clone,Debug,PartialEq,Serialize,Deserialize)]
35#[serde(tag="kind",rename_all="camelCase")]
36pub enum TurningGeometry {Insert(Insert),Blade(Blade),Thread(ThreadInsert),BoringBar(BoringBar)}
37
38#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
43#[derive(Clone,Debug,PartialEq,Serialize,Deserialize)]
44pub struct ToolEnvelope {
45 pub vertices:Vec<Point3>,
46 pub reference_to_tip:Point3,
47 pub chord_bound:f64,
48 pub clearance_deg:f64,
50}
51impl ToolEnvelope {
52 pub fn validate(&self)->Result<(),CamError> {
53 if self.vertices.len()<3 || self.vertices.iter().any(|p|!p.is_finite() || p.y!=0.) || !self.reference_to_tip.is_finite() || self.reference_to_tip.y!=0. || !positive(self.chord_bound) {return Err(invalid("invalid turning envelope"));}
54 Ok(())
55 }
56 pub fn support(&self,direction:Point3)->Result<f64,CamError> {
57 self.validate()?;
58 if !direction.is_finite() || direction.y!=0. {return Err(invalid("turning support direction must lie in XZ"));}
59 Ok(self.vertices.iter().map(|p|p.dot(direction)).fold(f64::NEG_INFINITY,f64::max))
60 }
61}
62fn invalid(s:&str)->CamError {CamError::InvalidInput(s.into())}
63fn positive(v:f64)->bool {v.is_finite() && v>0.}
64fn rotate(p:Point3,angle:f64)->Point3 {let (s,c)=angle.sin_cos();Point3::new(c*p.x-s*p.z,0.,s*p.x+c*p.z)}
65fn p(x:f64,z:f64)->Point3 {Point3::new(x,0.,z)}
66
67pub fn envelope(tool:&Tool,geometry:&TurningGeometry,chord:f64)->Result<ToolEnvelope,CamError> {
70 if !matches!(tool.kind,ToolKind::LatheInsert|ToolKind::BoringBar) || !positive(chord) {return Err(invalid("turning envelope needs a lathe tool and positive chord tolerance"));}
71 let result=match geometry {
72 TurningGeometry::Insert(g)=>insert_envelope(g,chord),
73 TurningGeometry::Blade(g)=>{
74 if !positive(g.width) || !positive(g.reach) || !g.corner_radius.is_finite() || g.corner_radius<0. || 2.*g.corner_radius>g.width.min(g.reach) || !g.holder_angle_deg.is_finite() {return Err(invalid("invalid grooving blade dimensions"));}
75 let raw=vec![p(0.,-g.width/2.),p(g.reach,-g.width/2.),p(g.reach,g.width/2.),p(0.,g.width/2.)];
76 let mut outline=rounded_polygon(&raw,g.corner_radius,chord)?;
77 outline.extend([p(g.reach,-g.width/2.),p(g.reach,g.width/2.)]);
80 let vertices=hull(outline).into_iter().map(|v|rotate(v,g.holder_angle_deg.to_radians())).collect();
81 Ok(ToolEnvelope{vertices,reference_to_tip:Point3::ZERO,chord_bound:chord,clearance_deg:0.})
82 }
83 TurningGeometry::Thread(g)=>{
84 if !positive(g.pitch_min) || g.pitch_max<g.pitch_min || !g.pitch_max.is_finite() || !positive(g.reach) || !g.angle_deg.is_finite() || g.angle_deg<=0. || g.angle_deg>=180. || !g.nose_radius.is_finite() || g.nose_radius<0. || !g.holder_angle_deg.is_finite() {return Err(invalid("invalid threading insert dimensions"));}
85 let half=g.reach*(0.5*g.angle_deg.to_radians()).tan();
86 let raw=vec![p(0.,0.),p(g.reach,-half),p(g.reach,half)];
87 let vertices=rounded_polygon(&raw,g.nose_radius,chord)?.into_iter().map(|v|rotate(v,g.holder_angle_deg.to_radians())).collect();
88 Ok(ToolEnvelope{vertices,reference_to_tip:rotate(p(g.nose_radius/(0.5*g.angle_deg.to_radians()).sin(),0.),g.holder_angle_deg.to_radians()),chord_bound:chord,clearance_deg:0.})
89 }
90 TurningGeometry::BoringBar(g)=>{
91 if !positive(g.diameter) || !positive(g.reach) || !g.insert_offset.is_finite() || g.insert_offset.y!=0. {return Err(invalid("invalid boring bar dimensions"));}
92 let mut e=insert_envelope(&g.insert,chord)?;
93 for v in &mut e.vertices {*v=v.add(g.insert_offset);}
94 e.reference_to_tip=e.reference_to_tip.add(g.insert_offset);
95 e.vertices.extend([p(-g.diameter/2.,0.),p(g.diameter/2.,0.),p(g.diameter/2.,g.reach),p(-g.diameter/2.,g.reach)]);
96 e.vertices=hull(e.vertices);Ok(e)
97 }
98 }?;
99 result.validate()?;Ok(result)
100}
101fn insert_envelope(g:&Insert,chord:f64)->Result<ToolEnvelope,CamError> {
102 if !positive(g.edge_length) || !g.nose_radius.is_finite() || g.nose_radius<0. || !g.clearance_deg.is_finite() || !(0. ..90.).contains(&g.clearance_deg) || !g.lead_angle_deg.is_finite() || !g.holder_angle_deg.is_finite() {return Err(invalid("invalid turning insert dimensions"));}
103 let l=g.edge_length;let angle=(g.lead_angle_deg+g.holder_angle_deg).to_radians();
104 if g.shape==InsertShape::R {
105 if (g.nose_radius-l/2.).abs()>chord {return Err(invalid("round insert nose radius must equal half its diameter"));}
106 let vertices=circle(p(0.,0.),l/2.,0.,TAU,chord)?.into_iter().map(|v|rotate(v,angle)).collect();
107 return Ok(ToolEnvelope{vertices,reference_to_tip:Point3::ZERO,chord_bound:chord,clearance_deg:g.clearance_deg});
108 }
109 let raw=match g.shape {
110 InsertShape::S=>vec![p(l/2.,l/2.),p(-l/2.,l/2.),p(-l/2.,-l/2.),p(l/2.,-l/2.)],
111 InsertShape::T=>(0..3).map(|i|p(l/3_f64.sqrt()*(i as f64*TAU/3.).cos(),l/3_f64.sqrt()*(i as f64*TAU/3.).sin())).collect(),
112 InsertShape::W=>{let t=40_f64.to_radians().tan();let k=2.*t/(3_f64.sqrt()+t);let r=l/(1.+k*k-k).sqrt();(0..6).map(|i|{let radius=if i%2==0{r}else{k*r};let a=i as f64*TAU/6.;p(radius*a.cos(),radius*a.sin())}).collect()},
113 shape=>{let deg:f64=match shape {InsertShape::C=>80.,InsertShape::D=>55.,InsertShape::V=>35.,_=>unreachable!()};let a=l*(deg.to_radians()/2.).cos();let b=l*(deg.to_radians()/2.).sin();vec![p(a,0.),p(0.,b),p(-a,0.),p(0.,-b)]},
114 };
115 let centre=corner_centre(&raw,0,g.nose_radius)?;
116 let vertices=rounded_polygon(&raw,g.nose_radius,chord)?.into_iter().map(|v|rotate(v,angle)).collect();
117 Ok(ToolEnvelope{vertices,reference_to_tip:rotate(centre,angle),chord_bound:chord,clearance_deg:g.clearance_deg})
118}
119fn corner_centre(poly:&[Point3],i:usize,r:f64)->Result<Point3,CamError> {
120 let at=poly[i];let a=poly[(i+poly.len()-1)%poly.len()].sub(at).normalized().ok_or_else(||invalid("degenerate insert edge"))?;
121 let b=poly[(i+1)%poly.len()].sub(at).normalized().ok_or_else(||invalid("degenerate insert edge"))?;
122 let bisector=a.add(b).normalized().ok_or_else(||invalid("degenerate insert corner"))?;
123 let sin=(0.5*(1.-a.dot(b))).max(0.).sqrt();
124 if sin<=0. {return Err(invalid("degenerate insert angle"));}
125 Ok(at.add(bisector.scale(r/sin)))
126}
127fn rounded_polygon(poly:&[Point3],r:f64,chord:f64)->Result<Vec<Point3>,CamError> {
128 if r==0. {return Ok(poly.to_vec());}
129 let mut out=Vec::new();
130 for i in 0..poly.len() {
131 let at=poly[i];let prev=poly[(i+poly.len()-1)%poly.len()];let next=poly[(i+1)%poly.len()];
132 let a=prev.sub(at).normalized().unwrap();let b=next.sub(at).normalized().unwrap();
133 let centre=corner_centre(poly,i,r)?;
134 let along=centre.sub(at).dot(a);
135 if along>0.5*at.distance(prev) || along>0.5*at.distance(next) {return Err(invalid("nose radius cannot fit insert edges"));}
136 let from=at.add(a.scale(along)).sub(centre);let to=at.add(b.scale(along)).sub(centre);
137 let start=from.z.atan2(from.x);let sweep=(to.z.atan2(to.x)-start).rem_euclid(TAU);
138 out.extend(circle(centre,r,start,sweep,chord)?);
139 }
140 Ok(out)
141}
142fn circle(centre:Point3,r:f64,start:f64,sweep:f64,chord:f64)->Result<Vec<Point3>,CamError> {
143 let max_angle=2.*(r/(r+chord)).acos();
144 let steps=(sweep/max_angle).ceil().max(1.);
145 if !steps.is_finite() || steps>100_000. {return Err(invalid("turning envelope exceeds arc work budget"));}
146 let n=steps as usize;let step=sweep/n as f64;
147 let mut out=vec![centre.add(p(r*start.cos(),r*start.sin()))];
148 for i in 0..n {let a=start+(i as f64+0.5)*step;let radius=r/(step*0.5).cos();out.push(centre.add(p(radius*a.cos(),radius*a.sin())));}
149 let end=start+sweep;out.push(centre.add(p(r*end.cos(),r*end.sin())));Ok(out)
150}
151fn cross(a:Point3,b:Point3,c:Point3)->f64 {(b.x-a.x)*(c.z-a.z)-(b.z-a.z)*(c.x-a.x)}
152fn hull(mut points:Vec<Point3>)->Vec<Point3> {
153 points.sort_by(|a,b|a.x.total_cmp(&b.x).then(a.z.total_cmp(&b.z)));points.dedup();
154 let chain=|iter:Vec<Point3>| {let mut c=Vec::new();for p in iter {while c.len()>=2 && cross(c[c.len()-2],c[c.len()-1],p)<=0. {c.pop();}c.push(p);}c};
155 let mut lower=chain(points.clone());let mut upper=chain(points.into_iter().rev().collect());lower.pop();upper.pop();lower.extend(upper);lower
156}
157
158impl ToolEnvelope {
159 pub fn swept_vertices(&self,start:Point3,end:Point3)->Result<Vec<Point3>,CamError> {
162 self.validate()?;
163 if !start.is_finite() || !end.is_finite() || start.y!=0. || end.y!=0. {return Err(invalid("turning sweep must lie in XZ"));}
164 Ok(hull(self.vertices.iter().flat_map(|v|[v.sub(self.reference_to_tip).add(start),v.sub(self.reference_to_tip).add(end)]).collect()))
165 }
166 pub fn collides(&self,start:Point3,end:Point3,material:&[Point3])->Result<bool,CamError> {
170 if material.len()<3 || material.iter().any(|p|!p.is_finite() || p.y!=0.) {return Err(invalid("material section needs finite XZ polygon vertices"));}
171 let swept=self.swept_vertices(start,end)?;
172 for i in 0..swept.len() {for j in 0..material.len() {
173 if segments_touch(swept[i],swept[(i+1)%swept.len()],material[j],material[(j+1)%material.len()]) {return Ok(true);}
174 }}
175 Ok(inside(swept[0],material)||inside(material[0],&swept))
176 }
177}
178fn segments_touch(a:Point3,b:Point3,c:Point3,d:Point3)->bool {
179 if a.x.max(b.x)<c.x.min(d.x) || c.x.max(d.x)<a.x.min(b.x) || a.z.max(b.z)<c.z.min(d.z) || c.z.max(d.z)<a.z.min(b.z) {return false;}
180 let [u,v,w,t]=[cross(a,b,c),cross(a,b,d),cross(c,d,a),cross(c,d,b)];
181 ((u<=0. && v>=0.)||(v<=0. && u>=0.)) && ((w<=0. && t>=0.)||(t<=0. && w>=0.))
182}
183fn inside(p:Point3,poly:&[Point3])->bool {
184 let mut yes=false;
185 for i in 0..poly.len() {let a=poly[i];let b=poly[(i+1)%poly.len()];if (a.z>p.z)!=(b.z>p.z) && p.x<(b.x-a.x)*(p.z-a.z)/(b.z-a.z)+a.x {yes=!yes;}}
186 yes
187}