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}