Skip to main content

axiolid_overlay/
rectangle.rs

1//! The minimum-area rectangle enclosing a point set (#182).
2//!
3//! # Exact choice, rounded output
4//!
5//! Some rectangle of least area has a side on an edge of the convex hull
6//! (Freeman and Shapira), so rotating calipers try each hull edge `d` in
7//! turn. Every decision is exact:
8//!
9//! - the hull comes from exact orientations;
10//! - the calipers advance while the next vertex projects no less far along
11//!   `d` (or along `d` turned a quarter) -- the sign of a dot product of
12//!   differences of `f64`s, decided in intervals and else in dyadics;
13//! - an orientation's area is `W H / |d|^2`, with `W` and `H` the spans of
14//!   those projections, so two orientations compare exactly by
15//!   `W_i H_i |d_j|^2` against `W_j H_j |d_i|^2`;
16//! - ties (a square has two minimal orientations) are found exactly and
17//!   broken by the least angle of the first axis, turned by quarter turns
18//!   into `[0, 90)` degrees -- itself an exact comparison, so the answer
19//!   does not depend on the order of the input.
20//!
21//! Only the output is rounded: the unit axes (a square root), the centre
22//! and the half extents. [`RectangleEvidence::error`] bounds how far any
23//! of them, and any corner, lies from the exact rectangle.
24
25use axiolid_core::{Point2, Vec2};
26use axiolid_exact::{certify, Arith, Dyadic, SignExpr};
27use axiolid_guarantees::Sign;
28
29/// A rectangle by its centre, two unit axes and the half extents along
30/// them.
31#[derive(Debug, Clone, Copy, PartialEq)]
32pub struct OrientedRectangle {
33    /// Centre.
34    pub centre: Point2,
35    /// Unit axes, counter-clockwise: the second is the first turned a
36    /// quarter. The first points into `[0, 90)` degrees.
37    pub axes: [Vec2; 2],
38    /// Half the side lengths along the axes; one is zero for collinear
39    /// input, both for a single point.
40    pub half_extents: [f64; 2],
41}
42
43impl OrientedRectangle {
44    /// Its area.
45    #[must_use]
46    pub fn area(&self) -> f64 {
47        4.0 * self.half_extents[0] * self.half_extents[1]
48    }
49
50    /// The corners, counter-clockwise.
51    #[must_use]
52    pub fn corners(&self) -> [Point2; 4] {
53        let u = self.axes[0] * self.half_extents[0];
54        let v = self.axes[1] * self.half_extents[1];
55        let c = self.centre;
56        [c - u - v, c + u - v, c + u + v, c - u + v]
57    }
58}
59
60/// What the construction did and how exact its output is.
61#[derive(Debug, Clone, Copy, PartialEq)]
62#[non_exhaustive]
63pub struct RectangleEvidence {
64    /// Vertices of the convex hull.
65    pub hull_vertices: usize,
66    /// Distinct orientations of least area (a square has two, a generic
67    /// set one). The one returned turns its first axis least from the
68    /// x-axis.
69    pub minimal_orientations: usize,
70    /// A bound on the distance between any output coordinate (of the
71    /// centre or a corner) or length (a half extent) and its exact value.
72    /// For an axis-aligned rectangle it is the rounding actually done,
73    /// measured exactly -- zero for a box with representable coordinates.
74    /// Otherwise a few ulps of the largest coordinate, from normalising the
75    /// axes and projecting onto them.
76    pub error: f64,
77}
78
79/// The rectangle and its evidence.
80#[derive(Debug, Clone, Copy, PartialEq)]
81#[non_exhaustive]
82pub struct MinimumRectangle {
83    /// The rectangle.
84    pub rectangle: OrientedRectangle,
85    /// What was rounded, and by how much at most.
86    pub evidence: RectangleEvidence,
87}
88
89/// Why no rectangle was built.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91#[non_exhaustive]
92pub enum RectangleError {
93    /// No points.
94    Empty,
95    /// A coordinate was not finite.
96    NonFinite,
97}
98
99/// `(b - a) . d`, or `(b - a) . d⊥` with `d⊥` the quarter turn of `d`.
100struct Along {
101    a: Point2,
102    b: Point2,
103    d: Vec2,
104    across: bool,
105}
106
107impl SignExpr for Along {
108    fn sign_in<T: Arith>(&self) -> Option<Sign> {
109        let f = T::from_f64;
110        let (dx, dy) = if self.across {
111            (f(self.d.y).neg(), f(self.d.x))
112        } else {
113            (f(self.d.x), f(self.d.y))
114        };
115        let ex = f(self.b.x).sub(&f(self.a.x));
116        let ey = f(self.b.y).sub(&f(self.a.y));
117        ex.mul(&dx).add(&ey.mul(&dy)).sign()
118    }
119}
120
121/// Orientation of `c` against `a -> b`.
122struct Orient {
123    a: Point2,
124    b: Point2,
125    c: Point2,
126}
127
128impl SignExpr for Orient {
129    fn sign_in<T: Arith>(&self) -> Option<Sign> {
130        let f = T::from_f64;
131        let (ux, uy) = (f(self.b.x).sub(&f(self.a.x)), f(self.b.y).sub(&f(self.a.y)));
132        let (vx, vy) = (f(self.c.x).sub(&f(self.a.x)), f(self.c.y).sub(&f(self.a.y)));
133        ux.mul(&vy).sub(&uy.mul(&vx)).sign()
134    }
135}
136
137/// The inputs are finite, so the exact tier always decides.
138fn sign<E: SignExpr>(e: &E) -> Sign {
139    certify(e).unwrap_or(Sign::Zero)
140}
141
142/// The convex hull, counter-clockwise, without collinear vertices; the
143/// two extremes for collinear input, one point for coincident input.
144fn hull(points: &[Point2]) -> Vec<Point2> {
145    let mut p = points.to_vec();
146    p.sort_by(|a, b| a.x.total_cmp(&b.x).then(a.y.total_cmp(&b.y)));
147    p.dedup();
148    if p.len() < 3 {
149        return p;
150    }
151    let chain = |order: &mut dyn Iterator<Item = Point2>| {
152        let mut out: Vec<Point2> = Vec::new();
153        for c in order {
154            while let [.., a, b] = out[..] {
155                if sign(&Orient { a, b, c }) == Sign::Positive {
156                    break;
157                }
158                out.pop();
159            }
160            out.push(c);
161        }
162        out.pop();
163        out
164    };
165    let mut lower = chain(&mut p.iter().copied());
166    lower.extend(chain(&mut p.iter().rev().copied()));
167    lower
168}
169
170fn exact(x: f64) -> Dyadic {
171    Dyadic::from_f64(x)
172}
173
174/// A direction turned by quarter turns into `[0, 90)` degrees; exact.
175fn canonical(mut d: Vec2) -> Vec2 {
176    while !(d.x > 0.0 && d.y >= 0.0) {
177        d = Vec2::new(d.y, -d.x);
178    }
179    d
180}
181
182/// Whether `a` turns strictly clockwise into `b`, exactly.
183fn clockwise(a: Vec2, b: Vec2) -> bool {
184    exact(a.x)
185        .mul(&exact(b.y))
186        .sub(&exact(a.y).mul(&exact(b.x)))
187        .sign()
188        == Some(Sign::Negative)
189}
190
191/// One orientation: a hull edge and the calipers' four vertices.
192#[derive(Debug, Clone, Copy)]
193struct Candidate {
194    d: Vec2,
195    /// Hull vertices least and most along `d`, and most across it; the
196    /// edge's own start is least across it.
197    lo: usize,
198    hi: usize,
199    base: usize,
200    top: usize,
201}
202
203impl Candidate {
204    /// `W H` and `|d|^2`, exactly.
205    fn area_parts(&self, h: &[Point2]) -> (Dyadic, Dyadic) {
206        let (dx, dy) = (exact(self.d.x), exact(self.d.y));
207        let span = |a: Point2, b: Point2, across: bool| {
208            let (ex, ey) = (exact(b.x).sub(&exact(a.x)), exact(b.y).sub(&exact(a.y)));
209            if across {
210                ey.mul(&dx).sub(&ex.mul(&dy))
211            } else {
212                ex.mul(&dx).add(&ey.mul(&dy))
213            }
214        };
215        let w = span(h[self.lo], h[self.hi], false);
216        let t = span(h[self.base], h[self.top], true);
217        (w.mul(&t), dx.mul(&dx).add(&dy.mul(&dy)))
218    }
219}
220
221/// The minimum-area rectangle enclosing `points`.
222///
223/// # Errors
224///
225/// [`RectangleError::Empty`] for no points, [`RectangleError::NonFinite`]
226/// for a coordinate that is not finite.
227pub fn minimum_area_rectangle(points: &[Point2]) -> Result<MinimumRectangle, RectangleError> {
228    if points.is_empty() {
229        return Err(RectangleError::Empty);
230    }
231    if !points.iter().all(|p| p.is_finite()) {
232        return Err(RectangleError::NonFinite);
233    }
234    let h = hull(points);
235    let size = h
236        .iter()
237        .fold(0.0f64, |m, p| m.max(p.x.abs()).max(p.y.abs()));
238    let evidence = |minimal, error| RectangleEvidence {
239        hull_vertices: h.len(),
240        minimal_orientations: minimal,
241        error,
242    };
243    let (rectangle, minimal) = match h.len() {
244        1 => {
245            let rectangle = OrientedRectangle {
246                centre: h[0],
247                axes: [Vec2::X, Vec2::Y],
248                half_extents: [0.0, 0.0],
249            };
250            return Ok(MinimumRectangle {
251                rectangle,
252                evidence: evidence(1, 0.0),
253            });
254        }
255        2 => {
256            // Nothing lies across the segment. An even number of quarter
257            // turns leaves the first axis along it, an odd number the second.
258            let d = h[1] - h[0];
259            let axis = canonical(d);
260            let mut rectangle = fit(&h, axis);
261            rectangle.half_extents[usize::from(axis == d || axis == -d)] = 0.0;
262            rectangle.centre = Point2::new(0.5 * (h[0].x + h[1].x), 0.5 * (h[0].y + h[1].y));
263            (rectangle, 1)
264        }
265        _ => {
266            let (best, minimal) = calipers(&h);
267            (fit(&h, canonical(best.d)), minimal)
268        }
269    };
270    Ok(MinimumRectangle {
271        rectangle,
272        evidence: evidence(
273            minimal,
274            measured_error(&h, &rectangle).unwrap_or(64.0 * f64::EPSILON * size),
275        ),
276    })
277}
278
279/// For an axis-aligned rectangle, the rounding actually done, measured
280/// exactly: its axes are exact, and its centre, half extents and corners
281/// are each one rounding of a dyadic value -- zero whenever that value is
282/// representable, as for any box with representable coordinates. `None`
283/// for other orientations, whose unit axes are irrational.
284fn measured_error(h: &[Point2], r: &OrientedRectangle) -> Option<f64> {
285    if r.axes != [Vec2::X, Vec2::Y] {
286        return None;
287    }
288    let low = |f: fn(&Point2) -> f64| h.iter().map(f).fold(f64::INFINITY, f64::min);
289    let high = |f: fn(&Point2) -> f64| h.iter().map(f).fold(f64::NEG_INFINITY, f64::max);
290    let (x0, x1, y0, y1) = (low(|p| p.x), high(|p| p.x), low(|p| p.y), high(|p| p.y));
291    let half = exact(0.5);
292    let mid = |a: f64, b: f64| exact(a).add(&exact(b)).mul(&half);
293    let span = |a: f64, b: f64| exact(b).sub(&exact(a)).mul(&half);
294    let [c0, c1, c2, c3] = r.corners();
295    let pairs = [
296        (r.centre.x, mid(x0, x1)),
297        (r.centre.y, mid(y0, y1)),
298        (r.half_extents[0], span(x0, x1)),
299        (r.half_extents[1], span(y0, y1)),
300        (c0.x, exact(x0)),
301        (c0.y, exact(y0)),
302        (c1.x, exact(x1)),
303        (c1.y, exact(y0)),
304        (c2.x, exact(x1)),
305        (c2.y, exact(y1)),
306        (c3.x, exact(x0)),
307        (c3.y, exact(y1)),
308    ];
309    let mut worst = 0.0f64;
310    for (rounded, true_value) in pairs {
311        let gap = exact(rounded).sub(&true_value);
312        let gap = if gap.sign() == Some(Sign::Negative) {
313            gap.neg()
314        } else {
315            gap
316        };
317        // Round the gap up, so the bound holds.
318        let mut bound = gap.to_f64();
319        if exact(bound).sub(&gap).sign() == Some(Sign::Negative) {
320            bound = bound.next_up();
321        }
322        worst = worst.max(bound);
323    }
324    Some(worst)
325}
326
327/// The orientation of least area over a hull of three or more vertices,
328/// and how many distinct orientations share it.
329fn calipers(h: &[Point2]) -> (Candidate, usize) {
330    let n = h.len();
331    // Whether the vertex after `at` lies no less far (or, `larger` false,
332    // no further) along `d` or across it.
333    let step = |at: usize, d: Vec2, across: bool, larger: bool| {
334        let s = sign(&Along {
335            a: h[at],
336            b: h[(at + 1) % n],
337            d,
338            across,
339        });
340        if larger {
341            s != Sign::Negative
342        } else {
343            s != Sign::Positive
344        }
345    };
346    let advance = |mut at: usize, d: Vec2, across: bool, larger: bool| {
347        // A hull turns left, so a caliper only moves forward, and never
348        // round the whole hull.
349        for _ in 0..n {
350            if !step(at, d, across, larger) {
351                break;
352            }
353            at = (at + 1) % n;
354        }
355        at
356    };
357    let mut candidates = Vec::with_capacity(n);
358    let (mut hi, mut top, mut lo) = (0, 0, 0);
359    for base in 0..n {
360        let d = h[(base + 1) % n] - h[base];
361        if base == 0 {
362            // First placement: each caliper starts where the previous one
363            // stopped, walking round from the edge.
364            hi = advance(0, d, false, true);
365            top = advance(hi, d, true, true);
366            lo = advance(top, d, false, false);
367        } else {
368            hi = advance(hi, d, false, true);
369            top = advance(top, d, true, true);
370            lo = advance(lo, d, false, false);
371        }
372        candidates.push(Candidate {
373            d,
374            lo,
375            hi,
376            base,
377            top,
378        });
379    }
380    let mut best = candidates[0];
381    let mut best_parts = best.area_parts(h);
382    let mut ties = vec![canonical(best.d)];
383    for c in &candidates[1..] {
384        let parts = c.area_parts(h);
385        let order = parts
386            .0
387            .mul(&best_parts.1)
388            .sub(&best_parts.0.mul(&parts.1))
389            .sign();
390        match order {
391            Some(Sign::Negative) => {
392                best = *c;
393                best_parts = parts;
394                ties = vec![canonical(c.d)];
395            }
396            Some(Sign::Zero) => {
397                let axis = canonical(c.d);
398                if clockwise(canonical(best.d), axis) {
399                    best = *c;
400                    best_parts = parts;
401                }
402                if !ties
403                    .iter()
404                    .any(|t| !clockwise(*t, axis) && !clockwise(axis, *t))
405                {
406                    ties.push(axis);
407                }
408            }
409            _ => {}
410        }
411    }
412    (best, ties.len())
413}
414
415/// The rectangle with first axis along `d` that encloses the hull,
416/// rounded once per quantity.
417fn fit(h: &[Point2], d: Vec2) -> OrientedRectangle {
418    let l = d.x.hypot(d.y);
419    let u = Vec2::new(d.x / l, d.y / l);
420    let v = Vec2::new(-u.y, u.x);
421    let span = |axis: Vec2| {
422        h.iter()
423            .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
424                let t = p.x * axis.x + p.y * axis.y;
425                (lo.min(t), hi.max(t))
426            })
427    };
428    let ((u0, u1), (v0, v1)) = (span(u), span(v));
429    let (mu, mv) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
430    OrientedRectangle {
431        centre: Point2::new(u.x * mu + v.x * mv, u.y * mu + v.y * mv),
432        axes: [u, v],
433        half_extents: [0.5 * (u1 - u0), 0.5 * (v1 - v0)],
434    }
435}