113 lines
4.2 KiB
Rust
113 lines
4.2 KiB
Rust
//! Independent direction checks for a right-handed cutter with M3 spindle rotation.
|
|
//! A climb cut keeps the material being finished to the right of tool travel.
|
|
use solidcraft_cam::{Boundary, JobInput, Strategy, corpus, generate, nc};
|
|
|
|
fn segments(input: &JobInput) -> Vec<nc::Segment> {
|
|
let path = generate::generate(input.clone()).unwrap();
|
|
let parsed = nc::round_trip(&path, &input.setup, &input.tools).unwrap();
|
|
nc::segments(&parsed, &input.setup).unwrap()
|
|
}
|
|
|
|
fn square(lo: f64, hi: f64, reversed: bool) -> Vec<[f64; 2]> {
|
|
let mut ring = vec![[lo, lo], [hi, lo], [hi, hi], [lo, hi]];
|
|
if reversed {
|
|
ring.reverse();
|
|
}
|
|
ring
|
|
}
|
|
|
|
// Collect continuous constant-depth cuts from the independently interpreted NC.
|
|
// Signed polygon area checks orientation without calling the offset adapter.
|
|
fn loops(input: &JobInput) -> Vec<Vec<nc::Segment>> {
|
|
let mut result = Vec::new();
|
|
let mut current = Vec::new();
|
|
for s in segments(input) {
|
|
if !s.rapid && (s.from.z - 5.).abs() < 1e-8 && (s.to.z - 5.).abs() < 1e-8 {
|
|
current.push(s);
|
|
} else if !current.is_empty() {
|
|
result.push(std::mem::take(&mut current));
|
|
}
|
|
}
|
|
if !current.is_empty() {
|
|
result.push(current);
|
|
}
|
|
result
|
|
}
|
|
|
|
fn ccw(path: &[nc::Segment]) -> bool {
|
|
let origin = path[0].from;
|
|
assert!((path.last().unwrap().to - origin).len() < 1e-5, "cut must close");
|
|
let area = path
|
|
.iter()
|
|
.map(|s| {
|
|
let a = s.from - origin;
|
|
let b = s.to - origin;
|
|
a.x * b.y - b.x * a.y
|
|
})
|
|
.sum::<f64>();
|
|
assert!(area.abs() > 1.);
|
|
area > 0.
|
|
}
|
|
|
|
#[test]
|
|
fn facing_direction_matches_uncut_side_at_every_depth() {
|
|
for climb in [false, true] {
|
|
let mut input = corpus::fixture("face").unwrap();
|
|
input.operations[0].climb = climb;
|
|
input.operations[0].stepdown = 0.5;
|
|
let cuts: Vec<_> = segments(&input).into_iter().filter(|s| !s.rapid && (s.from.z - s.to.z).abs() < 1e-8 && s.to.z < 6.).collect();
|
|
assert_eq!(cuts.len(), 22);
|
|
for depth in cuts.as_chunks::<11>().0 {
|
|
assert!(depth.windows(2).all(|pair| pair[1].from.y > pair[0].from.y));
|
|
// Advancing rows toward +Y leaves uncut material north of the tool.
|
|
// Right of a -X move is +Y; right of a +X move is -Y.
|
|
assert!(depth.iter().all(|s| (s.to.x < s.from.x) == climb));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn contour_direction_matches_inside_and_outside_material() {
|
|
for reversed in [false, true] {
|
|
for inside in [false, true] {
|
|
for climb in [false, true] {
|
|
let mut input = corpus::fixture("face").unwrap();
|
|
input.tools[0].diameter = 2.;
|
|
input.operations[0].climb = climb;
|
|
input.operations[0].geometry = Strategy::Contour { boundary: Boundary { outer: square(4., 16., reversed), islands: vec![] }, inside };
|
|
let paths = loops(&input);
|
|
assert_eq!(paths.len(), 1);
|
|
// Pocket wall: material outside => CCW climb. Boss: inside => CW climb.
|
|
assert_eq!(ccw(&paths[0]), inside == climb, "inside={inside}, climb={climb}, reversed={reversed}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pocket_direction_preserves_opposite_outer_and_island_winding() {
|
|
for reversed in [false, true] {
|
|
for climb in [false, true] {
|
|
let mut input = corpus::fixture("face").unwrap();
|
|
input.tools[0].diameter = 2.;
|
|
input.operations[0].climb = climb;
|
|
input.operations[0].geometry =
|
|
Strategy::Pocket { boundary: Boundary { outer: square(0., 20., reversed), islands: vec![square(4., 16., reversed)] } };
|
|
let paths = loops(&input);
|
|
assert_eq!(paths.len(), 2);
|
|
let mut outer = 0;
|
|
let mut island = 0;
|
|
for path in &paths {
|
|
let is_outer = path.iter().any(|s| s.from.x < 2.);
|
|
if is_outer {
|
|
outer += 1;
|
|
} else {
|
|
island += 1;
|
|
}
|
|
assert_eq!(ccw(path), is_outer == climb, "outer={is_outer}, climb={climb}, reversed={reversed}");
|
|
}
|
|
assert_eq!((outer, island), (1, 1));
|
|
}
|
|
}
|
|
}
|