exact-poly 0.3.0

Integer polygon geometry library — exact arithmetic, no float errors
Documentation
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Aabb {
    pub min_x: i64,
    pub min_y: i64,
    pub max_x: i64,
    pub max_y: i64,
}

impl Aabb {
    pub fn new(min_x: i64, min_y: i64, max_x: i64, max_y: i64) -> Self {
        assert!(min_x <= max_x, "AABB: min_x ({min_x}) > max_x ({max_x})");
        assert!(min_y <= max_y, "AABB: min_y ({min_y}) > max_y ({max_y})");
        Self {
            min_x,
            min_y,
            max_x,
            max_y,
        }
    }

    pub fn from_ring(ring: &[[i64; 2]]) -> Self {
        if ring.is_empty() {
            return Self {
                min_x: 0,
                max_x: 0,
                min_y: 0,
                max_y: 0,
            };
        }

        let mut min_x = ring[0][0];
        let mut max_x = ring[0][0];
        let mut min_y = ring[0][1];
        let mut max_y = ring[0][1];

        for pt in &ring[1..] {
            if pt[0] < min_x {
                min_x = pt[0];
            }
            if pt[0] > max_x {
                max_x = pt[0];
            }
            if pt[1] < min_y {
                min_y = pt[1];
            }
            if pt[1] > max_y {
                max_y = pt[1];
            }
        }

        Self {
            min_x,
            min_y,
            max_x,
            max_y,
        }
    }

    pub fn intersects(&self, other: &Aabb) -> bool {
        self.min_x < other.max_x
            && self.max_x > other.min_x
            && self.min_y < other.max_y
            && self.max_y > other.min_y
    }

    pub fn contains_point(&self, x: i64, y: i64) -> bool {
        x >= self.min_x && x <= self.max_x && y >= self.min_y && y <= self.max_y
    }

    pub fn merge(&self, other: &Aabb) -> Aabb {
        Aabb {
            min_x: self.min_x.min(other.min_x),
            min_y: self.min_y.min(other.min_y),
            max_x: self.max_x.max(other.max_x),
            max_y: self.max_y.max(other.max_y),
        }
    }

    pub fn width(&self) -> i64 {
        self.max_x - self.min_x
    }

    pub fn height(&self) -> i64 {
        self.max_y - self.min_y
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const M: i64 = 1_000_000;

    #[test]
    fn from_ring_tracks_extrema() {
        let ring = vec![[M, M], [2 * M, M], [2 * M, 2 * M], [M, 2 * M]];
        let aabb = Aabb::from_ring(&ring);
        assert_eq!(aabb.min_x, M);
        assert_eq!(aabb.max_x, 2 * M);
        assert_eq!(aabb.min_y, M);
        assert_eq!(aabb.max_y, 2 * M);
    }

    #[test]
    fn from_ring_handles_single_vertex() {
        let ring = vec![[5, 3]];
        let aabb = Aabb::from_ring(&ring);

        assert_eq!(aabb.min_x, 5);
        assert_eq!(aabb.max_x, 5);
        assert_eq!(aabb.min_y, 3);
        assert_eq!(aabb.max_y, 3);
    }

    #[test]
    fn from_ring_handles_negative_coordinates() {
        let ring = vec![[-10, -5], [10, 5]];
        let aabb = Aabb::from_ring(&ring);

        assert_eq!(aabb.min_x, -10);
        assert_eq!(aabb.max_x, 10);
        assert_eq!(aabb.min_y, -5);
        assert_eq!(aabb.max_y, 5);
    }

    #[test]
    fn intersects_overlapping_returns_true() {
        let a = Aabb::new(0, 0, 2 * M, 2 * M);
        let b = Aabb::new(M, M, 3 * M, 3 * M);
        assert!(a.intersects(&b));
        assert!(b.intersects(&a));
    }

    #[test]
    fn intersects_touching_edge_returns_false() {
        let a = Aabb::new(0, 0, M, M);
        let b = Aabb::new(M, 0, 2 * M, M);
        assert!(!a.intersects(&b), "touching edges should NOT intersect");
        assert!(
            !b.intersects(&a),
            "touching edges should NOT intersect (reversed)"
        );
    }

    #[test]
    fn intersects_touching_corner_returns_false() {
        let a = Aabb::new(0, 0, M, M);
        let b = Aabb::new(M, M, 2 * M, 2 * M);
        assert!(!a.intersects(&b), "touching corners should NOT intersect");
    }

    #[test]
    fn intersects_separated_returns_false() {
        let a = Aabb::new(0, 0, M, M);
        let b = Aabb::new(2 * M, 0, 3 * M, M);
        assert!(!a.intersects(&b));
    }

    #[test]
    fn intersects_contained_returns_true() {
        let outer = Aabb::new(0, 0, 10 * M, 10 * M);
        let inner = Aabb::new(2 * M, 2 * M, 5 * M, 5 * M);
        assert!(outer.intersects(&inner));
        assert!(inner.intersects(&outer));
    }

    #[test]
    fn contains_point_works() {
        let aabb = Aabb::new(0, 0, M, M);
        assert!(aabb.contains_point(M / 2, M / 2));
        assert!(aabb.contains_point(0, 0));
        assert!(aabb.contains_point(M, M));
        assert!(!aabb.contains_point(M + 1, 0));
    }

    #[test]
    fn merge_covers_both() {
        let a = Aabb::new(0, 0, M, M);
        let b = Aabb::new(2 * M, 2 * M, 3 * M, 3 * M);
        let merged = a.merge(&b);
        assert_eq!(merged.min_x, 0);
        assert_eq!(merged.max_x, 3 * M);
        assert_eq!(merged.min_y, 0);
        assert_eq!(merged.max_y, 3 * M);
    }

    #[test]
    fn merge_disjoint_boxes_contains_both() {
        let a = Aabb::new(-3 * M, -2 * M, -M, -M);
        let b = Aabb::new(2 * M, M, 4 * M, 3 * M);
        let merged = a.merge(&b);

        assert_eq!(merged.min_x, -3 * M);
        assert_eq!(merged.min_y, -2 * M);
        assert_eq!(merged.max_x, 4 * M);
        assert_eq!(merged.max_y, 3 * M);
        assert!(merged.contains_point(a.min_x, a.min_y));
        assert!(merged.contains_point(b.max_x, b.max_y));
    }

    #[test]
    fn merge_nested_boxes_equals_outer_box() {
        let outer = Aabb::new(0, 0, 10 * M, 10 * M);
        let inner = Aabb::new(2 * M, 3 * M, 4 * M, 5 * M);

        assert_eq!(outer.merge(&inner), outer);
        assert_eq!(inner.merge(&outer), outer);
    }

    #[test]
    fn contains_point_includes_corner_and_excludes_outside() {
        let aabb = Aabb::new(-M, -2 * M, M, 2 * M);

        assert!(aabb.contains_point(-M, -2 * M));
        assert!(!aabb.contains_point(M + 1, 2 * M));
    }

    #[test]
    fn intersects_is_symmetric() {
        let a = Aabb::new(0, 0, 3 * M, 3 * M);
        let b = Aabb::new(2 * M, 2 * M, 5 * M, 5 * M);
        assert_eq!(a.intersects(&b), b.intersects(&a));
    }
}