pub fn minkowski_sum_convex(p: &[(f64, f64)], q: &[(f64, f64)]) -> Vec<(f64, f64)> {
assert!(p.len() >= 3, "polygon P must have >= 3 vertices");
assert!(q.len() >= 3, "polygon Q must have >= 3 vertices");
let p = ensure_ccw_local(p);
let q = ensure_ccw_local(q);
let p_start = bottom_left_index(&p);
let q_start = bottom_left_index(&q);
let n = p.len();
let m = q.len();
let mut result = Vec::with_capacity(n + m);
let mut i = 0;
let mut j = 0;
while i < n || j < m {
let pi = (i + p_start) % n;
let qi = (j + q_start) % m;
result.push((p[pi].0 + q[qi].0, p[pi].1 + q[qi].1));
let p_edge = if i < n {
let next = ((i + 1) + p_start) % n;
(p[next].0 - p[pi].0, p[next].1 - p[pi].1)
} else {
(0.0, 0.0)
};
let q_edge = if j < m {
let next = ((j + 1) + q_start) % m;
(q[next].0 - q[qi].0, q[next].1 - q[qi].1)
} else {
(0.0, 0.0)
};
let cross = p_edge.0 * q_edge.1 - p_edge.1 * q_edge.0;
if i >= n {
j += 1;
} else if j >= m {
i += 1;
} else if cross > 0.0 {
i += 1;
} else if cross < 0.0 {
j += 1;
} else {
i += 1;
j += 1;
}
}
result
}
pub fn nfp_convex(stationary: &[(f64, f64)], orbiting: &[(f64, f64)]) -> Vec<(f64, f64)> {
let neg_orbiting: Vec<(f64, f64)> = orbiting.iter().map(|&(x, y)| (-x, -y)).collect();
minkowski_sum_convex(stationary, &neg_orbiting)
}
fn bottom_left_index(polygon: &[(f64, f64)]) -> usize {
let mut idx = 0;
for (i, &(x, y)) in polygon.iter().enumerate() {
let (bx, by) = polygon[idx];
if y < by || (y == by && x < bx) {
idx = i;
}
}
idx
}
fn ensure_ccw_local(polygon: &[(f64, f64)]) -> Vec<(f64, f64)> {
if crate::robust::is_ccw(polygon) {
polygon.to_vec()
} else {
let mut reversed = polygon.to_vec();
reversed.reverse();
reversed
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::polygon::area;
fn square_at(x: f64, y: f64, size: f64) -> Vec<(f64, f64)> {
vec![(x, y), (x + size, y), (x + size, y + size), (x, y + size)]
}
#[test]
fn test_minkowski_two_squares() {
let a = square_at(0.0, 0.0, 1.0);
let b = square_at(0.0, 0.0, 1.0);
let sum = minkowski_sum_convex(&a, &b);
let a = area(&sum);
assert!((a - 4.0).abs() < 1e-8, "expected area 4.0, got {a}");
}
#[test]
fn test_minkowski_square_triangle() {
let square = square_at(0.0, 0.0, 2.0);
let triangle = vec![(0.0, 0.0), (1.0, 0.0), (0.5, 1.0)];
let sum = minkowski_sum_convex(&square, &triangle);
let sum_area = area(&sum);
let sq_area = area(&square);
let tri_area = area(&triangle);
assert!(sum_area > sq_area);
assert!(sum_area > tri_area);
}
#[test]
fn test_minkowski_result_is_convex() {
let a = square_at(0.0, 0.0, 5.0);
let b = vec![(0.0, 0.0), (3.0, 0.0), (1.5, 2.0)];
let sum = minkowski_sum_convex(&a, &b);
assert!(crate::robust::is_convex(&sum));
}
#[test]
fn test_nfp_convex_squares() {
let stationary = square_at(0.0, 0.0, 10.0);
let orbiting = square_at(0.0, 0.0, 5.0);
let nfp = nfp_convex(&stationary, &orbiting);
assert!(nfp.len() >= 3);
let nfp_area = area(&nfp);
assert!(
(nfp_area - 225.0).abs() < 1e-6,
"expected NFP area 225.0, got {nfp_area}"
);
}
#[test]
fn test_nfp_convex_symmetric() {
let tri = vec![(0.0, 0.0), (4.0, 0.0), (2.0, 3.0)];
let nfp = nfp_convex(&tri, &tri);
assert!(nfp.len() >= 3);
assert!(crate::robust::is_convex(&nfp));
}
#[test]
#[should_panic(expected = "polygon P must have >= 3 vertices")]
fn test_minkowski_degenerate() {
let a = vec![(0.0, 0.0), (1.0, 0.0)];
let b = square_at(0.0, 0.0, 1.0);
minkowski_sum_convex(&a, &b);
}
#[test]
fn test_minkowski_area_lower_bound() {
let polygons = [
(square_at(0.0, 0.0, 1.0), square_at(0.0, 0.0, 2.0)),
(
square_at(0.0, 0.0, 3.0),
vec![(0.0, 0.0), (1.0, 0.0), (0.5, 1.0)], ),
(
vec![(0.0, 0.0), (4.0, 0.0), (2.0, 3.0)], vec![(0.0, 0.0), (2.0, 0.0), (1.0, 2.0)], ),
];
for (a, b) in &polygons {
let sum = minkowski_sum_convex(a, b);
let area_sum = area(&sum);
let area_a = area(a);
let area_b = area(b);
assert!(
area_sum >= area_a + area_b - 1e-8,
"A(A⊕B)={area_sum} < A(A)+A(B)={} (lower bound violated)",
area_a + area_b
);
}
}
#[test]
fn test_minkowski_two_unit_squares_area_is_4() {
let a = square_at(0.0, 0.0, 1.0);
let b = square_at(0.0, 0.0, 1.0);
let sum = minkowski_sum_convex(&a, &b);
let a_sum = area(&sum);
assert!(
(a_sum - 4.0).abs() < 1e-8,
"unit_square ⊕ unit_square area must be 4.0, got {a_sum}"
);
}
#[test]
fn test_minkowski_sum_is_convex() {
let a = square_at(0.0, 0.0, 2.0);
let b = vec![(0.0, 0.0), (3.0, 0.0), (1.5, 2.0)];
let sum = minkowski_sum_convex(&a, &b);
assert!(
crate::robust::is_convex(&sum),
"Minkowski sum of convex polygons must be convex"
);
}
#[test]
fn test_minkowski_area_strictly_larger_than_inputs() {
let a = square_at(0.0, 0.0, 3.0);
let b = square_at(0.0, 0.0, 2.0);
let sum = minkowski_sum_convex(&a, &b);
let area_a = area(&a);
let area_b = area(&b);
let area_sum = area(&sum);
assert!(
area_sum > area_a,
"A(A⊕B)={area_sum} should exceed A(A)={area_a}"
);
assert!(
area_sum > area_b,
"A(A⊕B)={area_sum} should exceed A(B)={area_b}"
);
}
}