axiolid_mesh/
adjacency.rs

1//! Edge adjacency derived once, so algorithms stop rebuilding it.
2//!
3//! # Why this exists
4//!
5//! Healing, genus, smoothing, decimation, and decomposition each needed the
6//! same fact: which triangles meet along each edge. Every one of them built
7//! its own `BTreeMap<(u32, u32), _>` inline, with its own key convention and
8//! its own degenerate-triangle handling. Five implementations of one idea is
9//! five places for the invariant to drift.
10//!
11//! This derives it once. The algorithms ask questions instead of rebuilding
12//! the answer.
13//!
14//! # Not a half-edge structure
15//!
16//! A true half-edge (or winged-edge) representation stores per-half-edge
17//! `next`/`twin`/`face` links and supports *mutation* through them. That is
18//! the right structure for algorithms that rewrite connectivity in place.
19//!
20//! This is deliberately less: an immutable, derived index answering
21//! adjacency queries over a `TriMesh` that stays the owner of the data.
22//! Every current consumer reads adjacency and writes a *new* mesh, so
23//! nothing needs mutable topology, and a mutable structure would add an
24//! invariant to maintain for no consumer.
25//!
26//! The name says what it is. If in-place connectivity editing ever appears,
27//! that is a separate type, not a field bolted onto this one.
28//!
29//! # Degenerate triangles
30//!
31//! A triangle with a repeated corner (`[4, 4, 7]`) has an edge from a vertex
32//! to itself. Counting it as adjacency makes a sound mesh look non-manifold.
33//! Such triangles are excluded and counted in
34//! [`EdgeAdjacency::degenerate_triangles`], matching what `audit_mesh`
35//! already does, so the two never disagree about what is usable.
36
37use crate::TriMesh;
38
39/// An undirected edge, canonically ordered so both sides collide.
40///
41/// Construction is the only place ordering is decided, which is what stops
42/// two call sites from disagreeing about whether `(3, 1)` and `(1, 3)` are
43/// the same edge.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
45pub struct EdgeKey {
46    lower: u32,
47    upper: u32,
48}
49
50impl EdgeKey {
51    /// Canonical key for an edge between two corners.
52    ///
53    /// Order-insensitive: `new(a, b) == new(b, a)`.
54    #[must_use]
55    pub fn new(a: u32, b: u32) -> Self {
56        if a <= b {
57            Self { lower: a, upper: b }
58        } else {
59            Self { lower: b, upper: a }
60        }
61    }
62
63    /// Lower-numbered endpoint.
64    #[must_use]
65    pub const fn lower(self) -> u32 {
66        self.lower
67    }
68
69    /// Higher-numbered endpoint.
70    #[must_use]
71    pub const fn upper(self) -> u32 {
72        self.upper
73    }
74
75    /// Both endpoints, lower first.
76    #[must_use]
77    pub const fn endpoints(self) -> (u32, u32) {
78        (self.lower, self.upper)
79    }
80}
81
82/// One triangle's use of an edge, and the direction it traversed.
83///
84/// Direction is what reveals winding: two triangles sharing an edge are
85/// consistently wound when they traverse it in *opposite* directions. A pair
86/// agreeing on direction is the classic flipped-face defect.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct EdgeUse {
89    /// Triangle index into the mesh.
90    pub triangle: usize,
91    /// Whether this triangle traversed the edge low-to-high.
92    pub forward: bool,
93}
94
95/// Sort edge records by key using two counting-sort passes.
96///
97/// `EdgeKey` is two vertex indices, so the key space is the vertex
98/// count rather than something unbounded: sorting by `upper` then
99/// `lower` orders the whole key in O(n) passes instead of O(n log n)
100/// comparisons. Same shape as `counting_sort_edges` in `audit`, which
101/// this follows deliberately.
102///
103/// Each pass is stable, and records are pushed in ascending triangle
104/// order, so the uses of one edge stay ascending by triangle without
105/// a third pass on the triangle index. That is load-bearing, not
106/// incidental: callers pair `uses[0]`/`uses[1]` to judge winding.
107fn counting_sort_records(
108    records: &mut Vec<(EdgeKey, EdgeUse)>,
109    scratch: &mut Vec<(EdgeKey, EdgeUse)>,
110    buckets: usize,
111) {
112    debug_assert_eq!(scratch.len(), records.len());
113    let mut counts: Vec<u32> = Vec::new();
114    // LSD: the less significant half of the key first, so the more
115    // significant pass decides the final order.
116    for pass in 0..2 {
117        counts.clear();
118        counts.resize(buckets + 2, 0);
119        for (key, _) in records.iter() {
120            let bucket = if pass == 0 { key.upper() } else { key.lower() } as usize;
121            counts[bucket + 1] += 1;
122        }
123        for index in 0..=buckets {
124            counts[index + 1] += counts[index];
125        }
126        for record in records.iter() {
127            let bucket = if pass == 0 {
128                record.0.upper()
129            } else {
130                record.0.lower()
131            } as usize;
132            scratch[counts[bucket] as usize] = *record;
133            counts[bucket] += 1;
134        }
135        std::mem::swap(records, scratch);
136    }
137}
138
139/// Edge-to-triangle adjacency over a triangle mesh.
140///
141/// Built once with [`EdgeAdjacency::build`], then queried. Iteration order is
142/// by [`EdgeKey`], so any diagnosis derived from it is reproducible -- a
143/// report that reorders between runs is useless as an audit record.
144///
145/// # Layout
146///
147/// Edges are held in compressed-sparse-row form: `keys` ascending, `starts`
148/// giving each key's span, and `uses` one flat run per edge. A
149/// `BTreeMap<EdgeKey, Vec<EdgeUse>>` costs a node allocation per distinct
150/// edge plus a `Vec` allocation per edge that is ever used twice -- roughly
151/// 245,000 allocations on an 82k-triangle sphere, against three here.
152///
153/// The structure is immutable after the build, which is what makes this
154/// affordable: CSR cannot accept a late insertion without reflowing, and
155/// nothing in the API offers one.
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct EdgeAdjacency {
158    /// Distinct edge keys, ascending.
159    keys: Vec<EdgeKey>,
160    /// `starts[i]..starts[i + 1]` is edge `i`'s span in `uses`.
161    ///
162    /// Length is `keys.len() + 1`, so the last span needs no special case.
163    starts: Vec<u32>,
164    /// Uses grouped by edge, ascending by triangle within each edge.
165    uses: Vec<EdgeUse>,
166    vertex_count: usize,
167    degenerate_triangles: usize,
168    /// Triangles that contributed adjacency, recorded during the build.
169    ///
170    /// Counting these by walking the edge map means inserting every
171    /// triangle index into a set -- three visits per triangle and an
172    /// allocation per node, to recover a number the build already knew.
173    face_count: usize,
174}
175
176impl EdgeAdjacency {
177    /// Derive adjacency from a mesh.
178    ///
179    /// Triangles with a repeated corner are skipped and counted. Corners are
180    /// taken as given: an out-of-range index is not adjacency data, and
181    /// validating it belongs to `audit_mesh`, not here.
182    #[must_use]
183    pub fn build(mesh: &TriMesh) -> Self {
184        let triangles = mesh.indices.len() / 3;
185        // Every usable triangle contributes exactly three edge records, so
186        // the whole build fits in one allocation sized up front.
187        let mut records: Vec<(EdgeKey, EdgeUse)> = Vec::with_capacity(triangles * 3);
188        let mut degenerate_triangles = 0;
189        let mut face_count = 0;
190
191        for (triangle, chunk) in mesh.indices.chunks_exact(3).enumerate() {
192            let (a, b, c) = (chunk[0], chunk[1], chunk[2]);
193            if a == b || b == c || c == a {
194                degenerate_triangles += 1;
195                continue;
196            }
197            // Past the guard this triangle contributes all three of its
198            // edges, so it is exactly one face.
199            face_count += 1;
200            for (from, to) in [(a, b), (b, c), (c, a)] {
201                records.push((
202                    EdgeKey::new(from, to),
203                    EdgeUse {
204                        triangle,
205                        forward: from <= to,
206                    },
207                ));
208            }
209        }
210
211        // Buckets must cover the largest index actually present. `build`
212        // takes corners as given, so an index past the position array is
213        // possible and sizing from `positions.len()` would index out of
214        // bounds -- validation belongs to `audit_mesh`, not here.
215        let buckets = records
216            .iter()
217            .map(|(key, _)| key.upper() as usize)
218            .max()
219            .map_or(0, |highest| highest + 1);
220        // Counting sort when the key space is dense enough to pay for the
221        // counts array -- the same trade `audit` makes. A mesh with few
222        // triangles over a huge index space would spend longer clearing
223        // counts than sorting, so that case keeps the comparison sort.
224        let dense = buckets <= records.len().saturating_mul(2).max(1024);
225        let fits = u32::try_from(records.len()).is_ok();
226        let mut scratch: Vec<(EdgeKey, EdgeUse)> = Vec::new();
227        let counted = dense && fits && scratch.try_reserve_exact(records.len()).is_ok();
228        if counted {
229            scratch.resize(
230                records.len(),
231                (
232                    EdgeKey::new(0, 0),
233                    EdgeUse {
234                        triangle: 0,
235                        forward: false,
236                    },
237                ),
238            );
239            counting_sort_records(&mut records, &mut scratch, buckets);
240        } else {
241            // Same order as the counting sort: by key, ties by triangle.
242            records.sort_unstable_by(|left, right| {
243                left.0
244                    .cmp(&right.0)
245                    .then_with(|| left.1.triangle.cmp(&right.1.triangle))
246            });
247        }
248
249        let mut keys: Vec<EdgeKey> = Vec::new();
250        let mut starts: Vec<u32> = Vec::new();
251        let mut uses: Vec<EdgeUse> = Vec::with_capacity(records.len());
252        for (key, use_) in records {
253            if keys.last() != Some(&key) {
254                keys.push(key);
255                // This edge's run begins where the previous one ended.
256                starts.push(uses.len() as u32);
257            }
258            uses.push(use_);
259        }
260        // Sentinel closing the last run, so `starts[i]..starts[i + 1]` is valid
261        // for every edge and `starts.len() == keys.len() + 1` even when empty.
262        starts.push(uses.len() as u32);
263
264        Self {
265            keys,
266            starts,
267            uses,
268            vertex_count: mesh.positions.len(),
269            degenerate_triangles,
270            face_count,
271        }
272    }
273
274    /// Distinct undirected edges.
275    #[must_use]
276    pub fn edge_count(&self) -> usize {
277        self.keys.len()
278    }
279
280    /// Triangles skipped for having a repeated corner.
281    #[must_use]
282    pub const fn degenerate_triangles(&self) -> usize {
283        self.degenerate_triangles
284    }
285
286    /// Uses of the edge at `index`, which must be in range.
287    fn span(&self, index: usize) -> &[EdgeUse] {
288        let from = self.starts[index] as usize;
289        let to = self.starts[index + 1] as usize;
290        &self.uses[from..to]
291    }
292
293    /// Every edge with its uses, ordered by [`EdgeKey`].
294    pub fn edges(&self) -> impl Iterator<Item = (EdgeKey, &[EdgeUse])> {
295        self.keys
296            .iter()
297            .enumerate()
298            .map(|(index, key)| (*key, self.span(index)))
299    }
300
301    /// Triangles incident to one edge, or an empty slice if it is absent.
302    #[must_use]
303    pub fn uses(&self, edge: EdgeKey) -> &[EdgeUse] {
304        // Keys are ascending, so this is the CSR equivalent of the map
305        // lookup it replaces.
306        self.keys
307            .binary_search(&edge)
308            .map_or(&[], |index| self.span(index))
309    }
310
311    /// Edges used by exactly one triangle: the mesh boundary.
312    ///
313    /// In a closed shell this is empty, which is what makes it a usable
314    /// definition of "the hole" after a clip or a cut.
315    pub fn boundary_edges(&self) -> impl Iterator<Item = EdgeKey> + '_ {
316        self.edges()
317            .filter(|(_, uses)| uses.len() == 1)
318            .map(|(key, _)| key)
319    }
320
321    /// Edges used by three or more triangles.
322    pub fn non_manifold_edges(&self) -> impl Iterator<Item = EdgeKey> + '_ {
323        self.edges()
324            .filter(|(_, uses)| uses.len() > 2)
325            .map(|(key, _)| key)
326    }
327
328    /// Edges whose two triangles traverse them the same way.
329    ///
330    /// Consistently wound neighbours traverse a shared edge in opposite
331    /// directions, so agreement means one of the pair is flipped. Reported
332    /// only for two-triangle edges: with three or more the pairing is
333    /// ambiguous, and that is already a non-manifold defect.
334    pub fn inconsistent_edges(&self) -> impl Iterator<Item = EdgeKey> + '_ {
335        self.edges()
336            .filter(|(_, uses)| uses.len() == 2 && uses[0].forward == uses[1].forward)
337            .map(|(key, _)| key)
338    }
339
340    /// Whether every edge has exactly two consistently wound triangles.
341    #[must_use]
342    pub fn is_closed_two_manifold(&self) -> bool {
343        self.edges().all(|(_, uses)| uses.len() == 2) && self.inconsistent_edges().next().is_none()
344    }
345
346    /// Vertices touched by a boundary edge.
347    #[must_use]
348    pub fn boundary_vertices(&self) -> Vec<u32> {
349        let mut seen = vec![false; self.vertex_count];
350        let mut out = Vec::new();
351        for key in self.boundary_edges() {
352            for corner in [key.lower(), key.upper()] {
353                if let Some(slot) = seen.get_mut(corner as usize) {
354                    if !*slot {
355                        *slot = true;
356                        out.push(corner);
357                    }
358                }
359            }
360        }
361        out.sort_unstable();
362        out
363    }
364
365    /// Vertex-to-vertex neighbours, indexed by vertex.
366    ///
367    /// Entry `v` lists the vertices sharing an edge with `v`, ascending.
368    /// Vertices used by no usable triangle get an empty list rather than
369    /// being omitted, so the result can be indexed directly.
370    #[must_use]
371    pub fn vertex_neighbours(&self) -> Vec<Vec<u32>> {
372        let mut out = vec![Vec::new(); self.vertex_count];
373        for key in &self.keys {
374            let (a, b) = key.endpoints();
375            if let Some(list) = out.get_mut(a as usize) {
376                list.push(b);
377            }
378            if let Some(list) = out.get_mut(b as usize) {
379                list.push(a);
380            }
381        }
382        for list in &mut out {
383            list.sort_unstable();
384            list.dedup();
385        }
386        out
387    }
388
389    /// Triangles sharing an edge with `triangle`, ascending.
390    #[must_use]
391    pub fn triangle_neighbours(&self, triangle: usize) -> Vec<usize> {
392        let mut out = Vec::new();
393        for (_, uses) in self.edges() {
394            if uses.iter().any(|use_| use_.triangle == triangle) {
395                out.extend(
396                    uses.iter()
397                        .map(|use_| use_.triangle)
398                        .filter(|other| *other != triangle),
399                );
400            }
401        }
402        out.sort_unstable();
403        out.dedup();
404        out
405    }
406
407    /// Euler characteristic `V - E + F` over usable triangles.
408    ///
409    /// Vertices are counted as those actually used by an edge, not the
410    /// length of the position array: an unreferenced position is not part of
411    /// the surface, and counting it shifts the characteristic silently.
412    #[must_use]
413    pub fn euler_characteristic(&self) -> i64 {
414        let mut used = vec![false; self.vertex_count];
415        for key in &self.keys {
416            for corner in [key.lower(), key.upper()] {
417                if let Some(slot) = used.get_mut(corner as usize) {
418                    *slot = true;
419                }
420            }
421        }
422        let vertices = used.iter().filter(|seen| **seen).count() as i64;
423        let edges = self.keys.len() as i64;
424        let faces = self.face_count() as i64;
425        vertices - edges + faces
426    }
427
428    /// Usable triangles, i.e. those that contributed adjacency.
429    #[must_use]
430    pub const fn face_count(&self) -> usize {
431        self.face_count
432    }
433}