Skip to main content

axiolid_overlay/
lib.rs

1#![forbid(unsafe_code)]
2//! Validated, deterministic planar boolean overlay and offset.
3//!
4//! A straight ring's orientation and its `ZeroArea` check are exact signs
5//! of its area taken as a fan of triangles from its first vertex (#274):
6//! a shoelace over the coordinates as given rounds at the size of the
7//! coordinates squared, which at georeferenced plan positions swamps the
8//! area of a small ring. Areas reported as values are summed the same way,
9//! so their rounding scales with the ring's extent, not its position.
10//! `scripts/probe_ring_orientation_mutants.py` lists the faults the tests
11//! in `tests/far_from_origin.rs` must catch.
12//!
13//! Whether two boundary edges meet, the `SelfIntersection` rule, is exact
14//! too, and validation and settling share it (`contact`, #272): an end on
15//! the other edge within the tolerance, or a proper crossing with each
16//! segment's ends strictly on both sides of the other's line. So an
17//! operation's settled output is a valid operand wherever it lies.
18//!
19//! A degenerate input ring refuses the operation by default. Asked
20//! explicitly, [`overlay_with`] and [`union_soup_with`] leave out a ring
21//! that is too short, has a repeated vertex, or has zero area under the
22//! exact `ZeroArea` rule, and report each one with its area
23//! (`degenerate`, #256); a self-intersecting ring is never dropped.
24//! `scripts/probe_degenerate_drop_mutants.py` lists the faults the tests
25//! in `tests/degenerate_drop.rs` must catch.
26//!
27//! A [`Region`]'s rings lie apart and nested: every hole inside its own
28//! outer ring and in no other of its holes, and no point in two polygons
29//! (`nesting`, #448). [`Region::new`] decides that exactly, on one
30//! arrangement of all the rings, and refuses by name; rings may touch.
31//! Operands of [`overlay`] and [`union_soup`] are not held to it: their
32//! rings may overlap, read by the fill rule.
33//! `scripts/probe_region_nesting_mutants.py` lists the faults the tests in
34//! `tests/region_nesting.rs` must catch.
35mod arc;
36mod arc_check;
37mod arc_clearance;
38mod arc_overlay;
39mod arrangement;
40mod budget;
41mod circle;
42mod contact;
43mod convex_parts;
44mod cover;
45mod degenerate;
46mod exact_arc;
47mod exact_overlay;
48mod fill;
49mod free_faces;
50mod minkowski;
51mod nesting;
52mod offset;
53mod rectangle;
54mod region;
55mod segment_sweep;
56mod settle;
57mod visibility;
58
59pub use arc::{
60    arc_edge_radius, arc_ring_area, reverse_arc_ring, validate_arc_ring, ArcRing, ArcVertex,
61};
62pub use arc_check::{
63    check_arc_polygon, check_arc_polygon_with, ArcPolygonCheck, ArcPolygonDefect, ArcRingContact,
64    ArcRingRef,
65};
66pub use arc_clearance::ArcCheckOptions;
67pub use arc_overlay::{arc_overlay, ArcOverlayEvidence, ArcOverlayResult, ArcPolygon};
68pub use arrangement::{
69    ArcArrangement, ArrangementEdge, ArrangementRegion, EdgeSource, EdgeUse as ArrangementEdgeUse,
70};
71pub use budget::OverlayRefusal;
72pub use circle::{
73    minimum_enclosing_circle, CircleError, CircleEvidence, EnclosingCircle, MinimumCircle,
74};
75pub use degenerate::{
76    DegenerateRings, DropReason, DroppedRing, OverlayOperand, OverlayOptions, ReportedOverlay,
77    ReportedUnion, RingRole,
78};
79pub use free_faces::{
80    free_faces, free_faces_within, BarrierDefect, Bridge, FaceLocation, FreeFace, FreeFaceEvidence,
81    FreeFaces, FreeFacesError, FreeRing, FreeSpaceInput, PieceSource,
82};
83pub use minkowski::{BoundSide, MinkowskiError, MorphologyBound, MorphologyMode};
84pub use offset::{
85    offset_polygons, polygon_area, ring_area, stroke_polyline, total_area, CapStyle, JoinStyle,
86    OffsetEvidence, OffsetResult,
87};
88pub use rectangle::{
89    minimum_area_rectangle, MinimumRectangle, OrientedRectangle, RectangleError, RectangleEvidence,
90};
91pub use region::{Region, RegionEvidence};
92pub use segment_sweep::{
93    segment_intersections, ExactPoint2, Incidence, IntersectionPoint, SegmentIntersections,
94    SegmentLocation, SegmentOverlap, SegmentSweepError, SweepEvidence,
95};
96pub use visibility::VisibilityError;
97
98use std::borrow::Cow;
99
100use axiolid_contracts::ExecutionOptions;
101use axiolid_core::{Frame2, Point2, Polygon2, Tolerance};
102use axiolid_exact::{certify, Arith, SignExpr};
103use axiolid_guarantees::Sign;
104use budget::Charge;
105use contact::{on_segment, segments_intersect, side};
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum FillRule {
109    EvenOdd,
110    NonZero,
111    Positive,
112    Negative,
113}
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum OverlayOperation {
116    Intersection,
117    Union,
118    Difference,
119    Xor,
120}
121#[derive(Debug, Clone, PartialEq)]
122pub struct Ring {
123    pub points: Vec<Point2>,
124}
125
126/// A `Ring` is a closed boundary and so is [`Polygon2`]; converting between
127/// them moves the points and nothing else.
128///
129/// The two exist separately because they are reached from different places:
130/// `Polygon2` is a foundation value type usable without this crate, while
131/// `Ring` is what the overlay consumes. Making them the same type
132/// would drag the planar boolean vocabulary into `axiolid-core`.
133impl From<Polygon2> for Ring {
134    fn from(polygon: Polygon2) -> Self {
135        Self {
136            points: polygon.vertices,
137        }
138    }
139}
140
141impl From<Ring> for Polygon2 {
142    fn from(ring: Ring) -> Self {
143        Self::new(ring.points)
144    }
145}
146
147impl From<&Ring> for Polygon2 {
148    fn from(ring: &Ring) -> Self {
149        Self::new(ring.points.clone())
150    }
151}
152
153#[derive(Debug, Clone, PartialEq)]
154pub struct Polygon {
155    pub outer: Ring,
156    pub holes: Vec<Ring>,
157}
158
159impl Polygon {
160    /// The outer boundary as a [`Polygon2`], discarding any holes.
161    ///
162    /// Named `outline` rather than offered as a `From` impl because the
163    /// conversion is lossy and the loss is silent: an annulus and a filled
164    /// disc have the same outline, so a caller that reaches for this to
165    /// compute area gets the wrong answer with no error. `Polygon2` models a
166    /// simple polygon and cannot represent a hole, which is exactly why this
167    /// has to be an explicit request rather than an implicit coercion.
168    ///
169    /// Use [`polygon_area`] when the holes matter.
170    pub fn outline(&self) -> Polygon2 {
171        Polygon2::from(&self.outer)
172    }
173
174    /// Whether the polygon has inner boundaries that [`outline`] would drop.
175    ///
176    /// [`outline`]: Polygon::outline
177    pub fn has_holes(&self) -> bool {
178        !self.holes.is_empty()
179    }
180}
181#[derive(Debug, Clone, PartialEq)]
182pub struct OverlayInput {
183    pub frame: Frame2,
184    pub polygons: Vec<Polygon>,
185}
186#[derive(Debug, Clone, PartialEq, Eq)]
187#[non_exhaustive]
188pub enum OverlayError {
189    InvalidFrame,
190    NonFinitePoint,
191    RingTooShort,
192    RepeatedVertex,
193    ZeroArea,
194    /// Non-adjacent boundary segments meet or cross.
195    SelfIntersection,
196    /// A hole is not inside its outer ring: it lies outside it or crosses
197    /// it.
198    HoleOutsideOuter,
199    /// An offset distance or stroke width was not finite, or a width was not
200    /// positive. A non-finite distance cannot produce a bounded region.
201    InvalidOffsetDistance,
202    /// A join or cap parameter was not a finite positive value.
203    ///
204    /// Separate from [`OverlayError::InvalidOffsetDistance`] because the fix is
205    /// different: the caller passed a malformed style, not a malformed measure.
206    InvalidOffsetStyle,
207    /// An arc edge's implied radius was not above tolerance.
208    ///
209    /// Distinct from [`OverlayError::RepeatedVertex`]: the chord can be long
210    /// enough while the bulge still implies a radius too small to be a real
211    /// boundary. Such an edge is a point, not an arc.
212    ZeroRadiusArc,
213    /// Two holes of one polygon of a [`Region`] overlap: one crosses, holds
214    /// or repeats the other (#448). Holes may touch at points and along a
215    /// stretch they share.
216    HolesOverlap,
217    /// Two polygons of a [`Region`] overlap: a point lies in both (#448).
218    /// Polygons may touch at points and along a stretch they share.
219    PolygonsOverlap,
220}
221
222impl core::fmt::Display for OverlayError {
223    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
224        f.write_str(match self {
225            Self::InvalidFrame => "a frame is not orthonormal, or the operands' frames differ",
226            Self::NonFinitePoint => "a point has a non-finite coordinate",
227            Self::RingTooShort => "a ring or polyline has too few vertices",
228            Self::RepeatedVertex => "a ring repeats a vertex within tolerance",
229            Self::ZeroArea => "a ring encloses no area above tolerance",
230            Self::SelfIntersection => "non-adjacent boundary segments meet or cross",
231            Self::HoleOutsideOuter => "a hole lies outside its outer ring or crosses it",
232            Self::InvalidOffsetDistance => {
233                "an offset distance or stroke width is not finite, or a width is not positive"
234            }
235            Self::InvalidOffsetStyle => "a join or cap parameter is not a finite positive value",
236            Self::ZeroRadiusArc => "an arc edge implies a radius not above tolerance",
237            Self::HolesOverlap => "two holes of a polygon overlap",
238            Self::PolygonsOverlap => "two polygons of a region overlap",
239        })
240    }
241}
242
243impl std::error::Error for OverlayError {}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub struct OverlayEvidence {
247    pub subject_rings: usize,
248    pub clip_rings: usize,
249    pub output_polygons: usize,
250    /// Number of inner boundary components across all result polygons.
251    pub output_holes: usize,
252}
253#[derive(Debug, Clone, PartialEq)]
254pub struct OverlayResult {
255    pub polygons: Vec<Polygon>,
256    pub evidence: OverlayEvidence,
257}
258/// A ring's signed area, positive counter-clockwise, as a measure.
259///
260/// Summed as triangles fanned from the first vertex, so each term is the
261/// size of the ring's extent squared, not of its coordinates squared: a
262/// shoelace over the coordinates as given loses a small ring's whole area
263/// to rounding far from the origin (#274). Decisions use [`orientation`]
264/// and [`zero_area`], which are exact; this is for magnitudes only.
265fn signed(r: &Ring) -> f64 {
266    let Some(&o) = r.points.first() else {
267        return 0.0;
268    };
269    r.points
270        .windows(2)
271        .skip(1)
272        .map(|w| (w[0] - o).perp_dot(w[1] - o))
273        .sum::<f64>()
274        * 0.5
275}
276
277/// The sign of `2 * area - offset` for the ring through `points`, exactly.
278///
279/// The area is the fan of triangles from the first vertex,
280/// `sum((p[i] - p[0]) x (p[i + 1] - p[0]))`, which equals the shoelace in
281/// exact arithmetic but keeps the interval filter tight far from the
282/// origin (#274). Points must be finite; a refusal of the exact tier,
283/// which finite points never cause, reads as zero.
284struct TwiceArea<'a> {
285    points: &'a [Point2],
286    offset: f64,
287}
288
289impl SignExpr for TwiceArea<'_> {
290    fn sign_in<T: Arith>(&self) -> Option<Sign> {
291        let f = T::from_f64;
292        let o = self.points.first()?;
293        let local = |p: &Point2| (f(p.x).sub(&f(o.x)), f(p.y).sub(&f(o.y)));
294        let mut sum = f(self.offset).neg();
295        for w in self.points.windows(2).skip(1) {
296            let ((ax, ay), (bx, by)) = (local(&w[0]), local(&w[1]));
297            sum = sum.add(&ax.mul(&by).sub(&ay.mul(&bx)));
298        }
299        sum.sign()
300    }
301}
302
303fn area_sign(points: &[Point2], offset: f64) -> Sign {
304    certify(&TwiceArea { points, offset }).unwrap_or(Sign::Zero)
305}
306
307/// A ring's orientation, exactly: `Positive` counter-clockwise, `Negative`
308/// clockwise, `Zero` when its signed area is exactly zero.
309pub(crate) fn orientation(points: &[Point2]) -> Sign {
310    area_sign(points, 0.0)
311}
312
313/// Whether a ring's area is at most `linear^2`, decided exactly against
314/// that double (#274): the [`OverlayError::ZeroArea`] rule.
315pub(crate) fn zero_area(points: &[Point2], linear: f64) -> bool {
316    let bound = 2.0 * linear * linear;
317    area_sign(points, bound) != Sign::Positive && area_sign(points, -bound) != Sign::Negative
318}
319fn self_intersects(r: &Ring, t: Tolerance) -> bool {
320    first_touching_pair(&r.points, t.linear()).is_some()
321}
322
323/// Rings with at most this many edges compare every pair of them; longer
324/// ones ask a box tree for the pairs that can meet.
325const ALL_PAIRS_UP_TO: usize = 32;
326
327/// The first pair of a ring's edges `(i, j)`, `i < j`, not neighbours,
328/// that meet within `linear` ([`segments_intersect`]): the lowest `i`, then
329/// the lowest `j`, as comparing every pair in that order finds it.
330///
331/// A long ring asks the crate's box tree (`exact_arc::boxes`, #341) for
332/// the edges whose boxes, grown by `linear` and padded far beyond any
333/// rounding of their own ([`exact_arc::Bounds::around`]), overlap edge
334/// `i`'s. Two edges that meet within `linear` have boxes no farther apart
335/// than `linear`, so every pair the exact test accepts is a candidate, and
336/// the candidates come in ascending order: the pair found is the one the
337/// all-pairs loop finds, at about `n log n` cost for a valid ring.
338pub(crate) fn first_touching_pair(points: &[Point2], linear: f64) -> Option<(usize, usize)> {
339    let n = points.len();
340    let edge = |i: usize| (points[i], points[(i + 1) % n]);
341    let meets = |i: usize, j: usize| {
342        if j == i + 1 || (i == 0 && j + 1 == n) {
343            return false;
344        }
345        let ((a, b), (c, d)) = (edge(i), edge(j));
346        segments_intersect(a, b, c, d, linear)
347    };
348    if n <= ALL_PAIRS_UP_TO {
349        return (0..n).find_map(|i| (i + 1..n).find(|&j| meets(i, j)).map(|j| (i, j)));
350    }
351    let boxes: Vec<exact_arc::Bounds> = (0..n)
352        .map(|i| {
353            let (a, b) = edge(i);
354            exact_arc::Bounds::around(&[a, b], linear)
355        })
356        .collect();
357    let tree = exact_arc::boxes::BoxTree::new(boxes.clone());
358    let (mut candidates, mut stack) = (Vec::new(), Vec::new());
359    for (i, own) in boxes.iter().enumerate() {
360        tree.query_into(|b| b.overlaps(own), &mut candidates, &mut stack);
361        if let Some(&j) = candidates.iter().find(|&&j| j > i && meets(i, j)) {
362            return Some((i, j));
363        }
364    }
365    None
366}
367
368/// Validate one ring: the contract every operand and every settled output
369/// meets.
370///
371/// Refused, in this order: fewer than three points, a non-finite point,
372/// an edge no longer than `t.linear()`, non-adjacent edges meeting within
373/// it, and an area of at most `t.linear()^2` (`ZeroArea`). The area is
374/// decided exactly, from the fan of triangles at the first vertex, so the
375/// same ring decides the same at any distance from the origin (#274). A
376/// ring whose vertices all lie exactly on one line is not refused: it
377/// encloses nothing, and a boolean leaves it out (#219).
378pub(crate) fn validate_ring(r: &Ring, t: Tolerance) -> Result<(), OverlayError> {
379    #[cfg(test)]
380    tests::VALIDATED.with(|count| count.set(count.get() + 1));
381    ring_checks(r, t)
382}
383
384/// [`validate_ring`]'s checks, for debug assertions that must not count
385/// as validating.
386pub(crate) fn ring_checks(r: &Ring, t: Tolerance) -> Result<(), OverlayError> {
387    if r.points.len() < 3 {
388        return Err(OverlayError::RingTooShort);
389    };
390    if !r.points.iter().all(|p| p.is_finite()) {
391        return Err(OverlayError::NonFinitePoint);
392    };
393    if r.points
394        .iter()
395        .zip(r.points.iter().cycle().skip(1))
396        .take(r.points.len())
397        .any(|(a, b)| (*a - *b).length() <= t.linear())
398    {
399        return Err(OverlayError::RepeatedVertex);
400    };
401    if self_intersects(r, t) {
402        return Err(OverlayError::SelfIntersection);
403    }
404    // Both exact, so a ring decides the same wherever it lies (#274). A
405    // ring whose vertices all lie on one line encloses nothing and a
406    // boolean leaves it out (#219); it stays a valid operand, as the
407    // shadow of a vertical face in a plan soup is.
408    if zero_area(&r.points, t.linear()) && !exact_overlay::on_one_line(&r.points) {
409        return Err(OverlayError::ZeroArea);
410    };
411    Ok(())
412}
413fn validate(input: &OverlayInput, t: Tolerance) -> Result<(), OverlayError> {
414    validate_frame(input, t)?;
415    for p in &input.polygons {
416        validate_ring(&p.outer, t)?;
417        for h in &p.holes {
418            validate_ring(h, t)?;
419            if hole_outside(&p.outer, h, t) {
420                return Err(OverlayError::HoleOutsideOuter);
421            }
422        }
423    }
424    Ok(())
425}
426/// [`degenerate::admit`], or for an operand whose rings are `known` valid
427/// and whose degenerate rings are refused, [`validate_known`] (#341).
428fn admit_known<'a>(
429    input: &'a OverlayInput,
430    operand: OverlayOperand,
431    tolerance: Tolerance,
432    options: OverlayOptions,
433    known: bool,
434    dropped: &mut Vec<DroppedRing>,
435) -> Result<Cow<'a, [Polygon]>, OverlayError> {
436    if known && options.degenerate_rings == DegenerateRings::Refuse {
437        validate_known(input, tolerance)?;
438        return Ok(Cow::Borrowed(&input.polygons));
439    }
440    degenerate::admit(input, operand, tolerance, options, dropped)
441}
442
443/// [`validate`] for an input whose rings are known valid (#341): the
444/// frame, and every hole inside its outer ring.
445fn validate_known(input: &OverlayInput, t: Tolerance) -> Result<(), OverlayError> {
446    validate_frame(input, t)?;
447    for p in &input.polygons {
448        if p.holes.iter().any(|h| hole_outside(&p.outer, h, t)) {
449            return Err(OverlayError::HoleOutsideOuter);
450        }
451    }
452    Ok(())
453}
454fn validate_frame(input: &OverlayInput, t: Tolerance) -> Result<(), OverlayError> {
455    let f = input.frame;
456    if !f.origin.is_finite()
457        || !f.x.is_finite()
458        || !f.y.is_finite()
459        || (f.x.length() - 1.).abs() > t.linear()
460        || (f.y.length() - 1.).abs() > t.linear()
461        || f.x.dot(f.y).abs() > t.linear()
462        || f.x.perp_dot(f.y) <= 0.
463    {
464        return Err(OverlayError::InvalidFrame);
465    }
466    Ok(())
467}
468/// Whether a hole leaves its outer ring: a vertex of it strictly outside.
469/// Vertices on the outer boundary decide nothing -- a hole may touch its
470/// outer ring at a vertex, which settled outputs do, and testing only the
471/// first vertex called such a hole outside whenever that was the touching
472/// one (axioval, #191).
473fn hole_outside(outer: &Ring, hole: &Ring, t: Tolerance) -> bool {
474    let n = outer.points.len();
475    let on_boundary = |q: Point2| {
476        (0..n).any(|i| {
477            let (a, b) = (outer.points[i], outer.points[(i + 1) % n]);
478            on_segment(a, b, q, t.linear())
479        })
480    };
481    hole.points
482        .iter()
483        .any(|&q| !on_boundary(q) && !contains(outer, q))
484}
485fn contains(r: &Ring, p: Point2) -> bool {
486    let mut inside = false;
487    for (a, b) in r
488        .points
489        .iter()
490        .zip(r.points.iter().cycle().skip(1))
491        .take(r.points.len())
492    {
493        // The edge spans the ray's height and `p` lies strictly left of
494        // where it crosses: left of an upward edge, right of a downward
495        // one, decided exactly (#272). A point on the edge counts no
496        // crossing, as the rounded comparison did.
497        if (a.y > p.y) != (b.y > p.y) {
498            let towards = if b.y > a.y {
499                Sign::Positive
500            } else {
501                Sign::Negative
502            };
503            if side(*a, *b, p) == towards {
504                inside = !inside
505            }
506        }
507    }
508    inside
509}
510pub(crate) fn canonical(mut r: Ring, want_positive: bool) -> Ring {
511    if (orientation(&r.points) == Sign::Positive) != want_positive {
512        r.points.reverse()
513    };
514    let k = r
515        .points
516        .iter()
517        .enumerate()
518        .min_by(|a, b| {
519            a.1.x
520                .total_cmp(&b.1.x)
521                .then(a.1.y.total_cmp(&b.1.y))
522                .then(a.0.cmp(&b.0))
523        })
524        .map(|x| x.0)
525        .unwrap_or(0);
526    r.points.rotate_left(k);
527    r
528}
529/// Performs a neutral planar overlay. Output ordering is deterministic: polygons sort by outer-ring lexicographic start, rings are canonicalized CCW/CW.
530///
531/// Exact (#173): where boundaries cross and which side of each piece lies
532/// in the result are exact decisions; the tolerance only validates the
533/// operands and settles the output. An input vertex the operation does not
534/// move comes back bit-identical, and a crossing of two edges is the double
535/// nearest to the exact crossing point.
536pub fn overlay(
537    subject: &OverlayInput,
538    clip: &OverlayInput,
539    operation: OverlayOperation,
540    fill: FillRule,
541    tolerance: Tolerance,
542) -> Result<OverlayResult, OverlayError> {
543    overlay_with(
544        subject,
545        clip,
546        operation,
547        fill,
548        tolerance,
549        OverlayOptions::new(),
550    )
551    .map(|reported| reported.result)
552}
553
554/// [`overlay`] with explicit options; the default options make it
555/// [`overlay`] exactly.
556///
557/// With [`OverlayOptions::dropping_degenerate_rings`] a ring that is too
558/// short, has a repeated vertex, or has zero area under the exact
559/// `ZeroArea` rule is left out instead of refusing the overlay, a hole
560/// goes with its dropped outer ring, and each such ring is reported in
561/// [`ReportedOverlay::dropped`] with its operand, polygon, ring, reason
562/// and area (#256). Every other fault is refused as by [`overlay`]; a
563/// self-intersecting ring is never dropped. The overlay of the kept rings
564/// is exact as usual, and its evidence counts the kept rings. The rules
565/// are in the docs of [`DropReason`] and of the `degenerate` module.
566pub fn overlay_with(
567    subject: &OverlayInput,
568    clip: &OverlayInput,
569    operation: OverlayOperation,
570    fill: FillRule,
571    tolerance: Tolerance,
572    options: OverlayOptions,
573) -> Result<ReportedOverlay, OverlayError> {
574    overlay_charged(
575        subject,
576        clip,
577        operation,
578        fill,
579        tolerance,
580        options,
581        &mut Charge::unlimited(),
582    )
583    .map_err(budget::unbudgeted)
584}
585
586/// [`overlay_with`] under the caller's execution options (#338): the
587/// tolerance is [`ExecutionOptions::tolerance`], and with
588/// [`ExecutionOptions::with_memory_budget`] the subdivision is refused
589/// with [`OverlayRefusal::BudgetExceeded`] before it would hold more than
590/// the budget. Without a budget it is [`overlay_with`] exactly, its
591/// refusals wrapped in [`OverlayRefusal::Invalid`]. An admitted call gives
592/// the same result as [`overlay_with`], bit for bit. No other option is
593/// read: a cancellation token is not polled.
594///
595/// The charges and their bounds are in the docs of the `budget` module;
596/// a budget covers the subdivision, not the operands or the output.
597pub fn overlay_within(
598    subject: &OverlayInput,
599    clip: &OverlayInput,
600    operation: OverlayOperation,
601    fill: FillRule,
602    options: OverlayOptions,
603    execution: &ExecutionOptions,
604) -> Result<ReportedOverlay, OverlayRefusal> {
605    overlay_charged(
606        subject,
607        clip,
608        operation,
609        fill,
610        execution.tolerance(),
611        options,
612        &mut Charge::within(execution.memory_budget_bytes()),
613    )
614}
615
616/// [`overlay_with`], charged against `charge`.
617fn overlay_charged(
618    subject: &OverlayInput,
619    clip: &OverlayInput,
620    operation: OverlayOperation,
621    fill: FillRule,
622    tolerance: Tolerance,
623    options: OverlayOptions,
624    charge: &mut Charge,
625) -> Result<ReportedOverlay, OverlayRefusal> {
626    let known = [false, false];
627    overlay_known(
628        subject, clip, operation, fill, tolerance, options, charge, known,
629    )
630    .map(|(reported, _)| reported)
631}
632
633/// [`overlay_charged`] for operands some of which are known valid, and the
634/// proof that the output is (#341).
635///
636/// An operand with `known` set, its rings proved valid under `tolerance`
637/// by a [`settle::Validated`] the caller holds, has its frame and its
638/// holes' places checked but no ring validated again; refused
639/// degenerate rings are the only mode it applies to, the others validate
640/// as usual. The proof returned is the settled output's.
641#[allow(clippy::too_many_arguments)]
642pub(crate) fn overlay_known(
643    subject: &OverlayInput,
644    clip: &OverlayInput,
645    operation: OverlayOperation,
646    fill: FillRule,
647    tolerance: Tolerance,
648    options: OverlayOptions,
649    charge: &mut Charge,
650    known: [bool; 2],
651) -> Result<(ReportedOverlay, Option<settle::Validated>), OverlayRefusal> {
652    let vertices = |input: &OverlayInput| -> usize {
653        input
654            .polygons
655            .iter()
656            .flat_map(|p| std::iter::once(&p.outer).chain(&p.holes))
657            .map(|ring| ring.points.len())
658            .sum()
659    };
660    charge.add(vertices(subject) + vertices(clip), budget::PER_VERTEX)?;
661    let mut dropped = Vec::new();
662    let subject_polygons = admit_known(
663        subject,
664        OverlayOperand::Subject,
665        tolerance,
666        options,
667        known[0],
668        &mut dropped,
669    )?;
670    let clip_polygons = admit_known(
671        clip,
672        OverlayOperand::Clip,
673        tolerance,
674        options,
675        known[1],
676        &mut dropped,
677    )?;
678    if subject.frame != clip.frame {
679        return Err(OverlayError::InvalidFrame.into());
680    };
681    let rings = exact_overlay::boolean(
682        &subject_polygons,
683        &clip_polygons,
684        operation,
685        fill,
686        tolerance.linear(),
687        charge,
688    )?;
689    let (polygons, validated) = settle::settle_validated(canonical_polygons(rings), tolerance);
690    let evidence = OverlayEvidence {
691        subject_rings: subject_polygons.iter().map(|p| 1 + p.holes.len()).sum(),
692        clip_rings: clip_polygons.iter().map(|p| 1 + p.holes.len()).sum(),
693        output_polygons: polygons.len(),
694        output_holes: polygons.iter().map(|polygon| polygon.holes.len()).sum(),
695    };
696    Ok((
697        ReportedOverlay {
698            result: OverlayResult { polygons, evidence },
699            dropped,
700        },
701        validated,
702    ))
703}
704
705/// Union a set of individually-valid rings that may overlap each other.
706///
707/// [`overlay`] rejects a self-intersecting *operand*, which is correct for a
708/// boolean between two shapes but wrong for a union of a triangle soup: a
709/// projected mesh routinely overlaps itself, and mutual overlap is exactly
710/// what a union is for. Each ring is still validated on its own, so malformed
711/// geometry is refused rather than absorbed.
712pub fn union_soup(rings: &[Ring], tolerance: Tolerance) -> Result<Vec<Polygon>, OverlayError> {
713    for ring in rings {
714        validate_ring(ring, tolerance)?;
715    }
716    union_valid(rings, tolerance, &mut Charge::unlimited()).map_err(budget::unbudgeted)
717}
718
719/// [`union_soup`] with explicit options; the default options make it
720/// [`union_soup`] exactly.
721///
722/// With [`OverlayOptions::dropping_degenerate_rings`] a ring that is too
723/// short, has a repeated vertex, or has zero area under the exact
724/// `ZeroArea` rule is left out and reported in [`ReportedUnion::dropped`],
725/// as [`overlay_with`] does (#256): operand
726/// [`OverlayOperand::Subject`], polygon the ring's index, role
727/// [`RingRole::Outer`]. Every other fault is refused as by
728/// [`union_soup`].
729pub fn union_soup_with(
730    rings: &[Ring],
731    tolerance: Tolerance,
732    options: OverlayOptions,
733) -> Result<ReportedUnion, OverlayError> {
734    union_soup_charged(rings, tolerance, options, &mut Charge::unlimited())
735        .map_err(budget::unbudgeted)
736}
737
738/// [`union_soup_with`] under the caller's execution options (#338), as
739/// [`overlay_within`] is [`overlay_with`]: the tolerance is
740/// [`ExecutionOptions::tolerance`], and a memory budget refuses the
741/// subdivision with [`OverlayRefusal::BudgetExceeded`] before it would
742/// hold more. Without a budget it is [`union_soup_with`] exactly.
743pub fn union_soup_within(
744    rings: &[Ring],
745    options: OverlayOptions,
746    execution: &ExecutionOptions,
747) -> Result<ReportedUnion, OverlayRefusal> {
748    union_soup_charged(
749        rings,
750        execution.tolerance(),
751        options,
752        &mut Charge::within(execution.memory_budget_bytes()),
753    )
754}
755
756/// [`union_soup_with`], charged against `charge`.
757fn union_soup_charged(
758    rings: &[Ring],
759    tolerance: Tolerance,
760    options: OverlayOptions,
761    charge: &mut Charge,
762) -> Result<ReportedUnion, OverlayRefusal> {
763    charge.add(
764        rings.iter().map(|ring| ring.points.len()).sum(),
765        budget::PER_VERTEX,
766    )?;
767    let mut dropped = Vec::new();
768    let mut kept = Vec::with_capacity(rings.len());
769    for (index, ring) in rings.iter().enumerate() {
770        if options.degenerate_rings == DegenerateRings::Refuse {
771            validate_ring(ring, tolerance)?;
772            kept.push(ring.clone());
773            continue;
774        }
775        match degenerate::checked(ring, tolerance)? {
776            None => kept.push(ring.clone()),
777            Some(reason) => {
778                let place = degenerate::Place {
779                    operand: OverlayOperand::Subject,
780                    polygon: index,
781                    ring: RingRole::Outer,
782                };
783                dropped.push(degenerate::report(ring, place, reason));
784            }
785        }
786    }
787    Ok(ReportedUnion {
788        polygons: union_valid(&kept, tolerance, charge)?,
789        dropped,
790    })
791}
792
793/// The union of rings already validated one by one.
794fn union_valid(
795    rings: &[Ring],
796    tolerance: Tolerance,
797    charge: &mut Charge,
798) -> Result<Vec<Polygon>, OverlayRefusal> {
799    if rings.is_empty() {
800        return Ok(Vec::new());
801    }
802    // All rings form one subject against an empty clip. The NonZero fill
803    // then resolves the mutual overlaps in a single pass, which is both
804    // correct and cheaper than folding pairwise.
805    let subject: Vec<Polygon> = rings
806        .iter()
807        .map(|ring| Polygon {
808            outer: ring.clone(),
809            holes: Vec::new(),
810        })
811        .collect();
812    let rings = exact_overlay::boolean(
813        &subject,
814        &[],
815        OverlayOperation::Union,
816        FillRule::NonZero,
817        tolerance.linear(),
818        charge,
819    )?;
820    // Settled like every other output, so the polygons are valid operands
821    // (#191).
822    Ok(settle::settle(canonical_polygons(rings), tolerance))
823}
824
825/// Canonical kernel polygons from a boolean's rings.
826///
827/// Shared by every operation so ring orientation, rotation and polygon
828/// ordering cannot drift between them.
829fn canonical_polygons(rings: Vec<(Ring, Vec<Ring>)>) -> Vec<Polygon> {
830    let mut polygons: Vec<Polygon> = rings
831        .into_iter()
832        .map(|(outer, holes)| Polygon {
833            outer: canonical(outer, true),
834            holes: holes.into_iter().map(|h| canonical(h, false)).collect(),
835        })
836        .collect();
837    polygons.sort_by(|a, b| {
838        a.outer.points[0]
839            .x
840            .total_cmp(&b.outer.points[0].x)
841            .then(a.outer.points[0].y.total_cmp(&b.outer.points[0].y))
842    });
843    polygons
844}
845
846#[cfg(test)]
847mod tests {
848    //! Ring validation's candidate search and the proof regions carry
849    //! (#341).
850    use super::*;
851    use std::cell::Cell;
852
853    thread_local! {
854        /// Rings validated on this thread.
855        pub(super) static VALIDATED: Cell<usize> = const { Cell::new(0) };
856    }
857
858    fn validated() -> usize {
859        VALIDATED.with(Cell::get)
860    }
861
862    /// The search before #341: every pair, lowest `i` then lowest `j`.
863    fn all_pairs(points: &[Point2], linear: f64) -> Option<(usize, usize)> {
864        let n = points.len();
865        for i in 0..n {
866            for j in i + 1..n {
867                if j == i + 1 || (i == 0 && j + 1 == n) {
868                    continue;
869                }
870                let (a, b) = (points[i], points[(i + 1) % n]);
871                let (c, d) = (points[j], points[(j + 1) % n]);
872                if segments_intersect(a, b, c, d, linear) {
873                    return Some((i, j));
874                }
875            }
876        }
877        None
878    }
879
880    /// A small deterministic generator, so failures reproduce.
881    struct Lcg(u64);
882
883    impl Lcg {
884        fn next(&mut self) -> f64 {
885            self.0 = self
886                .0
887                .wrapping_mul(6_364_136_223_846_793_005)
888                .wrapping_add(1_442_695_040_888_963_407);
889            (self.0 >> 11) as f64 / (1u64 << 53) as f64
890        }
891    }
892
893    /// A star of `n` corners around the origin at seeded radii, with
894    /// `faults` corners moved through the centre, onto another corner or
895    /// within the tolerance of a far edge, so that some stars cross or
896    /// touch themselves and others do not.
897    fn star(rng: &mut Lcg, n: usize, faults: usize) -> Vec<Point2> {
898        let mut points: Vec<Point2> = (0..n)
899            .map(|i| {
900                let angle = std::f64::consts::TAU * (i as f64 + 0.5 * rng.next()) / n as f64;
901                let radius = 1.0 + 0.2 * rng.next();
902                Point2::new(radius * angle.cos(), radius * angle.sin())
903            })
904            .collect();
905        for _ in 0..faults {
906            let k = (rng.next() * n as f64) as usize % n;
907            points[k] = match (rng.next() * 3.0) as usize {
908                0 => Point2::new(-0.9 * points[k].x, -0.9 * points[k].y),
909                1 => points[(k + 2) % n],
910                _ => {
911                    let j = (k + n / 2) % n;
912                    let (a, b) = (points[j], points[(j + 1) % n]);
913                    Point2::new(0.5 * (a.x + b.x) + 1e-10, 0.5 * (a.y + b.y))
914                }
915            };
916        }
917        points
918    }
919
920    /// The box tree finds the pair the all-pairs loop finds, on rings long
921    /// enough to use it, valid or crossing, touching or within the
922    /// tolerance.
923    #[test]
924    fn the_first_touching_pair_is_the_all_pairs_one() {
925        let mut rng = Lcg(341);
926        let (mut found, mut clean) = (0, 0);
927        for case in 0..400 {
928            let n = [5, 33, 40, 64, 200][case % 5];
929            let faults = case / 5 % 4;
930            let points = star(&mut rng, n, faults);
931            for linear in [1e-9, 1e-3, 0.05] {
932                let pair = first_touching_pair(&points, linear);
933                assert_eq!(pair, all_pairs(&points, linear), "case {case} at {linear}");
934                match pair {
935                    Some(_) => found += 1,
936                    None => clean += 1,
937                }
938            }
939        }
940        assert!(found > 200 && clean > 200, "{found} found, {clean} clean");
941    }
942
943    /// A comb whose last corner comes within the tolerance of the first
944    /// tooth's flank: a pair far apart in the ring, near in the plane.
945    #[test]
946    fn a_long_ring_touching_itself_far_along_is_found() {
947        let mut points = Vec::new();
948        for k in 0..2_000 {
949            let x = k as f64;
950            points.push(Point2::new(x, 0.0));
951            points.push(Point2::new(x + 0.5, 1.0));
952        }
953        points.push(Point2::new(2_000.0, 0.0));
954        points.push(Point2::new(2_000.0, -1.0));
955        points.push(Point2::new(0.0, -1.0));
956        let t = Tolerance::new(1e-9, 1e-9).expect("a tolerance");
957        let ring = Ring {
958            points: points.clone(),
959        };
960        assert_eq!(validate_ring(&ring, t), Ok(()));
961        assert_eq!(first_touching_pair(&points, t.linear()), None);
962        let n = points.len();
963        points[n - 1] = Point2::new(0.25, 0.5 - 1e-10);
964        let pair = first_touching_pair(&points, t.linear());
965        assert!(pair.is_some());
966        assert_eq!(pair, all_pairs(&points, t.linear()));
967        assert_eq!(
968            validate_ring(&Ring { points }, t),
969            Err(OverlayError::SelfIntersection)
970        );
971    }
972
973    fn square(x: f64, y: f64, side: f64) -> Polygon {
974        Polygon {
975            outer: Ring {
976                points: vec![
977                    Point2::new(x, y),
978                    Point2::new(x + side, y),
979                    Point2::new(x + side, y + side),
980                    Point2::new(x, y + side),
981                ],
982            },
983            holes: Vec::new(),
984        }
985    }
986
987    /// Booleans on regions validate no ring a region carries a proof for
988    /// under the call's tolerance, and the results carry one in turn; a
989    /// call under another tolerance, or on a region without a proof,
990    /// validates again and refuses what it refuses today.
991    #[test]
992    fn regions_are_validated_once() {
993        let t = Tolerance::new(1e-9, 1e-9).expect("a tolerance");
994        let a = Region::new(vec![square(0.0, 0.0, 2.0)], t).expect("valid");
995        let b = Region::new(vec![square(1.0, 1.0, 2.0)], t).expect("valid");
996        let before = validated();
997        let union = a.union(&b, t).expect("the union");
998        let cut = union.difference(&a, t).expect("the difference");
999        let again = cut.intersection(&b, t).expect("the intersection");
1000        assert_eq!(validated(), before, "no ring validated again");
1001        assert!(!again.is_empty());
1002        let input = |region: &Region| OverlayInput {
1003            frame: Frame2 {
1004                origin: Point2::ZERO,
1005                x: Point2::X,
1006                y: Point2::Y,
1007            },
1008            polygons: region.polygons().to_vec(),
1009        };
1010        let raw = overlay(
1011            &input(&a),
1012            &input(&b),
1013            OverlayOperation::Union,
1014            FillRule::NonZero,
1015            t,
1016        )
1017        .expect("the union");
1018        assert_eq!(raw.polygons, union.polygons());
1019        assert!(validated() > before, "overlay validates every ring");
1020
1021        // A ring with an edge of 1e-6 is valid at 1e-9, not at 1e-3.
1022        let mut short = square(5.0, 5.0, 1.0);
1023        short.outer.points.insert(1, Point2::new(5.5, 5.0));
1024        short.outer.points.insert(2, Point2::new(5.5 + 1e-6, 5.0));
1025        let fine = Region::new(vec![short], t).expect("valid at 1e-9");
1026        let coarse = Tolerance::new(1e-3, 1e-3).expect("a tolerance");
1027        assert_eq!(fine.union(&a, coarse), Err(OverlayError::RepeatedVertex));
1028        // A translated region carries no proof.
1029        let moved = a
1030            .translate(axiolid_core::Vec2::new(0.5, 0.5))
1031            .expect("moved");
1032        let before = validated();
1033        moved.union(&b, t).expect("the union");
1034        assert!(validated() > before, "a translate is validated again");
1035
1036        // A disc morphology's settled result carries a proof too.
1037        let eroded = union.erode_inner(0.1, t).expect("the erosion");
1038        let before = validated();
1039        eroded.difference(&b, t).expect("the difference");
1040        assert_eq!(validated(), before, "an erosion is not validated again");
1041
1042        // A hole outside its outer ring is valid ring by ring, so a region
1043        // holds it; a boolean refuses it, proof or not.
1044        let mut stray = square(10.0, 10.0, 1.0);
1045        let mut hole = square(20.0, 20.0, 0.5).outer;
1046        hole.points.reverse();
1047        stray.holes.push(hole);
1048        assert_eq!(
1049            Region::new(vec![stray.clone()], t),
1050            Err(OverlayError::HoleOutsideOuter)
1051        );
1052        let stray = Region::from_valid_polygons(vec![stray.clone()], false).with_validated(Some(
1053            crate::settle::Validated::check(&[stray], t).expect("each ring valid"),
1054        ));
1055        assert_eq!(stray.union(&a, t), Err(OverlayError::HoleOutsideOuter));
1056    }
1057
1058    #[test]
1059    fn overlay_error_displays_and_boxes() {
1060        let all = [
1061            OverlayError::InvalidFrame,
1062            OverlayError::NonFinitePoint,
1063            OverlayError::RingTooShort,
1064            OverlayError::RepeatedVertex,
1065            OverlayError::ZeroArea,
1066            OverlayError::SelfIntersection,
1067            OverlayError::HoleOutsideOuter,
1068            OverlayError::HolesOverlap,
1069            OverlayError::PolygonsOverlap,
1070            OverlayError::InvalidOffsetDistance,
1071            OverlayError::InvalidOffsetStyle,
1072            OverlayError::ZeroRadiusArc,
1073        ];
1074        let mut seen = std::collections::BTreeSet::new();
1075        for error in all {
1076            let text = error.to_string();
1077            assert!(!text.is_empty());
1078            assert!(seen.insert(text), "{error:?} has a distinct message");
1079            let boxed: Box<dyn std::error::Error> = Box::new(error);
1080            assert!(!boxed.to_string().is_empty());
1081        }
1082    }
1083
1084    /// A refusal wraps its operand error: the message carries the error's
1085    /// own wording and `source` hands the error back.
1086    #[test]
1087    fn overlay_refusal_wraps_and_exposes_its_error() {
1088        use crate::OverlayRefusal;
1089        use std::error::Error;
1090        let refusal = OverlayRefusal::from(OverlayError::ZeroArea);
1091        assert_eq!(
1092            refusal.to_string(),
1093            format!("invalid overlay operand: {}", OverlayError::ZeroArea)
1094        );
1095        assert_eq!(
1096            refusal.source().map(ToString::to_string),
1097            Some(OverlayError::ZeroArea.to_string())
1098        );
1099        let budget = OverlayRefusal::BudgetExceeded {
1100            needed: 2,
1101            budget: 1,
1102        };
1103        assert!(budget.source().is_none());
1104    }
1105}