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}