Skip to main content

axiolid_overlay/
free_faces.rs

1//! Bounded free faces between obstacle polygons and open segments (#490).
2//!
3//! Given obstacle polygons (walls), barriers (open segments that also stop
4//! the free space) and a closing width `gap`, [`free_faces`] returns the
5//! bounded faces of the plane that no obstacle covers and no barrier cuts,
6//! each with the input edges its boundary lies on, and [`FreeFaces::locate`]
7//! says which face a point is in. Sections of a floor plan come as thin
8//! wall pieces whose ends miss each other by float noise; a room must close
9//! across such a joint, and the closure must be visible.
10//!
11//! # Gaps close by bridges, not by snapping
12//!
13//! Input is never moved. Where two boundary pieces that share no vertex come
14//! closer than `gap` with nothing between them, a bridge, a straight segment
15//! from a vertex to the nearest point of the other piece, joins them. A
16//! bridge is a barrier the caller did not give: it is reported in
17//! [`FreeFaces::bridges`] with the inputs it joins, and a ring edge on it
18//! lists [`PieceSource::Bridge`]. The pipeline, on the exact arrangement of
19//! `axiolid-exact-arrangement` (ADR 0094, ADR 0100):
20//!
21//! 1. Arrange the obstacle edges and barriers (`bridges`).
22//! 2. For every vertex and every piece it is not on, whose closest point is
23//!    strictly nearer than `gap` and whose open segment to it meets no piece,
24//!    add a bridge. Every qualifying pair is bridged, so no choice is made
25//!    and nothing can be ambiguous. Two edges that meet at a vertex are never
26//!    bridged to each other there: a sharp corner stays sharp.
27//! 3. Arrange inputs and bridges again, number the faces of the complement
28//!    (`faces`) and read each face as obstacle or free by ring membership
29//!    (`classify`, ADR 0104). A bridge inside an obstacle is discarded.
30//! 4. A bounded free face that fits in a strip narrower than `gap` is a
31//!    closed gap: reported in [`FreeFaces::closed_gaps`], not returned as a
32//!    face. The strip rule is exact, on the convex hull of the face.
33//! 5. Rings are traced, snap-rounded and certified as the visible faces are
34//!    (`rings`, ADR 0100), so [`FreeFaceEvidence::snap_displacement`] bounds
35//!    how far the binary64 ring moved from the exact one.
36//!
37//! The strip rule is a width, not a clearance. At a corner of angle `theta`
38//! under 90 degrees that a wall wraps at clearance `w` under `gap`, the
39//! bridges from the wall's corner to the two sides cut off a kite of width
40//! `2 w cos^2(theta / 2)`: a closed gap while that is under `gap`, a face
41//! once it exceeds it (an equilateral hole of inradius 1 and a concentric
42//! island of inradius `1 - w`: 6 gaps at `w = 0.6 gap`, 3 faces and 3 gaps at
43//! `w = 0.8 gap`).
44//!
45//! A ring on one line is refused: a wall that vanished silently would make a
46//! room leak. A piece with the same face on both sides, a dangling barrier or
47//! bridge, is in no ring. `gap == 0` closes nothing and returns the exact
48//! faces.
49//!
50//! The sources of a ring edge are lists, because inputs may coincide.
51//! Everything is in `x`-then-`y` order (`axiolid_exact_arrangement`'s
52//! `point_order`), and the output depends on the input order only through the
53//! indices inside [`PieceSource`].
54//!
55//! `scripts/probe_free_faces_mutants.py` lists the faults the tests in
56//! `tests/free_faces.rs` must catch.
57
58mod bridges;
59mod classify;
60mod locate;
61mod rings;
62
63use axiolid_contracts::{DisplacementBound, ExecutionOptions, GeomError};
64use axiolid_core::{Point2, Tolerance};
65use axiolid_exact_arrangement::{arrange, faces, ExactPoints, PairBudget, PointId, Segment};
66
67use crate::{exact_overlay, nesting, validate_ring, OverlayError, Polygon};
68
69/// Bytes declared per segment of the arrangement: the pieces, vertices and
70/// cut lists one input segment leads to (ADR 0088).
71const SEGMENT_BYTES: usize = 256;
72
73/// What blocks free space: obstacle polygons and open segments.
74#[derive(Debug, Clone, PartialEq, Default)]
75pub struct FreeSpaceInput {
76    /// Walls and other solids. They may overlap or touch each other.
77    pub obstacles: Vec<Polygon>,
78    /// Open segments that stop free space like a wall of no thickness.
79    pub barriers: Vec<[Point2; 2]>,
80}
81
82/// The input an edge of a free face lies on.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
84#[non_exhaustive]
85pub enum PieceSource {
86    /// An edge of an obstacle. Ring 0 is the outer ring, `k >= 1` is
87    /// `holes[k - 1]`; the edge leaves `points[edge]`.
88    Obstacle {
89        /// Index into [`FreeSpaceInput::obstacles`].
90        polygon: usize,
91        /// The ring of the polygon.
92        ring: usize,
93        /// The edge of the ring.
94        edge: usize,
95    },
96    /// A barrier, by index into [`FreeSpaceInput::barriers`].
97    Barrier {
98        /// The index.
99        barrier: usize,
100    },
101    /// A bridge the operation added, by index into [`FreeFaces::bridges`].
102    Bridge {
103        /// The index.
104        bridge: usize,
105    },
106}
107
108/// A closed boundary of a free face.
109#[derive(Debug, Clone, PartialEq)]
110pub struct FreeRing {
111    /// The vertices, starting at the smallest by `x`, then `y`.
112    pub points: Vec<Point2>,
113    /// Per edge `points[i]` to `points[(i + 1) % n]`, the inputs it lies on:
114    /// not empty, ascending, without repeats.
115    pub sources: Vec<Vec<PieceSource>>,
116}
117
118/// A bounded free face.
119#[derive(Debug, Clone, PartialEq)]
120pub struct FreeFace {
121    /// The outer ring, counter-clockwise.
122    pub outer: FreeRing,
123    /// The holes, clockwise, in the order of their points.
124    pub holes: Vec<FreeRing>,
125}
126
127/// A segment added across a gap narrower than the closing width.
128#[derive(Debug, Clone, PartialEq)]
129pub struct Bridge {
130    /// The ends, rounded once, the smaller by `x`, then `y`, first.
131    pub ends: [Point2; 2],
132    /// Per end, the inputs meeting or carrying it, ascending.
133    pub joins: [Vec<PieceSource>; 2],
134    /// The distance of the two ends as returned. The exact length is less
135    /// than the closing width; this is `0.0` for a bridge shorter than the
136    /// rounding.
137    pub length: f64,
138}
139
140/// What the operation did besides the faces.
141#[derive(Debug, Clone, PartialEq)]
142#[non_exhaustive]
143pub struct FreeFaceEvidence {
144    /// How far snap rounding moved a boundary point at most (ADR 0100).
145    pub snap_displacement: DisplacementBound,
146    /// Edges snap rounding routed through another vertex's cell.
147    pub rerouted_edges: usize,
148    /// Faces whose boundary rounded away.
149    pub collapsed_faces: usize,
150    /// Holes whose boundary rounded away.
151    pub collapsed_holes: usize,
152    /// Closed gaps whose boundary rounded away.
153    pub collapsed_gaps: usize,
154    /// Candidate pairs examined, over every step.
155    pub candidate_pairs: usize,
156}
157
158/// Where a point lies relative to a [`FreeFaces`].
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160#[non_exhaustive]
161pub enum FaceLocation {
162    /// Inside the face of that index.
163    Inside(usize),
164    /// On a ring edge of a face. Ring 0 is the outer ring, `k >= 1` is
165    /// `holes[k - 1]`; the edge's sources say what it lies on.
166    OnBoundary {
167        /// The face.
168        face: usize,
169        /// The ring of the face.
170        ring: usize,
171        /// The edge of the ring.
172        edge: usize,
173    },
174    /// In or on the closed gap of that index.
175    InClosedGap(usize),
176    /// In or on the input obstacle of that index (the lowest).
177    InObstacle(usize),
178    /// In none of them: outside every bounded free face.
179    Unbounded,
180}
181
182/// Why a barrier is refused.
183#[derive(Debug, Clone, PartialEq)]
184#[non_exhaustive]
185pub enum BarrierDefect {
186    /// An end has a non-finite coordinate.
187    NonFinitePoint,
188    /// The ends are no further apart than the tolerance.
189    TooShort,
190}
191
192/// Why no free faces were built.
193#[derive(Debug, Clone, PartialEq)]
194#[non_exhaustive]
195pub enum FreeFacesError {
196    /// The closing width is not finite, or negative.
197    InvalidGap,
198    /// An obstacle ring is invalid, or lies on one line, or the rings of one
199    /// obstacle are not apart and nested.
200    InvalidObstacle {
201        /// Index into [`FreeSpaceInput::obstacles`].
202        polygon: usize,
203        /// The first defect found.
204        reason: OverlayError,
205    },
206    /// A barrier is invalid.
207    InvalidBarrier {
208        /// Index into [`FreeSpaceInput::barriers`].
209        barrier: usize,
210        /// The defect.
211        reason: BarrierDefect,
212    },
213    /// A point to locate has a non-finite coordinate.
214    NonFinitePoint,
215    /// A resource limit was exceeded: `"memory"` or the name of a count.
216    BudgetExceeded {
217        /// The resource.
218        resource: &'static str,
219    },
220    /// The execution options' token was cancelled.
221    Cancelled,
222    /// An exact step could not certify its result; the message names it.
223    NotCertified(String),
224}
225
226impl core::fmt::Display for FreeFacesError {
227    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
228        match self {
229            Self::InvalidGap => f.write_str("the closing width is not finite, or is negative"),
230            Self::InvalidObstacle { polygon, reason } => {
231                write!(f, "obstacle {polygon} is invalid: {reason}")
232            }
233            Self::InvalidBarrier { barrier, reason } => {
234                let why = match reason {
235                    BarrierDefect::NonFinitePoint => "has a non-finite coordinate",
236                    BarrierDefect::TooShort => "is not longer than the tolerance",
237                };
238                write!(f, "barrier {barrier} {why}")
239            }
240            Self::NonFinitePoint => f.write_str("a point has a non-finite coordinate"),
241            Self::BudgetExceeded { resource } => write!(f, "the {resource} budget is exceeded"),
242            Self::Cancelled => f.write_str("the operation was cancelled"),
243            Self::NotCertified(message) => write!(f, "a result could not be certified: {message}"),
244        }
245    }
246}
247
248impl std::error::Error for FreeFacesError {}
249
250impl From<GeomError> for FreeFacesError {
251    fn from(error: GeomError) -> Self {
252        match error {
253            GeomError::BudgetExceeded { resource } => Self::BudgetExceeded { resource },
254            GeomError::Cancelled => Self::Cancelled,
255            other => Self::NotCertified(other.to_string()),
256        }
257    }
258}
259
260/// The bounded free faces of a [`FreeSpaceInput`] (module docs).
261#[derive(Debug, Clone)]
262pub struct FreeFaces {
263    faces: Vec<FreeFace>,
264    closed_gaps: Vec<FreeFace>,
265    bridges: Vec<Bridge>,
266    evidence: FreeFaceEvidence,
267    obstacles: Vec<Polygon>,
268}
269
270impl FreeFaces {
271    /// The faces, sorted by their outer rings' points.
272    pub fn faces(&self) -> &[FreeFace] {
273        &self.faces
274    }
275
276    /// The bounded free regions narrower than the closing width, in the same
277    /// form and order as the faces.
278    pub fn closed_gaps(&self) -> &[FreeFace] {
279        &self.closed_gaps
280    }
281
282    /// The bridges, sorted by their ends.
283    pub fn bridges(&self) -> &[Bridge] {
284        &self.bridges
285    }
286
287    /// What the operation did besides the faces.
288    pub fn evidence(&self) -> &FreeFaceEvidence {
289        &self.evidence
290    }
291}
292
293/// The bounded free faces of `input`, closing gaps narrower than `gap`.
294///
295/// `gap` is a distance in the units of the points: `0` closes nothing, there
296/// is no upper limit, and a distance exactly `gap` is not closed.
297/// `tolerance` validates the input, as everywhere in the crate; no decision
298/// reads it.
299///
300/// # Errors
301///
302/// [`FreeFacesError`]; the first defect found in the order `gap`, obstacles
303/// by index, barriers by index.
304pub fn free_faces(
305    input: &FreeSpaceInput,
306    gap: f64,
307    tolerance: Tolerance,
308) -> Result<FreeFaces, FreeFacesError> {
309    free_faces_within(input, gap, &ExecutionOptions::new(tolerance))
310}
311
312/// [`free_faces`] with execution options: the memory budget (ADR 0088) and
313/// the cancellation token; the tolerance is `execution.tolerance()`.
314///
315/// The memory budget bounds what grows with the number of crossings,
316/// T-junctions and bridges: the cuts of both arrangements (as `arrange`
317/// charges them, on top of 256 bytes per input segment or bridge), each exact
318/// foot and each bridge the search for bridges keeps, and the cells and
319/// fragments of snap rounding. Exceeding it is `BudgetExceeded` for
320/// `"memory"`. It does not bound the arrangement's own vertices and pieces,
321/// the faces, the rings or the output, which are linear in the size of the
322/// arrangement; the search's candidate pairs, quadratic in the worst case,
323/// are bounded only through the feet and bridges they keep, and by
324/// cancellation.
325pub fn free_faces_within(
326    input: &FreeSpaceInput,
327    gap: f64,
328    execution: &ExecutionOptions,
329) -> Result<FreeFaces, FreeFacesError> {
330    validate(input, gap, execution.tolerance())?;
331    let mut model = Model::assemble(input)?;
332    let mut budget = PairBudget::new(usize::MAX, "free-face pairs");
333    let inputs = model.segments.len();
334    let first = arrange(
335        &mut model.points,
336        &model.segments,
337        &mut budget,
338        inputs * SEGMENT_BYTES,
339        execution,
340    )?;
341    let found = bridges::find(
342        &mut model.points,
343        &first,
344        &model.sources,
345        gap,
346        inputs * SEGMENT_BYTES,
347        &mut budget,
348        execution,
349    )?;
350    for (index, bridge) in found.iter().enumerate() {
351        model.segments.push(Segment { ends: bridge.ends });
352        model.sources.push(PieceSource::Bridge { bridge: index });
353    }
354    let arrangement = if found.is_empty() {
355        first
356    } else {
357        arrange(
358            &mut model.points,
359            &model.segments,
360            &mut budget,
361            model.segments.len() * SEGMENT_BYTES,
362            execution,
363        )?
364    };
365    let numbered = faces(&model.points, &arrangement, &mut budget, execution)?;
366    let classified = classify::classify(
367        &model,
368        &arrangement,
369        &numbered,
370        inputs,
371        found.len(),
372        gap,
373        &mut budget,
374        execution,
375    )?;
376    let built = rings::build(
377        &model.points,
378        &arrangement,
379        &classified,
380        &found,
381        &mut budget,
382        execution,
383    )?;
384    Ok(FreeFaces {
385        faces: built.faces,
386        closed_gaps: built.closed_gaps,
387        bridges: built.bridges,
388        evidence: FreeFaceEvidence {
389            snap_displacement: built.snap_displacement,
390            rerouted_edges: built.rerouted_edges,
391            collapsed_faces: built.collapsed_faces,
392            collapsed_holes: built.collapsed_holes,
393            collapsed_gaps: built.collapsed_gaps,
394            candidate_pairs: budget.used,
395        },
396        obstacles: input.obstacles.clone(),
397    })
398}
399
400/// The first defect of `input`: gap, obstacles by index, barriers by index.
401fn validate(input: &FreeSpaceInput, gap: f64, tolerance: Tolerance) -> Result<(), FreeFacesError> {
402    if !gap.is_finite() || gap < 0.0 {
403        return Err(FreeFacesError::InvalidGap);
404    }
405    for (index, polygon) in input.obstacles.iter().enumerate() {
406        let invalid = |reason| FreeFacesError::InvalidObstacle {
407            polygon: index,
408            reason,
409        };
410        let rings = std::iter::once(&polygon.outer).chain(&polygon.holes);
411        for ring in rings.clone() {
412            validate_ring(ring, tolerance).map_err(invalid)?;
413        }
414        // A ring on one line is no defect of an operand, but a wall that
415        // disappears would make a room leak.
416        if rings
417            .into_iter()
418            .any(|ring| exact_overlay::on_one_line(&ring.points))
419        {
420            return Err(invalid(OverlayError::ZeroArea));
421        }
422        nesting::check(std::slice::from_ref(polygon)).map_err(invalid)?;
423    }
424    for (index, barrier) in input.barriers.iter().enumerate() {
425        let invalid = |reason| FreeFacesError::InvalidBarrier {
426            barrier: index,
427            reason,
428        };
429        if !barrier[0].is_finite() || !barrier[1].is_finite() {
430            return Err(invalid(BarrierDefect::NonFinitePoint));
431        }
432        if (barrier[1] - barrier[0]).length() <= tolerance.linear() {
433            return Err(invalid(BarrierDefect::TooShort));
434        }
435    }
436    Ok(())
437}
438
439/// The exact points and segments of the input, with what each segment is.
440struct Model {
441    points: ExactPoints,
442    segments: Vec<Segment>,
443    /// Per segment, the input it is: obstacle edges, barriers, then bridges.
444    sources: Vec<PieceSource>,
445}
446
447impl Model {
448    fn assemble(input: &FreeSpaceInput) -> Result<Self, FreeFacesError> {
449        let mut points = ExactPoints::new();
450        let mut segments = Vec::new();
451        let mut sources = Vec::new();
452        for (polygon, obstacle) in input.obstacles.iter().enumerate() {
453            let rings = std::iter::once(&obstacle.outer).chain(&obstacle.holes);
454            for (ring, found) in rings.enumerate() {
455                let ids: Vec<PointId> = found
456                    .points
457                    .iter()
458                    .map(|&point| points.push_flat(point))
459                    .collect::<Result<_, _>>()?;
460                for edge in 0..ids.len() {
461                    segments.push(Segment {
462                        ends: [ids[edge], ids[(edge + 1) % ids.len()]],
463                    });
464                    sources.push(PieceSource::Obstacle {
465                        polygon,
466                        ring,
467                        edge,
468                    });
469                }
470            }
471        }
472        for (barrier, ends) in input.barriers.iter().enumerate() {
473            segments.push(Segment {
474                ends: [points.push_flat(ends[0])?, points.push_flat(ends[1])?],
475            });
476            sources.push(PieceSource::Barrier { barrier });
477        }
478        Ok(Self {
479            points,
480            segments,
481            sources,
482        })
483    }
484}