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}