Skip to main content

axiolid_overlay/
minkowski.rs

1//! Minkowski sums and erosions of a region by a polygon or a region,
2//! convex or not (#145), and disc morphology with a known side of error
3//! (#163).
4//!
5//! # One arrangement per operation
6//!
7//! For a region `R`, a connected shape `K` and any point `k0` of `K`,
8//!
9//! ```text
10//! R + K = (R + k0)  union  (union over boundary edges e of R: e + K)
11//! ```
12//!
13//! since a point `x` has `x - K` meeting `R` either through the boundary
14//! (then `x` lies in some `e + K`) or wholly inside (then `x - k0` is in
15//! `R`). For a convex `K`, `e + K` is the convex hull of `K + a` and
16//! `K + b`, `e` running from `a` to `b`. A non-convex `K` -- with holes, if
17//! it is a region's polygon -- is cut into convex pieces `P` with its own
18//! vertices (`convex_parts`: ear clipping and Hertel-Mehlhorn
19//! merging, the cut certified to tile `K`), and `e + K` is the union of the
20//! hulls `e + P`. A shape of several polygons is the union of their sums.
21//! Should no certified cut be found, `R + B` is taken from the
22//! parallelograms `f + g` of boundary edge pairs, `R` moved by a vertex of
23//! `B`, and `B` moved by a vertex of every ring of `R`: if neither
24//! boundary meets the other yet the two sets do, an outer ring of one lies
25//! inside the other.
26//!
27//! The erosion `R - K` (the points `x` with `x + K` inside `R`) is `R - k0`
28//! minus the sum of the complement of `R` with `-K`, the complement taken
29//! within a box large enough that nothing beyond it matters; when `K`
30//! holds the origin, `R` itself stands in for `R - k0`.
31//!
32//! All the rings -- the region's own, the translated ones and the pieces --
33//! go into one [`ArcArrangement`] (ADR 0070): where boundaries cross, which
34//! pieces coincide and which ring holds what are decided exactly, and the
35//! result's faces are chosen by ring membership. Vertices are rounded once.
36//! The one rounding before that is each vertex sum `a + k`, a sum of two
37//! `f64`s, within half an ulp.
38//!
39//! # Reading the faces
40//!
41//! Each side of each piece is read from the rings that hold it, listed
42//! ascending (#292), never from a flag for every ring. The union holds a
43//! side when a convex piece does (any ring from the first piece on), or
44//! when a translated copy does; only the copies with a ring in the list
45//! are asked. A copy's polygons are consecutive rings, outer first, so a
46//! polygon holds the side exactly when its outer ring is listed and the
47//! next listed ring belongs to a later polygon. A side lies in a handful
48//! of rings, so reading every face costs about the pieces times that
49//! handful. The flags it replaces cost, in time and memory, the pieces
50//! times every ring: one hull per boundary edge made that quadratic in
51//! the region's corners, past 12 GB for a floor of 3,400 corners.
52//! `cargo bench -p axiolid-benchmark --bench minkowski_plan` measures time
53//! and peak memory against corner count; `scripts/probe_minkowski_mutants.py`
54//! lists the faults the tests must catch.
55//!
56//! # Walls of many short edges
57//!
58//! One hull per boundary edge overlaps hundreds of its neighbours along a
59//! wall of sub-millimetre teeth or exactly collinear corners, and their
60//! crossings grew with the square of the corners (#323). Before the hulls
61//! are built, exactly collinear corners are merged, and chords proved to
62//! lie in the summed set trim the hulls they span to slivers, the union
63//! unchanged ([`crate::cover`]). A chord whose hull, or any hull it
64//! trims, would be a ring of zero area is not used at all (#331). A part
65//! holding the origin is anchored there, so the set's copy is its own
66//! rings, which the subdivision shares.
67//! `cargo bench -p axiolid-benchmark --bench disc_erosion` measures time
68//! and peak memory against corner count;
69//! `scripts/probe_minkowski_cover_mutants.py` and
70//! `scripts/probe_minkowski_degenerate_mutants.py` list the faults the
71//! tests must catch.
72//!
73//! # Discs: inner and outer
74//!
75//! No polygon is a disc, so [`Region::dilate`] and [`Region::erode`] are
76//! approximations of unstated side. The inscribed polygon `P_in` of a disc
77//! `D` lies inside it and the circumscribed `P_out` around it, so
78//!
79//! ```text
80//! R + P_in  <=  R + D  <=  R + P_out        R - P_out  <=  R - D  <=  R - P_in
81//! ```
82//!
83//! Each polygon is moved a margin further to its side, so that the
84//! roundings above -- and the dropping of edges shorter than the tolerance
85//! when the result is presented -- cannot carry a point across. What each
86//! side proves, and how far it can be from the true disc morphology, is in
87//! [`MorphologyBound`].
88//!
89//! # Exact and fast
90//!
91//! Where a wall's teeth stand closer than the slivers #323 leaves of
92//! their hulls are wide, those slivers still overlap dozens of others, and
93//! the cost grows with the square of the corners (#330). The exact union
94//! of the slivers has cusps that are crossing points, not vertex sums, so
95//! [`MorphologyMode::Exact`], the default, keeps them all. In
96//! [`MorphologyMode::Fast`], the set's notches are first filled by chords
97//! across its outermost corners, each within the caller's budget of
98//! excess ([`crate::fill`]). Each side then absorbs the excess: a
99//! circumscribed polygon adds it to the deviation, and an inscribed one is
100//! drawn in by it. A rule engine takes the fast mode, a CAD kernel the
101//! exact one (ADR 0087).
102
103use axiolid_core::{Point2, Tolerance};
104use axiolid_exact::{certify, Arith, SignExpr};
105use axiolid_guarantees::Sign;
106
107use crate::arc::ArcRing;
108use crate::arrangement::ArcArrangement;
109use crate::region::Region;
110use crate::{validate_ring, OverlayError, Polygon, Ring};
111
112/// Why a Minkowski operation was refused.
113#[derive(Debug, Clone, PartialEq, Eq)]
114#[non_exhaustive]
115pub enum MinkowskiError {
116    /// The structuring polygon is not convex. No longer returned: sums
117    /// and erosions with a non-convex polygon are built since 0.3.10
118    /// (#145); kept so that matches on it still compile.
119    NotConvex,
120    /// An operand or the result failed the overlay's own checks.
121    Overlay(OverlayError),
122    /// An erosion by an empty region, under which every point qualifies.
123    EmptyStructuring,
124}
125
126impl From<OverlayError> for MinkowskiError {
127    fn from(error: OverlayError) -> Self {
128        Self::Overlay(error)
129    }
130}
131
132/// Which side of the exact disc morphology a region lies on.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134#[non_exhaustive]
135pub enum BoundSide {
136    /// Contained in the exact result. A route found in an inner erosion
137    /// proves reachability; "unreachable" proves nothing.
138    Inner,
139    /// Containing the exact result. "Unreachable" in an outer erosion is a
140    /// proof; a route found in it may be invalid.
141    Outer,
142}
143
144/// How the disc morphology ([`Region::erode_inner_with`] and its
145/// siblings) is computed (#330).
146///
147/// Both modes keep the stated side: an inner result lies within the
148/// exact disc morphology, an outer one holds it, and [`MorphologyBound`]
149/// says by how much they may differ.
150#[derive(Debug, Clone, Copy, PartialEq, Default)]
151#[non_exhaustive]
152pub enum MorphologyMode {
153    /// The exact route of [`Region::erode_inner`] and its siblings, bit
154    /// for bit. Its cost grows with the square of the corners along walls
155    /// of teeth closer together than about a tenth of the radius. The
156    /// choice of a CAD kernel.
157    #[default]
158    Exact,
159    /// The set is summed with its notches filled by chords across its
160    /// outermost corners, where that makes the sum at most `budget`
161    /// larger (in metres, for a disc of the radius): each wall of teeth
162    /// costs about its corners. The excess actually taken, at most
163    /// `budget` and often far less, is added to the
164    /// [`MorphologyBound::deviation`] and reported by
165    /// [`Region::excess`]. The choice of a rule engine.
166    Fast {
167        /// The greatest excess a filled notch may add.
168        budget: f64,
169    },
170}
171
172impl MorphologyMode {
173    /// The fast mode with the given budget.
174    #[must_use]
175    pub const fn fast(budget: f64) -> Self {
176        Self::Fast { budget }
177    }
178}
179
180/// How a disc morphology's result relates to the exact one.
181#[derive(Debug, Clone, Copy, PartialEq)]
182pub struct MorphologyBound {
183    /// Which side of the exact result it lies on.
184    pub side: BoundSide,
185    /// The greatest distance between the two boundaries: the polygonal
186    /// disc's deviation from the true disc, plus the margin kept for
187    /// rounding and presentation.
188    pub deviation: f64,
189}
190
191/// Orientation of `c` against the directed line `a -> b`, exactly.
192struct Orient {
193    a: Point2,
194    b: Point2,
195    c: Point2,
196}
197
198impl SignExpr for Orient {
199    fn sign_in<T: Arith>(&self) -> Option<Sign> {
200        let f = T::from_f64;
201        let (ux, uy) = (f(self.b.x).sub(&f(self.a.x)), f(self.b.y).sub(&f(self.a.y)));
202        let (vx, vy) = (f(self.c.x).sub(&f(self.a.x)), f(self.c.y).sub(&f(self.a.y)));
203        ux.mul(&vy).sub(&uy.mul(&vx)).sign()
204    }
205}
206
207pub(crate) fn orient(a: Point2, b: Point2, c: Point2) -> Sign {
208    certify(&Orient { a, b, c }).unwrap_or(Sign::Zero)
209}
210
211/// A convex polygon's vertices, counter-clockwise, collinear ones dropped.
212fn convex(ring: &Ring, tolerance: Tolerance) -> Result<Vec<Point2>, MinkowskiError> {
213    validate_ring(ring, tolerance)?;
214    let hull = hull(ring.points.clone());
215    // Convex exactly when every vertex is on its hull.
216    let on_hull = ring.points.iter().all(|p| {
217        hull.contains(p) || {
218            // Or on a hull edge (a collinear vertex).
219            (0..hull.len()).any(|i| {
220                let (a, b) = (hull[i], hull[(i + 1) % hull.len()]);
221                orient(a, b, *p) == Sign::Zero
222                    && p.x >= a.x.min(b.x)
223                    && p.x <= a.x.max(b.x)
224                    && p.y >= a.y.min(b.y)
225                    && p.y <= a.y.max(b.y)
226            })
227        }
228    });
229    if !on_hull || hull.len() < 3 {
230        return Err(MinkowskiError::NotConvex);
231    }
232    Ok(hull)
233}
234
235/// The convex hull, counter-clockwise, without collinear vertices
236/// (Andrew's monotone chain on exact orientations).
237fn hull(mut points: Vec<Point2>) -> Vec<Point2> {
238    points.sort_by(|a, b| a.x.total_cmp(&b.x).then(a.y.total_cmp(&b.y)));
239    points.dedup();
240    if points.len() < 3 {
241        return points;
242    }
243    let mut lower: Vec<Point2> = Vec::new();
244    for &p in &points {
245        while lower.len() >= 2
246            && orient(lower[lower.len() - 2], lower[lower.len() - 1], p) != Sign::Positive
247        {
248            lower.pop();
249        }
250        lower.push(p);
251    }
252    let mut upper: Vec<Point2> = Vec::new();
253    for &p in points.iter().rev() {
254        while upper.len() >= 2
255            && orient(upper[upper.len() - 2], upper[upper.len() - 1], p) != Sign::Positive
256        {
257            upper.pop();
258        }
259        upper.push(p);
260    }
261    lower.pop();
262    upper.pop();
263    lower.extend(upper);
264    lower
265}
266
267/// The region's rings in order (per polygon: outer, then holes), and
268/// whether a point with the given ring flags lies in the region.
269fn rings_of(polygons: &[Polygon]) -> Vec<Vec<Point2>> {
270    polygons
271        .iter()
272        .flat_map(|p| std::iter::once(&p.outer).chain(&p.holes))
273        .map(|r| r.points.clone())
274        .collect()
275}
276
277/// Polygons with holes laid out as [`rings_of`] lays them out: each
278/// polygon's outer ring, then its holes.
279#[derive(Debug, Clone)]
280struct Layout {
281    /// The index of each polygon's outer ring, ascending.
282    starts: Vec<usize>,
283    /// The number of rings.
284    len: usize,
285}
286
287impl Layout {
288    fn new(polygons: &[Polygon]) -> Self {
289        let mut starts = Vec::with_capacity(polygons.len());
290        let mut len = 0;
291        for polygon in polygons {
292            starts.push(len);
293            len += 1 + polygon.holes.len();
294        }
295        Self { starts, len }
296    }
297
298    /// One polygon with `holes` holes.
299    fn single(holes: usize) -> Self {
300        Self {
301            starts: vec![0],
302            len: 1 + holes,
303        }
304    }
305
306    /// Whether a point lies in the polygon set, given the rings that hold
307    /// it from the set's first on, ascending, each `offset` past its index
308    /// here. A polygon's rings are consecutive, outer first, so the point
309    /// lies in a polygon exactly when the polygon's outer ring holds it
310    /// and the next ring that does belongs to a later polygon. Rings past
311    /// the set may follow: none is an outer ring here, and each reads as
312    /// one of a later polygon.
313    fn member(&self, rings: &[usize], offset: usize) -> bool {
314        rings.iter().enumerate().any(|(i, &ring)| {
315            let ring = ring - offset;
316            let polygon = self.starts.partition_point(|&s| s <= ring) - 1;
317            let end = self.starts.get(polygon + 1).copied().unwrap_or(self.len);
318            ring == self.starts[polygon] && rings.get(i + 1).is_none_or(|&r| r - offset >= end)
319        })
320    }
321}
322
323/// The ascending `rings` from `first` on.
324fn from(rings: &[usize], first: usize) -> &[usize] {
325    &rings[rings.partition_point(|&r| r < first)..]
326}
327
328/// A ring's points, with neighbours closer than the tolerance merged (a
329/// hull of two nearly equal translates has such pairs).
330fn tidy(points: Vec<Point2>, tolerance: Tolerance) -> Option<ArcRing> {
331    tidy_points(points, tolerance).map(|out| ArcRing::from_points(&out))
332}
333
334/// The points [`tidy`] keeps, when three or more remain.
335fn tidy_points(points: Vec<Point2>, tolerance: Tolerance) -> Option<Vec<Point2>> {
336    let mut out: Vec<Point2> = Vec::with_capacity(points.len());
337    for p in points {
338        if out
339            .last()
340            .is_none_or(|q: &Point2| (p - *q).length() > tolerance.linear())
341        {
342            out.push(p);
343        }
344    }
345    while out.len() > 1 && (out[0] - out[out.len() - 1]).length() <= tolerance.linear() {
346        out.pop();
347    }
348    (out.len() >= 3).then_some(out)
349}
350
351/// The term an edge gives with one convex piece (#323).
352#[derive(Debug, Clone)]
353enum EdgeTerm {
354    /// The edge's whole hull.
355    Whole(Vec<Point2>),
356    /// The hull trimmed by the hull of a chord spanning the edge.
357    Trimmed(Vec<Point2>),
358    /// No term: tidying left too few points, or the chord's hull holds
359    /// the whole hull.
360    Nothing,
361}
362
363/// One polygon of a structuring shape, as the sum takes it.
364#[derive(Debug, Clone)]
365pub(crate) enum Part {
366    /// Convex pieces, counter-clockwise, whose union is the polygon.
367    Pieces(Vec<Vec<Point2>>),
368    /// The polygon's rings, outer first: when no certified convex
369    /// decomposition was found, the sum is taken edge by edge instead.
370    Rings(Vec<Vec<Point2>>),
371}
372
373impl Part {
374    /// The part reflected through the origin.
375    fn flipped(&self) -> Self {
376        let flip = |v: &Vec<Point2>| v.iter().map(|q| Point2::new(-q.x, -q.y)).collect();
377        match self {
378            Self::Pieces(p) => Self::Pieces(p.iter().map(flip).collect()),
379            Self::Rings(r) => Self::Rings(r.iter().map(flip).collect()),
380        }
381    }
382
383    /// A point of the part, by which the other operand is translated: the
384    /// origin when a piece holds it, so that the copy is the operand's own
385    /// rings, which the subdivision shares (#323); else its first vertex.
386    fn anchor(&self) -> Point2 {
387        match self {
388            _ if self.holds_origin() => Point2::new(0.0, 0.0),
389            Self::Pieces(p) => p[0][0],
390            Self::Rings(r) => r[0][0],
391        }
392    }
393
394    fn points(&self) -> impl Iterator<Item = &Point2> {
395        match self {
396            Self::Pieces(p) | Self::Rings(p) => p.iter().flatten(),
397        }
398    }
399
400    /// Whether the origin lies in one of the convex pieces (closed). A
401    /// part taken by its rings answers `false`, which is always safe.
402    fn holds_origin(&self) -> bool {
403        let o = Point2::new(0.0, 0.0);
404        match self {
405            Self::Pieces(pieces) => pieces.iter().any(|piece| {
406                let h = hull(piece.clone());
407                h.len() >= 3
408                    && (0..h.len()).all(|i| orient(h[i], h[(i + 1) % h.len()], o) != Sign::Negative)
409            }),
410            Self::Rings(_) => false,
411        }
412    }
413}
414
415/// The part for one polygon: its convex hull when it is convex (the
416/// convex path, unchanged), else certified convex pieces, else its rings.
417fn polygon_part(
418    outer: &Ring,
419    holes: &[Ring],
420    tolerance: Tolerance,
421) -> Result<Part, MinkowskiError> {
422    if holes.is_empty() {
423        match convex(outer, tolerance) {
424            Ok(k) => return Ok(Part::Pieces(vec![k])),
425            Err(MinkowskiError::NotConvex) => {}
426            Err(other) => return Err(other),
427        }
428    }
429    let holes: Vec<Vec<Point2>> = holes.iter().map(|h| h.points.clone()).collect();
430    Ok(
431        match crate::convex_parts::convex_parts(&outer.points, &holes) {
432            Some(pieces) => Part::Pieces(pieces),
433            None => Part::Rings(std::iter::once(outer.points.clone()).chain(holes).collect()),
434        },
435    )
436}
437
438/// A set given by rings in the arrangement, starting at its first ring.
439#[derive(Debug, Clone)]
440enum Set {
441    /// Polygons with holes.
442    Region(Layout),
443    /// A frame (the first ring) minus such polygons (the rings after it).
444    Outside(Layout),
445}
446
447impl Set {
448    /// For each of the set's `rings`, the side of its travel the set lies
449    /// on: `Positive` left, `Negative` right, `Zero` for a ring of zero
450    /// area. An outer ring of a polygon holds the set inside, a hole
451    /// outside; for [`Self::Outside`] the frame holds it inside and the
452    /// polygons' rings the other way round.
453    fn sides(&self, rings: &[Vec<Point2>]) -> Vec<Sign> {
454        let (offset, shape, outer_inside) = match self {
455            Self::Region(shape) => (0, shape, true),
456            Self::Outside(shape) => (1, shape, false),
457        };
458        rings
459            .iter()
460            .enumerate()
461            .map(|(i, ring)| {
462                let inside = i < offset
463                    || (shape.starts.binary_search(&(i - offset)).is_ok() == outer_inside);
464                let turn = crate::orientation(ring);
465                if inside {
466                    turn
467                } else {
468                    turn.flip()
469                }
470            })
471            .collect()
472    }
473
474    /// The number of rings the set takes.
475    fn span(&self) -> usize {
476        match self {
477            Self::Region(shape) => shape.len,
478            Self::Outside(shape) => 1 + shape.len,
479        }
480    }
481
482    /// Whether a point lies in the set, given the set's rings that hold
483    /// it, ascending, each `offset` past its index in the set.
484    fn holds(&self, rings: &[usize], offset: usize) -> bool {
485        match self {
486            Self::Region(shape) => shape.member(rings, offset),
487            Self::Outside(shape) => {
488                rings.first() == Some(&offset) && !shape.member(&rings[1..], offset + 1)
489            }
490        }
491    }
492}
493
494/// The rings whose union is the sum of a set with a structuring shape,
495/// and how to read it from the rings holding a point.
496struct Terms {
497    rings: Vec<ArcRing>,
498    /// Translated copies of a set, one after the other: the first ring's
499    /// index, and the set.
500    sets: Vec<(usize, Set)>,
501    /// Index of the first convex piece; every ring from there on is one.
502    pieces: usize,
503}
504
505impl Terms {
506    /// Whether a point lies in the union, given the rings holding it,
507    /// ascending, each `offset` past its index in [`Self::rings`].
508    ///
509    /// The union holds the point when a piece does, or when one of the
510    /// translated sets does. Only the sets with a ring holding the point
511    /// are asked, so the cost is the rings holding it, not every ring
512    /// (#292).
513    fn holds(&self, rings: &[usize], offset: usize) -> bool {
514        if rings.last().is_some_and(|&r| r - offset >= self.pieces) {
515            return true;
516        }
517        let mut i = 0;
518        while i < rings.len() {
519            let at = rings[i] - offset;
520            let (first, set) = &self.sets[self.sets.partition_point(|(s, _)| *s <= at) - 1];
521            let end = offset + first + set.span();
522            let next = i + rings[i..].partition_point(|&r| r < end);
523            if set.holds(&rings[i..next], offset + first) {
524                return true;
525            }
526            i = next;
527        }
528        false
529    }
530}
531
532/// How a Minkowski operation reads its result from the rings holding a
533/// point (#292): ascending ring indices, never a flag for every ring.
534enum Rule {
535    /// The union of the terms, whose rings are the arrangement's.
536    Sum(Terms),
537    /// The region's own rings, then the terms of the complement's sum: a
538    /// point lies in the erosion when the region moved by the anchor holds
539    /// it and that sum does not.
540    Erosion {
541        /// The number of the region's own rings.
542        own: usize,
543        /// The first ring of the region moved by the anchor: `0` for the
544        /// region itself.
545        anchor: usize,
546        shape: Layout,
547        terms: Terms,
548    },
549}
550
551impl Rule {
552    fn holds(&self, rings: &[usize]) -> bool {
553        match self {
554            Self::Sum(terms) => terms.holds(rings, 0),
555            Self::Erosion {
556                own,
557                anchor,
558                shape,
559                terms,
560            } => {
561                shape.member(from(rings, *anchor), *anchor) && !terms.holds(from(rings, *own), *own)
562            }
563        }
564    }
565}
566
567/// The sum of the set `set`, bounded by `rings`, with the union of `parts`.
568///
569/// For a connected part `K` and a point `k0` of it,
570/// `S + K = (S + k0) union (union over boundary edges e of S: e + K)`, and
571/// `e + K` is the union of `e + P` over convex pieces `P` of `K`, each the
572/// convex hull of `P + a` and `P + b`. A part taken by its rings `B` uses
573/// `S + B = (dS + dB) union (S + b0) union (a + B for a vertex a of every
574/// ring of S)`, `dS + dB` the parallelograms of edge pairs: if the
575/// boundaries of `x - B` and `S` do not meet yet the two do, an outer ring
576/// of one lies inside the other.
577fn sum_terms(
578    rings: &[Vec<Point2>],
579    set: &Set,
580    parts: &[Part],
581    covers: bool,
582    tolerance: Tolerance,
583) -> Terms {
584    let shift = |p: Point2, by: Point2| Point2::new(p.x + by.x, p.y + by.y);
585    let moved = |r: &[Point2], by: Point2| {
586        ArcRing::from_points(&r.iter().map(|&p| shift(p, by)).collect::<Vec<_>>())
587    };
588    let mut out = Vec::new();
589    let mut sets = Vec::new();
590    for part in parts {
591        sets.push((out.len(), set.clone()));
592        out.extend(rings.iter().map(|r| moved(r, part.anchor())));
593        if let Part::Rings(own) = part {
594            let shape = Layout::single(own.len() - 1);
595            for ring in rings {
596                sets.push((out.len(), Set::Region(shape.clone())));
597                out.extend(own.iter().map(|r| moved(r, ring[0])));
598            }
599        }
600    }
601    let pieces = out.len();
602    // The convex pieces of all parts, and how far they reach: chords are
603    // taken within a sixteenth of that of the vertices they span (#323).
604    let convex: Vec<&Vec<Point2>> = parts
605        .iter()
606        .filter_map(|part| match part {
607            Part::Pieces(p) => Some(p),
608            Part::Rings(_) => None,
609        })
610        .flatten()
611        .collect();
612    let depth = parts
613        .iter()
614        .flat_map(Part::points)
615        .fold(0.0_f64, |m, q| m.max(q.x.abs()).max(q.y.abs()))
616        / 16.0;
617    let sweep = |k: &[Point2], a: Point2, b: Point2| -> Option<Vec<Point2>> {
618        let points: Vec<Point2> = k.iter().flat_map(|&q| [shift(a, q), shift(b, q)]).collect();
619        tidy_points(hull(points), tolerance)
620    };
621    let index = crate::cover::Index::new(rings);
622    let sides = set.sides(rings);
623    // A ring the arrangement refuses (`ZeroArea`, #331).
624    let degenerate = |points: &[Point2]| crate::zero_area(points, tolerance.linear());
625    for (r, ring) in rings.iter().enumerate() {
626        // Exactly collinear edges merged: one hull per straight edge.
627        let boundary = crate::cover::merged(ring);
628        let edges = &boundary.points;
629        let n = edges.len();
630        let edge = |i: usize, k: &[Point2]| sweep(k, edges[i], edges[(i + 1) % n]);
631        // The terms of each edge spanned by a chord proved to lie in the
632        // set, one per convex piece.
633        let mut spanned: Vec<Option<Vec<EdgeTerm>>> = vec![None; n];
634        if covers && !convex.is_empty() {
635            for chord in crate::cover::chords(r, &boundary, sides[r], depth, &index) {
636                let ends = (edges[chord.from], edges[(chord.from + chord.span) % n]);
637                let hulls: Vec<Option<Vec<Point2>>> =
638                    convex.iter().map(|k| sweep(k, ends.0, ends.1)).collect();
639                let terms: Vec<Vec<EdgeTerm>> = (0..chord.span)
640                    .map(|step| {
641                        let i = (chord.from + step) % n;
642                        convex
643                            .iter()
644                            .zip(&hulls)
645                            .map(|(k, g)| match (edge(i, k), g) {
646                                (None, _) => EdgeTerm::Nothing,
647                                (Some(h), None) => EdgeTerm::Whole(h),
648                                (Some(h), Some(g)) => crate::cover::trim(h, g, ends, |points| {
649                                    tidy_points(points, tolerance)
650                                })
651                                .map_or(EdgeTerm::Nothing, EdgeTerm::Trimmed),
652                            })
653                            .collect()
654                    })
655                    .collect();
656                // A chord whose hull, or any hull it trims, has degenerated
657                // is not used: its edges keep their whole hulls (#331).
658                if hulls.iter().flatten().any(|g| degenerate(g))
659                    || terms
660                        .iter()
661                        .flatten()
662                        .any(|term| matches!(term, EdgeTerm::Trimmed(kept) if degenerate(kept)))
663                {
664                    continue;
665                }
666                out.extend(hulls.iter().flatten().map(|h| ArcRing::from_points(h)));
667                for (step, terms) in terms.into_iter().enumerate() {
668                    spanned[(chord.from + step) % n] = Some(terms);
669                }
670            }
671        }
672        // Trimmed hulls already given: the two edges of a tooth often
673        // leave the same sliver, which one copy covers.
674        let mut given: std::collections::HashSet<Vec<(u64, u64)>> =
675            std::collections::HashSet::new();
676        for i in 0..n {
677            let (a, b) = (edges[i], edges[(i + 1) % n]);
678            let terms = spanned[i].take().unwrap_or_else(|| {
679                convex
680                    .iter()
681                    .map(|k| edge(i, k).map_or(EdgeTerm::Nothing, EdgeTerm::Whole))
682                    .collect()
683            });
684            for term in terms {
685                match term {
686                    EdgeTerm::Whole(h) => out.push(ArcRing::from_points(&h)),
687                    EdgeTerm::Trimmed(kept) => {
688                        if given.insert(
689                            kept.iter()
690                                .map(|p| (p.x.to_bits(), p.y.to_bits()))
691                                .collect(),
692                        ) {
693                            out.push(ArcRing::from_points(&kept));
694                        }
695                    }
696                    EdgeTerm::Nothing => {}
697                }
698            }
699            for part in parts {
700                if let Part::Rings(own) = part {
701                    for r in own {
702                        for j in 0..r.len() {
703                            let (p, q) = (r[j], r[(j + 1) % r.len()]);
704                            let points = vec![shift(a, p), shift(a, q), shift(b, p), shift(b, q)];
705                            out.extend(tidy(hull(points), tolerance));
706                        }
707                    }
708                }
709            }
710        }
711    }
712    Terms {
713        rings: out,
714        sets,
715        pieces,
716    }
717}
718
719/// One part per polygon of a region.
720fn parts_of(region: &Region, tolerance: Tolerance) -> Result<Vec<Part>, MinkowskiError> {
721    region
722        .polygons()
723        .iter()
724        .map(|p| polygon_part(&p.outer, &p.holes, tolerance))
725        .collect()
726}
727
728/// The faces of `arrangement` where `inside` holds, as a region.
729///
730/// `inside` receives the rings that contain a point, ascending.
731pub(crate) fn region_of(
732    arrangement: &ArcArrangement,
733    inside: impl Fn(&[usize]) -> bool,
734    had_input: bool,
735    tolerance: Tolerance,
736) -> Result<Region, OverlayError> {
737    let straight = |ring: ArcRing| Ring {
738        points: ring.vertices.iter().map(|v| v.point).collect(),
739    };
740    let mut polygons = Vec::new();
741    for region in arrangement.regions_holding(inside)? {
742        let Some(outer) = crate::arc_overlay::presented(arrangement.ring(&region.outer), tolerance)
743        else {
744            continue;
745        };
746        let holes = region
747            .holes
748            .iter()
749            .filter_map(|h| crate::arc_overlay::presented(arrangement.ring(h), tolerance))
750            .map(straight)
751            .collect();
752        polygons.push(Polygon {
753            outer: straight(outer),
754            holes,
755        });
756    }
757    // Settled, so it carries the proof its rings are valid (#341).
758    let (polygons, validated) = crate::settle::settle_validated(polygons, tolerance);
759    Ok(Region::from_valid_polygons(polygons, had_input).with_validated(validated))
760}
761
762impl Region {
763    /// The Minkowski sum with a polygon `ring`: every point of the region
764    /// moved by every point of the polygon. The polygon may be non-convex.
765    ///
766    /// Decided exactly (ADR 0070), with each vertex sum `a + k` rounded
767    /// once to `f64` and output vertices rounded once. A convex polygon is
768    /// summed whole; a non-convex one is cut into convex pieces (its own
769    /// vertices, the cut certified to tile it) whose sums are united in
770    /// the same arrangement.
771    ///
772    /// # Errors
773    ///
774    /// [`MinkowskiError::Overlay`] for a polygon the overlay refuses or a
775    /// refusal of the arrangement.
776    pub fn minkowski_sum(&self, ring: &Ring, tolerance: Tolerance) -> Result<Self, MinkowskiError> {
777        let part = polygon_part(ring, &[], tolerance)?;
778        Ok(self.sum_parts(&[part], tolerance)?)
779    }
780
781    /// The Minkowski erosion by a polygon `ring`, convex or not: the points
782    /// `x` for which the polygon moved by `x` lies within the region.
783    ///
784    /// # Errors
785    ///
786    /// As [`Self::minkowski_sum`].
787    pub fn minkowski_erosion(
788        &self,
789        ring: &Ring,
790        tolerance: Tolerance,
791    ) -> Result<Self, MinkowskiError> {
792        let part = polygon_part(ring, &[], tolerance)?;
793        Ok(self.erode_parts(&[part], tolerance)?)
794    }
795
796    /// The Minkowski sum with another region -- both may be non-convex,
797    /// have holes and several components: every point of one moved by
798    /// every point of the other. Symmetric in the two regions.
799    ///
800    /// The region with fewer vertices is cut into convex pieces and the
801    /// other summed with each, all in one exact arrangement, rounded as
802    /// [`Self::minkowski_sum`] rounds.
803    ///
804    /// # Errors
805    ///
806    /// [`MinkowskiError::Overlay`] for a refusal of the arrangement.
807    pub fn minkowski_sum_region(
808        &self,
809        other: &Self,
810        tolerance: Tolerance,
811    ) -> Result<Self, MinkowskiError> {
812        if self.is_empty() || other.is_empty() {
813            return Ok(Self::empty());
814        }
815        let size = |r: &Self| {
816            r.polygons()
817                .iter()
818                .flat_map(|p| std::iter::once(&p.outer).chain(&p.holes))
819                .map(|r| r.points.len())
820                .sum::<usize>()
821        };
822        let (base, cut) = if size(self) < size(other) {
823            (other, self)
824        } else {
825            (self, other)
826        };
827        let parts = parts_of(cut, tolerance)?;
828        Ok(base.sum_parts(&parts, tolerance)?)
829    }
830
831    /// The Minkowski erosion by another region: the points `x` for which
832    /// the other region moved by `x` lies within this one.
833    ///
834    /// # Errors
835    ///
836    /// [`MinkowskiError::EmptyStructuring`] for an empty `other` (every
837    /// point would qualify), and as [`Self::minkowski_sum_region`].
838    pub fn minkowski_erosion_region(
839        &self,
840        other: &Self,
841        tolerance: Tolerance,
842    ) -> Result<Self, MinkowskiError> {
843        if other.is_empty() {
844            return Err(MinkowskiError::EmptyStructuring);
845        }
846        let parts = parts_of(other, tolerance)?;
847        Ok(self.erode_parts(&parts, tolerance)?)
848    }
849
850    fn sum_with(&self, k: &[Point2], tolerance: Tolerance) -> Result<Self, OverlayError> {
851        self.sum_parts(&[Part::Pieces(vec![k.to_vec()])], tolerance)
852    }
853
854    fn erode_with(&self, k: &[Point2], tolerance: Tolerance) -> Result<Self, OverlayError> {
855        self.erode_parts(&[Part::Pieces(vec![k.to_vec()])], tolerance)
856    }
857
858    pub(crate) fn sum_parts(
859        &self,
860        parts: &[Part],
861        tolerance: Tolerance,
862    ) -> Result<Self, OverlayError> {
863        if self.is_empty() {
864            return Ok(Self::empty());
865        }
866        let (rings, rule) = self.sum_rule(parts, true, tolerance);
867        let arrangement = ArcArrangement::new(&rings, tolerance)?;
868        region_of(&arrangement, |rings| rule.holds(rings), true, tolerance)
869    }
870
871    /// The rings of a sum's arrangement and the rule that reads the sum
872    /// from them, with the edge hulls trimmed by chords when `covers`
873    /// (#323; the tests compare both). The region must not be empty.
874    fn sum_rule(&self, parts: &[Part], covers: bool, tolerance: Tolerance) -> (Vec<ArcRing>, Rule) {
875        let own = rings_of(self.polygons());
876        let shape = Layout::new(self.polygons());
877        sum_rule_of(&own, shape, parts, covers, tolerance)
878    }
879
880    pub(crate) fn erode_parts(
881        &self,
882        parts: &[Part],
883        tolerance: Tolerance,
884    ) -> Result<Self, OverlayError> {
885        if self.is_empty() {
886            return Ok(Self::empty());
887        }
888        let (rings, rule) = self.erosion_rule(parts, true, tolerance);
889        let arrangement = ArcArrangement::new(&rings, tolerance)?;
890        region_of(&arrangement, |rings| rule.holds(rings), true, tolerance)
891    }
892
893    /// The rings of an erosion's arrangement and the rule that reads the
894    /// erosion from them, the edge hulls trimmed as [`Self::sum_rule`]
895    /// trims them. The region must not be empty.
896    fn erosion_rule(
897        &self,
898        parts: &[Part],
899        covers: bool,
900        tolerance: Tolerance,
901    ) -> (Vec<ArcRing>, Rule) {
902        let own = rings_of(self.polygons());
903        let shape = Layout::new(self.polygons());
904        erosion_rule_of(own, shape, parts, covers, tolerance)
905    }
906}
907
908/// The rings of a sum's arrangement for the set bounded by `own`, laid out
909/// as `shape`, and the rule that reads the sum from them.
910fn sum_rule_of(
911    own: &[Vec<Point2>],
912    shape: Layout,
913    parts: &[Part],
914    covers: bool,
915    tolerance: Tolerance,
916) -> (Vec<ArcRing>, Rule) {
917    let mut terms = sum_terms(own, &Set::Region(shape), parts, covers, tolerance);
918    (std::mem::take(&mut terms.rings), Rule::Sum(terms))
919}
920
921/// The rings of an erosion's arrangement for the region bounded by `own`,
922/// laid out as `shape`, and the rule that reads the erosion from them.
923fn erosion_rule_of(
924    own: Vec<Vec<Point2>>,
925    shape: Layout,
926    parts: &[Part],
927    covers: bool,
928    tolerance: Tolerance,
929) -> (Vec<ArcRing>, Rule) {
930    // A box holding the region: its complement within the box, summed
931    // with -K, reaches every point of the region that K cannot sit
932    // around (a translate of K leaving the box crosses its boundary,
933    // whose own edge pieces cover that). The room about the region only
934    // keeps the box's edges off the region's.
935    let (mut lo, mut hi) = (own[0][0], own[0][0]);
936    for p in own.iter().flatten() {
937        lo = lo.min(*p);
938        hi = hi.max(*p);
939    }
940    let reach = parts
941        .iter()
942        .flat_map(Part::points)
943        .fold(0.0_f64, |m, q| m.max(q.x.abs()).max(q.y.abs()));
944    let pad = 2.0 * reach + (hi - lo).max_element() + 1.0;
945    let frame = vec![
946        Point2::new(lo.x - pad, lo.y - pad),
947        Point2::new(hi.x + pad, lo.y - pad),
948        Point2::new(hi.x + pad, hi.y + pad),
949        Point2::new(lo.x - pad, hi.y + pad),
950    ];
951    let mut outside = vec![frame];
952    outside.extend(own.iter().cloned());
953    let flipped: Vec<Part> = parts.iter().map(Part::flipped).collect();
954    let mut terms = sum_terms(
955        &outside,
956        &Set::Outside(shape.clone()),
957        &flipped,
958        covers,
959        tolerance,
960    );
961    let m = own.len();
962    let mut rings: Vec<ArcRing> = own.iter().map(|r| ArcRing::from_points(r)).collect();
963    rings.append(&mut terms.rings);
964    // A point of the erosion moved by the anchor `k0` of the first
965    // part lies in the region: when the origin is in K that is implied
966    // by the region itself; otherwise it is read from the region moved
967    // by `-k0`, whose rings follow the frame's in the first translated
968    // copy (frame minus region).
969    let origin = parts.iter().any(Part::holds_origin);
970    let first = m + terms.sets[0].0 + 1;
971    (
972        rings,
973        Rule::Erosion {
974            anchor: if origin { 0 } else { first },
975            own: m,
976            shape,
977            terms,
978        },
979    )
980}
981
982impl Region {
983    /// Dilation by a disc of `radius`, contained in the exact one: the sum
984    /// with an inscribed polygon, drawn in by a margin.
985    ///
986    /// # Errors
987    ///
988    /// [`OverlayError::InvalidOffsetDistance`] for a radius that is not
989    /// finite and nonnegative, and overlay refusals.
990    pub fn dilate_inner(&self, radius: f64, tolerance: Tolerance) -> Result<Self, OverlayError> {
991        self.disc(
992            radius,
993            BoundSide::Inner,
994            false,
995            MorphologyMode::Exact,
996            tolerance,
997        )
998    }
999
1000    /// Dilation by a disc of `radius`, containing the exact one: the sum
1001    /// with a circumscribed polygon, pushed out by a margin.
1002    ///
1003    /// # Errors
1004    ///
1005    /// As [`Self::dilate_inner`].
1006    pub fn dilate_outer(&self, radius: f64, tolerance: Tolerance) -> Result<Self, OverlayError> {
1007        self.disc(
1008            radius,
1009            BoundSide::Outer,
1010            false,
1011            MorphologyMode::Exact,
1012            tolerance,
1013        )
1014    }
1015
1016    /// Erosion by a disc of `radius`, contained in the exact one: eroded by
1017    /// a circumscribed polygon, pushed out by a margin. A route found in it
1018    /// proves reachability.
1019    ///
1020    /// # Errors
1021    ///
1022    /// As [`Self::dilate_inner`].
1023    pub fn erode_inner(&self, radius: f64, tolerance: Tolerance) -> Result<Self, OverlayError> {
1024        self.disc(
1025            radius,
1026            BoundSide::Inner,
1027            true,
1028            MorphologyMode::Exact,
1029            tolerance,
1030        )
1031    }
1032
1033    /// Erosion by a disc of `radius`, containing the exact one: eroded by
1034    /// an inscribed polygon, drawn in by a margin. No route in it proves
1035    /// unreachability.
1036    ///
1037    /// # Errors
1038    ///
1039    /// As [`Self::dilate_inner`].
1040    pub fn erode_outer(&self, radius: f64, tolerance: Tolerance) -> Result<Self, OverlayError> {
1041        self.disc(
1042            radius,
1043            BoundSide::Outer,
1044            true,
1045            MorphologyMode::Exact,
1046            tolerance,
1047        )
1048    }
1049
1050    /// [`Self::dilate_inner`] in the given mode: [`MorphologyMode::Exact`]
1051    /// gives the same result, [`MorphologyMode::Fast`] one whose
1052    /// [`MorphologyBound`] (still `Inner`) carries the excess too.
1053    ///
1054    /// # Errors
1055    ///
1056    /// As [`Self::dilate_inner`], and
1057    /// [`OverlayError::InvalidOffsetDistance`] for a fast mode's budget that
1058    /// is not finite and nonnegative.
1059    pub fn dilate_inner_with(
1060        &self,
1061        radius: f64,
1062        tolerance: Tolerance,
1063        mode: MorphologyMode,
1064    ) -> Result<Self, OverlayError> {
1065        self.disc(radius, BoundSide::Inner, false, mode, tolerance)
1066    }
1067
1068    /// [`Self::dilate_outer`] in the given mode, as
1069    /// [`Self::dilate_inner_with`].
1070    ///
1071    /// # Errors
1072    ///
1073    /// As [`Self::dilate_inner_with`].
1074    pub fn dilate_outer_with(
1075        &self,
1076        radius: f64,
1077        tolerance: Tolerance,
1078        mode: MorphologyMode,
1079    ) -> Result<Self, OverlayError> {
1080        self.disc(radius, BoundSide::Outer, false, mode, tolerance)
1081    }
1082
1083    /// [`Self::erode_inner`] in the given mode, as
1084    /// [`Self::dilate_inner_with`].
1085    ///
1086    /// # Errors
1087    ///
1088    /// As [`Self::dilate_inner_with`].
1089    pub fn erode_inner_with(
1090        &self,
1091        radius: f64,
1092        tolerance: Tolerance,
1093        mode: MorphologyMode,
1094    ) -> Result<Self, OverlayError> {
1095        self.disc(radius, BoundSide::Inner, true, mode, tolerance)
1096    }
1097
1098    /// [`Self::erode_outer`] in the given mode, as
1099    /// [`Self::dilate_inner_with`].
1100    ///
1101    /// # Errors
1102    ///
1103    /// As [`Self::dilate_inner_with`].
1104    pub fn erode_outer_with(
1105        &self,
1106        radius: f64,
1107        tolerance: Tolerance,
1108        mode: MorphologyMode,
1109    ) -> Result<Self, OverlayError> {
1110        self.disc(radius, BoundSide::Outer, true, mode, tolerance)
1111    }
1112
1113    fn disc(
1114        &self,
1115        radius: f64,
1116        side: BoundSide,
1117        erode: bool,
1118        mode: MorphologyMode,
1119        tolerance: Tolerance,
1120    ) -> Result<Self, OverlayError> {
1121        if !radius.is_finite() || radius < 0.0 {
1122            return Err(OverlayError::InvalidOffsetDistance);
1123        }
1124        if let MorphologyMode::Fast { budget } = mode {
1125            if !budget.is_finite() || budget < 0.0 {
1126                return Err(OverlayError::InvalidOffsetDistance);
1127            }
1128        }
1129        if self.is_empty() || radius == 0.0 {
1130            let mut same = Self::from_valid_polygons(self.polygons().to_vec(), false)
1131                .with_validated(self.validated());
1132            same.set_bound(MorphologyBound {
1133                side,
1134                deviation: 0.0,
1135            });
1136            return Ok(same);
1137        }
1138        // Sixty-four sides: the inscribed polygon's sagitta is
1139        // r (1 - cos(pi / 64)), about 0.12% of the radius.
1140        let n = 64;
1141        let half = core::f64::consts::PI / n as f64;
1142        // The margin: past the vertex sums' and output vertices' rounding
1143        // (a few ulps of the largest coordinate), the sin/cos of the
1144        // polygon's own vertices (relative), and the presentation's dropping
1145        // of edges shorter than the tolerance.
1146        let size = self
1147            .polygons()
1148            .iter()
1149            .flat_map(|p| std::iter::once(&p.outer).chain(&p.holes))
1150            .flat_map(|r| r.points.iter())
1151            .fold(0.0_f64, |m, p| m.max(p.x.abs()).max(p.y.abs()));
1152        let margin =
1153            2.0 * tolerance.linear() + 1e-12 * radius + 64.0 * f64::EPSILON * (size + radius);
1154        // Inner dilation and outer erosion use the inscribed polygon, the
1155        // other two the circumscribed one.
1156        let inscribed = (side == BoundSide::Inner) != erode;
1157        let (mut reach, mut deviation) = if inscribed {
1158            let reach = radius - margin;
1159            (reach, radius - reach * half.cos())
1160        } else {
1161            let reach = (radius + margin) / half.cos();
1162            (reach, reach - radius)
1163        };
1164        if reach <= 0.0 {
1165            // The margin swallows the disc: the polygon is a point for the
1166            // inner dilation (the region itself) and the region itself is an
1167            // outer erosion.
1168            let mut same = Self::from_valid_polygons(self.polygons().to_vec(), true)
1169                .with_validated(self.validated());
1170            same.set_bound(MorphologyBound {
1171                side,
1172                deviation: radius + margin,
1173            });
1174            return Ok(same);
1175        }
1176        // The fast mode sums the set with its notches filled, `excess`
1177        // larger (`crate::fill`, #330): for discs of half the inscribed
1178        // radius or more, the budget no more than that half, so that an
1179        // inscribed polygon drawn in by the excess still reaches it.
1180        let filled = match mode {
1181            MorphologyMode::Exact => None,
1182            MorphologyMode::Fast { budget } => {
1183                let own = rings_of(self.polygons());
1184                let shape = Layout::new(self.polygons());
1185                let mut sides = Set::Region(shape.clone()).sides(&own);
1186                if erode {
1187                    // The sum is the complement's.
1188                    for s in &mut sides {
1189                        *s = s.flip();
1190                    }
1191                }
1192                let t = 0.5 * (radius - margin);
1193                let filled = crate::fill::fill(&own, &sides, t, budget.min(t), tolerance.linear());
1194                if inscribed {
1195                    reach -= filled.excess;
1196                    deviation = radius - reach * half.cos();
1197                } else {
1198                    deviation += filled.excess;
1199                }
1200                Some((filled, shape))
1201            }
1202        };
1203        let k: Vec<Point2> = (0..n)
1204            .map(|i| {
1205                let t = 2.0 * half * i as f64;
1206                Point2::new(reach * t.cos(), reach * t.sin())
1207            })
1208            .collect();
1209        let mut out = match filled {
1210            None if erode => self.erode_with(&k, tolerance)?,
1211            None => self.sum_with(&k, tolerance)?,
1212            Some((filled, shape)) => {
1213                let parts = [Part::Pieces(vec![k])];
1214                let (rings, rule) = if erode {
1215                    erosion_rule_of(filled.rings, shape, &parts, true, tolerance)
1216                } else {
1217                    sum_rule_of(&filled.rings, shape, &parts, true, tolerance)
1218                };
1219                let arrangement = ArcArrangement::new(&rings, tolerance)?;
1220                let mut out = region_of(&arrangement, |r| rule.holds(r), true, tolerance)?;
1221                out.set_excess(filled.excess);
1222                out
1223            }
1224        };
1225        out.set_bound(MorphologyBound { side, deviation });
1226        Ok(out)
1227    }
1228}
1229
1230#[cfg(test)]
1231mod tests {
1232    use super::*;
1233    use crate::Polygon;
1234
1235    fn ring(points: &[(f64, f64)]) -> Ring {
1236        Ring {
1237            points: points.iter().map(|&(x, y)| Point2::new(x, y)).collect(),
1238        }
1239    }
1240
1241    fn u_with_hole() -> Region {
1242        Region::new(
1243            vec![Polygon {
1244                outer: ring(&[
1245                    (0.0, 0.0),
1246                    (6.0, 0.0),
1247                    (6.0, 5.0),
1248                    (4.0, 5.0),
1249                    (4.0, 2.0),
1250                    (2.0, 2.0),
1251                    (2.0, 5.0),
1252                    (0.0, 5.0),
1253                ]),
1254                holes: vec![ring(&[(0.5, 0.5), (1.5, 0.5), (1.5, 1.5), (0.5, 1.5)])],
1255            }],
1256            Tolerance::METRE,
1257        )
1258        .unwrap()
1259    }
1260
1261    fn shapes() -> Vec<(Ring, Vec<Ring>)> {
1262        vec![
1263            // An L-shape.
1264            (
1265                ring(&[
1266                    (0., 0.),
1267                    (0.8, 0.),
1268                    (0.8, 0.4),
1269                    (0.4, 0.4),
1270                    (0.4, 0.8),
1271                    (0., 0.8),
1272                ]),
1273                vec![],
1274            ),
1275            // A dart off the origin, clockwise.
1276            (
1277                ring(&[(0.6, 0.4), (0.3, 1.0), (1.5, 0.4), (0.3, -0.2)]),
1278                vec![],
1279            ),
1280            // A square ring.
1281            (
1282                ring(&[(-0.6, -0.6), (0.6, -0.6), (0.6, 0.6), (-0.6, 0.6)]),
1283                vec![ring(&[(-0.3, -0.3), (0.3, -0.3), (0.3, 0.3), (-0.3, 0.3)])],
1284            ),
1285        ]
1286    }
1287
1288    fn apart(a: &Region, b: &Region) -> f64 {
1289        let t = Tolerance::METRE;
1290        a.difference(b, t).unwrap().area() + b.difference(a, t).unwrap().area()
1291    }
1292
1293    /// The rule as it was read before #292, from a flag for every ring:
1294    /// a polygon holds a point when its outer ring does and none of its
1295    /// holes do.
1296    fn dense_member(shape: &Layout, flags: &[bool]) -> bool {
1297        (0..shape.starts.len()).any(|p| {
1298            let (at, end) = (
1299                shape.starts[p],
1300                shape.starts.get(p + 1).copied().unwrap_or(shape.len),
1301            );
1302            flags[at] && !flags[at + 1..end].iter().any(|&f| f)
1303        })
1304    }
1305
1306    fn dense_terms(terms: &Terms, flags: &[bool]) -> bool {
1307        terms.sets.iter().any(|(at, set)| match set {
1308            Set::Region(shape) => dense_member(shape, &flags[*at..]),
1309            Set::Outside(shape) => flags[*at] && !dense_member(shape, &flags[at + 1..]),
1310        }) || flags[terms.pieces..].iter().any(|&f| f)
1311    }
1312
1313    fn dense_rule(rule: &Rule, flags: &[bool]) -> bool {
1314        match rule {
1315            Rule::Sum(terms) => dense_terms(terms, flags),
1316            Rule::Erosion {
1317                own,
1318                anchor,
1319                shape,
1320                terms,
1321            } => dense_member(shape, &flags[*anchor..]) && !dense_terms(terms, &flags[*own..]),
1322        }
1323    }
1324
1325    #[test]
1326    fn the_sparse_rule_reads_every_piece_as_the_flags_did() {
1327        // Several polygons, holes, convex and cut shapes, the edge-pair
1328        // route, and erosions anchored on the region and on its copy: on
1329        // both sides of every piece, the rings holding the side decide as
1330        // a flag per ring did (#292).
1331        let t = Tolerance::METRE;
1332        let square =
1333            |x: f64, y: f64, s: f64| ring(&[(x, y), (x + s, y), (x + s, y + s), (x, y + s)]);
1334        let two_holes = Region::new(
1335            vec![Polygon {
1336                outer: square(8.0, 0.0, 4.0),
1337                holes: vec![square(8.5, 0.5, 1.0), square(10.0, 2.0, 1.5)],
1338            }],
1339            t,
1340        )
1341        .unwrap();
1342        let region = u_with_hole()
1343            .union(&two_holes, t)
1344            .unwrap()
1345            .union(
1346                &Region::new(
1347                    vec![Polygon {
1348                        outer: square(3.0, 3.0, 0.1),
1349                        holes: vec![],
1350                    }],
1351                    t,
1352                )
1353                .unwrap(),
1354                t,
1355            )
1356            .unwrap();
1357        assert_eq!(region.polygons().len(), 3);
1358        let mut parts = vec![Part::Pieces(vec![vec![
1359            Point2::new(-0.3, -0.3),
1360            Point2::new(0.3, -0.3),
1361            Point2::new(0.3, 0.3),
1362            Point2::new(-0.3, 0.3),
1363        ]])];
1364        for (outer, holes) in shapes() {
1365            parts.push(polygon_part(&outer, &holes, t).unwrap());
1366            parts.push(Part::Rings(
1367                std::iter::once(outer.points.clone())
1368                    .chain(holes.iter().map(|h| h.points.clone()))
1369                    .collect(),
1370            ));
1371        }
1372        let mut sides = 0;
1373        for part in &parts {
1374            for (rings, rule) in [
1375                region.sum_rule(std::slice::from_ref(part), true, t),
1376                region.erosion_rule(std::slice::from_ref(part), true, t),
1377            ] {
1378                let arrangement = ArcArrangement::new(&rings, t).unwrap();
1379                let mut held = 0;
1380                for edge in arrangement.edges() {
1381                    for left in [true, false] {
1382                        let flags: Vec<bool> = (0..rings.len())
1383                            .map(|r| {
1384                                if left {
1385                                    edge.inside_left(r)
1386                                } else {
1387                                    edge.inside_right(r)
1388                                }
1389                            })
1390                            .collect();
1391                        let holding: Vec<usize> = (0..rings.len()).filter(|&r| flags[r]).collect();
1392                        let mut listed = Vec::new();
1393                        edge.holding(left, &mut listed);
1394                        assert_eq!(listed, holding);
1395                        let dense = dense_rule(&rule, &flags);
1396                        assert_eq!(rule.holds(&holding), dense, "{holding:?}");
1397                        held += usize::from(dense);
1398                        sides += 1;
1399                    }
1400                }
1401                assert!(held > 0);
1402            }
1403        }
1404        assert!(sides > 10_000, "{sides}");
1405    }
1406
1407    #[test]
1408    fn non_convex_polygons_are_cut_into_few_convex_pieces() {
1409        let pieces: Vec<usize> = shapes()
1410            .iter()
1411            .map(
1412                |(outer, holes)| match polygon_part(outer, holes, Tolerance::METRE).unwrap() {
1413                    Part::Pieces(p) => p.len(),
1414                    Part::Rings(_) => panic!("not decomposed"),
1415                },
1416            )
1417            .collect();
1418        assert_eq!(pieces[0], 2);
1419        assert_eq!(pieces[1], 2);
1420        assert!(pieces[2] <= 6, "{pieces:?}");
1421    }
1422
1423    #[test]
1424    fn the_edge_pair_route_agrees_with_the_convex_pieces() {
1425        // The route taken when no certified decomposition is found. The
1426        // small square fits wholly inside the shapes, where only the shape
1427        // moved by its vertex finds it.
1428        let t = Tolerance::METRE;
1429        let small = Region::new(
1430            vec![Polygon {
1431                outer: ring(&[(3.0, 3.0), (3.1, 3.0), (3.1, 3.1), (3.0, 3.1)]),
1432                holes: vec![],
1433            }],
1434            t,
1435        )
1436        .unwrap();
1437        let r = u_with_hole().union(&small, t).unwrap();
1438        for (outer, holes) in shapes() {
1439            let cut = polygon_part(&outer, &holes, t).unwrap();
1440            let whole = Part::Rings(
1441                std::iter::once(outer.points.clone())
1442                    .chain(holes.iter().map(|h| h.points.clone()))
1443                    .collect(),
1444            );
1445            let (a, b) = (
1446                r.sum_parts(std::slice::from_ref(&cut), t).unwrap(),
1447                r.sum_parts(std::slice::from_ref(&whole), t).unwrap(),
1448            );
1449            assert!(!a.is_empty() && apart(&a, &b) < 1e-9, "{}", apart(&a, &b));
1450            let (a, b) = (
1451                r.erode_parts(std::slice::from_ref(&cut), t).unwrap(),
1452                r.erode_parts(std::slice::from_ref(&whole), t).unwrap(),
1453            );
1454            assert!(!a.is_empty() && apart(&a, &b) < 1e-9, "{}", apart(&a, &b));
1455        }
1456    }
1457
1458    /// A wall from `a` to `b` with `teeth` teeth of `depth` to its left,
1459    /// without its end `b`.
1460    fn teeth(a: (f64, f64), b: (f64, f64), teeth: usize, depth: f64) -> Vec<(f64, f64)> {
1461        let (dx, dy) = (b.0 - a.0, b.1 - a.1);
1462        let len = dx.hypot(dy);
1463        let (nx, ny) = (-dy / len, dx / len);
1464        (0..2 * teeth)
1465            .map(|k| {
1466                let t = k as f64 / (2 * teeth) as f64;
1467                let d = if k % 2 == 1 { depth } else { 0.0 };
1468                (a.0 + t * dx + d * nx, a.1 + t * dy + d * ny)
1469            })
1470            .collect()
1471    }
1472
1473    /// Regions whose walls carry teeth, exactly collinear corners or a
1474    /// tessellated column (#323), and shapes to sum and erode them by.
1475    fn walls() -> Vec<(&'static str, Region)> {
1476        let t = Tolerance::new(1e-9, 1e-9).unwrap();
1477        let region = |rings: Vec<Vec<(f64, f64)>>| {
1478            Region::new(
1479                vec![Polygon {
1480                    outer: ring(&rings[0]),
1481                    holes: rings[1..].iter().map(|h| ring(h)).collect(),
1482                }],
1483                t,
1484            )
1485            .unwrap()
1486        };
1487        let room = |depth: f64| {
1488            let mut outer = teeth((0.0, 0.0), (4.0, 0.0), 30, depth);
1489            outer.extend(teeth((4.0, 0.0), (4.0, 3.0), 2, 0.0));
1490            outer.extend(teeth((4.0, 3.0), (0.0, 3.0), 30, depth));
1491            outer.push((0.0, 3.0));
1492            outer
1493        };
1494        let notch = {
1495            // An L whose inner walls, in the notch, carry teeth.
1496            let mut outer = vec![(0.0, 0.0), (4.0, 0.0)];
1497            outer.extend(teeth((4.0, 1.5), (2.0, 1.5), 20, 5e-4));
1498            outer.extend(teeth((2.0, 1.5), (2.0, 3.0), 15, 5e-4));
1499            outer.extend([(2.0, 3.0), (0.0, 3.0)]);
1500            outer
1501        };
1502        let column: Vec<(f64, f64)> = (0..40)
1503            .map(|i| {
1504                let a = -(i as f64) * std::f64::consts::TAU / 40.0;
1505                (2.0 + 0.4 * a.cos(), 1.5 + 0.4 * a.sin())
1506            })
1507            .collect();
1508        let mut pillar = teeth((1.0, 1.0), (1.0, 2.0), 10, -5e-4);
1509        pillar.extend(teeth((1.0, 2.0), (1.4, 2.0), 4, -5e-4));
1510        pillar.extend(teeth((1.4, 2.0), (1.4, 1.0), 10, -5e-4));
1511        pillar.extend(teeth((1.4, 1.0), (1.0, 1.0), 4, -5e-4));
1512        let (s, c) = 2.3f64.to_radians().sin_cos();
1513        let placed: Vec<(f64, f64)> = room(5e-4)
1514            .into_iter()
1515            .map(|(x, y)| (640_000.0 + c * x - s * y, 5_650_000.0 + s * x + c * y))
1516            .collect();
1517        // A diamond of the region straddling the line through the valleys
1518        // under a tooth: no chord may run along that line.
1519        let peak = 4.0 * 21.0 / 60.0;
1520        let island = Region::new(
1521            vec![Polygon {
1522                outer: ring(&[
1523                    (peak, -1e-4),
1524                    (peak + 2e-4, 0.0),
1525                    (peak, 1e-4),
1526                    (peak - 2e-4, 0.0),
1527                ]),
1528                holes: vec![],
1529            }],
1530            t,
1531        )
1532        .unwrap();
1533        vec![
1534            ("teeth in", region(vec![room(5e-4)])),
1535            ("teeth out", region(vec![room(-5e-4)])),
1536            ("collinear", region(vec![room(0.0)])),
1537            ("notch", region(vec![notch])),
1538            (
1539                "column and pillar",
1540                region(vec![room(5e-4), column, pillar]),
1541            ),
1542            ("placed", region(vec![placed])),
1543            (
1544                "island",
1545                region(vec![room(5e-4)]).union(&island, t).unwrap(),
1546            ),
1547        ]
1548    }
1549
1550    #[test]
1551    fn chord_covers_leave_sums_and_erosions_unchanged() {
1552        // Trimming the edge hulls by chords proved to lie in the set keeps
1553        // the union of the terms, so every result is the one built from
1554        // the whole hulls (#323), bit for bit, while the terms shrink.
1555        let t = Tolerance::new(1e-9, 1e-9).unwrap();
1556        let disc: Vec<Point2> = (0..64)
1557            .map(|i| {
1558                let a = i as f64 * std::f64::consts::TAU / 64.0;
1559                Point2::new(0.3 * a.cos(), 0.3 * a.sin())
1560            })
1561            .collect();
1562        let l_shape = polygon_part(&shapes()[0].0, &[], t).unwrap();
1563        let size = |rings: &[ArcRing]| rings.iter().map(|r| r.vertices.len()).sum::<usize>();
1564        for (name, region) in walls() {
1565            for (round, part) in [
1566                (true, Part::Pieces(vec![disc.clone()])),
1567                (false, l_shape.clone()),
1568            ] {
1569                let parts = std::slice::from_ref(&part);
1570                for erosion in [false, true] {
1571                    let build = |covers: bool| {
1572                        let (rings, rule) = if erosion {
1573                            region.erosion_rule(parts, covers, t)
1574                        } else {
1575                            region.sum_rule(parts, covers, t)
1576                        };
1577                        let arrangement = ArcArrangement::new(&rings, t).unwrap();
1578                        let out = region_of(&arrangement, |r| rule.holds(r), true, t).unwrap();
1579                        (size(&rings), out)
1580                    };
1581                    let ((trimmed, a), (whole, b)) = (build(true), build(false));
1582                    assert!(!a.is_empty(), "{name}");
1583                    assert_eq!(a.polygons(), b.polygons(), "{name}, erosion {erosion}");
1584                    // A disc's hulls lose most of their vertices, an L's
1585                    // few pieces less.
1586                    match name {
1587                        "collinear" => {}
1588                        _ if round && name != "island" => {
1589                            assert!(2 * trimmed < whole, "{name}: {trimmed} of {whole}");
1590                        }
1591                        _ => assert!(trimmed < whole, "{name}: {trimmed} of {whole}"),
1592                    }
1593                }
1594            }
1595        }
1596    }
1597
1598    #[test]
1599    fn exactly_collinear_corners_cost_and_give_what_the_plain_room_does() {
1600        // Merged before the hulls are built (#323): the same rings as for
1601        // the rectangle, so the same result bit for bit.
1602        let t = Tolerance::new(1e-9, 1e-9).unwrap();
1603        let walls = walls();
1604        let collinear = &walls.iter().find(|(n, _)| *n == "collinear").unwrap().1;
1605        let plain = Region::new(
1606            vec![Polygon {
1607                outer: ring(&[(0.0, 0.0), (4.0, 0.0), (4.0, 3.0), (0.0, 3.0)]),
1608                holes: vec![],
1609            }],
1610            t,
1611        )
1612        .unwrap();
1613        for radius in [0.3, 0.75] {
1614            for (a, b) in [
1615                (
1616                    collinear.erode_inner(radius, t),
1617                    plain.erode_inner(radius, t),
1618                ),
1619                (
1620                    collinear.erode_outer(radius, t),
1621                    plain.erode_outer(radius, t),
1622                ),
1623                (
1624                    collinear.dilate_inner(radius, t),
1625                    plain.dilate_inner(radius, t),
1626                ),
1627                (
1628                    collinear.dilate_outer(radius, t),
1629                    plain.dilate_outer(radius, t),
1630                ),
1631            ] {
1632                assert_eq!(a.unwrap().polygons(), b.unwrap().polygons());
1633            }
1634        }
1635    }
1636}