use super::*;
use crate::core::{
area, area_paths, get_bounds_paths, point_in_polygon, Point64, PointD, PointInPolygonResult,
};
fn make_square(half_size: i64) -> Path64 {
vec![
Point64::new(-half_size, -half_size),
Point64::new(half_size, -half_size),
Point64::new(half_size, half_size),
Point64::new(-half_size, half_size),
]
}
fn make_square_d(half_size: f64) -> PathD {
vec![
PointD::new(-half_size, -half_size),
PointD::new(half_size, -half_size),
PointD::new(half_size, half_size),
PointD::new(-half_size, half_size),
]
}
fn make_triangle(size: i64) -> Path64 {
vec![
Point64::new(0, 0),
Point64::new(size, 0),
Point64::new(size / 2, size),
]
}
fn make_line_segment(x1: i64, y1: i64, x2: i64, y2: i64) -> Path64 {
vec![Point64::new(x1, y1), Point64::new(x2, y2)]
}
#[test]
fn test_minkowski_sum_empty_pattern() {
let pattern: Path64 = vec![];
let path = make_square(100);
let result = minkowski_sum(&pattern, &path, true);
assert!(
result.is_empty(),
"Empty pattern should produce empty result"
);
}
#[test]
fn test_minkowski_sum_empty_path() {
let pattern = make_square(10);
let path: Path64 = vec![];
let result = minkowski_sum(&pattern, &path, true);
assert!(result.is_empty(), "Empty path should produce empty result");
}
#[test]
fn test_minkowski_sum_both_empty() {
let pattern: Path64 = vec![];
let path: Path64 = vec![];
let result = minkowski_sum(&pattern, &path, true);
assert!(result.is_empty(), "Both empty should produce empty result");
}
#[test]
fn test_minkowski_diff_empty_pattern() {
let pattern: Path64 = vec![];
let path = make_square(100);
let result = minkowski_diff(&pattern, &path, true);
assert!(
result.is_empty(),
"Empty pattern should produce empty result"
);
}
#[test]
fn test_minkowski_diff_empty_path() {
let pattern = make_square(10);
let path: Path64 = vec![];
let result = minkowski_diff(&pattern, &path, true);
assert!(result.is_empty(), "Empty path should produce empty result");
}
#[test]
fn test_minkowski_sum_square_with_square_closed() {
let pattern = make_square(10);
let path = make_square(50);
let result = minkowski_sum(&pattern, &path, true);
assert!(!result.is_empty(), "Result should not be empty");
let total_area = area_paths(&result).abs();
assert!(
(total_area - 8000.0).abs() < 200.0,
"Area should be approximately 8000 (frame shape), got {}",
total_area
);
let bounds = get_bounds_paths(&result);
assert!(
bounds.left >= -61 && bounds.left <= -59,
"Left bound should be ~-60, got {}",
bounds.left
);
assert!(
bounds.right >= 59 && bounds.right <= 61,
"Right bound should be ~60, got {}",
bounds.right
);
assert!(
bounds.top >= -61 && bounds.top <= -59,
"Top bound should be ~-60, got {}",
bounds.top
);
assert!(
bounds.bottom >= 59 && bounds.bottom <= 61,
"Bottom bound should be ~60, got {}",
bounds.bottom
);
}
#[test]
fn test_minkowski_sum_single_point_pattern() {
let pattern = vec![Point64::new(10, 20)];
let path = make_square(50);
let result = minkowski_sum(&pattern, &path, true);
let _ = area_paths(&result);
}
#[test]
fn test_minkowski_sum_point_on_boundary() {
let pattern = make_square(5);
let path = make_square(20);
let result = minkowski_sum(&pattern, &path, true);
assert!(!result.is_empty());
if !result.is_empty() && result[0].len() >= 3 {
let center = Point64::new(0, 0);
let pip = point_in_polygon(center, &result[0]);
assert_ne!(
pip,
PointInPolygonResult::IsOutside,
"Center should be inside the Minkowski sum"
);
}
}
#[test]
fn test_minkowski_sum_triangle_with_square_closed() {
let pattern = make_square(10);
let path = make_triangle(100);
let result = minkowski_sum(&pattern, &path, true);
assert!(!result.is_empty(), "Result should not be empty");
let original_area = area(&path).abs();
let result_area = area_paths(&result).abs();
assert!(
result_area > original_area,
"Minkowski sum area ({}) should be larger than original ({})",
result_area,
original_area
);
}
#[test]
fn test_minkowski_sum_open_path() {
let pattern = make_square(10);
let path = make_line_segment(0, 0, 100, 0);
let result = minkowski_sum(&pattern, &path, false);
assert!(
!result.is_empty(),
"Result should not be empty for open path"
);
let bounds = get_bounds_paths(&result);
assert!(bounds.left <= -9, "Left should extend past -10");
assert!(bounds.right >= 109, "Right should extend past 110");
}
#[test]
fn test_minkowski_sum_preserves_positive_orientation() {
let pattern = make_square(5);
let path = make_square(50);
let result = minkowski_sum(&pattern, &path, true);
assert!(!result.is_empty());
let total_area = area_paths(&result);
assert!(
total_area > 0.0,
"Total area should be positive (CCW), got {}",
total_area
);
}
#[test]
fn test_minkowski_diff_square_with_square_closed() {
let pattern = make_square(10);
let path = make_square(50);
let result = minkowski_diff(&pattern, &path, true);
assert!(!result.is_empty(), "Result should not be empty");
let total_area = area_paths(&result).abs();
assert!(total_area > 0.0, "Result should have non-zero area");
}
#[test]
fn test_minkowski_diff_same_shape() {
let shape = make_square(50);
let result = minkowski_diff(&shape, &shape, true);
assert!(
!result.is_empty(),
"Diff of shape with itself should not be empty"
);
if !result.is_empty() && result[0].len() >= 3 {
let origin = Point64::new(0, 0);
let pip = point_in_polygon(origin, &result[0]);
assert_ne!(
pip,
PointInPolygonResult::IsOutside,
"Origin should be inside Minkowski diff of shape with itself"
);
}
}
#[test]
fn test_minkowski_diff_open_path() {
let pattern = make_square(10);
let path = make_line_segment(0, 0, 100, 0);
let result = minkowski_diff(&pattern, &path, false);
assert!(
!result.is_empty(),
"Diff with open path should produce result"
);
}
#[test]
fn test_minkowski_sum_d_basic() {
let pattern = make_square_d(10.0);
let path = make_square_d(50.0);
let result = minkowski_sum_d(&pattern, &path, true, 2);
assert!(!result.is_empty(), "PathD sum should produce result");
let mut min_x = f64::MAX;
let mut max_x = f64::MIN;
let mut min_y = f64::MAX;
let mut max_y = f64::MIN;
for path in &result {
for pt in path {
if pt.x < min_x {
min_x = pt.x;
}
if pt.x > max_x {
max_x = pt.x;
}
if pt.y < min_y {
min_y = pt.y;
}
if pt.y > max_y {
max_y = pt.y;
}
}
}
assert!(
(min_x - (-60.0)).abs() < 1.0,
"min_x should be ~-60, got {}",
min_x
);
assert!(
(max_x - 60.0).abs() < 1.0,
"max_x should be ~60, got {}",
max_x
);
assert!(
(min_y - (-60.0)).abs() < 1.0,
"min_y should be ~-60, got {}",
min_y
);
assert!(
(max_y - 60.0).abs() < 1.0,
"max_y should be ~60, got {}",
max_y
);
}
#[test]
fn test_minkowski_sum_d_empty_inputs() {
let pattern: PathD = vec![];
let path = make_square_d(50.0);
let result = minkowski_sum_d(&pattern, &path, true, 2);
assert!(result.is_empty());
let pattern = make_square_d(10.0);
let path: PathD = vec![];
let result = minkowski_sum_d(&pattern, &path, true, 2);
assert!(result.is_empty());
}
#[test]
fn test_minkowski_sum_d_precision() {
let pattern = make_square_d(1.5);
let path = make_square_d(3.5);
let result2 = minkowski_sum_d(&pattern, &path, true, 2);
assert!(!result2.is_empty());
let result4 = minkowski_sum_d(&pattern, &path, true, 4);
assert!(!result4.is_empty());
let area2: f64 = result2
.iter()
.map(|p| {
let n = p.len();
if n < 3 {
return 0.0;
}
let mut a = 0.0;
for i in 0..n {
let j = (i + 1) % n;
a += p[i].x * p[j].y - p[j].x * p[i].y;
}
a / 2.0
})
.sum::<f64>()
.abs();
let area4: f64 = result4
.iter()
.map(|p| {
let n = p.len();
if n < 3 {
return 0.0;
}
let mut a = 0.0;
for i in 0..n {
let j = (i + 1) % n;
a += p[i].x * p[j].y - p[j].x * p[i].y;
}
a / 2.0
})
.sum::<f64>()
.abs();
assert!(
(area2 - area4).abs() < 1.0,
"Areas should be similar regardless of decimal places: {} vs {}",
area2,
area4
);
}
#[test]
fn test_minkowski_diff_d_basic() {
let pattern = make_square_d(10.0);
let path = make_square_d(50.0);
let result = minkowski_diff_d(&pattern, &path, true, 2);
assert!(!result.is_empty(), "PathD diff should produce result");
}
#[test]
fn test_minkowski_diff_d_empty_inputs() {
let pattern: PathD = vec![];
let path = make_square_d(50.0);
let result = minkowski_diff_d(&pattern, &path, true, 2);
assert!(result.is_empty());
let pattern = make_square_d(10.0);
let path: PathD = vec![];
let result = minkowski_diff_d(&pattern, &path, true, 2);
assert!(result.is_empty());
}
#[test]
fn test_minkowski_sum_commutativity_area() {
let a = make_square(20);
let b = make_triangle(60);
let result_ab = minkowski_sum(&a, &b, true);
let result_ba = minkowski_sum(&b, &a, true);
let area_ab = area_paths(&result_ab).abs();
let area_ba = area_paths(&result_ba).abs();
assert!(
(area_ab - area_ba).abs() < 10.0,
"Minkowski sum should be commutative in area: {} vs {}",
area_ab,
area_ba
);
}
#[test]
fn test_minkowski_sum_contains_original_shifted_points() {
let pattern = make_square(10);
let path = vec![
Point64::new(100, 100),
Point64::new(200, 100),
Point64::new(200, 200),
Point64::new(100, 200),
];
let result = minkowski_sum(&pattern, &path, true);
assert!(!result.is_empty());
if result.len() == 1 && result[0].len() >= 3 {
for p in &path {
let pip = point_in_polygon(*p, &result[0]);
assert_ne!(
pip,
PointInPolygonResult::IsOutside,
"Path point ({},{}) should be inside Minkowski sum",
p.x,
p.y
);
}
}
}
#[test]
fn test_minkowski_sum_large_coordinates() {
let pattern = make_square(100);
let path = vec![
Point64::new(1_000_000, 1_000_000),
Point64::new(2_000_000, 1_000_000),
Point64::new(2_000_000, 2_000_000),
Point64::new(1_000_000, 2_000_000),
];
let result = minkowski_sum(&pattern, &path, true);
assert!(!result.is_empty(), "Large coordinates should work");
let bounds = get_bounds_paths(&result);
assert!(
bounds.left <= 999_901,
"Left bound should account for pattern"
);
assert!(
bounds.right >= 2_000_099,
"Right bound should account for pattern"
);
}
#[test]
fn test_minkowski_sum_collinear_path() {
let pattern = make_square(10);
let path = vec![
Point64::new(0, 0),
Point64::new(50, 0),
Point64::new(100, 0),
];
let result = minkowski_sum(&pattern, &path, false);
let _ = area_paths(&result);
}
#[test]
fn test_minkowski_diff_not_same_as_sum() {
let pattern = make_square(15);
let path = vec![
Point64::new(100, 100),
Point64::new(200, 100),
Point64::new(200, 200),
Point64::new(100, 200),
];
let sum_result = minkowski_sum(&pattern, &path, true);
let diff_result = minkowski_diff(&pattern, &path, true);
let sum_area = area_paths(&sum_result).abs();
let diff_area = area_paths(&diff_result).abs();
assert!(sum_area > 0.0, "Sum should have positive area");
assert!(diff_area > 0.0, "Diff should have positive area");
}
#[test]
fn test_minkowski_sum_asymmetric_pattern() {
let pattern = vec![
Point64::new(0, 0),
Point64::new(20, 0),
Point64::new(20, 10),
Point64::new(0, 10),
];
let path = vec![
Point64::new(50, 50),
Point64::new(150, 50),
Point64::new(150, 150),
Point64::new(50, 150),
];
let sum_result = minkowski_sum(&pattern, &path, true);
let diff_result = minkowski_diff(&pattern, &path, true);
let sum_bounds = get_bounds_paths(&sum_result);
let diff_bounds = get_bounds_paths(&diff_result);
assert_ne!(
sum_bounds.left, diff_bounds.left,
"Asymmetric pattern should produce different sum vs diff bounds"
);
}
#[test]
fn test_minkowski_sum_two_point_path_closed() {
let pattern = make_square(10);
let path = vec![Point64::new(0, 0), Point64::new(100, 0)];
let result = minkowski_sum(&pattern, &path, true);
let _ = area_paths(&result);
}
#[test]
fn test_minkowski_sum_three_point_path_open() {
let pattern = make_square(5);
let path = vec![
Point64::new(0, 0),
Point64::new(100, 0),
Point64::new(100, 100),
];
let result = minkowski_sum(&pattern, &path, false);
assert!(
!result.is_empty(),
"L-shaped open path should produce result"
);
let total_area = area_paths(&result).abs();
assert!(total_area > 0.0, "Result should have positive area");
}
#[test]
fn test_union_paths_via_minkowski_produces_clean_output() {
let pattern = make_square(20);
let path = make_square(50);
let result = minkowski_sum(&pattern, &path, true);
assert!(
result.len() <= 2,
"Convex + convex should produce few contours, got {}",
result.len()
);
}
#[test]
fn test_minkowski_sum_many_sided_polygon() {
let pattern = vec![
Point64::new(10, 0),
Point64::new(7, 7),
Point64::new(0, 10),
Point64::new(-7, 7),
Point64::new(-10, 0),
Point64::new(-7, -7),
Point64::new(0, -10),
Point64::new(7, -7),
];
let path = make_square(50);
let result = minkowski_sum(&pattern, &path, true);
assert!(!result.is_empty());
let total_area = area_paths(&result).abs();
assert!(
total_area > 5000.0,
"Frame area should be > 5000, got {}",
total_area
);
let outer_area = result.iter().map(area).filter(|a| *a > 0.0).sum::<f64>();
assert!(
outer_area > 10000.0,
"Outer boundary area should be > original square area, got {}",
outer_area
);
}
#[test]
fn test_minkowski_operations_dont_crash_with_single_points() {
let pattern = vec![Point64::new(5, 5)];
let path = vec![Point64::new(10, 10)];
let _ = minkowski_sum(&pattern, &path, true);
let _ = minkowski_sum(&pattern, &path, false);
let _ = minkowski_diff(&pattern, &path, true);
let _ = minkowski_diff(&pattern, &path, false);
}
#[test]
fn test_minkowski_sum_d_zero_decimal_places() {
let pattern = make_square_d(10.0);
let path = make_square_d(50.0);
let result = minkowski_sum_d(&pattern, &path, true, 0);
assert!(!result.is_empty());
}
#[test]
fn test_minkowski_sum_d_high_decimal_places() {
let pattern = make_square_d(0.001);
let path = make_square_d(0.005);
let result = minkowski_sum_d(&pattern, &path, true, 6);
assert!(!result.is_empty());
}
#[test]
fn test_minkowski_internal_axis_aligned_squares_produce_degenerate_quads() {
let pattern = make_square(10);
let path = make_square(50);
let quads = minkowski_internal(&pattern, &path, true, true);
assert_eq!(quads.len(), 16);
let non_degenerate: Vec<_> = quads.iter().filter(|q| area(q).abs() > 0.0).collect();
let degenerate: Vec<_> = quads.iter().filter(|q| area(q).abs() == 0.0).collect();
assert_eq!(non_degenerate.len(), 8);
assert_eq!(degenerate.len(), 8);
let result = union_paths(&quads, FillRule::NonZero);
assert_eq!(result.len(), 2, "Should produce outer boundary + hole");
let outer_area = result.iter().map(area).filter(|a| *a > 0.0).sum::<f64>();
let hole_area = result
.iter()
.map(area)
.filter(|a| *a < 0.0)
.sum::<f64>()
.abs();
assert!((outer_area - 14400.0).abs() < 100.0);
assert!((hole_area - 6400.0).abs() < 100.0);
}