Skip to main content

scirs2_spatial/computational_geometry/
bounding.rs

1//! Minimum bounding rectangle and computational geometry utilities
2//!
3//! This module provides algorithms for computing:
4//! - Axis-aligned bounding boxes (AABB)
5//! - Minimum area bounding rectangles (using rotating calipers on convex hull)
6//! - Oriented bounding boxes
7//! - Polygon perimeter computation
8//! - Convex hull area and perimeter convenience functions
9//!
10//! # Examples
11//!
12//! ```
13//! use scirs2_spatial::computational_geometry::bounding::{
14//!     axis_aligned_bounding_box, minimum_bounding_rectangle, polygon_perimeter,
15//! };
16//! use scirs2_core::ndarray::array;
17//!
18//! let points = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
19//!
20//! let aabb = axis_aligned_bounding_box(&points.view()).expect("Operation failed");
21//! let mbr = minimum_bounding_rectangle(&points.view()).expect("Operation failed");
22//! let perim = polygon_perimeter(&points.view());
23//! ```
24
25use crate::error::{SpatialError, SpatialResult};
26use scirs2_core::ndarray::{Array2, ArrayView2};
27
28/// An axis-aligned bounding box defined by its minimum and maximum coordinates
29#[derive(Debug, Clone)]
30pub struct AABB {
31    /// Minimum corner (min_x, min_y)
32    pub min: [f64; 2],
33    /// Maximum corner (max_x, max_y)
34    pub max: [f64; 2],
35}
36
37impl AABB {
38    /// Width of the bounding box (x-extent)
39    pub fn width(&self) -> f64 {
40        self.max[0] - self.min[0]
41    }
42
43    /// Height of the bounding box (y-extent)
44    pub fn height(&self) -> f64 {
45        self.max[1] - self.min[1]
46    }
47
48    /// Area of the bounding box
49    pub fn area(&self) -> f64 {
50        self.width() * self.height()
51    }
52
53    /// Perimeter of the bounding box
54    pub fn perimeter(&self) -> f64 {
55        2.0 * (self.width() + self.height())
56    }
57
58    /// Center of the bounding box
59    pub fn center(&self) -> [f64; 2] {
60        [
61            (self.min[0] + self.max[0]) / 2.0,
62            (self.min[1] + self.max[1]) / 2.0,
63        ]
64    }
65
66    /// Check if a point is inside the bounding box
67    pub fn contains(&self, point: &[f64; 2]) -> bool {
68        point[0] >= self.min[0]
69            && point[0] <= self.max[0]
70            && point[1] >= self.min[1]
71            && point[1] <= self.max[1]
72    }
73
74    /// Check if this AABB intersects another AABB
75    pub fn intersects(&self, other: &AABB) -> bool {
76        self.min[0] <= other.max[0]
77            && self.max[0] >= other.min[0]
78            && self.min[1] <= other.max[1]
79            && self.max[1] >= other.min[1]
80    }
81}
82
83/// An oriented bounding rectangle defined by center, axes, and half-extents
84#[derive(Debug, Clone)]
85pub struct OrientedBoundingRect {
86    /// Center of the rectangle
87    pub center: [f64; 2],
88    /// First axis direction (unit vector)
89    pub axis_u: [f64; 2],
90    /// Second axis direction (unit vector, perpendicular to axis_u)
91    pub axis_v: [f64; 2],
92    /// Half-extent along axis_u
93    pub half_width: f64,
94    /// Half-extent along axis_v
95    pub half_height: f64,
96    /// Rotation angle in radians (angle of axis_u from positive x-axis)
97    pub angle: f64,
98    /// The four corner vertices of the rectangle
99    pub corners: [[f64; 2]; 4],
100}
101
102impl OrientedBoundingRect {
103    /// Area of the oriented bounding rectangle
104    pub fn area(&self) -> f64 {
105        4.0 * self.half_width * self.half_height
106    }
107
108    /// Perimeter of the oriented bounding rectangle
109    pub fn perimeter(&self) -> f64 {
110        4.0 * (self.half_width + self.half_height)
111    }
112
113    /// Check if a point is inside the oriented bounding rectangle
114    pub fn contains(&self, point: &[f64; 2]) -> bool {
115        // Project the point onto the rectangle's local coordinate system
116        let dx = point[0] - self.center[0];
117        let dy = point[1] - self.center[1];
118
119        let proj_u = dx * self.axis_u[0] + dy * self.axis_u[1];
120        let proj_v = dx * self.axis_v[0] + dy * self.axis_v[1];
121
122        proj_u.abs() <= self.half_width && proj_v.abs() <= self.half_height
123    }
124}
125
126/// Compute the axis-aligned bounding box of a set of 2D points
127///
128/// # Arguments
129///
130/// * `points` - A 2D array of points (n x 2)
131///
132/// # Returns
133///
134/// * `SpatialResult<AABB>` - The axis-aligned bounding box
135///
136/// # Examples
137///
138/// ```
139/// use scirs2_spatial::computational_geometry::bounding::axis_aligned_bounding_box;
140/// use scirs2_core::ndarray::array;
141///
142/// let points = array![[0.0, 1.0], [2.0, 3.0], [1.0, 0.5]];
143/// let aabb = axis_aligned_bounding_box(&points.view()).expect("Operation failed");
144/// assert!((aabb.min[0] - 0.0).abs() < 1e-10);
145/// assert!((aabb.max[0] - 2.0).abs() < 1e-10);
146/// ```
147pub fn axis_aligned_bounding_box(points: &ArrayView2<'_, f64>) -> SpatialResult<AABB> {
148    if points.nrows() == 0 {
149        return Err(SpatialError::ValueError(
150            "Cannot compute bounding box of empty point set".to_string(),
151        ));
152    }
153    if points.ncols() != 2 {
154        return Err(SpatialError::DimensionError(
155            "Points must be 2D for bounding box computation".to_string(),
156        ));
157    }
158
159    let mut min_x = f64::INFINITY;
160    let mut min_y = f64::INFINITY;
161    let mut max_x = f64::NEG_INFINITY;
162    let mut max_y = f64::NEG_INFINITY;
163
164    for i in 0..points.nrows() {
165        let x = points[[i, 0]];
166        let y = points[[i, 1]];
167        if x < min_x {
168            min_x = x;
169        }
170        if y < min_y {
171            min_y = y;
172        }
173        if x > max_x {
174            max_x = x;
175        }
176        if y > max_y {
177            max_y = y;
178        }
179    }
180
181    Ok(AABB {
182        min: [min_x, min_y],
183        max: [max_x, max_y],
184    })
185}
186
187/// Compute the minimum area bounding rectangle of a set of 2D points
188/// using the rotating calipers algorithm on the convex hull.
189///
190/// The minimum bounding rectangle is the rectangle with the smallest area
191/// that contains all the input points. One edge of this rectangle is always
192/// collinear with an edge of the convex hull.
193///
194/// # Algorithm
195///
196/// 1. Compute the convex hull of the points
197/// 2. For each edge of the hull, compute the bounding rectangle aligned to that edge
198/// 3. Return the rectangle with minimum area
199///
200/// Time complexity: O(n log n) for hull computation + O(h) for rotating calipers,
201/// where h is the number of hull vertices.
202///
203/// # Arguments
204///
205/// * `points` - A 2D array of points (n x 2)
206///
207/// # Returns
208///
209/// * `SpatialResult<OrientedBoundingRect>` - The minimum area bounding rectangle
210///
211/// # Examples
212///
213/// ```
214/// use scirs2_spatial::computational_geometry::bounding::minimum_bounding_rectangle;
215/// use scirs2_core::ndarray::array;
216///
217/// // A rotated square
218/// let points = array![
219///     [1.0, 0.0],
220///     [2.0, 1.0],
221///     [1.0, 2.0],
222///     [0.0, 1.0],
223/// ];
224///
225/// let mbr = minimum_bounding_rectangle(&points.view()).expect("Operation failed");
226/// // Area should be 2.0 (side = sqrt(2), area = 2)
227/// assert!((mbr.area() - 2.0).abs() < 0.1);
228/// ```
229pub fn minimum_bounding_rectangle(
230    points: &ArrayView2<'_, f64>,
231) -> SpatialResult<OrientedBoundingRect> {
232    if points.nrows() < 3 {
233        return Err(SpatialError::ValueError(
234            "Need at least 3 points for minimum bounding rectangle".to_string(),
235        ));
236    }
237    if points.ncols() != 2 {
238        return Err(SpatialError::DimensionError(
239            "Points must be 2D for bounding rectangle computation".to_string(),
240        ));
241    }
242
243    // Compute convex hull using Graham scan
244    let hull = compute_convex_hull_ccw(points)?;
245    let n = hull.len();
246
247    if n < 3 {
248        return Err(SpatialError::ComputationError(
249            "Convex hull has fewer than 3 vertices (degenerate case)".to_string(),
250        ));
251    }
252
253    let mut best_area = f64::INFINITY;
254    let mut best_rect: Option<OrientedBoundingRect> = None;
255
256    // For each edge of the convex hull, compute the aligned bounding rectangle
257    for i in 0..n {
258        let j = (i + 1) % n;
259
260        // Edge direction
261        let edge_dx = hull[j][0] - hull[i][0];
262        let edge_dy = hull[j][1] - hull[i][1];
263        let edge_len = (edge_dx * edge_dx + edge_dy * edge_dy).sqrt();
264
265        if edge_len < 1e-15 {
266            continue;
267        }
268
269        // Unit vectors along and perpendicular to the edge
270        let ux = edge_dx / edge_len;
271        let uy = edge_dy / edge_len;
272        let vx = -uy;
273        let vy = ux;
274
275        // Project all hull points onto the edge coordinate system
276        let mut min_u = f64::INFINITY;
277        let mut max_u = f64::NEG_INFINITY;
278        let mut min_v = f64::INFINITY;
279        let mut max_v = f64::NEG_INFINITY;
280
281        for pt in &hull {
282            let dx = pt[0] - hull[i][0];
283            let dy = pt[1] - hull[i][1];
284
285            let proj_u = dx * ux + dy * uy;
286            let proj_v = dx * vx + dy * vy;
287
288            if proj_u < min_u {
289                min_u = proj_u;
290            }
291            if proj_u > max_u {
292                max_u = proj_u;
293            }
294            if proj_v < min_v {
295                min_v = proj_v;
296            }
297            if proj_v > max_v {
298                max_v = proj_v;
299            }
300        }
301
302        let width = max_u - min_u;
303        let height = max_v - min_v;
304        let area = width * height;
305
306        if area < best_area {
307            best_area = area;
308
309            // Compute the center of the rectangle
310            let center_u = (min_u + max_u) / 2.0;
311            let center_v = (min_v + max_v) / 2.0;
312
313            let center_x = hull[i][0] + center_u * ux + center_v * vx;
314            let center_y = hull[i][1] + center_u * uy + center_v * vy;
315
316            // Compute corners
317            let hw = width / 2.0;
318            let hh = height / 2.0;
319
320            let corners = [
321                [center_x - hw * ux - hh * vx, center_y - hw * uy - hh * vy],
322                [center_x + hw * ux - hh * vx, center_y + hw * uy - hh * vy],
323                [center_x + hw * ux + hh * vx, center_y + hw * uy + hh * vy],
324                [center_x - hw * ux + hh * vx, center_y - hw * uy + hh * vy],
325            ];
326
327            let angle = uy.atan2(ux);
328
329            best_rect = Some(OrientedBoundingRect {
330                center: [center_x, center_y],
331                axis_u: [ux, uy],
332                axis_v: [vx, vy],
333                half_width: hw,
334                half_height: hh,
335                angle,
336                corners,
337            });
338        }
339    }
340
341    best_rect.ok_or_else(|| {
342        SpatialError::ComputationError("Failed to compute minimum bounding rectangle".to_string())
343    })
344}
345
346/// Compute the perimeter (circumference) of a polygon defined by ordered vertices
347///
348/// # Arguments
349///
350/// * `vertices` - Polygon vertices in order (n x 2)
351///
352/// # Returns
353///
354/// * The perimeter of the polygon
355///
356/// # Examples
357///
358/// ```
359/// use scirs2_spatial::computational_geometry::bounding::polygon_perimeter;
360/// use scirs2_core::ndarray::array;
361///
362/// let square = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
363/// let perim = polygon_perimeter(&square.view());
364/// assert!((perim - 4.0).abs() < 1e-10);
365/// ```
366pub fn polygon_perimeter(vertices: &ArrayView2<'_, f64>) -> f64 {
367    let n = vertices.nrows();
368    if n < 2 {
369        return 0.0;
370    }
371
372    let mut perimeter = 0.0;
373    for i in 0..n {
374        let j = (i + 1) % n;
375        let dx = vertices[[j, 0]] - vertices[[i, 0]];
376        let dy = vertices[[j, 1]] - vertices[[i, 1]];
377        perimeter += (dx * dx + dy * dy).sqrt();
378    }
379
380    perimeter
381}
382
383/// Compute the signed area of a polygon (positive for CCW, negative for CW)
384///
385/// Uses the Shoelace formula.
386///
387/// # Arguments
388///
389/// * `vertices` - Polygon vertices in order (n x 2)
390///
391/// # Returns
392///
393/// * The signed area (positive for counter-clockwise orientation)
394pub fn signed_polygon_area(vertices: &ArrayView2<'_, f64>) -> f64 {
395    let n = vertices.nrows();
396    if n < 3 {
397        return 0.0;
398    }
399
400    let mut area = 0.0;
401    for i in 0..n {
402        let j = (i + 1) % n;
403        area += vertices[[i, 0]] * vertices[[j, 1]] - vertices[[j, 0]] * vertices[[i, 1]];
404    }
405
406    area / 2.0
407}
408
409/// Compute the area and perimeter of a convex hull given as a set of points
410///
411/// First computes the convex hull, then returns its area and perimeter.
412///
413/// # Arguments
414///
415/// * `points` - Input points (n x 2)
416///
417/// # Returns
418///
419/// * `SpatialResult<(f64, f64)>` - (area, perimeter) of the convex hull
420///
421/// # Examples
422///
423/// ```
424/// use scirs2_spatial::computational_geometry::bounding::convex_hull_area_perimeter;
425/// use scirs2_core::ndarray::array;
426///
427/// let points = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.5, 0.5]];
428/// let (area, perim) = convex_hull_area_perimeter(&points.view()).expect("Operation failed");
429/// assert!((area - 1.0).abs() < 1e-10);
430/// assert!((perim - 4.0).abs() < 1e-10);
431/// ```
432pub fn convex_hull_area_perimeter(points: &ArrayView2<'_, f64>) -> SpatialResult<(f64, f64)> {
433    if points.nrows() < 3 {
434        return Err(SpatialError::ValueError(
435            "Need at least 3 points".to_string(),
436        ));
437    }
438
439    let hull = compute_convex_hull_ccw(points)?;
440    let n = hull.len();
441
442    // Compute area using Shoelace formula
443    let mut area = 0.0;
444    for i in 0..n {
445        let j = (i + 1) % n;
446        area += hull[i][0] * hull[j][1] - hull[j][0] * hull[i][1];
447    }
448    area = area.abs() / 2.0;
449
450    // Compute perimeter
451    let mut perimeter = 0.0;
452    for i in 0..n {
453        let j = (i + 1) % n;
454        let dx = hull[j][0] - hull[i][0];
455        let dy = hull[j][1] - hull[i][1];
456        perimeter += (dx * dx + dy * dy).sqrt();
457    }
458
459    Ok((area, perimeter))
460}
461
462/// Compute just the convex hull area for a point set
463///
464/// # Arguments
465///
466/// * `points` - Input points (n x 2)
467///
468/// # Returns
469///
470/// * `SpatialResult<f64>` - Area of the convex hull
471pub fn convex_hull_area(points: &ArrayView2<'_, f64>) -> SpatialResult<f64> {
472    let (area, _) = convex_hull_area_perimeter(points)?;
473    Ok(area)
474}
475
476/// Compute just the convex hull perimeter for a point set
477///
478/// # Arguments
479///
480/// * `points` - Input points (n x 2)
481///
482/// # Returns
483///
484/// * `SpatialResult<f64>` - Perimeter of the convex hull
485pub fn convex_hull_perimeter(points: &ArrayView2<'_, f64>) -> SpatialResult<f64> {
486    let (_, perim) = convex_hull_area_perimeter(points)?;
487    Ok(perim)
488}
489
490/// Determine if a polygon (given as ordered vertices) is convex
491///
492/// # Arguments
493///
494/// * `vertices` - Polygon vertices in order (n x 2)
495///
496/// # Returns
497///
498/// * `bool` - True if the polygon is convex
499pub fn is_convex(vertices: &ArrayView2<'_, f64>) -> bool {
500    let n = vertices.nrows();
501    if n < 3 {
502        return false;
503    }
504
505    let mut sign = 0i32;
506
507    for i in 0..n {
508        let j = (i + 1) % n;
509        let k = (i + 2) % n;
510
511        let dx1 = vertices[[j, 0]] - vertices[[i, 0]];
512        let dy1 = vertices[[j, 1]] - vertices[[i, 1]];
513        let dx2 = vertices[[k, 0]] - vertices[[j, 0]];
514        let dy2 = vertices[[k, 1]] - vertices[[j, 1]];
515
516        let cross = dx1 * dy2 - dy1 * dx2;
517
518        if cross.abs() > 1e-10 {
519            let current_sign = if cross > 0.0 { 1 } else { -1 };
520            if sign == 0 {
521                sign = current_sign;
522            } else if sign != current_sign {
523                return false;
524            }
525        }
526    }
527
528    true
529}
530
531/// Compute the diameter of a point set (maximum distance between any two points)
532///
533/// Uses the rotating calipers method on the convex hull for efficiency.
534///
535/// # Arguments
536///
537/// * `points` - Input points (n x 2)
538///
539/// # Returns
540///
541/// * `SpatialResult<(f64, [f64; 2], [f64; 2])>` - (diameter, point_a, point_b)
542pub fn point_set_diameter(
543    points: &ArrayView2<'_, f64>,
544) -> SpatialResult<(f64, [f64; 2], [f64; 2])> {
545    if points.nrows() < 2 {
546        return Err(SpatialError::ValueError(
547            "Need at least 2 points to compute diameter".to_string(),
548        ));
549    }
550
551    let hull = compute_convex_hull_ccw(points)?;
552    let n = hull.len();
553
554    if n < 2 {
555        return Err(SpatialError::ComputationError(
556            "Convex hull has fewer than 2 vertices".to_string(),
557        ));
558    }
559
560    // Rotating calipers to find antipodal pairs
561    let mut max_dist = 0.0;
562    let mut best_a = hull[0];
563    let mut best_b = hull[1];
564
565    // Find initial antipodal pair: start at the bottom and top of the hull
566    let mut j = 1;
567
568    for i in 0..n {
569        let i_next = (i + 1) % n;
570
571        // Edge direction
572        let edge_x = hull[i_next][0] - hull[i][0];
573        let edge_y = hull[i_next][1] - hull[i][1];
574
575        // Advance j until the cross product starts decreasing
576        loop {
577            let j_next = (j + 1) % n;
578            let cross_curr =
579                edge_x * (hull[j_next][1] - hull[j][1]) - edge_y * (hull[j_next][0] - hull[j][0]);
580
581            if cross_curr > 0.0 {
582                j = j_next;
583            } else {
584                break;
585            }
586        }
587
588        // Check distance between i and j
589        let dx = hull[j][0] - hull[i][0];
590        let dy = hull[j][1] - hull[i][1];
591        let dist = (dx * dx + dy * dy).sqrt();
592
593        if dist > max_dist {
594            max_dist = dist;
595            best_a = hull[i];
596            best_b = hull[j];
597        }
598    }
599
600    Ok((max_dist, best_a, best_b))
601}
602
603/// Compute the width of a point set (minimum distance across)
604///
605/// The width is the minimum distance between two parallel lines that enclose all points.
606///
607/// # Arguments
608///
609/// * `points` - Input points (n x 2)
610///
611/// # Returns
612///
613/// * `SpatialResult<f64>` - The width of the point set
614pub fn point_set_width(points: &ArrayView2<'_, f64>) -> SpatialResult<f64> {
615    if points.nrows() < 3 {
616        return Err(SpatialError::ValueError(
617            "Need at least 3 points to compute width".to_string(),
618        ));
619    }
620
621    let hull = compute_convex_hull_ccw(points)?;
622    let n = hull.len();
623
624    let mut min_width = f64::INFINITY;
625
626    // For each edge of the hull, find the farthest point and compute the distance
627    for i in 0..n {
628        let j = (i + 1) % n;
629
630        let edge_dx = hull[j][0] - hull[i][0];
631        let edge_dy = hull[j][1] - hull[i][1];
632        let edge_len = (edge_dx * edge_dx + edge_dy * edge_dy).sqrt();
633
634        if edge_len < 1e-15 {
635            continue;
636        }
637
638        // Normal direction (perpendicular to edge)
639        let nx = -edge_dy / edge_len;
640        let ny = edge_dx / edge_len;
641
642        // Project all hull points onto the normal direction
643        let mut min_proj = f64::INFINITY;
644        let mut max_proj = f64::NEG_INFINITY;
645
646        for pt in &hull {
647            let proj = pt[0] * nx + pt[1] * ny;
648            if proj < min_proj {
649                min_proj = proj;
650            }
651            if proj > max_proj {
652                max_proj = proj;
653            }
654        }
655
656        let width = max_proj - min_proj;
657        if width < min_width {
658            min_width = width;
659        }
660    }
661
662    Ok(min_width)
663}
664
665/// Internal: compute the convex hull of a point set in CCW order (Graham scan)
666fn compute_convex_hull_ccw(points: &ArrayView2<'_, f64>) -> SpatialResult<Vec<[f64; 2]>> {
667    let n = points.nrows();
668
669    if n < 3 {
670        let mut hull = Vec::with_capacity(n);
671        for i in 0..n {
672            hull.push([points[[i, 0]], points[[i, 1]]]);
673        }
674        return Ok(hull);
675    }
676
677    // Find the lowest (and leftmost) point
678    let mut lowest = 0;
679    for i in 1..n {
680        if points[[i, 1]] < points[[lowest, 1]]
681            || (points[[i, 1]] == points[[lowest, 1]] && points[[i, 0]] < points[[lowest, 0]])
682        {
683            lowest = i;
684        }
685    }
686
687    let pivot_x = points[[lowest, 0]];
688    let pivot_y = points[[lowest, 1]];
689
690    // Sort points by polar angle relative to pivot
691    let mut indexed: Vec<(usize, f64)> = (0..n)
692        .map(|i| {
693            let dx = points[[i, 0]] - pivot_x;
694            let dy = points[[i, 1]] - pivot_y;
695            let angle = dy.atan2(dx);
696            (i, angle)
697        })
698        .collect();
699
700    indexed.sort_by(|a, b| {
701        let angle_cmp = a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal);
702        if angle_cmp == std::cmp::Ordering::Equal {
703            let dist_a =
704                (points[[a.0, 0]] - pivot_x).powi(2) + (points[[a.0, 1]] - pivot_y).powi(2);
705            let dist_b =
706                (points[[b.0, 0]] - pivot_x).powi(2) + (points[[b.0, 1]] - pivot_y).powi(2);
707            dist_a
708                .partial_cmp(&dist_b)
709                .unwrap_or(std::cmp::Ordering::Equal)
710        } else {
711            angle_cmp
712        }
713    });
714
715    // Graham scan
716    let mut hull_indices = vec![lowest];
717
718    for &(idx, _) in &indexed {
719        if idx == lowest {
720            continue;
721        }
722
723        while hull_indices.len() >= 2 {
724            let top = hull_indices.len() - 1;
725            let a = hull_indices[top - 1];
726            let b = hull_indices[top];
727            let c = idx;
728
729            let cross = (points[[b, 0]] - points[[a, 0]]) * (points[[c, 1]] - points[[a, 1]])
730                - (points[[b, 1]] - points[[a, 1]]) * (points[[c, 0]] - points[[a, 0]]);
731
732            if cross > 0.0 {
733                break;
734            }
735            hull_indices.pop();
736        }
737
738        hull_indices.push(idx);
739    }
740
741    let hull: Vec<[f64; 2]> = hull_indices
742        .iter()
743        .map(|&i| [points[[i, 0]], points[[i, 1]]])
744        .collect();
745
746    Ok(hull)
747}
748
749#[cfg(test)]
750mod tests {
751    use super::*;
752    use scirs2_core::ndarray::array;
753
754    #[test]
755    fn test_aabb_basic() {
756        let points = array![[0.0, 0.0], [2.0, 3.0], [1.0, 1.0], [-1.0, 2.0]];
757        let aabb = axis_aligned_bounding_box(&points.view()).expect("Operation failed");
758
759        assert!((aabb.min[0] - (-1.0)).abs() < 1e-10);
760        assert!((aabb.min[1] - 0.0).abs() < 1e-10);
761        assert!((aabb.max[0] - 2.0).abs() < 1e-10);
762        assert!((aabb.max[1] - 3.0).abs() < 1e-10);
763        assert!((aabb.area() - 9.0).abs() < 1e-10);
764        assert!((aabb.perimeter() - 12.0).abs() < 1e-10);
765    }
766
767    #[test]
768    fn test_aabb_contains() {
769        let points = array![[0.0, 0.0], [2.0, 2.0]];
770        let aabb = axis_aligned_bounding_box(&points.view()).expect("Operation failed");
771
772        assert!(aabb.contains(&[1.0, 1.0]));
773        assert!(aabb.contains(&[0.0, 0.0]));
774        assert!(aabb.contains(&[2.0, 2.0]));
775        assert!(!aabb.contains(&[3.0, 1.0]));
776        assert!(!aabb.contains(&[-1.0, 1.0]));
777    }
778
779    #[test]
780    fn test_aabb_intersects() {
781        let p1 = array![[0.0, 0.0], [2.0, 2.0]];
782        let p2 = array![[1.0, 1.0], [3.0, 3.0]];
783        let p3 = array![[5.0, 5.0], [6.0, 6.0]];
784
785        let aabb1 = axis_aligned_bounding_box(&p1.view()).expect("Operation failed");
786        let aabb2 = axis_aligned_bounding_box(&p2.view()).expect("Operation failed");
787        let aabb3 = axis_aligned_bounding_box(&p3.view()).expect("Operation failed");
788
789        assert!(aabb1.intersects(&aabb2));
790        assert!(!aabb1.intersects(&aabb3));
791    }
792
793    #[test]
794    fn test_aabb_center() {
795        let points = array![[0.0, 0.0], [4.0, 6.0]];
796        let aabb = axis_aligned_bounding_box(&points.view()).expect("Operation failed");
797        let center = aabb.center();
798        assert!((center[0] - 2.0).abs() < 1e-10);
799        assert!((center[1] - 3.0).abs() < 1e-10);
800    }
801
802    #[test]
803    fn test_aabb_empty_input() {
804        let points = Array2::<f64>::zeros((0, 2));
805        let result = axis_aligned_bounding_box(&points.view());
806        assert!(result.is_err());
807    }
808
809    #[test]
810    fn test_polygon_perimeter_square() {
811        let square = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
812        let perim = polygon_perimeter(&square.view());
813        assert!((perim - 4.0).abs() < 1e-10);
814    }
815
816    #[test]
817    fn test_polygon_perimeter_triangle() {
818        let triangle = array![[0.0, 0.0], [3.0, 0.0], [0.0, 4.0]];
819        let perim = polygon_perimeter(&triangle.view());
820        // 3 + 4 + 5 = 12
821        assert!((perim - 12.0).abs() < 1e-10);
822    }
823
824    #[test]
825    fn test_signed_area() {
826        // CCW square -> positive area
827        let ccw = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
828        let area = signed_polygon_area(&ccw.view());
829        assert!((area - 1.0).abs() < 1e-10);
830
831        // CW square -> negative area
832        let cw = array![[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]];
833        let area = signed_polygon_area(&cw.view());
834        assert!((area - (-1.0)).abs() < 1e-10);
835    }
836
837    #[test]
838    fn test_convex_hull_area_perimeter_square() {
839        let points = array![
840            [0.0, 0.0],
841            [1.0, 0.0],
842            [1.0, 1.0],
843            [0.0, 1.0],
844            [0.5, 0.5] // interior point
845        ];
846
847        let (area, perim) = convex_hull_area_perimeter(&points.view()).expect("Operation failed");
848        assert!((area - 1.0).abs() < 1e-10);
849        assert!((perim - 4.0).abs() < 1e-10);
850    }
851
852    #[test]
853    fn test_convex_hull_area_function() {
854        let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
855        let area = convex_hull_area(&points.view()).expect("Operation failed");
856        assert!((area - 0.5).abs() < 1e-10);
857    }
858
859    #[test]
860    fn test_convex_hull_perimeter_function() {
861        let points = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
862        let perim = convex_hull_perimeter(&points.view()).expect("Operation failed");
863        assert!((perim - 4.0).abs() < 1e-10);
864    }
865
866    #[test]
867    fn test_minimum_bounding_rectangle_square() {
868        let points = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
869        let mbr = minimum_bounding_rectangle(&points.view()).expect("Operation failed");
870
871        // For an axis-aligned square, the MBR should have area 1.0
872        assert!((mbr.area() - 1.0).abs() < 0.1);
873    }
874
875    #[test]
876    fn test_minimum_bounding_rectangle_triangle() {
877        let points = array![[0.0, 0.0], [2.0, 0.0], [1.0, 1.0]];
878        let mbr = minimum_bounding_rectangle(&points.view()).expect("Operation failed");
879
880        // MBR area should be less than or equal to AABB area
881        let aabb = axis_aligned_bounding_box(&points.view()).expect("Operation failed");
882        assert!(mbr.area() <= aabb.area() + 1e-6);
883    }
884
885    #[test]
886    fn test_minimum_bounding_rectangle_contains_all_points() {
887        let points = array![[0.0, 0.0], [3.0, 1.0], [2.0, 4.0], [-1.0, 3.0], [1.0, 2.0]];
888
889        let mbr = minimum_bounding_rectangle(&points.view()).expect("Operation failed");
890
891        // All points should be inside the MBR (with tolerance)
892        for i in 0..points.nrows() {
893            let pt = [points[[i, 0]], points[[i, 1]]];
894            // Expand the check tolerance slightly
895            let dx = pt[0] - mbr.center[0];
896            let dy = pt[1] - mbr.center[1];
897            let proj_u = dx * mbr.axis_u[0] + dy * mbr.axis_u[1];
898            let proj_v = dx * mbr.axis_v[0] + dy * mbr.axis_v[1];
899            assert!(
900                proj_u.abs() <= mbr.half_width + 1e-6,
901                "Point {} outside MBR along u: {} > {}",
902                i,
903                proj_u.abs(),
904                mbr.half_width
905            );
906            assert!(
907                proj_v.abs() <= mbr.half_height + 1e-6,
908                "Point {} outside MBR along v: {} > {}",
909                i,
910                proj_v.abs(),
911                mbr.half_height
912            );
913        }
914    }
915
916    #[test]
917    fn test_is_convex() {
918        let convex = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
919        assert!(is_convex(&convex.view()));
920
921        let concave = array![
922            [0.0, 0.0],
923            [2.0, 0.0],
924            [2.0, 2.0],
925            [1.0, 0.5], // dent inward
926            [0.0, 2.0]
927        ];
928        assert!(!is_convex(&concave.view()));
929    }
930
931    #[test]
932    fn test_point_set_diameter() {
933        let points = array![[0.0, 0.0], [3.0, 4.0], [1.0, 1.0]];
934        let (diameter, _, _) = point_set_diameter(&points.view()).expect("Operation failed");
935        assert!((diameter - 5.0).abs() < 1e-10);
936    }
937
938    #[test]
939    fn test_point_set_diameter_square() {
940        let points = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
941        let (diameter, _, _) = point_set_diameter(&points.view()).expect("Operation failed");
942        // Diagonal of unit square = sqrt(2)
943        assert!((diameter - std::f64::consts::SQRT_2).abs() < 1e-10);
944    }
945
946    #[test]
947    fn test_point_set_width() {
948        let points = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
949        let width = point_set_width(&points.view()).expect("Operation failed");
950        assert!((width - 1.0).abs() < 1e-10);
951    }
952
953    #[test]
954    fn test_oriented_bounding_rect_contains() {
955        let points = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
956        let mbr = minimum_bounding_rectangle(&points.view()).expect("Operation failed");
957
958        assert!(mbr.contains(&[0.5, 0.5]));
959        assert!(!mbr.contains(&[5.0, 5.0]));
960    }
961}