use crate::{NoAllocError, Point2D};
pub struct FixedRing<const N: usize> {
vertices: [Point2D; N],
len: usize,
}
impl<const N: usize> FixedRing<N> {
#[must_use]
#[inline]
pub fn new() -> Self {
Self {
vertices: [Point2D::new(0.0, 0.0); N],
len: 0,
}
}
#[inline]
pub fn push(&mut self, p: Point2D) -> Result<(), NoAllocError> {
if self.len >= N {
return Err(NoAllocError::CapacityExceeded);
}
self.vertices[self.len] = p;
self.len += 1;
Ok(())
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.len
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
#[inline]
pub fn vertices(&self) -> &[Point2D] {
&self.vertices[..self.len]
}
#[must_use]
#[inline]
pub fn get(&self, index: usize) -> Option<&Point2D> {
if index < self.len {
Some(&self.vertices[index])
} else {
None
}
}
#[must_use]
pub fn signed_area(&self) -> f64 {
if self.len < 3 {
return 0.0;
}
let verts = self.vertices();
let mut sum = 0.0_f64;
let mut i = 0;
while i < self.len {
let j = if i + 1 < self.len { i + 1 } else { 0 };
sum += verts[i].x * verts[j].y;
sum -= verts[j].x * verts[i].y;
i += 1;
}
sum * 0.5
}
#[must_use]
#[inline]
pub fn area(&self) -> f64 {
let sa = self.signed_area();
if sa < 0.0 { -sa } else { sa }
}
#[must_use]
#[inline]
pub fn is_counter_clockwise(&self) -> bool {
self.signed_area() > 0.0
}
#[must_use]
#[inline]
pub fn is_clockwise(&self) -> bool {
self.signed_area() < 0.0
}
#[must_use]
pub fn contains_point(&self, p: Point2D) -> bool {
if self.len < 3 {
return false;
}
let verts = self.vertices();
let mut inside = false;
let mut j = self.len - 1;
let mut i = 0;
while i < self.len {
let vi = &verts[i];
let vj = &verts[j];
let yi_above = vi.y > p.y;
let yj_above = vj.y > p.y;
if yi_above != yj_above {
let slope_inv = (vj.x - vi.x) / (vj.y - vi.y);
let x_intersect = vi.x + (p.y - vi.y) * slope_inv;
if p.x < x_intersect {
inside = !inside;
}
}
j = i;
i += 1;
}
inside
}
#[must_use]
pub fn perimeter(&self) -> f64 {
if self.len < 2 {
return 0.0;
}
let verts = self.vertices();
let mut total = 0.0_f64;
let mut i = 0;
while i < self.len {
let j = if i + 1 < self.len { i + 1 } else { 0 };
total += verts[i].distance_to(&verts[j]);
i += 1;
}
total
}
#[must_use]
#[inline]
pub const fn capacity(&self) -> usize {
N
}
#[inline]
pub fn clear(&mut self) {
self.len = 0;
}
}
impl<const N: usize> Default for FixedRing<N> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_ccw_square() -> FixedRing<8> {
let mut ring: FixedRing<8> = FixedRing::new();
let _ = ring.push(Point2D::new(0.0, 0.0));
let _ = ring.push(Point2D::new(1.0, 0.0));
let _ = ring.push(Point2D::new(1.0, 1.0));
let _ = ring.push(Point2D::new(0.0, 1.0));
ring
}
fn make_cw_square() -> FixedRing<8> {
let mut ring: FixedRing<8> = FixedRing::new();
let _ = ring.push(Point2D::new(0.0, 0.0));
let _ = ring.push(Point2D::new(0.0, 1.0));
let _ = ring.push(Point2D::new(1.0, 1.0));
let _ = ring.push(Point2D::new(1.0, 0.0));
ring
}
#[test]
fn test_new_empty() {
let ring: FixedRing<10> = FixedRing::new();
assert!(ring.is_empty());
assert_eq!(ring.len(), 0);
assert_eq!(ring.capacity(), 10);
}
#[test]
fn test_push_and_get() {
let mut ring: FixedRing<4> = FixedRing::new();
assert!(ring.push(Point2D::new(1.0, 2.0)).is_ok());
assert!(ring.push(Point2D::new(3.0, 4.0)).is_ok());
assert_eq!(ring.len(), 2);
let p = ring.get(0).expect("index 0 exists");
assert!((p.x - 1.0).abs() < f64::EPSILON);
assert!(ring.get(5).is_none());
}
#[test]
fn test_capacity_exceeded() {
let mut ring: FixedRing<2> = FixedRing::new();
assert!(ring.push(Point2D::new(0.0, 0.0)).is_ok());
assert!(ring.push(Point2D::new(1.0, 1.0)).is_ok());
assert_eq!(
ring.push(Point2D::new(2.0, 2.0)),
Err(NoAllocError::CapacityExceeded)
);
}
#[test]
fn test_signed_area_ccw() {
let ring = make_ccw_square();
let area = ring.signed_area();
assert!(
(area - 1.0).abs() < 1e-10,
"CCW square should have positive area ~1.0, got {area}"
);
}
#[test]
fn test_signed_area_cw() {
let ring = make_cw_square();
let area = ring.signed_area();
assert!(
(area - (-1.0)).abs() < 1e-10,
"CW square should have negative area ~-1.0, got {area}"
);
}
#[test]
fn test_area_absolute() {
let ccw = make_ccw_square();
let cw = make_cw_square();
assert!((ccw.area() - 1.0).abs() < 1e-10);
assert!((cw.area() - 1.0).abs() < 1e-10);
}
#[test]
fn test_area_degenerate() {
let ring: FixedRing<10> = FixedRing::new();
assert!((ring.area() - 0.0).abs() < f64::EPSILON);
let mut ring2: FixedRing<10> = FixedRing::new();
let _ = ring2.push(Point2D::new(0.0, 0.0));
let _ = ring2.push(Point2D::new(1.0, 1.0));
assert!((ring2.area() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_is_ccw_cw() {
let ccw = make_ccw_square();
let cw = make_cw_square();
assert!(ccw.is_counter_clockwise());
assert!(!ccw.is_clockwise());
assert!(cw.is_clockwise());
assert!(!cw.is_counter_clockwise());
}
#[test]
fn test_contains_point_inside() {
let ring = make_ccw_square();
assert!(ring.contains_point(Point2D::new(0.5, 0.5)));
assert!(ring.contains_point(Point2D::new(0.1, 0.9)));
}
#[test]
fn test_contains_point_outside() {
let ring = make_ccw_square();
assert!(!ring.contains_point(Point2D::new(2.0, 0.5)));
assert!(!ring.contains_point(Point2D::new(-0.1, 0.5)));
assert!(!ring.contains_point(Point2D::new(0.5, -0.1)));
assert!(!ring.contains_point(Point2D::new(0.5, 1.1)));
}
#[test]
fn test_contains_point_degenerate() {
let ring: FixedRing<10> = FixedRing::new();
assert!(!ring.contains_point(Point2D::new(0.0, 0.0)));
}
#[test]
fn test_contains_point_triangle() {
let mut ring: FixedRing<8> = FixedRing::new();
let _ = ring.push(Point2D::new(0.0, 0.0));
let _ = ring.push(Point2D::new(4.0, 0.0));
let _ = ring.push(Point2D::new(2.0, 3.0));
assert!(ring.contains_point(Point2D::new(2.0, 1.0)));
assert!(!ring.contains_point(Point2D::new(0.0, 3.0)));
}
#[test]
fn test_perimeter() {
let ring = make_ccw_square();
assert!((ring.perimeter() - 4.0).abs() < 1e-10);
}
#[test]
fn test_perimeter_degenerate() {
let ring: FixedRing<10> = FixedRing::new();
assert!((ring.perimeter() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_clear() {
let mut ring = make_ccw_square();
ring.clear();
assert!(ring.is_empty());
}
#[test]
fn test_default() {
let ring: FixedRing<5> = FixedRing::default();
assert!(ring.is_empty());
}
#[test]
fn test_vertices_slice() {
let ring = make_ccw_square();
let verts = ring.vertices();
assert_eq!(verts.len(), 4);
}
#[test]
fn test_contains_point_cw_ring() {
let ring = make_cw_square();
assert!(ring.contains_point(Point2D::new(0.5, 0.5)));
assert!(!ring.contains_point(Point2D::new(2.0, 2.0)));
}
}