use crate::model::{ApproximationLimit, Motion, MotionKind, Point3, Toolpath};
use crate::CamError;
#[derive(Clone, Debug, PartialEq)]
pub struct Filtered {
pub points: Vec<Point3>,
pub kept: Vec<usize>,
pub removed: usize,
}
pub fn distance_to_chord(p: Point3, a: Point3, b: Point3) -> f64 {
let d = b.sub(a);
let len2 = d.length_squared();
if len2 <= 0.0 {
return p.distance(a);
}
let t = (p.sub(a).dot(d) / len2).clamp(0.0, 1.0);
p.distance(a.add(d.scale(t)))
}
pub fn filter_points(points: &[Point3], tolerance: f64, preserve: &[usize]) -> Result<Filtered, CamError> {
if !tolerance.is_finite() || tolerance < 0.0 {
return Err(CamError::InvalidInput("filter tolerance must be finite and non-negative".into()));
}
if let Some(p) = points.iter().find(|p| !p.is_finite()) {
return Err(CamError::NonFinite { what: format!("filter point {p:?}") });
}
if let Some(&i) = preserve.iter().find(|&&i| i >= points.len()) {
return Err(CamError::InvalidInput(format!("preserved index {i} is outside the polyline")));
}
let n = points.len();
if n < 3 {
return Ok(Filtered { points: points.to_vec(), kept: (0..n).collect(), removed: 0 });
}
let mut keep = vec![false; n];
keep[0] = true;
keep[n - 1] = true;
for &i in preserve {
keep[i] = true;
}
let mut kept = vec![0usize];
let mut anchor = 0usize;
while anchor + 1 < n {
let mut end = anchor + 1;
while end + 1 < n && !keep[end] {
let candidate = end + 1;
let ok = (anchor + 1..candidate)
.all(|k| distance_to_chord(points[k], points[anchor], points[candidate]) <= tolerance);
if !ok {
break;
}
end = candidate;
}
kept.push(end);
anchor = end;
}
let out: Vec<Point3> = kept.iter().map(|&i| points[i]).collect();
let removed = n - out.len();
Ok(Filtered { points: out, kept, removed })
}
fn is_straight(kind: &MotionKind) -> bool {
matches!(
kind,
MotionKind::Rapid { .. }
| MotionKind::Plunge { .. }
| MotionKind::Cut { .. }
| MotionKind::Retract { .. }
| MotionKind::LeadIn { .. }
| MotionKind::LeadOut { .. }
| MotionKind::Link { .. }
)
}
fn same_run(a: &Motion, b: &Motion) -> bool {
is_straight(&a.kind)
&& is_straight(&b.kind)
&& a.kind.kind_name() == b.kind.kind_name()
&& a.feed == b.feed
&& a.path_id == b.path_id
&& a.tool_axis == b.tool_axis
}
fn with_end(template: &Motion, to: Point3) -> Motion {
let kind = match &template.kind {
MotionKind::Rapid { .. } => MotionKind::Rapid { to },
MotionKind::Plunge { .. } => MotionKind::Plunge { to },
MotionKind::Cut { .. } => MotionKind::Cut { to },
MotionKind::Retract { .. } => MotionKind::Retract { to },
MotionKind::LeadIn { .. } => MotionKind::LeadIn { to },
MotionKind::LeadOut { .. } => MotionKind::LeadOut { to },
MotionKind::Link { .. } => MotionKind::Link { to },
other => other.clone(),
};
Motion { kind, feed: template.feed, tool_axis: template.tool_axis, path_id: template.path_id }
}
pub fn filter_motions(motions: &[Motion], tolerance: f64) -> Result<(Vec<Motion>, usize), CamError> {
let mut out: Vec<Motion> = Vec::with_capacity(motions.len());
let mut removed = 0usize;
let mut position: Option<Point3> = None;
let mut i = 0usize;
while i < motions.len() {
let m = &motions[i];
if !is_straight(&m.kind) {
out.push(m.clone());
if let Some(p) = m.end() {
position = Some(p);
}
i += 1;
continue;
}
let mut j = i + 1;
while j < motions.len() && same_run(&motions[j - 1], &motions[j]) {
j += 1;
}
let run = &motions[i..j];
let ends: Vec<Point3> = run.iter().map(|m| m.end().expect("straight motions end somewhere")).collect();
match position {
Some(start) if run.len() >= 2 => {
let mut points = Vec::with_capacity(ends.len() + 1);
points.push(start);
points.extend(ends.iter().copied());
let filtered = filter_points(&points, tolerance, &[])?;
for &k in filtered.kept.iter().skip(1) {
out.push(with_end(&run[k - 1], points[k]));
}
removed += filtered.removed;
}
_ => {
if run.len() >= 3 {
let filtered = filter_points(&ends, tolerance, &[])?;
for &k in &filtered.kept {
out.push(with_end(&run[k], ends[k]));
}
removed += filtered.removed;
} else {
out.extend(run.iter().cloned());
}
}
}
position = ends.last().copied();
i = j;
}
Ok((out, removed))
}
pub fn filter_toolpath(path: &Toolpath, tolerance: f64) -> Result<Toolpath, CamError> {
let (motions, removed) = filter_motions(&path.motions, tolerance)?;
let mut out = path.clone();
out.motions = motions;
out.report.counts.motions = out.motions.len() as u64;
if removed > 0 {
out.report.approximations.push(ApproximationLimit {
kind: "line-filter".into(),
bound: Some(tolerance),
detail: Some(format!("{removed} redundant points removed within {tolerance}")),
});
}
Ok(out)
}