axiolid_arrangement/
validate.rs

1// SPDX-License-Identifier: MPL-2.0
2
3//! Structural audit of the arrangement.
4//!
5//! Every edit is supposed to preserve these invariants. Having them as a
6//! checkable report rather than scattered `debug_assert!`s means a test can
7//! prove an edit left the structure sound, and a caller integrating a new
8//! edit can find out where it went wrong instead of getting a hang.
9
10use crate::id::{FaceId, HalfEdgeId};
11use crate::Arrangement;
12
13/// What an audit found.
14///
15/// Counts rather than a bare bool: "the structure is broken" is not
16/// actionable, "three half-edges have a twin that does not point back" is.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub struct ArrangementHealth {
19    /// Half-edges whose twin does not name them back.
20    pub broken_twins: usize,
21    /// Half-edges where `next`'s `prev` is not the half-edge itself.
22    pub broken_links: usize,
23    /// Half-edges whose `next` lies on a different face.
24    pub face_mismatches: usize,
25    /// Boundary walks that did not close within the arena bound.
26    pub unclosed_faces: usize,
27    /// Bounded faces whose signed area is not strictly positive.
28    pub inverted_faces: usize,
29}
30
31impl ArrangementHealth {
32    /// Whether every invariant holds.
33    #[must_use]
34    pub fn is_sound(&self) -> bool {
35        *self == Self::default()
36    }
37}
38
39impl Arrangement {
40    /// Check every structural invariant.
41    #[must_use]
42    pub fn audit(&self) -> ArrangementHealth {
43        let mut health = ArrangementHealth::default();
44
45        for (index, he) in self.halfedges.iter().enumerate() {
46            let id = HalfEdgeId::from_index(index);
47            if self.halfedges[he.twin.index()].twin != id {
48                health.broken_twins += 1;
49            }
50            if self.halfedges[he.next.index()].prev != id {
51                health.broken_links += 1;
52            }
53            if self.halfedges[he.next.index()].face != he.face {
54                health.face_mismatches += 1;
55            }
56        }
57
58        for index in 0..self.faces.len() {
59            let face = FaceId::from_index(index);
60            let Some(start) = self.faces[index].boundary else {
61                continue;
62            };
63            // Walk the boundary and confirm it returns to the start.
64            let mut steps = 0usize;
65            let mut current = self.halfedges[start.index()].next;
66            while current != start {
67                steps += 1;
68                if steps > self.halfedges.len() {
69                    health.unclosed_faces += 1;
70                    break;
71                }
72                current = self.halfedges[current.index()].next;
73            }
74            if face != FaceId::OUTER && self.face_area(face) <= 0.0 {
75                health.inverted_faces += 1;
76            }
77        }
78
79        health
80    }
81}