brep_cam/pattern/
transform.rs1use crate::{CamError,Point3,Runner};
2use crate::model::{RigidTransform,Toolpath,MotionKind,ArcPlane};
3use crate::link::Pass;
4use crate::multiaxis::indexed::linearize_arc;
5fn invalid(s:&str)->CamError {CamError::InvalidInput(format!("pattern: {s}"))}
6#[derive(Clone,Debug)]
9pub enum Placement { Rigid(RigidTransform), Reflection{origin:Point3,normal:Point3} }
10impl Placement {
11 pub fn rigid(t:RigidTransform)->Result<Self,CamError> {
12 t.validate()?;
13 if t.apply_vector(Point3::Z).distance(Point3::Z)>1e-10 {return Err(invalid("only +Z-preserving placements are supported; use indexed generation for tilted instances"));}
14 Ok(Self::Rigid(t))
15 }
16 pub fn mirror(origin:Point3,normal:Point3)->Result<Self,CamError> {
17 let n=normal.normalized().ok_or_else(||invalid("mirror normal is zero or invalid"))?;
18 if !origin.is_finite() || n.z.abs()>1e-10 {return Err(invalid("mirror plane must be vertical and finite"));}
19 Ok(Self::Reflection{origin,normal:n})
20 }
21 pub fn point(&self,p:Point3)->Point3 {match self {
22 Self::Rigid(t)=>t.apply_point(p),Self::Reflection{origin,normal}=>p.sub(normal.scale(2.*p.sub(*origin).dot(*normal))),
23 }}
24 pub fn reverses(&self)->bool {matches!(self,Self::Reflection{..})}
25}
26pub fn transform_passes(path:&Toolpath,placement:&Placement,chord:f64,runner:&mut Runner<'_>)->Result<Vec<Pass>,CamError> {
32 runner.token().check()?;
33 if !path.report.is_usable() || !chord.is_finite() || chord<=0. || path.motions.len()>200_000 {return Err(invalid("unusable path or invalid transform work budget"));}
34 let placement=match placement {Placement::Rigid(t)=>Placement::rigid(t.clone())?,Placement::Reflection{origin,normal}=>Placement::mirror(*origin,*normal)?};
35 let mut out=Vec::new();let mut current:Option<Pass>=None;let mut at:Option<Point3>=None;let mut count=0usize;
36 for m in &path.motions {
37 runner.work(1,"pattern-transform",count as u64,0)?;
38 if !m.kind.is_finite() || m.tool_axis.is_some_and(|a|a.distance(Point3::Z)>1e-10) {return Err(invalid("requires finite +Z source motions"));}
39 let points=match &m.kind {
40 MotionKind::Cut{to}=>Some(vec![*to]),
41 MotionKind::ArcCw{to,center,plane,pitch}|MotionKind::ArcCcw{to,center,plane,pitch}=>{
42 if *plane!=ArcPlane::Xy || *pitch!=0. {return Err(invalid("pattern requires planar XY arcs; helical/indexed arcs must be explicitly expanded"));}
43 let start=at.ok_or_else(||invalid("arc has no start"))?;
44 let radius=start.distance(*center);
45 if radius<=0. || (to.distance(*center)-radius).abs()>1e-9*radius.max(1.) || (start.z-to.z).abs()>1e-9 || (start.z-center.z).abs()>1e-9 {return Err(invalid("source arc has inconsistent radius or plane"));}
46 let pts=linearize_arc(start,*to,*center,*plane,*pitch,matches!(m.kind,MotionKind::ArcCw{..}),chord);
47 let r=start.distance(*center);
49 if pts.is_empty() || pts.windows(2).any(|w|w[0].lerp(w[1],0.5).distance(*center)<r-chord-1e-9) || start.lerp(pts[0],0.5).distance(*center)<r-chord-1e-9 {return Err(invalid("arc chord budget cannot meet tolerance"));}
50 Some(pts)
51 }
52 MotionKind::Rapid{..}|MotionKind::Plunge{..}|MotionKind::Retract{..}|MotionKind::Link{..}|MotionKind::LeadIn{..}|MotionKind::LeadOut{..}=>None,
53 _=>return Err(invalid("source contains unsupported control/cycle/rotary/manual motions")),
54 };
55 if let Some(points)=points {
56 let start=at.ok_or_else(||invalid("cut has no start"))?;
57 if current.as_ref().is_some_and(|p|p.path_id!=m.path_id) {out.push(current.take().unwrap());}
58 let p=current.get_or_insert_with(||Pass::new(vec![placement.point(start)],m.path_id));
59 count+=points.len();if count>200_000 {return Err(invalid("transformed point budget exceeded"));}
60 p.points.extend(points.into_iter().map(|p|placement.point(p)));
61 } else if let Some(p)=current.take() {out.push(p);}
62 if let Some(p)=m.end(){at=Some(p);}
63 }
64 if let Some(p)=current {out.push(p);}
65 if out.is_empty(){return Err(invalid("source has no cut passes"));}
66 if placement.reverses(){for p in &mut out {p.points.reverse();}}
67 runner.token().check()?;Ok(out)
68}