axiolid_overlay/
minkowski.rs

1//! Minkowski sums and erosions of a region by a convex polygon (#145), and
2//! disc morphology with a known side of error (#163).
3//!
4//! # One arrangement per operation
5//!
6//! For a region `R` and a convex polygon `K`, and any vertex `k0` of `K`,
7//!
8//! ```text
9//! R + K = (R + k0)  union  (union over boundary edges e of R: e + K)
10//! ```
11//!
12//! since a point `x` has `x - K` meeting `R` either through the boundary
13//! (then `x` lies in some `e + K`) or wholly inside (then `x - k0` is in
14//! `R`). Each `e + K` is the convex hull of `K + a` and `K + b`, `e` running
15//! from `a` to `b`. The erosion `R - K` (the points `x` with `x + K` inside
16//! `R`) is `R` minus the sum of its complement with `-K`, the complement
17//! taken within a box large enough that nothing beyond it matters.
18//!
19//! All the rings -- the region's own, the translated ones and the pieces --
20//! go into one [`ArcArrangement`] (ADR 0070): where boundaries cross, which
21//! pieces coincide and which ring holds what are decided exactly, and the
22//! result's faces are chosen by ring membership. Vertices are rounded once.
23//! The one rounding before that is each vertex sum `a + k`, a sum of two
24//! `f64`s, within half an ulp.
25//!
26//! # Discs: inner and outer
27//!
28//! No polygon is a disc, so [`Region::dilate`] and [`Region::erode`] are
29//! approximations of unstated side. The inscribed polygon `P_in` of a disc
30//! `D` lies inside it and the circumscribed `P_out` around it, so
31//!
32//! ```text
33//! R + P_in  <=  R + D  <=  R + P_out        R - P_out  <=  R - D  <=  R - P_in
34//! ```
35//!
36//! Each polygon is moved a margin further to its side, so that the
37//! roundings above -- and the dropping of edges shorter than the tolerance
38//! when the result is presented -- cannot carry a point across. What each
39//! side proves, and how far it can be from the true disc morphology, is in
40//! [`MorphologyBound`].
41
42use axiolid_core::{Point2, Tolerance};
43use axiolid_exact::{certify, Arith, SignExpr};
44use axiolid_guarantees::Sign;
45
46use crate::arc::ArcRing;
47use crate::arrangement::ArcArrangement;
48use crate::region::Region;
49use crate::{validate_ring, OverlayError, Polygon, Ring};
50
51/// Why a Minkowski operation was refused.
52#[derive(Debug, Clone, PartialEq, Eq)]
53#[non_exhaustive]
54pub enum MinkowskiError {
55    /// The structuring polygon is not convex. Sums with a non-convex
56    /// polygon are not built yet (#145).
57    NotConvex,
58    /// An operand or the result failed the overlay's own checks.
59    Overlay(OverlayError),
60}
61
62impl From<OverlayError> for MinkowskiError {
63    fn from(error: OverlayError) -> Self {
64        Self::Overlay(error)
65    }
66}
67
68/// Which side of the exact disc morphology a region lies on.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum BoundSide {
72    /// Contained in the exact result. A route found in an inner erosion
73    /// proves reachability; "unreachable" proves nothing.
74    Inner,
75    /// Containing the exact result. "Unreachable" in an outer erosion is a
76    /// proof; a route found in it may be invalid.
77    Outer,
78}
79
80/// How a disc morphology's result relates to the exact one.
81#[derive(Debug, Clone, Copy, PartialEq)]
82pub struct MorphologyBound {
83    /// Which side of the exact result it lies on.
84    pub side: BoundSide,
85    /// The greatest distance between the two boundaries: the polygonal
86    /// disc's deviation from the true disc, plus the margin kept for
87    /// rounding and presentation.
88    pub deviation: f64,
89}
90
91/// Orientation of `c` against the directed line `a -> b`, exactly.
92struct Orient {
93    a: Point2,
94    b: Point2,
95    c: Point2,
96}
97
98impl SignExpr for Orient {
99    fn sign_in<T: Arith>(&self) -> Option<Sign> {
100        let f = T::from_f64;
101        let (ux, uy) = (f(self.b.x).sub(&f(self.a.x)), f(self.b.y).sub(&f(self.a.y)));
102        let (vx, vy) = (f(self.c.x).sub(&f(self.a.x)), f(self.c.y).sub(&f(self.a.y)));
103        ux.mul(&vy).sub(&uy.mul(&vx)).sign()
104    }
105}
106
107fn orient(a: Point2, b: Point2, c: Point2) -> Sign {
108    certify(&Orient { a, b, c }).unwrap_or(Sign::Zero)
109}
110
111/// A convex polygon's vertices, counter-clockwise, collinear ones dropped.
112fn convex(ring: &Ring, tolerance: Tolerance) -> Result<Vec<Point2>, MinkowskiError> {
113    validate_ring(ring, tolerance)?;
114    let hull = hull(ring.points.clone());
115    // Convex exactly when every vertex is on its hull.
116    let on_hull = ring.points.iter().all(|p| {
117        hull.contains(p) || {
118            // Or on a hull edge (a collinear vertex).
119            (0..hull.len()).any(|i| {
120                let (a, b) = (hull[i], hull[(i + 1) % hull.len()]);
121                orient(a, b, *p) == Sign::Zero
122                    && p.x >= a.x.min(b.x)
123                    && p.x <= a.x.max(b.x)
124                    && p.y >= a.y.min(b.y)
125                    && p.y <= a.y.max(b.y)
126            })
127        }
128    });
129    if !on_hull || hull.len() < 3 {
130        return Err(MinkowskiError::NotConvex);
131    }
132    Ok(hull)
133}
134
135/// The convex hull, counter-clockwise, without collinear vertices
136/// (Andrew's monotone chain on exact orientations).
137fn hull(mut points: Vec<Point2>) -> Vec<Point2> {
138    points.sort_by(|a, b| a.x.total_cmp(&b.x).then(a.y.total_cmp(&b.y)));
139    points.dedup();
140    if points.len() < 3 {
141        return points;
142    }
143    let mut lower: Vec<Point2> = Vec::new();
144    for &p in &points {
145        while lower.len() >= 2
146            && orient(lower[lower.len() - 2], lower[lower.len() - 1], p) != Sign::Positive
147        {
148            lower.pop();
149        }
150        lower.push(p);
151    }
152    let mut upper: Vec<Point2> = Vec::new();
153    for &p in points.iter().rev() {
154        while upper.len() >= 2
155            && orient(upper[upper.len() - 2], upper[upper.len() - 1], p) != Sign::Positive
156        {
157            upper.pop();
158        }
159        upper.push(p);
160    }
161    lower.pop();
162    upper.pop();
163    lower.extend(upper);
164    lower
165}
166
167/// The region's rings in order (per polygon: outer, then holes), and
168/// whether a point with the given ring flags lies in the region.
169fn rings_of(polygons: &[Polygon]) -> Vec<Vec<Point2>> {
170    polygons
171        .iter()
172        .flat_map(|p| std::iter::once(&p.outer).chain(&p.holes))
173        .map(|r| r.points.clone())
174        .collect()
175}
176
177/// Membership in a polygon set, from per-ring flags laid out as
178/// [`rings_of`] lays them out.
179fn member(polygons: &[usize], flags: &[bool]) -> bool {
180    let mut at = 0;
181    for &holes in polygons {
182        let inside = flags[at] && !flags[at + 1..at + 1 + holes].iter().any(|&f| f);
183        if inside {
184            return true;
185        }
186        at += 1 + holes;
187    }
188    false
189}
190
191/// A ring's points, with neighbours closer than the tolerance merged (a
192/// hull of two nearly equal translates has such pairs).
193fn tidy(points: Vec<Point2>, tolerance: Tolerance) -> Option<ArcRing> {
194    let mut out: Vec<Point2> = Vec::with_capacity(points.len());
195    for p in points {
196        if out
197            .last()
198            .is_none_or(|q: &Point2| (p - *q).length() > tolerance.linear())
199        {
200            out.push(p);
201        }
202    }
203    while out.len() > 1 && (out[0] - out[out.len() - 1]).length() <= tolerance.linear() {
204        out.pop();
205    }
206    (out.len() >= 3).then(|| ArcRing::from_points(&out))
207}
208
209/// The rings whose union with the translated set is the sum of the set
210/// bounded by `rings` with `k`: the rings translated by `k[0]`, and each
211/// boundary edge's piece.
212fn sum_rings(
213    rings: &[Vec<Point2>],
214    k: &[Point2],
215    tolerance: Tolerance,
216) -> (Vec<ArcRing>, Vec<ArcRing>) {
217    let shift = |p: Point2, by: Point2| Point2::new(p.x + by.x, p.y + by.y);
218    let translated = rings
219        .iter()
220        .map(|r| ArcRing::from_points(&r.iter().map(|&p| shift(p, k[0])).collect::<Vec<_>>()))
221        .collect();
222    let mut pieces = Vec::new();
223    for ring in rings {
224        for i in 0..ring.len() {
225            let (a, b) = (ring[i], ring[(i + 1) % ring.len()]);
226            let points: Vec<Point2> = k.iter().flat_map(|&q| [shift(a, q), shift(b, q)]).collect();
227            if let Some(piece) = tidy(hull(points), tolerance) {
228                pieces.push(piece);
229            }
230        }
231    }
232    (translated, pieces)
233}
234
235/// The faces of `arrangement` where `inside` holds, as a region.
236pub(crate) fn region_of(
237    arrangement: &ArcArrangement,
238    inside: impl Fn(&[bool]) -> bool,
239    had_input: bool,
240    tolerance: Tolerance,
241) -> Result<Region, OverlayError> {
242    let straight = |ring: ArcRing| Ring {
243        points: ring.vertices.iter().map(|v| v.point).collect(),
244    };
245    let mut polygons = Vec::new();
246    for region in arrangement.regions(inside)? {
247        let Some(outer) = crate::arc_overlay::presented(arrangement.ring(&region.outer), tolerance)
248        else {
249            continue;
250        };
251        let holes = region
252            .holes
253            .iter()
254            .filter_map(|h| crate::arc_overlay::presented(arrangement.ring(h), tolerance))
255            .map(straight)
256            .collect();
257        polygons.push(Polygon {
258            outer: straight(outer),
259            holes,
260        });
261    }
262    Ok(Region::from_valid_polygons(
263        crate::settle::settle(polygons, tolerance),
264        had_input,
265    ))
266}
267
268impl Region {
269    /// The Minkowski sum with a convex polygon `convex`: every point of the
270    /// region moved by every point of the polygon.
271    ///
272    /// Decided exactly (ADR 0070), with each vertex sum `a + k` rounded
273    /// once to `f64` and output vertices rounded once.
274    ///
275    /// # Errors
276    ///
277    /// [`MinkowskiError::NotConvex`] for a polygon that is not convex; the
278    /// sum with a non-convex one is not built yet (#145).
279    pub fn minkowski_sum(
280        &self,
281        convex_ring: &Ring,
282        tolerance: Tolerance,
283    ) -> Result<Self, MinkowskiError> {
284        let k = convex(convex_ring, tolerance)?;
285        Ok(self.sum_with(&k, tolerance)?)
286    }
287
288    /// The Minkowski erosion by a convex polygon: the points `x` for which
289    /// the polygon moved by `x` lies within the region.
290    ///
291    /// # Errors
292    ///
293    /// As [`Self::minkowski_sum`].
294    pub fn minkowski_erosion(
295        &self,
296        convex_ring: &Ring,
297        tolerance: Tolerance,
298    ) -> Result<Self, MinkowskiError> {
299        let k = convex(convex_ring, tolerance)?;
300        Ok(self.erode_with(&k, tolerance)?)
301    }
302
303    fn sum_with(&self, k: &[Point2], tolerance: Tolerance) -> Result<Self, OverlayError> {
304        if self.is_empty() {
305            return Ok(Self::empty());
306        }
307        let own = rings_of(self.polygons());
308        let shape: Vec<usize> = self.polygons().iter().map(|p| p.holes.len()).collect();
309        let (translated, pieces) = sum_rings(&own, k, tolerance);
310        let n = translated.len();
311        let mut rings = translated;
312        rings.extend(pieces);
313        let arrangement = ArcArrangement::new(&rings, tolerance)?;
314        region_of(
315            &arrangement,
316            |flags| member(&shape, &flags[..n]) || flags[n..].iter().any(|&f| f),
317            true,
318            tolerance,
319        )
320    }
321
322    fn erode_with(&self, k: &[Point2], tolerance: Tolerance) -> Result<Self, OverlayError> {
323        if self.is_empty() {
324            return Ok(Self::empty());
325        }
326        let own = rings_of(self.polygons());
327        let shape: Vec<usize> = self.polygons().iter().map(|p| p.holes.len()).collect();
328        // A box holding the region: its complement within the box, summed
329        // with -K, reaches every point of the region that K cannot sit
330        // around (a translate of K leaving the box crosses its boundary,
331        // whose own edge pieces cover that). The room about the region only
332        // keeps the box's edges off the region's.
333        let (mut lo, mut hi) = (own[0][0], own[0][0]);
334        for p in own.iter().flatten() {
335            lo = lo.min(*p);
336            hi = hi.max(*p);
337        }
338        let reach = k
339            .iter()
340            .fold(0.0_f64, |m, q| m.max(q.x.abs()).max(q.y.abs()));
341        let pad = 2.0 * reach + (hi - lo).max_element() + 1.0;
342        let frame = vec![
343            Point2::new(lo.x - pad, lo.y - pad),
344            Point2::new(hi.x + pad, lo.y - pad),
345            Point2::new(hi.x + pad, hi.y + pad),
346            Point2::new(lo.x - pad, hi.y + pad),
347        ];
348        let mut outside = vec![frame];
349        outside.extend(own.iter().cloned());
350        let flipped: Vec<Point2> = k.iter().map(|q| Point2::new(-q.x, -q.y)).collect();
351        let (translated, pieces) = sum_rings(&outside, &flipped, tolerance);
352        let m = own.len();
353        let t = translated.len();
354        let mut rings: Vec<ArcRing> = own.iter().map(|r| ArcRing::from_points(r)).collect();
355        rings.extend(translated);
356        rings.extend(pieces);
357        let arrangement = ArcArrangement::new(&rings, tolerance)?;
358        let in_outside = |flags: &[bool]| flags[0] && !member(&shape, &flags[1..]);
359        region_of(
360            &arrangement,
361            |flags| {
362                member(&shape, &flags[..m])
363                    && !(in_outside(&flags[m..m + t]) || flags[m + t..].iter().any(|&f| f))
364            },
365            true,
366            tolerance,
367        )
368    }
369
370    /// Dilation by a disc of `radius`, contained in the exact one: the sum
371    /// with an inscribed polygon, drawn in by a margin.
372    ///
373    /// # Errors
374    ///
375    /// [`OverlayError::InvalidOffsetDistance`] for a radius that is not
376    /// finite and nonnegative, and overlay refusals.
377    pub fn dilate_inner(&self, radius: f64, tolerance: Tolerance) -> Result<Self, OverlayError> {
378        self.disc(radius, BoundSide::Inner, false, tolerance)
379    }
380
381    /// Dilation by a disc of `radius`, containing the exact one: the sum
382    /// with a circumscribed polygon, pushed out by a margin.
383    ///
384    /// # Errors
385    ///
386    /// As [`Self::dilate_inner`].
387    pub fn dilate_outer(&self, radius: f64, tolerance: Tolerance) -> Result<Self, OverlayError> {
388        self.disc(radius, BoundSide::Outer, false, tolerance)
389    }
390
391    /// Erosion by a disc of `radius`, contained in the exact one: eroded by
392    /// a circumscribed polygon, pushed out by a margin. A route found in it
393    /// proves reachability.
394    ///
395    /// # Errors
396    ///
397    /// As [`Self::dilate_inner`].
398    pub fn erode_inner(&self, radius: f64, tolerance: Tolerance) -> Result<Self, OverlayError> {
399        self.disc(radius, BoundSide::Inner, true, tolerance)
400    }
401
402    /// Erosion by a disc of `radius`, containing the exact one: eroded by
403    /// an inscribed polygon, drawn in by a margin. No route in it proves
404    /// unreachability.
405    ///
406    /// # Errors
407    ///
408    /// As [`Self::dilate_inner`].
409    pub fn erode_outer(&self, radius: f64, tolerance: Tolerance) -> Result<Self, OverlayError> {
410        self.disc(radius, BoundSide::Outer, true, tolerance)
411    }
412
413    fn disc(
414        &self,
415        radius: f64,
416        side: BoundSide,
417        erode: bool,
418        tolerance: Tolerance,
419    ) -> Result<Self, OverlayError> {
420        if !radius.is_finite() || radius < 0.0 {
421            return Err(OverlayError::InvalidOffsetDistance);
422        }
423        if self.is_empty() || radius == 0.0 {
424            let mut same = Self::from_valid_polygons(self.polygons().to_vec(), false);
425            same.set_bound(MorphologyBound {
426                side,
427                deviation: 0.0,
428            });
429            return Ok(same);
430        }
431        // Sixty-four sides: the inscribed polygon's sagitta is
432        // r (1 - cos(pi / 64)), about 0.12% of the radius.
433        let n = 64;
434        let half = core::f64::consts::PI / n as f64;
435        // The margin: past the vertex sums' and output vertices' rounding
436        // (a few ulps of the largest coordinate), the sin/cos of the
437        // polygon's own vertices (relative), and the presentation's dropping
438        // of edges shorter than the tolerance.
439        let size = self
440            .polygons()
441            .iter()
442            .flat_map(|p| std::iter::once(&p.outer).chain(&p.holes))
443            .flat_map(|r| r.points.iter())
444            .fold(0.0_f64, |m, p| m.max(p.x.abs()).max(p.y.abs()));
445        let margin =
446            2.0 * tolerance.linear() + 1e-12 * radius + 64.0 * f64::EPSILON * (size + radius);
447        // Inner dilation and outer erosion use the inscribed polygon, the
448        // other two the circumscribed one.
449        let inscribed = (side == BoundSide::Inner) != erode;
450        let (reach, deviation) = if inscribed {
451            let reach = radius - margin;
452            (reach, radius - reach * half.cos())
453        } else {
454            let reach = (radius + margin) / half.cos();
455            (reach, reach - radius)
456        };
457        if reach <= 0.0 {
458            // The margin swallows the disc: the polygon is a point for the
459            // inner dilation (the region itself) and the region itself is an
460            // outer erosion.
461            let mut same = Self::from_valid_polygons(self.polygons().to_vec(), true);
462            same.set_bound(MorphologyBound {
463                side,
464                deviation: radius + margin,
465            });
466            return Ok(same);
467        }
468        let k: Vec<Point2> = (0..n)
469            .map(|i| {
470                let t = 2.0 * half * i as f64;
471                Point2::new(reach * t.cos(), reach * t.sin())
472            })
473            .collect();
474        let mut out = if erode {
475            self.erode_with(&k, tolerance)?
476        } else {
477            self.sum_with(&k, tolerance)?
478        };
479        out.set_bound(MorphologyBound { side, deviation });
480        Ok(out)
481    }
482}