use crate::{Bounds3, CamError, Contact, ContactType, Cutter, CutterProfile, MeshSnapshot,
Point3, ProjectionMode, Runner, Tolerance, TriangleIndex};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Direction { X, Y }
#[derive(Clone, Debug, PartialEq)]
pub struct Interval {
pub lower: f64,
pub upper: f64,
pub lower_contact: Contact,
pub upper_contact: Contact,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Fiber {
pub direction: Direction,
pub start: Point3,
pub end: Point3,
pub intervals: Vec<Interval>,
}
impl Fiber {
pub fn new(direction: Direction, start: Point3, end: Point3) -> Self {
Self { direction, start, end, intervals: Vec::new() }
}
pub fn coordinate(&self) -> f64 {
match self.direction { Direction::X => self.start.y, Direction::Y => self.start.x }
}
}
pub struct PushContext<'a> {
pub mesh: &'a MeshSnapshot,
pub cutter: &'a Cutter,
pub index: Option<&'a dyn TriangleIndex>,
pub tolerance: Tolerance,
pub allowance: f64,
}
fn invalid(message: &str) -> CamError { CamError::InvalidInput(message.into()) }
impl PushContext<'_> {
pub fn validate(&self) -> Result<(), CamError> {
self.cutter.validate()?;
self.tolerance.validate()?;
if !self.allowance.is_finite() || self.allowance < 0.0 { return Err(invalid("push allowance must be finite and non-negative")); }
if self.mesh.triangles.iter().any(|t| t.face as usize >= self.mesh.faces.len() || !t.bounds.is_finite() || !t.normal.is_finite() || t.vertices().iter().any(|p| !p.is_finite())) {
return Err(invalid("push mesh contains invalid coordinates or face references"));
}
Ok(())
}
pub fn fiber(&self, input: &Fiber, runner: &mut Runner<'_>) -> Result<Fiber, CamError> {
self.validate()?;
let d = input.end.sub(input.start);
let axial = match input.direction { Direction::X => d.y == 0.0 && d.x > 0.0, Direction::Y => d.x == 0.0 && d.y > 0.0 };
if !axial || d.z != 0.0 || !input.start.is_finite() || !input.end.is_finite() {
return Err(invalid("fiber must have finite increasing axis coordinates and constant Z"));
}
let intervals = self.segment(input.start, input.end, runner)?;
Ok(Fiber { intervals, ..input.clone() })
}
pub fn segment(&self, start: Point3, end: Point3, runner: &mut Runner<'_>) -> Result<Vec<Interval>, CamError> {
runner.token().check()?;
self.validate()?;
if !start.is_finite() || !end.is_finite() || start.z != end.z { return Err(invalid("sweep requires a finite horizontal segment")); }
let length = start.distance_xy(end);
if length <= self.tolerance.distance_eps { return Err(invalid("sweep segment is too short")); }
let u = end.sub(start).scale(1.0 / length);
let v = Point3::new(-u.y, u.x, 0.0);
let radius = self.cutter.radius().max(self.cutter.shaft_radius()) + self.allowance;
let bounds = Bounds3::new(Point3::new(start.x.min(end.x)-radius, start.y.min(end.y)-radius, start.z),
Point3::new(start.x.max(end.x)+radius, start.y.max(end.y)+radius, start.z+self.cutter.total_height()));
let mode = if u.y == 0.0 { ProjectionMode::Yz } else if u.x == 0.0 { ProjectionMode::Xz } else { ProjectionMode::Xyz };
let query_bounds=if mode==ProjectionMode::Xyz {
Bounds3::new(Point3::new(self.mesh.bounds.min.x,self.mesh.bounds.min.y,start.z),
Point3::new(self.mesh.bounds.max.x,self.mesh.bounds.max.y,bounds.max.z))
} else {bounds};
let candidates = match self.index {
Some(index) => index.query(&query_bounds, mode),
None => (0..self.mesh.triangle_count() as u32).collect(),
};
let mut result = Vec::new();
let mut crossings: std::collections::BTreeMap<String, Vec<(f64, i32, u32)>> = std::collections::BTreeMap::new();
let mut solids: Vec<(u32, [Point3; 3])> = Vec::with_capacity(candidates.len());
for (n, id) in candidates.iter().copied().enumerate() {
runner.work(1, "push-triangles", n as u64, candidates.len() as u64)?;
let t = self.mesh.triangles.get(id as usize).ok_or_else(|| invalid("push index contains an invalid triangle id"))?;
if !mode.overlaps(&query_bounds, &t.bounds) { continue; }
let local = t.vertices().map(|p| { let d = p.sub(start); Point3::new(d.dot(u), d.dot(v), d.z) });
let face = &self.mesh.faces[t.face as usize];
let cross = section_above(local, 0.0);
for pair in cross.windows(2) {
let (a,b) = (pair[0],pair[1]);
if (a.y <= 0.0 && b.y > 0.0) || (b.y <= 0.0 && a.y > 0.0) {
let x = a.x+(b.x-a.x)*(-a.y)/(b.y-a.y);
let normal = t.normal.dot(u);
if normal != 0.0 { crossings.entry(face.solid.clone()).or_default().push((x, if normal < 0.0 { 1 } else { -1 }, id)); }
break;
}
}
if mode.overlaps(&bounds, &t.bounds) { solids.push((id, local)); }
}
let mut material = Vec::new();
for (_, mut events) in crossings {
events.sort_by(|a,b| a.0.total_cmp(&b.0).then(a.2.cmp(&b.2)));
let mut winding = 0i32;
let mut entry = 0.0;
let mut entry_id = 0;
for (x, change, id) in events {
if winding == 0 { entry=x; entry_id=id; }
winding += change;
if winding == 0 && x > entry {
let lower=(entry/length).max(0.0); let upper=(x/length).min(1.0);
if upper > lower {
material.push(Interval { lower, upper,
lower_contact: Contact { triangle_id: Some(entry_id), contact_type: ContactType::Facet, ..Contact::default() },
upper_contact: Contact { triangle_id: Some(id), contact_type: ContactType::Facet, ..Contact::default() } });
}
}
}
if winding != 0 { return Err(invalid("push cross-section is open or inconsistently oriented")); }
}
let mut union: std::collections::BTreeMap<u64, (f64, f64)> = std::collections::BTreeMap::new();
let key = |x: f64| x.to_bits() ^ if x.is_sign_negative() { u64::MAX } else { 1u64 << 63 };
let add = |union: &mut std::collections::BTreeMap<u64, (f64, f64)>, lo: f64, hi: f64| {
let (mut lo, mut hi) = (lo, hi);
let overlapping: Vec<u64> = union.range(..=key(hi)).rev().take_while(|(_, p)| p.1 >= lo).map(|(k, _)| *k).collect();
for k in overlapping {
let p = union.remove(&k).unwrap();
lo = lo.min(p.0);
hi = hi.max(p.1);
}
union.insert(key(lo), (lo, hi));
};
for m in &material { add(&mut union, m.lower, m.upper); }
let pad = radius + 2.0 * self.tolerance.distance_eps;
let mut order: Vec<(f64, usize)> = solids.iter().enumerate().map(|(i, (_, local))| {
let ymin = local.iter().map(|p| p.y).fold(f64::INFINITY, f64::min);
let ymax = local.iter().map(|p| p.y).fold(f64::NEG_INFINITY, f64::max);
(if ymin > 0.0 { ymin } else if ymax < 0.0 { -ymax } else { 0.0 }, i)
}).collect();
order.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
for (_, i) in order {
let (id, local) = solids[i];
let xmin = local.iter().map(|p| p.x).fold(f64::INFINITY, f64::min);
let xmax = local.iter().map(|p| p.x).fold(f64::NEG_INFINITY, f64::max);
let (blo, bhi) = ((xmin - pad) / length, (xmax + pad) / length);
if let Some((_, piece)) = union.range(..key(blo)).next_back() {
if piece.0 < blo && bhi < piece.1 { continue; }
}
let solved = triangle_interval(local, self.cutter, self.allowance, self.tolerance, runner)?;
if let Some((lo, hi, lower_point, upper_point)) = solved {
let lo = (lo / length).max(0.0);
let hi = (hi / length).min(1.0);
if hi > lo {
add(&mut union, lo, hi);
let contact = Contact { contact_type: ContactType::NumericFallback, triangle_id: Some(id),
detail: Some("bounded convex slice support solve (profile, facet and edge)".into()), ..Contact::default() };
let world = |p:Point3| start.add(u.scale(p.x)).add(v.scale(p.y)).add(Point3::new(0.0,0.0,p.z));
let mut lower_contact=contact.clone(); lower_contact.point=Some(world(lower_point));
let mut upper_contact=contact; upper_contact.point=Some(world(upper_point));
result.push(Interval { lower: lo, upper: hi, lower_contact, upper_contact });
}
}
}
result.extend(material);
merge(&mut result, self.tolerance.distance_eps / length);
Ok(result)
}
}
pub fn merge(intervals: &mut Vec<Interval>, epsilon: f64) {
intervals.sort_by(|a,b| a.lower.total_cmp(&b.lower).then(a.upper.total_cmp(&b.upper))
.then(a.lower_contact.triangle_id.cmp(&b.lower_contact.triangle_id)));
let mut merged: Vec<Interval> = Vec::new();
for item in intervals.drain(..) {
if let Some(last) = merged.last_mut() {
if item.lower <= last.upper + epsilon {
if item.upper > last.upper { last.upper = item.upper; last.upper_contact = item.upper_contact; }
continue;
}
}
merged.push(item);
}
*intervals = merged;
}
pub fn batch(context: &PushContext<'_>, direction: Direction, fibers: &[Fiber], runner: &mut Runner<'_>) -> Result<Vec<Fiber>, CamError> {
context.validate()?;
if fibers.iter().any(|f| f.direction != direction) { return Err(invalid("batch fibers have mixed directions")); }
runner.stage("push-batch", "Push fiber batch", 0, fibers.len() as u64)?;
#[cfg(not(feature = "parallel"))]
let output = {
let mut output = Vec::with_capacity(fibers.len());
for (i, fiber) in fibers.iter().enumerate() {
output.push(context.fiber(fiber, runner)?);
runner.work(1, "push-fibers", (i+1) as u64, fibers.len() as u64)?;
}
output
};
#[cfg(feature = "parallel")]
let output = crate::par::map_ordered(fibers, runner, "push-fibers", |fiber, worker| context.fiber(fiber, worker))?
.into_iter()
.collect::<Result<Vec<Fiber>, CamError>>()?;
runner.stage("push-complete", "Fiber batch complete", output.len() as u64, fibers.len() as u64)?;
Ok(output)
}
fn section(t: [Point3; 3], z: f64) -> Vec<Point3> {
let mut p = Vec::new();
for (a,b) in [(t[0],t[1]), (t[1],t[2]), (t[2],t[0])] {
if a.z == z { p.push(a); }
if (a.z < z && b.z > z) || (a.z > z && b.z < z) { p.push(a.lerp(b, (z-a.z)/(b.z-a.z))); }
}
p
}
fn section_above(t: [Point3; 3], z: f64) -> Vec<Point3> {
let mut p = Vec::new();
for (a,b) in [(t[0],t[1]), (t[1],t[2]), (t[2],t[0])] {
if (a.z <= z) != (b.z <= z) { p.push(if a.z == z { a } else if b.z == z { b } else { a.lerp(b, (z-a.z)/(b.z-a.z)) }); }
}
p
}
fn capsule(a: Point3, b: Point3, radius: f64, range: &mut [f64; 2]) {
for p in [a,b] {
if p.y.abs() <= radius {
let dx = ((radius-p.y.abs())*(radius+p.y.abs())).max(0.0).sqrt();
range[0] = range[0].min(p.x-dx); range[1] = range[1].max(p.x+dx);
}
}
let d = b.sub(a); let length = d.x.hypot(d.y);
if length == 0.0 || d.y == 0.0 { return; }
for sign in [-1.0,1.0] {
let x = a.x-sign*radius*d.y/length; let y = a.y+sign*radius*d.x/length;
let s = -y/d.y;
if (0.0..=1.0).contains(&s) {
let value = x+s*d.x; range[0] = range[0].min(value); range[1] = range[1].max(value);
}
}
}
fn slice(t: [Point3; 3], z: f64, cutter: &Cutter, allowance: f64) -> (f64, [f64; 2]) {
let points = section(t,z);
let radius = cutter.max_radius_at_height(z).unwrap_or(0.0)+allowance;
let min_y = points.iter().map(|p| p.y).fold(f64::INFINITY,f64::min);
let max_y = points.iter().map(|p| p.y).fold(f64::NEG_INFINITY,f64::max);
let separation = if min_y > 0.0 { min_y } else if max_y < 0.0 { -max_y } else { 0.0 };
let mut range = [f64::INFINITY,f64::NEG_INFINITY];
for a in &points { for b in &points { capsule(*a,*b,radius,&mut range); } }
(radius-separation, range)
}
fn slice_contact(t:[Point3;3],z:f64,cutter:&Cutter,allowance:f64,right:bool)->Point3 {
let points=section(t,z);let radius=cutter.max_radius_at_height(z).unwrap_or(0.0)+allowance;
let mut best=if right {f64::NEG_INFINITY}else{f64::INFINITY};let mut witness=points[0];
let mut accept=|x:f64,p:Point3| {if (right && x>best)||(!right && x<best) {best=x;witness=p;}};
for a in &points {
if a.y.abs()<=radius {let dx=((radius-a.y.abs())*(radius+a.y.abs())).max(0.0).sqrt();accept(a.x+if right {dx}else{-dx},*a);}
for b in &points {
let d=b.sub(*a);let length=d.x.hypot(d.y);if length==0.0 || d.y==0.0 {continue;}
for sign in [-1.0,1.0] {
let x=a.x-sign*radius*d.y/length;let y=a.y+sign*radius*d.x/length;let s=-y/d.y;
if (0.0..=1.0).contains(&s) {accept(x+s*d.x,a.lerp(*b,s));}
}
}
}
witness
}
fn maximum(mut a: f64, mut b: f64, f: impl Fn(f64)->f64) -> (f64,f64) {
let ratio = (5.0_f64.sqrt()-1.0)*0.5;
let mut c = b-ratio*(b-a); let mut d = a+ratio*(b-a);
let mut fc = f(c); let mut fd = f(d);
for _ in 0..80 {
if fc < fd { a=c; c=d; fc=fd; d=a+ratio*(b-a); fd=f(d); }
else { b=d; d=c; fd=fc; c=b-ratio*(b-a); fc=f(c); }
}
[(a,f(a)),(b,f(b)),(c,fc),(d,fd)].into_iter().max_by(|a,b| a.1.total_cmp(&b.1)).unwrap()
}
fn triangle_interval(t: [Point3; 3], cutter: &Cutter, allowance: f64, tol: Tolerance, runner: &mut Runner<'_>) -> Result<Option<(f64,f64,Point3,Point3)>,CamError> {
let lo = t.iter().map(|p| p.z).fold(f64::INFINITY,f64::min).max(0.0);
let hi = t.iter().map(|p| p.z).fold(f64::NEG_INFINITY,f64::max).min(cutter.total_height());
if hi < lo { return Ok(None); }
let mut levels = vec![lo,hi];
levels.extend(t.iter().map(|p| p.z).filter(|z| *z>lo && *z<hi));
for s in cutter.segments() { levels.extend(s.height_range.into_iter().filter(|z| *z>lo && *z<hi)); }
levels.push(cutter.cutting_length().clamp(lo,hi));
levels.sort_by(f64::total_cmp); levels.dedup();
let mut range = [f64::INFINITY,f64::NEG_INFINITY];
let mut support_z=[lo,lo];
for z in &levels {
let (_, r) = slice(t,*z,cutter,allowance);
if r[0]<range[0] {range[0]=r[0];support_z[0]=*z;}
if r[1]>range[1] {range[1]=r[1];support_z[1]=*z;}
}
for ab in levels.windows(2) {
runner.work(1,"push-solve",0,0)?;
let start = if ab[0] == cutter.cutting_length() && cutter.shaft_radius() != cutter.radius() { ab[0].next_up() } else { ab[0] };
if start > ab[1] { continue; }
let clear = |z| slice(t,z,cutter,allowance).0;
let (peak, feasible) = maximum(start,ab[1],clear);
if feasible < 0.0 { continue; }
let mut lower=start; let mut upper=ab[1];
if clear(lower)<0.0 {
let mut inside=peak;
for _ in 0..64 { let mid=(lower+inside)*0.5; if clear(mid)<0.0 { lower=mid; } else { inside=mid; } }
lower=inside;
}
if clear(upper)<0.0 {
let mut inside=peak;
for _ in 0..64 { let mid=(upper+inside)*0.5; if clear(mid)<0.0 { upper=mid; } else { inside=mid; } }
upper=inside;
}
let (left_z, left)=maximum(lower,upper,|z| -slice(t,z,cutter,allowance).1[0]);
let (right_z, right)=maximum(lower,upper,|z| slice(t,z,cutter,allowance).1[1]);
if -left<range[0] {range[0]=-left;support_z[0]=left_z;}
if right>range[1] {range[1]=right;support_z[1]=right_z;}
}
if range[0].is_finite() && range[1].is_finite() {
Ok(Some((range[0]-tol.distance_eps,range[1]+tol.distance_eps,
slice_contact(t,support_z[0],cutter,allowance,false),slice_contact(t,support_z[1],cutter,allowance,true))))
} else { Ok(None) }
}