Skip to main content

oxigdal_algorithms/vector/
min_bounds.rs

1//! Minimum bounding geometry algorithms.
2//!
3//! This module provides two fundamental bounding-geometry computations:
4//!
5//! 1. **Minimum-area rotated rectangle** via the rotating-calipers method on the
6//!    convex hull.  Expected O(n log n) dominated by the hull computation.
7//!
8//! 2. **Smallest enclosing circle** via Welzl's randomised incremental algorithm.
9//!    Expected O(n) after a one-time LCG shuffle (no `rand` dependency).
10//!
11//! # Stack depth note
12//!
13//! The recursive Welzl implementation has worst-case stack depth O(n).  For
14//! inputs larger than ~100 000 points the recursion may overflow the default
15//! thread stack.  If you need to handle very large point clouds, either run the
16//! call in a thread spawned with a larger stack size, or switch to the iterative
17//! Ritter / Shamos–Hoey version.
18
19use crate::error::Result;
20use crate::vector::convex_hull;
21use oxigdal_core::vector::Coordinate;
22
23// ─────────────────────────────────────────────────────────────────────────────
24// Public types
25// ─────────────────────────────────────────────────────────────────────────────
26
27/// A minimum-area bounding rectangle (possibly rotated).
28#[derive(Debug, Clone, PartialEq)]
29pub struct RotatedRect {
30    /// Centre of the rectangle in world coordinates.
31    pub center: Coordinate,
32    /// Extent along the primary axis (the edge direction, `angle_rad`).
33    pub width: f64,
34    /// Extent perpendicular to the primary axis.
35    pub height: f64,
36    /// Orientation in radians, counter-clockwise from the +X axis.
37    pub angle_rad: f64,
38}
39
40impl RotatedRect {
41    /// Returns the four corner points in CCW order.
42    pub fn corners(&self) -> [Coordinate; 4] {
43        let (cos_a, sin_a) = (self.angle_rad.cos(), self.angle_rad.sin());
44        let hw = self.width * 0.5;
45        let hh = self.height * 0.5;
46        // Local offsets: (±hw, ±hh)
47        let local: [(f64, f64); 4] = [(hw, hh), (-hw, hh), (-hw, -hh), (hw, -hh)];
48        local.map(|(lx, ly)| {
49            Coordinate::new_2d(
50                self.center.x + lx * cos_a - ly * sin_a,
51                self.center.y + lx * sin_a + ly * cos_a,
52            )
53        })
54    }
55
56    /// Area of the rectangle.
57    #[inline]
58    pub fn area(&self) -> f64 {
59        self.width * self.height
60    }
61}
62
63/// A circle defined by its centre and radius.
64#[derive(Debug, Clone, PartialEq)]
65pub struct Circle {
66    /// Centre of the circle.
67    pub center: Coordinate,
68    /// Radius of the circle (always ≥ 0).
69    pub radius: f64,
70}
71
72impl Circle {
73    /// Returns `true` when `p` is inside or on the boundary of the circle.
74    ///
75    /// A small epsilon (1e-10) is added to the radius to absorb floating-point
76    /// rounding that arises during the Welzl recursion.
77    #[inline]
78    pub fn contains(&self, p: Coordinate) -> bool {
79        let dx = p.x - self.center.x;
80        let dy = p.y - self.center.y;
81        dx * dx + dy * dy <= (self.radius + 1e-10) * (self.radius + 1e-10)
82    }
83
84    /// The degenerate circle at the origin with zero radius.
85    #[inline]
86    pub fn zero() -> Self {
87        Self {
88            center: Coordinate::new_2d(0.0, 0.0),
89            radius: 0.0,
90        }
91    }
92}
93
94// ─────────────────────────────────────────────────────────────────────────────
95// Axis-aligned bounding box
96// ─────────────────────────────────────────────────────────────────────────────
97
98/// Computes the axis-aligned bounding box of a point set.
99///
100/// Returns `(min_x, min_y, max_x, max_y)`, or `None` for empty input.
101pub fn aabb(points: &[Coordinate]) -> Option<(f64, f64, f64, f64)> {
102    if points.is_empty() {
103        return None;
104    }
105    let mut min_x = points[0].x;
106    let mut min_y = points[0].y;
107    let mut max_x = min_x;
108    let mut max_y = min_y;
109    for c in &points[1..] {
110        if c.x < min_x {
111            min_x = c.x;
112        }
113        if c.y < min_y {
114            min_y = c.y;
115        }
116        if c.x > max_x {
117            max_x = c.x;
118        }
119        if c.y > max_y {
120            max_y = c.y;
121        }
122    }
123    Some((min_x, min_y, max_x, max_y))
124}
125
126// ─────────────────────────────────────────────────────────────────────────────
127// Minimum-area rotated rectangle (rotating calipers)
128// ─────────────────────────────────────────────────────────────────────────────
129
130/// Computes the minimum-area bounding rectangle for a point set.
131///
132/// Uses the *rotating calipers* technique on the convex hull.  The algorithm
133/// iterates over each hull edge, projects all hull vertices onto the edge's
134/// local frame, and records the extent.  The orientation that minimises the
135/// product of the two extents is returned.
136///
137/// Returns `None` if the input has fewer than 2 points.
138pub fn min_area_rotated_rect(points: &[Coordinate]) -> Option<RotatedRect> {
139    if points.is_empty() {
140        return None;
141    }
142    if points.len() == 1 {
143        return Some(RotatedRect {
144            center: points[0],
145            width: 0.0,
146            height: 0.0,
147            angle_rad: 0.0,
148        });
149    }
150    if points.len() == 2 {
151        let (p, q) = (points[0], points[1]);
152        let dx = q.x - p.x;
153        let dy = q.y - p.y;
154        let length = (dx * dx + dy * dy).sqrt();
155        let angle = dy.atan2(dx);
156        return Some(RotatedRect {
157            center: Coordinate::new_2d((p.x + q.x) * 0.5, (p.y + q.y) * 0.5),
158            width: length,
159            height: 0.0,
160            angle_rad: angle,
161        });
162    }
163
164    // --- convex hull --------------------------------------------------------
165    // convex_hull returns Err for < 3 points; we already handled that above.
166    let hull: Vec<Coordinate> = match convex_hull(points) {
167        Ok(h) => h,
168        Err(_) => return None,
169    };
170    let n = hull.len();
171    if n < 2 {
172        return None;
173    }
174    // For a degenerate two-point hull (collinear case), fall through to the
175    // edge-iteration loop which will handle it correctly.
176
177    // --- rotating calipers --------------------------------------------------
178    let mut best_area = f64::INFINITY;
179    let mut best_rect: Option<RotatedRect> = None;
180
181    for i in 0..n {
182        let j = (i + 1) % n;
183        let edge_x = hull[j].x - hull[i].x;
184        let edge_y = hull[j].y - hull[i].y;
185        let edge_len = (edge_x * edge_x + edge_y * edge_y).sqrt();
186        if edge_len < 1e-12 {
187            continue;
188        }
189        // Unit vector along the edge (u-axis)
190        let ux = edge_x / edge_len;
191        let uy = edge_y / edge_len;
192        // Perpendicular unit vector (v-axis, 90° CCW)
193        let vx = -uy;
194        let vy = ux;
195
196        // Project all hull points onto (u, v)
197        let mut min_u = f64::INFINITY;
198        let mut max_u = f64::NEG_INFINITY;
199        let mut min_v = f64::INFINITY;
200        let mut max_v = f64::NEG_INFINITY;
201        for c in &hull {
202            let u = c.x * ux + c.y * uy;
203            let v = c.x * vx + c.y * vy;
204            if u < min_u {
205                min_u = u;
206            }
207            if u > max_u {
208                max_u = u;
209            }
210            if v < min_v {
211                min_v = v;
212            }
213            if v > max_v {
214                max_v = v;
215            }
216        }
217
218        let w = max_u - min_u;
219        let h = max_v - min_v;
220        let area = w * h;
221
222        if area < best_area {
223            best_area = area;
224
225            // Centre in world coordinates:
226            //   c_world = c_u * u_hat + c_v * v_hat
227            let cu = (min_u + max_u) * 0.5;
228            let cv = (min_v + max_v) * 0.5;
229            let cx = cu * ux + cv * vx;
230            let cy = cu * uy + cv * vy;
231
232            best_rect = Some(RotatedRect {
233                center: Coordinate::new_2d(cx, cy),
234                width: w,
235                height: h,
236                // angle: direction of the u-axis in world frame
237                angle_rad: uy.atan2(ux),
238            });
239        }
240    }
241
242    best_rect
243}
244
245// ─────────────────────────────────────────────────────────────────────────────
246// Smallest enclosing circle (Welzl's algorithm)
247// ─────────────────────────────────────────────────────────────────────────────
248
249/// Circle through exactly one point (radius 0).
250#[inline]
251fn circle_from_1(a: Coordinate) -> Circle {
252    Circle {
253        center: a,
254        radius: 0.0,
255    }
256}
257
258/// Smallest circle with diameter defined by two points.
259#[inline]
260fn circle_from_2(a: Coordinate, b: Coordinate) -> Circle {
261    let cx = (a.x + b.x) * 0.5;
262    let cy = (a.y + b.y) * 0.5;
263    let dx = b.x - a.x;
264    let dy = b.y - a.y;
265    Circle {
266        center: Coordinate::new_2d(cx, cy),
267        radius: (dx * dx + dy * dy).sqrt() * 0.5,
268    }
269}
270
271/// Circumscribed circle of three points, or `None` if they are collinear.
272fn circumcircle(a: Coordinate, b: Coordinate, c: Coordinate) -> Option<Circle> {
273    let ax = b.x - a.x;
274    let ay = b.y - a.y;
275    let bx = c.x - a.x;
276    let by = c.y - a.y;
277    let d = 2.0 * (ax * by - ay * bx);
278    if d.abs() < 1e-12 {
279        return None;
280    }
281    let a_sq = ax * ax + ay * ay;
282    let b_sq = bx * bx + by * by;
283    let ux = (by * a_sq - ay * b_sq) / d;
284    let uy = (ax * b_sq - bx * a_sq) / d;
285    let r = (ux * ux + uy * uy).sqrt();
286    Some(Circle {
287        center: Coordinate::new_2d(a.x + ux, a.y + uy),
288        radius: r,
289    })
290}
291
292/// Smallest circle determined solely by the `boundary` set (0–3 points).
293#[inline]
294fn trivial_circle(boundary: &[Coordinate]) -> Circle {
295    match boundary.len() {
296        0 => Circle::zero(),
297        1 => circle_from_1(boundary[0]),
298        2 => circle_from_2(boundary[0], boundary[1]),
299        3 => circumcircle(boundary[0], boundary[1], boundary[2]).unwrap_or_else(|| {
300            // Collinear: the enclosing circle is the diameter of the two
301            // outermost points.
302            let mut best = circle_from_2(boundary[0], boundary[1]);
303            let c02 = circle_from_2(boundary[0], boundary[2]);
304            let c12 = circle_from_2(boundary[1], boundary[2]);
305            if c02.radius > best.radius {
306                best = c02;
307            }
308            if c12.radius > best.radius {
309                best = c12;
310            }
311            best
312        }),
313        _ => unreachable!("boundary set may not exceed 3 points"),
314    }
315}
316
317/// Recursive Welzl helper.
318///
319/// `pts` is the (shuffled) full point slice; `n` is the count of points still
320/// under consideration; `boundary` is the set of points on the current
321/// boundary (at most 3).
322fn welzl(pts: &[Coordinate], boundary: &mut Vec<Coordinate>, n: usize) -> Circle {
323    if n == 0 || boundary.len() == 3 {
324        return trivial_circle(boundary);
325    }
326    let p = pts[n - 1];
327    let d = welzl(pts, boundary, n - 1);
328    if d.contains(p) {
329        return d;
330    }
331    boundary.push(p);
332    let result = welzl(pts, boundary, n - 1);
333    boundary.pop();
334    result
335}
336
337/// Simple LCG (linear congruential generator) shuffle — no `rand` dependency.
338///
339/// Uses the Knuth/MMIX multiplicative constants with a Fisher–Yates sweep.
340fn lcg_shuffle(v: &mut [Coordinate], seed: u64) {
341    let n = v.len();
342    if n <= 1 {
343        return;
344    }
345    let mut state = seed;
346    for i in (1..n).rev() {
347        // Advance LCG state
348        state = state
349            .wrapping_mul(6_364_136_223_846_793_005)
350            .wrapping_add(1_442_695_040_888_963_407);
351        // Map to index in [0, i]
352        let j = (state >> 33) as usize % (i + 1);
353        v.swap(i, j);
354    }
355}
356
357/// Computes the smallest enclosing circle for a set of points.
358///
359/// Uses Welzl's randomised incremental algorithm (expected O(n)).  The shuffle
360/// is driven by a deterministic LCG seeded from the coordinates, so results
361/// are reproducible.
362///
363/// Returns a zero-radius circle at the origin for empty input.
364///
365/// # Stack depth
366///
367/// The underlying recursion has O(n) worst-case depth.  For inputs ≥ 100 000
368/// points consider running in a thread with a larger stack.
369pub fn smallest_enclosing_circle(points: &[Coordinate]) -> Circle {
370    match points.len() {
371        0 => Circle::zero(),
372        1 => circle_from_1(points[0]),
373        2 => circle_from_2(points[0], points[1]),
374        _ => {
375            let mut pts: Vec<Coordinate> = points.to_vec();
376            // Seed from XOR of all coordinate bits — deterministic, avoids
377            // clustering on regular grids by mixing both fields.
378            let seed: u64 = pts.iter().fold(0u64, |acc, c| {
379                acc.wrapping_add(c.x.to_bits() ^ c.y.to_bits())
380            });
381            lcg_shuffle(&mut pts, seed.wrapping_add(42));
382            let mut boundary: Vec<Coordinate> = Vec::with_capacity(3);
383            welzl(&pts, &mut boundary, pts.len())
384        }
385    }
386}
387
388// ─────────────────────────────────────────────────────────────────────────────
389// Unit tests (inline)
390// ─────────────────────────────────────────────────────────────────────────────
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395
396    #[test]
397    fn test_aabb_basic() {
398        let pts = vec![
399            Coordinate::new_2d(1.0, 2.0),
400            Coordinate::new_2d(3.0, 0.0),
401            Coordinate::new_2d(0.0, 4.0),
402        ];
403        let (min_x, min_y, max_x, max_y) = aabb(&pts).expect("non-empty");
404        assert!((min_x - 0.0).abs() < 1e-12);
405        assert!((min_y - 0.0).abs() < 1e-12);
406        assert!((max_x - 3.0).abs() < 1e-12);
407        assert!((max_y - 4.0).abs() < 1e-12);
408    }
409
410    #[test]
411    fn test_rotated_rect_corners_roundtrip() {
412        let rect = RotatedRect {
413            center: Coordinate::new_2d(1.0, 1.0),
414            width: 4.0,
415            height: 2.0,
416            angle_rad: 0.0,
417        };
418        let corners = rect.corners();
419        // At angle 0 the corners should be axis-aligned
420        assert!((corners[0].x - 3.0).abs() < 1e-10); // center.x + hw
421        assert!((corners[0].y - 2.0).abs() < 1e-10); // center.y + hh
422    }
423
424    #[test]
425    fn test_circle_contains_boundary() {
426        let c = Circle {
427            center: Coordinate::new_2d(0.0, 0.0),
428            radius: 1.0,
429        };
430        // Exact boundary point — epsilon in `contains` should accept it
431        assert!(c.contains(Coordinate::new_2d(1.0, 0.0)));
432        assert!(!c.contains(Coordinate::new_2d(1.1, 0.0)));
433    }
434}