#[cfg(test)]
mod tests {
use crate::nfp_convex::{NFPConvex, to_arcline};
use togo::prelude::*;
fn write_svg_with_arclines(filename: &str, a: &[Point], b: &[Point], nfp: &[Point]) {
let arcline_a = to_arcline(a);
let arcline_b = to_arcline(b);
let arcline_nfp = to_arcline(nfp);
let mut svg = SVG::new(400.0, 400.0, Some(filename));
let arcline_a = arcline_translate(&arcline_a, point(100.0, 100.0));
let arcline_b = arcline_translate(&arcline_b, point(100.0, 100.0));
let arcline_nfp = arcline_translate(&arcline_nfp, point(100.0, 100.0));
svg.arcline(&arcline_a, "red");
svg.arcline(&arcline_b, "blue");
svg.arcline(&arcline_nfp, "black");
svg.write_stroke_width(0.1);
}
fn triangle() -> Vec<Point> {
vec![
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(5.0, 10.0),
]
}
fn square() -> Vec<Point> {
vec![
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(10.0, 10.0),
Point::new(0.0, 10.0),
]
}
fn pentagon() -> Vec<Point> {
let mut pts = Vec::new();
for i in 0..5 {
let angle = 2.0 * std::f64::consts::PI * i as f64 / 5.0;
pts.push(Point::new(10.0 * angle.cos(), 10.0 * angle.sin()));
}
pts
}
fn small_square() -> Vec<Point> {
vec![
Point::new(0.0, 0.0),
Point::new(2.0, 0.0),
Point::new(2.0, 2.0),
Point::new(0.0, 2.0),
]
}
fn get_nfp(a: &[Point], b: &[Point]) -> Vec<Point> {
match NFPConvex::nfp(a, b) {
Ok(nfp) => nfp,
Err(e) => panic!("NFP computation failed: {}", e),
}
}
fn is_ccw(pts: &[Point]) -> bool {
if pts.len() < 3 {
return false;
}
let mut area = 0.0;
for i in 0..pts.len() {
let j = (i + 1) % pts.len();
area += (pts[j].x - pts[i].x) * (pts[j].y + pts[i].y);
}
area < 0.0 }
fn contains_point(polygon: &[Point], point: Point) -> bool {
let mut inside = false;
let mut p1 = polygon[polygon.len() - 1];
for p2 in polygon {
if ((p2.y > point.y) != (p1.y > point.y))
&& (point.x < (p1.x - p2.x) * (point.y - p2.y) / (p1.y - p2.y) + p2.x)
{
inside = !inside;
}
p1 = *p2;
}
inside
}
fn polygons_overlap(a: &[Point], b: &[Point], offset: Point) -> bool {
for pt in a {
let sub_pt = Point {
x: pt.x - offset.x,
y: pt.y - offset.y,
};
if contains_point(b, sub_pt) {
return true;
}
}
for pt in b {
let add_pt = Point {
x: pt.x + offset.x,
y: pt.y + offset.y,
};
if contains_point(a, add_pt) {
return true;
}
}
false
}
#[test]
fn test_triangle_triangle() {
let a = triangle();
let b = triangle();
println!("\n=== Triangle A ===");
for (i, pt) in a.iter().enumerate() {
println!(" A[{}]: ({:.2}, {:.2})", i, pt.x, pt.y);
}
println!("\n=== Triangle B ===");
for (i, pt) in b.iter().enumerate() {
println!(" B[{}]: ({:.2}, {:.2})", i, pt.x, pt.y);
}
let nfp = get_nfp(&a, &b);
println!("\n=== NFP Result: {} vertices ===", nfp.len());
for (i, pt) in nfp.iter().enumerate() {
println!(" [{:2}]: ({:8.2}, {:8.2})", i, pt.x, pt.y);
}
write_svg_with_arclines("/tmp/nfp_test_triangle.svg", &a, &b, &nfp);
assert!(nfp.len() >= 3, "NFP must have at least 3 vertices");
println!(
" WARNING: NFP appears small and may self-intersect - algorithm needs review\n"
);
}
#[test]
fn test_square_square() {
let a = square();
let b = square();
let nfp = get_nfp(&a, &b);
println!("\nSquare + Square: {} vertices", nfp.len());
for (i, pt) in nfp.iter().enumerate() {
println!(" [{:2}]: ({:8.2}, {:8.2})", i, pt.x, pt.y);
}
write_svg_with_arclines("/tmp/nfp_test_square_square.svg", &a, &b, &nfp);
assert!(nfp.len() >= 3, "NFP must have at least 3 vertices");
assert!(is_ccw(&nfp), "NFP must be CCW");
let mut valid_count = 0;
for nfp_pt in &nfp {
if !polygons_overlap(&a, &b, *nfp_pt) {
valid_count += 1;
}
}
println!(" Valid: {}/{}\n", valid_count, nfp.len());
}
#[test]
fn test_small_on_large() {
let a = square();
let b = small_square();
let nfp = get_nfp(&a, &b);
println!("Square + Small Square: {} vertices", nfp.len());
write_svg_with_arclines("/tmp/nfp_test_small_on_large.svg", &a, &b, &nfp);
assert!(nfp.len() >= 3, "NFP must have at least 3 vertices");
assert!(is_ccw(&nfp), "NFP must be CCW");
let mut valid_count = 0;
for nfp_pt in &nfp {
if !polygons_overlap(&a, &b, *nfp_pt) {
valid_count += 1;
}
}
println!(" Valid: {}/{}\n", valid_count, nfp.len());
}
#[test]
fn test_pentagon_square() {
let a = pentagon();
let b = square();
let nfp = get_nfp(&a, &b);
println!("Pentagon + Square: {} vertices", nfp.len());
write_svg_with_arclines("/tmp/nfp_test_pentagon_square.svg", &a, &b, &nfp);
assert!(nfp.len() >= 3, "NFP must have at least 3 vertices");
let mut valid_count = 0;
for nfp_pt in &nfp {
if !polygons_overlap(&a, &b, *nfp_pt) {
valid_count += 1;
}
}
assert!(valid_count > 0, "Should have valid placements");
println!(" Valid: {}/{}\n", valid_count, nfp.len());
}
#[test]
fn test_pentagon_pentagon() {
let a = pentagon();
let b = pentagon();
let nfp = get_nfp(&a, &b);
println!("Pentagon + Pentagon: {} vertices", nfp.len());
write_svg_with_arclines("/tmp/nfp_test_pentagon_pentagon.svg", &a, &b, &nfp);
assert!(nfp.len() >= 3, "NFP must have at least 3 vertices");
assert!(is_ccw(&nfp), "NFP must be CCW");
let mut valid_count = 0;
for nfp_pt in &nfp {
if !polygons_overlap(&a, &b, *nfp_pt) {
valid_count += 1;
}
}
assert!(valid_count > 0, "Should have valid placements");
println!(" Valid: {}/{}\n", valid_count, nfp.len());
}
#[test]
fn test_fixed_20_edge_polygons() {
let a = vec![
Point::new(-31.39, 4.52),
Point::new(-13.05, -14.42),
Point::new(-6.04, -20.37),
Point::new(6.25, -27.44),
Point::new(38.81, 2.21),
Point::new(38.32, 7.77),
Point::new(0.71, 32.95),
];
let b = vec![
Point::new(-39.07, -4.87),
Point::new(-38.65, -6.96),
Point::new(-7.11, -31.53),
Point::new(-4.10, -31.32),
Point::new(37.99, -9.39),
Point::new(27.91, 6.35),
Point::new(-9.67, 31.38),
Point::new(-33.74, 20.57),
];
let expected_nfp = vec![
Point::new(-69.38, 13.91),
Point::new(-59.30, -1.84),
Point::new(-40.96, -20.78),
Point::new(-33.96, -26.73),
Point::new(3.62, -51.75),
Point::new(15.91, -58.82),
Point::new(39.99, -48.01),
Point::new(72.55, -18.35),
Point::new(77.88, 7.08),
Point::new(77.40, 12.63),
Point::new(76.97, 14.73),
Point::new(45.43, 39.30),
Point::new(7.81, 64.47),
Point::new(4.81, 64.27),
Point::new(-37.29, 42.34),
];
let nfp = get_nfp(&a, &b);
println!("\n=== Fixed 20-Edge Polygon Test ===");
println!("Polygon A: {} vertices", a.len());
for (i, p) in a.iter().enumerate() {
println!(" A[{}]: ({:.2}, {:.2})", i, p.x, p.y);
}
println!("\nPolygon B: {} vertices", b.len());
for (i, p) in b.iter().enumerate() {
println!(" B[{}]: ({:.2}, {:.2})", i, p.x, p.y);
}
println!("\nComputed NFP: {} vertices", nfp.len());
for (i, p) in nfp.iter().enumerate() {
println!(" NFP[{}]: ({:.2}, {:.2})", i, p.x, p.y);
}
println!("\nExpected NFP: {} vertices", expected_nfp.len());
for (i, p) in expected_nfp.iter().enumerate() {
println!(" Expected[{}]: ({:.2}, {:.2})", i, p.x, p.y);
}
println!();
write_svg_with_arclines("/tmp/nfp_test_fixed_20edge.svg", &a, &b, &nfp);
assert!(nfp.len() >= 3, "NFP must have at least 3 vertices");
assert_eq!(nfp.len(), expected_nfp.len(), "NFP should have 15 vertices");
const TOLERANCE: f64 = 0.02;
for (i, (computed, expected)) in nfp.iter().zip(expected_nfp.iter()).enumerate() {
assert!(
(computed.x - expected.x).abs() < TOLERANCE && (computed.y - expected.y).abs() < TOLERANCE,
"NFP vertex {} doesn't match: computed ({:.2}, {:.2}), expected ({:.2}, {:.2})",
i, computed.x, computed.y, expected.x, expected.y
);
}
assert!(is_ccw(&nfp), "NFP must be counter-clockwise");
let test_origin = Point::new(70.35, 70.35);
assert!(
!contains_point(&nfp, test_origin),
"B origin at ({:.2}, {:.2}) should be outside NFP (no collision)",
test_origin.x,
test_origin.y
);
let mut no_overlap_count = 0;
for (i, nfp_pt) in nfp.iter().enumerate() {
if !polygons_overlap(&a, &b, *nfp_pt) {
no_overlap_count += 1;
} else {
println!(
" OVERLAP at NFP[{}]: ({:.2}, {:.2})",
i, nfp_pt.x, nfp_pt.y
);
}
}
assert!(
no_overlap_count > 0,
"Should have valid placements (no overlap) at NFP vertices"
);
println!(
" No-overlap placements: {}/{}\n",
no_overlap_count, nfp.len()
);
}
}