1use crate::{CamError,Operation,Setup,Tool,StockCylinder,Runner,Point2,Point3,Tolerance,Toolpath,FeedsSpeeds,Strategy};
3use crate::planar::{Chain,Loop,Region,Segment};
4use super::{TurningGeometry,ToolEnvelope,profile::TurningProfile,generate::*};
5#[derive(Clone,Copy,Debug,PartialEq,Eq)]
6pub enum RoughingDirection { Axial, Radial }
7#[derive(Clone,Debug)]
11pub struct ProfileInput {pub contour:Chain,pub pilot_radius:f64}
12pub(crate) struct Context {
13 pub path:Toolpath,pub envelope:ToolEnvelope,pub feeds:FeedsSpeeds,
14 pub material:Region,pub contour:Chain,pub nose:f64,pub clear_x:f64,pub safe_z:f64,
15 pub front:f64,pub back:f64,pub min_x:f64,pub max_x:f64,pub outside:bool,
16}
17pub(crate) fn p2(p:Point3)->Point2 {Point2::new(p.x,p.z)}
18pub(crate) fn p3(p:Point2)->Point3 {point(p.x,p.y)}
19pub fn chain_from_edges(edges:&[super::profile::ProfileEdge])->Chain {
20 use super::profile::ProfileEdge;
21 Chain::open(edges.iter().map(|e|match *e {
22 ProfileEdge::Line{start,end}=>Segment::line(p2(start),p2(end)),
23 ProfileEdge::Arc{start,end,centre,clockwise,..}=>Segment::arc(p2(start),p2(end),p2(centre),!clockwise),
24 }).collect())
25}
26pub(crate) fn context(op:&Operation,setup:&Setup,tool:&Tool,g:&TurningGeometry,stock:&StockCylinder,profile:&TurningProfile,input:&ProfileInput,tol:Tolerance,runner:&mut Runner<'_>)->Result<Context,CamError> {
27 tol.validate()?;runner.token().check()?;
28 let params=match &op.strategy {Strategy::TurnProfile(p)=>p,_=>return Err(invalid("profile turning requires TurnProfile parameters"))};
29 if !params.depth_of_cut.is_finite() || params.depth_of_cut<=0. || ![op.stock_to_leave.radial,op.stock_to_leave.axial].iter().all(|v|v.is_finite() && *v>=0.) {return Err(invalid("invalid profile depth or stock allowance"));}
30 let (path,envelope,feeds)=prepare(op,setup,tool,g,profile)?;
31 super::profile::bar_section(stock,stock.height)?;
32 let nose=nose(g)?;
33 if nose<=op.tolerance || input.contour.closed || input.contour.segments.is_empty() || input.contour.segments.len()>512 || profile.loops.iter().map(Vec::len).sum::<usize>()>1024 {return Err(invalid("profile needs positive nose radius and a bounded open contour"));}
34 let mut contour=input.contour.clone();contour.validate(&tol)?;
35 let material=Region::from_unoriented(profile.loops.iter().map(|l| {
38 Loop::new(l.iter().zip(l.iter().cycle().skip(1)).take(l.len()).filter(|(a,b)|a!=b).map(|(a,b)|Segment::line(p2(*a),p2(*b))).collect())
39 }).collect(),&tol)?;
40 let mut min_x=f64::INFINITY;let mut max_x=f64::NEG_INFINITY;
41 let mut front=f64::NEG_INFINITY;let mut back=f64::INFINITY;
42 for s in &contour.segments {
43 let (low,high)=s.bounds();min_x=min_x.min(low.x);max_x=max_x.max(high.x);front=front.max(high.y);back=back.min(low.y);
44 if s.length()/op.tolerance>100_000. {return Err(invalid("turning contour sampling budget exceeded"));}
45 let points=s.discretize(op.tolerance*0.125);
46 for p in points {
47 runner.work(1,"turn-profile-selection",0,0)?;
48 if p.x<0. || material.nearest_boundary(p).map_or(true,|(_,d)|d>profile.chord_bound+op.tolerance*0.5) {return Err(invalid("selected turning contour is not on the protected profile"));}
49 }
50 if s.canonical(&tol).iter().any(|c|c.end.y>c.start.y+tol.point_eps) {return Err(invalid("turning contour must run front to back without axial folds"));}
52 }
53 if front-back<=tol.point_eps {return Err(invalid("profile selection has no axial extent"));}
54 if front>stock.base.z+stock.height+tol.point_eps || back<stock.base.z-tol.point_eps || max_x>stock.radius+tol.point_eps {return Err(invalid("selected profile lies outside stock"));}
55 let clearance=op.linking.safe_distance.max(op.tolerance);
56 let min_tool=envelope.vertices.iter().map(|p|p.x-envelope.reference_to_tip.x).fold(f64::INFINITY,f64::min);
57 let max_tool=envelope.vertices.iter().map(|p|p.x-envelope.reference_to_tip.x).fold(f64::NEG_INFINITY,f64::max);
58 let min_z=envelope.vertices.iter().map(|p|p.z-envelope.reference_to_tip.z).fold(f64::INFINITY,f64::min);
59 let clear_x=if params.outside {stock.radius-min_tool+clearance} else {
60 if !input.pilot_radius.is_finite() || input.pilot_radius<=0. || input.pilot_radius>min_x || input.pilot_radius>=stock.radius {return Err(invalid("internal profiling requires a certified existing pilot bore"));}
61 let x=input.pilot_radius-max_tool-clearance;
62 if x+min_tool < -input.pilot_radius+clearance || x<0. {return Err(invalid("turning tool does not fit pilot bore clearance"));}x
63 };
64 let safe_z=stock.base.z+stock.height-min_z+clearance;
65 Ok(Context{path,envelope,feeds,material,contour,nose,clear_x,safe_z,front,back,min_x,max_x,outside:params.outside})
66}
67pub(crate) fn rough_forbidden(c:&Context,op:&Operation,tol:&Tolerance,runner:&mut Runner<'_>)->Result<Region,CamError> {
72 let margin=op.tolerance*0.5;
73 let mut region=c.material.clone();
74 if op.stock_to_leave.radial>0. || op.stock_to_leave.axial>0. {
75 let x=op.stock_to_leave.radial.max(margin);let z=op.stock_to_leave.axial.max(margin);
76 let scaled:Vec<_>=region.loops.iter().map(|l|Loop::polygon(&l.discretize(op.tolerance*0.125).iter().map(|p|Point2::new(p.x/x,p.y/z)).collect::<Vec<_>>())).collect();
77 let count=scaled.iter().map(|l|l.segments.len()).sum::<usize>();
78 runner.work((count*count) as u64,"turn-profile-allowance",0,0)?;
79 let scale_tol=Tolerance::scaled_for_diagonal(region.bounds().1.x.abs().max(region.bounds().1.y.abs())/x.min(z));
80 let expanded=Region::new(scaled,&scale_tol)?.offset(1.,&scale_tol)?;
81 let loops=expanded.loops.iter().map(|l|Loop::polygon(&l.discretize(op.tolerance*0.125/x.max(z)).iter().map(|p|Point2::new(p.x*x,p.y*z)).collect::<Vec<_>>())).collect();
82 region=Region::new(loops,tol)?;
83 }
84 let count=region.loops.iter().map(|l|l.segments.len()).sum::<usize>();
85 if count>4096 {return Err(invalid("turning allowance offset budget exceeded"));}
86 runner.work((count*count) as u64,"turn-profile-nose-offset",0,0)?;
87 let result=region.offset(c.nose+margin,tol)?;runner.token().check()?;Ok(result)
88}