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}