Skip to main content

axiolid_overlay/
degenerate.rs

1//! Opt-in dropping of degenerate input rings, every drop reported (#256).
2//!
3//! By default a degenerate input ring refuses the whole operation with a
4//! named [`OverlayError`]. [`OverlayOptions::dropping_degenerate_rings`]
5//! asks [`crate::overlay_with`] and [`crate::union_soup_with`] instead to
6//! leave such a ring out, to report it as a [`DroppedRing`], and to run
7//! the exact boolean over the rings that remain. Nothing is dropped
8//! silently and nothing is repaired: a ring is either used as given or
9//! left out whole, and the report says which, why and how much area went
10//! with it.
11//!
12//! # Which faults are droppable
13//!
14//! A fault is droppable when leaving the ring out has a stated, bounded
15//! effect, and the ring is otherwise well formed:
16//!
17//! - [`DropReason::TooShort`]: fewer than three points, all finite. Such
18//!   a ring encloses nothing, so its area is exactly zero.
19//! - [`DropReason::ZeroArea`]: the crate's exact `ZeroArea` rule, an area
20//!   of at most `tolerance.linear()^2`, decided exactly from the fan of
21//!   triangles at the first vertex (#274). It is decided after the
22//!   self-intersection test, so the ring is simple and its area is the
23//!   area it encloses. The threshold is that rule, not a second one.
24//! - [`DropReason::RepeatedVertex`]: an edge no longer than
25//!   `tolerance.linear()`. It is droppable only when the ring with each
26//!   run of such vertices merged into its first vertex does not intersect
27//!   itself, so its signed area is the area it encloses and the reported
28//!   area is what leaving it out removes. That area can be large: a
29//!   square with a doubled corner is dropped whole, and the report says
30//!   how much went.
31//! - [`DropReason::OuterDropped`]: a hole whose outer ring was dropped
32//!   goes with it, since a hole has no meaning without its outer. The
33//!   hole must still be droppable or valid on its own; its own fault is
34//!   reported in preference to this reason. Whether it lies inside the
35//!   dropped outer is not tested.
36//!
37//! Never droppable, refused as without the option: a non-finite point
38//! (no area can be stated), a ring that intersects itself (its interior
39//! is ambiguous and may be large, so no bounded removal can be named), a
40//! hole outside its kept outer ring, and an invalid frame. A
41//! [`DropReason::RepeatedVertex`] ring that still intersects itself once
42//! its short edges are merged is refused as
43//! [`OverlayError::SelfIntersection`].
44//!
45//! A degenerate hole of a kept outer ring is dropped alone: the operand
46//! then covers that hole's area too, which the report states as the
47//! ring's area.
48//!
49//! # The reported area
50//!
51//! [`DroppedRing::area`] is the magnitude of the ring's signed area, the
52//! fan of triangles from its first vertex summed exactly and rounded once
53//! for output. [`DroppedRing::area_bound`] is a proven upper bound on that
54//! exact magnitude. A [`DropReason::ZeroArea`] ring's exact area is at
55//! most `tolerance.linear()^2` by the rule that dropped it.
56use std::borrow::Cow;
57
58use axiolid_core::{Point2, Tolerance};
59use axiolid_exact::{Arith, Dyadic};
60use axiolid_guarantees::Sign;
61
62use crate::{hole_outside, self_intersects, validate_frame, validate_ring};
63use crate::{OverlayError, OverlayInput, OverlayResult, Polygon, Ring};
64
65/// What an operation does with a degenerate input ring (#256).
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
67#[non_exhaustive]
68pub enum DegenerateRings {
69    /// Refuse the whole operation with the ring's named error. The default.
70    #[default]
71    Refuse,
72    /// Leave each droppable degenerate ring out and report it; refuse
73    /// every other fault as [`DegenerateRings::Refuse`] does.
74    DropReported,
75}
76
77/// Options for [`crate::overlay_with`] and [`crate::union_soup_with`].
78///
79/// The default is the behaviour of [`crate::overlay`] and
80/// [`crate::union_soup`]: every degenerate ring is refused.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
82#[non_exhaustive]
83pub struct OverlayOptions {
84    /// What to do with a degenerate input ring.
85    pub degenerate_rings: DegenerateRings,
86}
87
88impl OverlayOptions {
89    /// The default options: degenerate rings are refused.
90    #[must_use]
91    pub const fn new() -> Self {
92        Self {
93            degenerate_rings: DegenerateRings::Refuse,
94        }
95    }
96
97    /// These options with degenerate rings dropped and reported
98    /// ([`DegenerateRings::DropReported`]).
99    #[must_use]
100    pub const fn dropping_degenerate_rings(mut self) -> Self {
101        self.degenerate_rings = DegenerateRings::DropReported;
102        self
103    }
104}
105
106/// Which operand a dropped ring came from.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108#[non_exhaustive]
109pub enum OverlayOperand {
110    /// The subject, or the rings of a [`crate::union_soup_with`].
111    Subject,
112    /// The clip.
113    Clip,
114}
115
116/// Which ring of its polygon a dropped ring was.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118#[non_exhaustive]
119pub enum RingRole {
120    /// The outer ring.
121    Outer,
122    /// The hole at this index in [`Polygon::holes`].
123    Hole(usize),
124}
125
126/// Why a ring was dropped.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128#[non_exhaustive]
129pub enum DropReason {
130    /// Fewer than three points ([`OverlayError::RingTooShort`]).
131    TooShort,
132    /// An edge no longer than the tolerance
133    /// ([`OverlayError::RepeatedVertex`]).
134    RepeatedVertex,
135    /// An area of at most `tolerance.linear()^2`, decided exactly
136    /// ([`OverlayError::ZeroArea`]).
137    ZeroArea,
138    /// A hole, valid on its own, whose outer ring was dropped.
139    OuterDropped,
140}
141
142/// One input ring an operation left out, and why.
143#[derive(Debug, Clone, Copy, PartialEq)]
144#[non_exhaustive]
145pub struct DroppedRing {
146    /// The operand the ring belonged to.
147    pub operand: OverlayOperand,
148    /// The index of its polygon in the operand; for
149    /// [`crate::union_soup_with`], the index of the ring.
150    pub polygon: usize,
151    /// Which ring of that polygon it was.
152    pub ring: RingRole,
153    /// Why it was dropped.
154    pub reason: DropReason,
155    /// The magnitude of its signed area, computed exactly and rounded once
156    /// for output. For a dropped outer ring, the area the operand lost; for
157    /// a dropped hole, the area it gained.
158    pub area: f64,
159    /// A proven upper bound on the exact magnitude of its signed area.
160    pub area_bound: f64,
161}
162
163/// An overlay's result and the rings it dropped (#256).
164#[derive(Debug, Clone, PartialEq)]
165#[non_exhaustive]
166pub struct ReportedOverlay {
167    /// The exact overlay of the rings that were kept. Its evidence counts
168    /// those rings, not the dropped ones.
169    pub result: OverlayResult,
170    /// Every dropped ring, subject before clip, then by polygon, outer
171    /// ring before holes. Empty when nothing was dropped.
172    pub dropped: Vec<DroppedRing>,
173}
174
175/// A soup union's polygons and the rings it dropped (#256).
176#[derive(Debug, Clone, PartialEq)]
177#[non_exhaustive]
178pub struct ReportedUnion {
179    /// The union of the rings that were kept.
180    pub polygons: Vec<Polygon>,
181    /// Every dropped ring, in input order, as [`OverlayOperand::Subject`]
182    /// with [`RingRole::Outer`].
183    pub dropped: Vec<DroppedRing>,
184}
185
186/// Where a ring sits, for its report.
187#[derive(Clone, Copy)]
188pub(crate) struct Place {
189    pub(crate) operand: OverlayOperand,
190    pub(crate) polygon: usize,
191    pub(crate) ring: RingRole,
192}
193
194/// The operand's polygons as the boolean should see them: refused as
195/// [`crate::overlay`] refuses them, or with each droppable degenerate ring
196/// left out and pushed onto `dropped`.
197pub(crate) fn admit<'a>(
198    input: &'a OverlayInput,
199    operand: OverlayOperand,
200    tolerance: Tolerance,
201    options: OverlayOptions,
202    dropped: &mut Vec<DroppedRing>,
203) -> Result<Cow<'a, [Polygon]>, OverlayError> {
204    if options.degenerate_rings == DegenerateRings::Refuse {
205        crate::validate(input, tolerance)?;
206        return Ok(Cow::Borrowed(&input.polygons));
207    }
208    validate_frame(input, tolerance)?;
209    let mut kept = Vec::with_capacity(input.polygons.len());
210    for (index, polygon) in input.polygons.iter().enumerate() {
211        let place = |ring| Place {
212            operand,
213            polygon: index,
214            ring,
215        };
216        let outer = checked(&polygon.outer, tolerance)?;
217        if let Some(reason) = outer {
218            dropped.push(report(&polygon.outer, place(RingRole::Outer), reason));
219        }
220        let mut holes = Vec::with_capacity(polygon.holes.len());
221        for (h, hole) in polygon.holes.iter().enumerate() {
222            let own = checked(hole, tolerance)?;
223            let reason = match (own, outer) {
224                (Some(reason), _) => reason,
225                (None, Some(_)) => DropReason::OuterDropped,
226                (None, None) => {
227                    if hole_outside(&polygon.outer, hole, tolerance) {
228                        return Err(OverlayError::HoleOutsideOuter);
229                    }
230                    holes.push(hole.clone());
231                    continue;
232                }
233            };
234            dropped.push(report(hole, place(RingRole::Hole(h)), reason));
235        }
236        if outer.is_none() {
237            kept.push(Polygon {
238                outer: polygon.outer.clone(),
239                holes,
240            });
241        }
242    }
243    Ok(Cow::Owned(kept))
244}
245
246/// `None` for a valid ring, the reason for a droppable degenerate one,
247/// and the error for any other fault.
248pub(crate) fn checked(
249    ring: &Ring,
250    tolerance: Tolerance,
251) -> Result<Option<DropReason>, OverlayError> {
252    let error = match validate_ring(ring, tolerance) {
253        Ok(()) => return Ok(None),
254        Err(error) => error,
255    };
256    // No area can be stated for a ring with a non-finite point, and
257    // `validate_ring` tests the point count first.
258    if !ring.points.iter().all(|p| p.is_finite()) {
259        return Err(OverlayError::NonFinitePoint);
260    }
261    match error {
262        OverlayError::RingTooShort => Ok(Some(DropReason::TooShort)),
263        OverlayError::ZeroArea => Ok(Some(DropReason::ZeroArea)),
264        OverlayError::RepeatedVertex => {
265            let merged = Ring {
266                points: merge_short_edges(&ring.points, tolerance.linear()),
267            };
268            if merged.points.len() >= 3 && self_intersects(&merged, tolerance) {
269                Err(OverlayError::SelfIntersection)
270            } else {
271                Ok(Some(DropReason::RepeatedVertex))
272            }
273        }
274        other => Err(other),
275    }
276}
277
278/// The ring with every vertex within `linear` of the last one kept left
279/// out, and trailing vertices within `linear` of the first: no edge of
280/// the result is that short, so the self-intersection test sees the ring's
281/// shape rather than the short edges, whose neighbours always touch.
282fn merge_short_edges(points: &[Point2], linear: f64) -> Vec<Point2> {
283    let mut merged: Vec<Point2> = Vec::with_capacity(points.len());
284    for &p in points {
285        if merged.last().is_none_or(|&q| (p - q).length() > linear) {
286            merged.push(p);
287        }
288    }
289    while merged.len() > 1 && (merged[merged.len() - 1] - merged[0]).length() <= linear {
290        merged.pop();
291    }
292    merged
293}
294
295pub(crate) fn report(ring: &Ring, place: Place, reason: DropReason) -> DroppedRing {
296    let (area, area_bound) = exact_area(&ring.points);
297    DroppedRing {
298        operand: place.operand,
299        polygon: place.polygon,
300        ring: place.ring,
301        reason,
302        area,
303        area_bound,
304    }
305}
306
307/// The magnitude of a ring's signed area rounded once, and a proven upper
308/// bound on it. The points must be finite.
309fn exact_area(points: &[Point2]) -> (f64, f64) {
310    let Some(o) = points.first() else {
311        return (0.0, 0.0);
312    };
313    let f = Dyadic::from_f64;
314    let local = |p: &Point2| (f(p.x).sub(&f(o.x)), f(p.y).sub(&f(o.y)));
315    let mut twice = Dyadic::zero();
316    for w in points.windows(2).skip(1) {
317        let ((ax, ay), (bx, by)) = (local(&w[0]), local(&w[1]));
318        twice = twice.add(&ax.mul(&by).sub(&ay.mul(&bx)));
319    }
320    if twice.sign() == Some(Sign::Negative) {
321        twice = twice.neg();
322    }
323    let enclosure = twice.enclosure();
324    let half_up = |v: f64| {
325        let h = v * 0.5;
326        // Halving is exact unless it leaves the normal range.
327        if h * 2.0 == v {
328            h
329        } else {
330            h.next_up()
331        }
332    };
333    let bound = half_up(enclosure.hi());
334    (twice.to_f64() * 0.5, bound)
335}