use super::*;
#[test]
fn closed_loop_self_intersects_detects_bowtie() {
let square = [
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(1.0, 1.0),
Point2::new(0.0, 1.0),
];
assert!(!closed_loop_self_intersects(&square));
let bowtie = [
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(0.0, 1.0),
Point2::new(1.0, 1.0),
];
assert!(closed_loop_self_intersects(&bowtie));
assert!(!closed_loop_self_intersects(&square[..3]));
}
fn annular_sector_loop(radius: f64, thickness: f64, span: f64, seg: usize) -> Vec<Point2<f64>> {
let c = Point2::new(0.0, -radius);
let mut loop_ = Vec::with_capacity(2 * seg + 2);
for i in 0..=seg {
let a = -span / 2.0 + span * (i as f64 / seg as f64);
loop_.push(Point2::new(c.x + radius * a.cos(), c.y + radius * a.sin()));
}
let inner = radius - thickness;
for i in 0..=seg {
let a = span / 2.0 - span * (i as f64 / seg as f64);
loop_.push(Point2::new(c.x + inner * a.cos(), c.y + inner * a.sin()));
}
loop_
}
fn loop_area(loop_: &[Point2<f64>]) -> f64 {
let n = loop_.len();
let mut a = 0.0;
for i in 0..n {
let p = loop_[i];
let q = loop_[(i + 1) % n];
a += p.x * q.y - q.x * p.y;
}
(a * 0.5).abs()
}
#[test]
fn simplify_thin_annular_sector_stays_simple_and_area_preserving() {
let cases = [
(12000.0, 100.0, 240.0_f64),
(12000.0, 300.0, 240.0),
(12000.0, 600.0, 90.0),
(12000.0, 600.0, 180.0),
];
for (radius, thickness, span_deg) in cases {
let span = span_deg.to_radians();
let dense = annular_sector_loop(radius, thickness, span, 200);
assert!(
!closed_loop_self_intersects(&dense),
"input sector r={radius} t={thickness} {span_deg}° should start simple",
);
let out = simplify_smooth_curve_polyline(&dense, 1.0);
assert!(
!closed_loop_self_intersects(&out),
"simplified sector r={radius} t={thickness} {span_deg}° self-intersects",
);
let (a_in, a_out) = (loop_area(&dense), loop_area(&out));
let rel = (a_out - a_in).abs() / a_in;
assert!(
rel < 0.02,
"sector r={radius} t={thickness} {span_deg}°: area drift {:.1}% \
(in={a_in:.0} out={a_out:.0}) — thin curved wall was distorted",
rel * 100.0,
);
}
}
#[test]
fn simplify_still_reduces_fat_round_disk() {
let mut disk = Vec::new();
let seg = 127;
for i in 0..seg {
let a = std::f64::consts::TAU * (i as f64 / seg as f64);
disk.push(Point2::new(0.5 * a.cos(), 0.5 * a.sin()));
}
let out = simplify_smooth_curve_polyline(&disk, 1.0);
assert!(
out.len() < disk.len() && out.len() >= SIMPLIFIED_MIN_VERTICES,
"round disk should simplify from {} to [{SIMPLIFIED_MIN_VERTICES}, {}) verts, got {}",
disk.len(),
disk.len(),
out.len(),
);
assert!(!closed_loop_self_intersects(&out));
}
fn ellipse_loop(ar: f64, seg: usize) -> Vec<Point2<f64>> {
let (a, b) = (ar * 0.5, 0.5);
(0..seg)
.map(|i| {
let t = std::f64::consts::TAU * (i as f64 / seg as f64);
Point2::new(a * t.cos(), b * t.sin())
})
.collect()
}
#[test]
fn elongated_filled_ellipse_is_not_thin_gated() {
for ar in [6.0_f64, 8.0, 12.0, 20.0] {
assert!(
!loop_doubles_back(&ellipse_loop(ar, 128)),
"AR={ar} filled ellipse is convex — must not read as doubling back",
);
}
let ellipse = ellipse_loop(8.0, 128);
let out = simplify_smooth_curve_polyline(&ellipse, 1.0);
assert!(
out.len() < ellipse.len() && out.len() >= SIMPLIFIED_MIN_VERTICES,
"8:1 ellipse must still simplify (was wrongly thin-gated): {} -> {} verts",
ellipse.len(),
out.len(),
);
assert!(!closed_loop_self_intersects(&out));
let rel = (loop_area(&out) - loop_area(&ellipse)).abs() / loop_area(&ellipse);
assert!(rel < 0.08, "8:1 ellipse area drift {:.1}%", rel * 100.0);
}
#[test]
fn doubles_back_ignores_localized_spikes() {
assert!(loop_doubles_back(&annular_sector_loop(
12000.0,
100.0,
(240.0_f64).to_radians(),
128
)));
let mut e = ellipse_loop(8.0, 128);
assert!(!loop_doubles_back(&e));
let tip = (0..e.len())
.max_by(|&i, &j| e[i].x.abs().partial_cmp(&e[j].x.abs()).unwrap())
.unwrap();
e[tip].x *= 0.75;
assert!(
!loop_doubles_back(&e),
"a single notch on a convex ellipse must not read as doubling back",
);
let mut disk: Vec<Point2<f64>> = (0..128)
.map(|i| {
let t = std::f64::consts::TAU * (i as f64 / 128.0);
Point2::new(t.cos(), t.sin())
})
.collect();
disk.insert(1, Point2::new(disk[0].x * 0.9999, disk[0].y * 0.9999));
assert!(
!loop_doubles_back(&disk),
"a sub-mm seam jog on a convex loop must not read as doubling back",
);
}
#[test]
fn doubles_back_independent_of_per_boundary_sampling_density() {
let centre = Point2::new(0.0, -12000.0);
let (r_out, r_in) = (12000.0, 12000.0 - 10.0);
let span = (90.0_f64).to_radians();
let mut loop_ = Vec::new();
for i in 0..=96 {
let a = -span / 2.0 + span * (i as f64 / 96.0);
loop_.push(Point2::new(
centre.x + r_out * a.cos(),
centre.y + r_out * a.sin(),
));
}
for i in 0..=16 {
let a = span / 2.0 - span * (i as f64 / 16.0);
loop_.push(Point2::new(
centre.x + r_in * a.cos(),
centre.y + r_in * a.sin(),
));
}
assert!(
loop_doubles_back(&loop_),
"a non-uniformly tessellated thin sector must still read as doubling back",
);
let out = simplify_smooth_curve_polyline(&loop_, 1.0);
assert_eq!(
out.len(),
loop_.len(),
"skewed-sampling thin sector must be gated"
);
}
#[test]
fn cap_is_unit_invariant_for_round_profile() {
let seg = 127;
let m: Vec<Point2<f64>> = (0..seg)
.map(|i| {
let a = std::f64::consts::TAU * (i as f64 / seg as f64);
Point2::new(a.cos(), a.sin()) })
.collect();
let mm: Vec<Point2<f64>> = m.iter().map(|p| Point2::new(p.x * 1000.0, p.y * 1000.0)).collect();
let out_m = simplify_smooth_curve_polyline(&m, 1.0);
let out_mm = simplify_smooth_curve_polyline(&mm, 0.001);
assert!(
out_m.len() < m.len(),
"metre-unit 2 m disk must simplify ({} -> {})",
m.len(),
out_m.len()
);
assert_eq!(
out_m.len(),
out_mm.len(),
"physically identical mm profile must simplify identically (m: {}, mm: {})",
out_m.len(),
out_mm.len()
);
for (a, b) in out_m.iter().zip(out_mm.iter()) {
assert!(
(a.x * 1000.0 - b.x).abs() < 1e-6 && (a.y * 1000.0 - b.y).abs() < 1e-6,
"mm output must be the metre output scaled x1000"
);
}
}
#[test]
fn round_profile_still_simplifies_under_mm_units() {
let seg = 127;
let disk_mm: Vec<Point2<f64>> = (0..seg)
.map(|i| {
let a = std::f64::consts::TAU * (i as f64 / seg as f64);
Point2::new(500.0 * a.cos(), 500.0 * a.sin()) })
.collect();
let out = simplify_smooth_curve_polyline(&disk_mm, 0.001);
assert!(
out.len() < disk_mm.len() && out.len() >= SIMPLIFIED_MIN_VERTICES,
"mm-unit round disk should simplify from {} to [{SIMPLIFIED_MIN_VERTICES}, {}), got {}",
disk_mm.len(),
disk_mm.len(),
out.len(),
);
assert!(!closed_loop_self_intersects(&out));
}
#[test]
fn ellipse_simplification_is_unit_invariant() {
let ellipse_m = ellipse_loop(8.0, 128);
let ellipse_mm: Vec<Point2<f64>> =
ellipse_m.iter().map(|p| Point2::new(p.x * 1000.0, p.y * 1000.0)).collect();
let out_m = simplify_smooth_curve_polyline(&ellipse_m, 1.0);
let out_mm = simplify_smooth_curve_polyline(&ellipse_mm, 0.001);
assert!(out_m.len() < ellipse_m.len(), "metre ellipse must simplify");
assert_eq!(
out_m.len(),
out_mm.len(),
"mm-authored ellipse must simplify identically (m: {}, mm: {})",
out_m.len(),
out_mm.len()
);
for (a, b) in out_m.iter().zip(out_mm.iter()) {
assert!(
(a.x * 1000.0 - b.x).abs() < 1e-6 && (a.y * 1000.0 - b.y).abs() < 1e-6,
"mm ellipse output must be the metre output scaled x1000, not merely equal in count"
);
}
}
#[test]
fn thin_profile_gate_holds_under_mm_units() {
let loop_mm = annular_sector_loop(12_500.0, 100.0, 0.3, 64);
let out = simplify_smooth_curve_polyline(&loop_mm, 0.001);
assert_eq!(
out.len(),
loop_mm.len(),
"thin mm-unit annular sector must be gated (kept verbatim)"
);
for (a, b) in out.iter().zip(loop_mm.iter()) {
assert_eq!((a.x, a.y), (b.x, b.y), "gated profile must be kept verbatim");
}
}