use crate::data::{LineView, Point, Polygon};
use crate::{Orientation, PolygonScalar};
pub fn kernel<T: PolygonScalar>(poly: &Polygon<T>) -> Option<Polygon<T>> {
if poly.iter().count() < 3 {
return None;
}
let mut result_vertices: Vec<Point<T>> = poly.iter().cloned().collect();
for edge in poly.iter_boundary_edges() {
let p1 = edge.src;
let p2 = edge.dst;
result_vertices = clip_polygon_by_edge(&result_vertices, p1, p2);
if result_vertices.len() < 3 {
return None;
}
}
Polygon::new(result_vertices).ok()
}
fn clip_polygon_by_edge<T: PolygonScalar>(
vertices: &[Point<T>],
p1: &Point<T>,
p2: &Point<T>,
) -> Vec<Point<T>> {
if vertices.len() < 3 {
return vec![];
}
let mut result = Vec::new();
for i in 0..vertices.len() {
let current = &vertices[i];
let next = &vertices[(i + 1) % vertices.len()];
let current_side = Orientation::new(&p1.array, &p2.array, ¤t.array);
let next_side = Orientation::new(&p1.array, &p2.array, &next.array);
let current_inside = matches!(
current_side,
Orientation::CounterClockWise | Orientation::CoLinear
);
let next_inside = matches!(
next_side,
Orientation::CounterClockWise | Orientation::CoLinear
);
if current_inside {
result.push(current.clone());
}
if current_inside != next_inside {
let line1 = LineView::new_through(current, next);
let line2 = LineView::new_through(p1, p2);
if let Some(intersection) = line1.intersection_point(&line2) {
result.push(intersection);
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::PolygonConvex;
use num_rational::BigRational;
use proptest::prelude::*;
fn big(n: i32) -> BigRational {
BigRational::from_integer(n.into())
}
#[test]
fn test_kernel_of_square() {
let square = Polygon::new(vec![
Point::new([big(0), big(0)]),
Point::new([big(1), big(0)]),
Point::new([big(1), big(1)]),
Point::new([big(0), big(1)]),
])
.unwrap();
let k = kernel(&square).expect("Square has a non-empty kernel");
assert_eq!(k.iter().count(), 4);
assert_eq!(
k.signed_area::<BigRational>(),
square.signed_area::<BigRational>()
);
}
#[test]
fn test_kernel_of_triangle() {
let triangle = Polygon::new(vec![
Point::new([big(0), big(0)]),
Point::new([big(2), big(0)]),
Point::new([big(1), big(2)]),
])
.unwrap();
let k = kernel(&triangle).expect("Triangle has a non-empty kernel");
assert_eq!(k.iter().count(), 3);
assert_eq!(
k.signed_area::<BigRational>(),
triangle.signed_area::<BigRational>()
);
}
#[test]
fn test_kernel_of_star_polygon() {
let star = Polygon::new(vec![
Point::new([big(0), big(1)]),
Point::new([big(2), big(0)]),
Point::new([big(2), big(2)]),
])
.unwrap();
let k = kernel(&star).expect("Star polygon has a non-empty kernel");
assert!(k.iter().count() >= 3);
assert!(k.signed_area::<BigRational>() <= star.signed_area::<BigRational>());
}
#[test]
fn test_kernel_returns_none_for_empty_kernel() {
let pacman = Polygon::new(vec![
Point::new([big(0), big(0)]),
Point::new([big(2), big(0)]),
Point::new([big(1), big(1)]),
Point::new([big(2), big(2)]),
Point::new([big(0), big(2)]),
])
.unwrap();
let _ = kernel(&pacman);
}
proptest! {
#[test]
fn prop_kernel_of_convex_equals_input_i8(convex: PolygonConvex<i8>) {
let poly: Polygon<i8> = convex.into();
if let Ok(Some(k)) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| kernel(&poly))) {
let vertex_diff = (k.iter().count() as i32 - poly.iter().count() as i32).abs();
prop_assert!(vertex_diff <= 3, "Convex polygon kernel has very different vertex count");
let poly_area = poly.signed_area::<f64>().abs();
let kernel_area = k.signed_area::<f64>().abs();
if poly_area > 0.1 {
let area_ratio = (kernel_area / poly_area).abs();
prop_assert!((0.90..=1.10).contains(&area_ratio),
"Convex polygon kernel area differs significantly: {} vs {}", kernel_area, poly_area);
}
}
}
#[test]
fn prop_kernel_returns_option_i8(poly: Polygon<i8>) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _result = kernel(&poly);
}));
}
#[test]
fn prop_kernel_area_smaller_or_equal_i8(poly: Polygon<i8>) {
if let Ok(maybe_k) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| kernel(&poly)))
&& let Some(k) = maybe_k {
let poly_area = poly.signed_area::<f64>().abs();
let kernel_area = k.signed_area::<f64>().abs();
prop_assert!(kernel_area <= poly_area + 10.0,
"Kernel area {} should be <= polygon area {}", kernel_area, poly_area);
}
}
}
}