Skip to main content

axiolid_overlay/
region.rs

1//! A persistent planar region with set algebra, morphology and components.
2//!
3//! # Why a type rather than more free functions
4//!
5//! [`crate::overlay`] is stateless: it validates both operands on every
6//! call and returns a fresh result. That is the right primitive, but a
7//! consumer that builds free space, subtracts obstacles, erodes by a body
8//! radius and then counts what is left has to thread raw polygon vectors
9//! between calls and pay revalidation each time. Area and component count
10//! are not reachable at all.
11//!
12//! A [`Region`] is validated once at construction. Every operation on it
13//! consumes already-valid geometry and produces already-valid geometry, so
14//! the invariant is established at the boundary rather than re-proven at
15//! each step.
16//!
17//! # Emptiness is a reported fact, not an absence
18//!
19//! Every operation that can annihilate a region reports whether it did.
20//! An erosion that removes the last of a corridor and an intersection of
21//! two disjoint regions both yield no polygons, but they are different
22//! facts, and a bare empty vector cannot tell a caller which happened.
23//! [`RegionEvidence::emptied`] records that the operation consumed a
24//! non-empty input, so "nothing was there" stays distinguishable from
25//! "this operation destroyed it".
26//!
27//! # Boundary
28//!
29//! The kernel owns the region, the operations and the reported measures.
30//! It does not own what the region represents, which radius is correct, or
31//! whether a resulting area or component count is acceptable.
32
33use axiolid_core::{Point2, Tolerance, Vec2};
34
35use crate::minkowski::MorphologyBound;
36use crate::offset::{offset_polygons, total_area, JoinStyle};
37use crate::settle::Validated;
38use crate::{canonical, OverlayError, Polygon, Ring};
39
40/// What an operation did to produce a region.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42pub struct RegionEvidence {
43    /// Number of disjoint polygons in the result.
44    pub polygons: usize,
45    /// Number of inner boundary components across all polygons.
46    pub holes: usize,
47    /// The operation consumed a non-empty input and produced nothing.
48    ///
49    /// Distinguishes "eroded out of existence" or "disjoint operands" from
50    /// "the input was already empty", which a bare empty result cannot.
51    pub emptied: bool,
52}
53
54/// A validated planar region: a normalised set of polygons with holes,
55/// whose holes lie apart inside their outer rings and whose polygons do
56/// not overlap (#448).
57///
58/// Construction validates; operations preserve validity: a boolean or a
59/// morphology returns the faces of an exact arrangement, apart and nested
60/// by construction, and [`Region::translate`], which rounds each vertex on
61/// its own, checks its result. Ring ordering is
62/// canonical (outer counter-clockwise, holes clockwise, each rotated to its
63/// lexicographically smallest vertex) and polygons are sorted, so equal
64/// regions compare equal regardless of how they were built.
65///
66/// A region built by [`Region::new`] or by a boolean on regions carries
67/// the proof that its rings are valid under that call's tolerance (#341),
68/// and a boolean on it under the same tolerance does not validate them
69/// again. The proof is not part of equality.
70#[derive(Debug, Clone, Default)]
71pub struct Region {
72    polygons: Vec<Polygon>,
73    evidence: RegionEvidence,
74    /// For a disc morphology with a stated side, that side and the
75    /// deviation from the exact result (#163).
76    bound: Option<MorphologyBound>,
77    /// For a fast disc morphology, the excess its filled notches add
78    /// (#330).
79    excess: Option<f64>,
80    /// Every ring passes validation under this tolerance (#341): set by
81    /// [`Region::new`] and by settled outputs only.
82    validated: Option<Validated>,
83}
84
85impl PartialEq for Region {
86    fn eq(&self, other: &Self) -> bool {
87        // The proof says how the region was reached, not what it is.
88        self.polygons == other.polygons
89            && self.evidence == other.evidence
90            && self.bound == other.bound
91            && self.excess == other.excess
92    }
93}
94
95/// Normalise a polygon set: canonical ring order, deterministic sorting.
96///
97/// Applied to every operation result so that two regions holding the same
98/// point set are structurally equal, which is what makes idempotence and
99/// round-trip assertions meaningful rather than incidental.
100fn normalise(mut polygons: Vec<Polygon>) -> Vec<Polygon> {
101    polygons = polygons
102        .into_iter()
103        .map(|polygon| Polygon {
104            outer: canonical(polygon.outer, true),
105            holes: polygon
106                .holes
107                .into_iter()
108                .map(|hole| canonical(hole, false))
109                .collect(),
110        })
111        .collect();
112    for polygon in &mut polygons {
113        polygon.holes.sort_by(|a, b| {
114            a.points[0]
115                .x
116                .total_cmp(&b.points[0].x)
117                .then(a.points[0].y.total_cmp(&b.points[0].y))
118        });
119    }
120    polygons.sort_by(|a, b| {
121        a.outer.points[0]
122            .x
123            .total_cmp(&b.outer.points[0].x)
124            .then(a.outer.points[0].y.total_cmp(&b.outer.points[0].y))
125    });
126    polygons
127}
128
129impl Region {
130    /// The empty region. Not a failure: the identity for union.
131    #[must_use]
132    pub fn empty() -> Self {
133        Self::default()
134    }
135
136    /// Build a region from polygons, validating every ring and where the
137    /// rings lie.
138    ///
139    /// Each ring is validated as [`crate::overlay`] validates an operand's,
140    /// so the two cannot disagree about what a well-formed ring is. Then,
141    /// decided exactly and with no tolerance, every hole must lie inside its
142    /// own outer ring and in no other of its holes, and no point may lie in
143    /// two polygons (#448). Rings may touch at points, and along a stretch
144    /// they run with the polygons' insides on opposite sides, as settled outputs
145    /// do. An `overlay` operand is not held to this: its rings may overlap,
146    /// read by its fill rule.
147    ///
148    /// # Errors
149    ///
150    /// A ring's own refusal ([`OverlayError::RingTooShort`] and the
151    /// others [`crate::overlay`] gives); [`OverlayError::HoleOutsideOuter`]
152    /// for a hole crossing or leaving its outer ring;
153    /// [`OverlayError::HolesOverlap`] for holes of one polygon that cross,
154    /// nest or repeat; [`OverlayError::PolygonsOverlap`] for polygons that
155    /// overlap. With several defects, one is named.
156    pub fn new(polygons: Vec<Polygon>, tolerance: Tolerance) -> Result<Self, OverlayError> {
157        let validated = Validated::check(&polygons, tolerance)?;
158        crate::nesting::check(&polygons)?;
159        let mut region = Self::from_valid(normalise(polygons), false);
160        // Turning and rotating a ring, and ordering rings, change nothing
161        // validation reads.
162        region.validated = Some(validated);
163        Ok(region)
164    }
165
166    /// Wrap already-valid geometry, recording whether the producing operation
167    /// annihilated a non-empty input. No proof of validity is carried; see
168    /// [`Self::with_validated`].
169    fn from_valid(polygons: Vec<Polygon>, had_input: bool) -> Self {
170        let evidence = RegionEvidence {
171            polygons: polygons.len(),
172            holes: polygons.iter().map(|p| p.holes.len()).sum(),
173            emptied: had_input && polygons.is_empty(),
174        };
175        Self {
176            polygons: normalise(polygons),
177            evidence,
178            bound: None,
179            excess: None,
180            validated: None,
181        }
182    }
183
184    /// This region with the proof `validated` of its rings, from settling
185    /// them or from the region it equals.
186    pub(crate) fn with_validated(mut self, validated: Option<Validated>) -> Self {
187        self.validated = validated;
188        self
189    }
190
191    /// The proof this region's rings are valid, if it carries one.
192    pub(crate) fn validated(&self) -> Option<Validated> {
193        self.validated
194    }
195
196    /// [`Self::from_valid`] for the other modules of the crate.
197    pub(crate) fn from_valid_polygons(polygons: Vec<Polygon>, had_input: bool) -> Self {
198        Self::from_valid(polygons, had_input)
199    }
200
201    /// Record the side and deviation of a disc morphology.
202    pub(crate) fn set_bound(&mut self, bound: MorphologyBound) {
203        self.bound = Some(bound);
204    }
205
206    /// For a result of [`Self::dilate_inner`], [`Self::dilate_outer`],
207    /// [`Self::erode_inner`] or [`Self::erode_outer`]: which side of the
208    /// exact disc morphology it lies on and how far from it it can be.
209    /// `None` for every other region, including [`Self::dilate`] and
210    /// [`Self::erode`], whose side is not stated.
211    #[must_use]
212    pub const fn bound(&self) -> Option<MorphologyBound> {
213        self.bound
214    }
215
216    /// Record the excess of a fast disc morphology.
217    pub(crate) fn set_excess(&mut self, excess: f64) {
218        self.excess = Some(excess);
219    }
220
221    /// For a result of [`Self::erode_inner_with`] and its siblings in
222    /// [`crate::MorphologyMode::Fast`]: how much larger the filled notches
223    /// made the sum, at most the mode's budget, zero where no notch was
224    /// filled (#330). It is part of [`MorphologyBound::deviation`], which
225    /// states the whole distance from the exact result. `None` for every
226    /// other region, the exact mode's results included.
227    #[must_use]
228    pub const fn excess(&self) -> Option<f64> {
229        self.excess
230    }
231
232    /// The region's polygons, in canonical order.
233    #[must_use]
234    pub fn polygons(&self) -> &[Polygon] {
235        &self.polygons
236    }
237
238    /// What the producing operation did.
239    #[must_use]
240    pub const fn evidence(&self) -> RegionEvidence {
241        self.evidence
242    }
243
244    /// True when the region covers no area.
245    #[must_use]
246    pub fn is_empty(&self) -> bool {
247        self.polygons.is_empty()
248    }
249
250    /// Total covered area. Holes subtract.
251    #[must_use]
252    pub fn area(&self) -> f64 {
253        total_area(&self.polygons)
254    }
255
256    /// Number of disjoint connected components.
257    ///
258    /// One polygon is one component: [`Self::new`] refuses polygons that
259    /// overlap (#448) and every operation returns polygons that do not, so
260    /// counting them is the component count rather than an approximation
261    /// of it. Polygons touching at a point or along an edge count apart.
262    #[must_use]
263    pub fn component_count(&self) -> usize {
264        self.polygons.len()
265    }
266
267    /// Every boundary ring, outer boundaries first then holes.
268    #[must_use]
269    pub fn boundary_rings(&self) -> Vec<Ring> {
270        let mut rings: Vec<Ring> = self.polygons.iter().map(|p| p.outer.clone()).collect();
271        for polygon in &self.polygons {
272            rings.extend(polygon.holes.iter().cloned());
273        }
274        rings
275    }
276
277    /// Set union.
278    pub fn union(&self, other: &Self, tolerance: Tolerance) -> Result<Self, OverlayError> {
279        self.combine(other, crate::OverlayOperation::Union, tolerance)
280    }
281
282    /// Set intersection.
283    pub fn intersection(&self, other: &Self, tolerance: Tolerance) -> Result<Self, OverlayError> {
284        self.combine(other, crate::OverlayOperation::Intersection, tolerance)
285    }
286
287    /// Set difference: this region minus `other`.
288    pub fn difference(&self, other: &Self, tolerance: Tolerance) -> Result<Self, OverlayError> {
289        self.combine(other, crate::OverlayOperation::Difference, tolerance)
290    }
291
292    /// Apply a boolean operation through the validated overlay primitive.
293    ///
294    /// Empty operands are handled here rather than pushed into the backend:
295    /// the identities are exact, and routing them through a general overlay
296    /// would risk an incidental simplification pass changing geometry that
297    /// set algebra says must be returned unchanged.
298    fn combine(
299        &self,
300        other: &Self,
301        operation: crate::OverlayOperation,
302        tolerance: Tolerance,
303    ) -> Result<Self, OverlayError> {
304        let had_input = !self.is_empty() || !other.is_empty();
305        match operation {
306            crate::OverlayOperation::Union if other.is_empty() => {
307                return Ok(Self::from_valid(self.polygons.clone(), had_input)
308                    .with_validated(self.validated));
309            }
310            crate::OverlayOperation::Union if self.is_empty() => {
311                return Ok(Self::from_valid(other.polygons.clone(), had_input)
312                    .with_validated(other.validated));
313            }
314            crate::OverlayOperation::Intersection if self.is_empty() || other.is_empty() => {
315                return Ok(Self::from_valid(Vec::new(), had_input));
316            }
317            crate::OverlayOperation::Difference if other.is_empty() => {
318                return Ok(Self::from_valid(self.polygons.clone(), had_input)
319                    .with_validated(self.validated));
320            }
321            crate::OverlayOperation::Difference if self.is_empty() => {
322                return Ok(Self::from_valid(Vec::new(), had_input));
323            }
324            _ => {}
325        }
326        // The identity frame. A region carries no frame of its own: it is a
327        // point set in whatever frame the caller established, and overlay
328        // only requires both operands to agree.
329        let frame = axiolid_core::Frame2 {
330            origin: Point2::new(0.0, 0.0),
331            x: Vec2::new(1.0, 0.0),
332            y: Vec2::new(0.0, 1.0),
333        };
334        let subject = crate::OverlayInput {
335            frame,
336            polygons: self.polygons.clone(),
337        };
338        let clip = crate::OverlayInput {
339            frame,
340            polygons: other.polygons.clone(),
341        };
342        // Rings known valid under this tolerance are not validated again
343        // (#341); the settled output carries its own proof.
344        let known = |region: &Self| region.validated.is_some_and(|v| v.holds_for(tolerance));
345        let (reported, validated) = crate::overlay_known(
346            &subject,
347            &clip,
348            operation,
349            crate::FillRule::NonZero,
350            tolerance,
351            crate::OverlayOptions::new(),
352            &mut crate::budget::Charge::unlimited(),
353            [known(self), known(other)],
354        )
355        .map_err(crate::budget::unbudgeted)?;
356        Ok(Self::from_valid(reported.result.polygons, had_input).with_validated(validated))
357    }
358
359    /// Dilate by `radius`: the Minkowski sum with a disc, approximated by
360    /// round joins on an unstated side of the exact result. Where a verdict
361    /// must be proven, use [`Self::dilate_inner`] or [`Self::dilate_outer`].
362    ///
363    /// This is the disc-expansion form used for clearance envelopes. A zero
364    /// radius is the identity.
365    pub fn dilate(&self, radius: f64, tolerance: Tolerance) -> Result<Self, OverlayError> {
366        self.morphology(radius, tolerance)
367    }
368
369    /// Erode by `radius`: the Minkowski erosion by a disc, approximated on
370    /// an unstated side of the exact result. Where a verdict must be
371    /// proven, use [`Self::erode_inner`] (a route found proves
372    /// reachability) or [`Self::erode_outer`] (no route proves
373    /// unreachability).
374    ///
375    /// A region thinner than `2 * radius` anywhere is cut there, which is
376    /// how a corridor narrower than a body radius becomes impassable. If
377    /// that removes everything the result is empty and
378    /// [`RegionEvidence::emptied`] is set.
379    pub fn erode(&self, radius: f64, tolerance: Tolerance) -> Result<Self, OverlayError> {
380        if radius < 0.0 {
381            return Err(OverlayError::InvalidOffsetDistance);
382        }
383        self.morphology(-radius, tolerance)
384    }
385
386    /// Shared offset path. Round joins approximate the disc, which is what
387    /// makes this morphology rather than a polygonal offset.
388    fn morphology(&self, distance: f64, tolerance: Tolerance) -> Result<Self, OverlayError> {
389        if !distance.is_finite() {
390            return Err(OverlayError::InvalidOffsetDistance);
391        }
392        if self.is_empty() || distance == 0.0 {
393            return Ok(Self::from_valid(self.polygons.clone(), false));
394        }
395        // A disc is approximated by round joins; the parameter is the maximum
396        // segment-length-to-radius ratio, so smaller is closer to a true disc.
397        let join = JoinStyle::Round {
398            max_segment_ratio: 0.1,
399        };
400        let result = offset_polygons(&self.polygons, distance, join, tolerance)?;
401        Ok(Self::from_valid(result.polygons, true))
402    }
403
404    /// Translate by `offset`. Rigid: area and component count are preserved.
405    ///
406    /// # Errors
407    ///
408    /// [`OverlayError::NonFinitePoint`] for a non-finite offset. Each vertex
409    /// moves by its own rounding, which can turn a contact into a crossing,
410    /// so the result is checked as [`Region::new`] checks its rings: the
411    /// refusals of `Region::new` for rings crossing into each other (#448).
412    /// That arranges a region of more than one ring, as `Region::new` does:
413    /// 0.34 s for a floor of 100,000 corners with 200 holes.
414    pub fn translate(&self, offset: Vec2) -> Result<Self, OverlayError> {
415        let polygons = self.shifted(offset)?;
416        crate::nesting::check(&polygons)?;
417        Ok(Self::from_valid(polygons, !self.is_empty()))
418    }
419
420    /// The region moved by `offset`, each vertex by its own rounding, not
421    /// checked: a copy that only feeds `union`, which reads overlapping
422    /// operands by their fill rule (#448).
423    fn shifted(&self, offset: Vec2) -> Result<Vec<Polygon>, OverlayError> {
424        if !offset.is_finite() {
425            return Err(OverlayError::NonFinitePoint);
426        }
427        let shift = |ring: &Ring| Ring {
428            points: ring
429                .points
430                .iter()
431                .map(|p| Point2::new(p.x + offset.x, p.y + offset.y))
432                .collect(),
433        };
434        Ok(self
435            .polygons
436            .iter()
437            .map(|polygon| Polygon {
438                outer: shift(&polygon.outer),
439                holes: polygon.holes.iter().map(shift).collect(),
440            })
441            .collect())
442    }
443
444    /// Sweep along `direction`: the union of the region with every
445    /// translate of itself along the vector.
446    ///
447    /// Equivalent to the Minkowski sum with the segment `[0, direction]`.
448    /// Built as the union of the region and its translate plus the hull
449    /// swept between them, which for a polygon set is exactly the union of
450    /// the two end positions with the stroke of each boundary edge; using
451    /// the union of endpoints alone would miss the swept middle whenever
452    /// the translation exceeds the region's own extent.
453    pub fn sweep(&self, direction: Vec2, tolerance: Tolerance) -> Result<Self, OverlayError> {
454        if !direction.is_finite() {
455            return Err(OverlayError::NonFinitePoint);
456        }
457        if self.is_empty() || direction.length() == 0.0 {
458            return Ok(Self::from_valid(self.polygons.clone(), false));
459        }
460        // Not checked: the copy only feeds `union`, which reads overlapping
461        // operands by fill rule, so a rounding that makes the copy's rings
462        // cross still gives the sound sweep.
463        let moved = Self::from_valid(self.shifted(direction)?, true);
464        let mut swept = self.union(&moved, tolerance)?;
465        // Fill the corridor between the two positions: each boundary edge
466        // sweeps a parallelogram, and their union with the endpoints is the
467        // Minkowski sum. Built from quads so no offset approximation enters.
468        for ring in self.boundary_rings() {
469            let count = ring.points.len();
470            for index in 0..count {
471                let a = ring.points[index];
472                let b = ring.points[(index + 1) % count];
473                let quad = Polygon {
474                    outer: Ring {
475                        points: vec![
476                            a,
477                            b,
478                            Point2::new(b.x + direction.x, b.y + direction.y),
479                            Point2::new(a.x + direction.x, a.y + direction.y),
480                        ],
481                    },
482                    holes: Vec::new(),
483                };
484                // A degenerate quad (edge parallel to the sweep) contributes
485                // nothing and is skipped rather than rejected.
486                if let Ok(band) = Self::new(vec![quad], tolerance) {
487                    swept = swept.union(&band, tolerance)?;
488                }
489            }
490        }
491        Ok(swept)
492    }
493}