Skip to main content

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