Skip to main content

axiolid_inspect/
topology.rs

1//! Topology of a triangle mesh, per connected component (#144).
2//!
3//! [`genus`](fn@crate::genus) answers one number for one closed orientable
4//! surface and refuses everything else. This reports every component of a
5//! two-manifold mesh, with or without boundary and whether or not it is
6//! orientable: its counts, Euler characteristic, boundary loops,
7//! orientability and the surface it is (genus `g` orientable, or `k`
8//! crosscaps). Every component also gets a basis of its first homology
9//! with `Z2` (GF(2)) coefficients: closed edge loops by the tree-cotree
10//! construction (Eppstein, "Dynamic generators of topologically embedded
11//! graphs", 2003), generalised past its closed-orientable origin to a
12//! closed non-orientable component (the construction never reads
13//! orientability, only that every interior edge is shared by exactly two
14//! triangles) and to a component with `b >= 1` boundary loops (`b - 1` of
15//! them close out the rest; all `b` sum to zero over `Z2`, being the
16//! boundary of the component's triangles taken all at once).
17//!
18//! Everything is combinatorial: positions are never read, so the answers
19//! are exact. A mesh that is not a two-manifold -- an edge on three or more
20//! triangles, or a vertex whose triangles form more than one fan -- is
21//! refused, since the classification of surfaces does not describe it.
22//!
23//! Not provided: homotopy questions (whether a given closed path is
24//! contractible, or two paths homotopic).
25
26use std::collections::{BTreeMap, BTreeSet};
27
28use axiolid_mesh::TriMesh;
29use thiserror::Error;
30
31/// Why a mesh's topology could not be classified.
32#[derive(Debug, Clone, PartialEq, Eq, Error)]
33#[non_exhaustive]
34pub enum TopologyError {
35    /// A triangle names a vertex the mesh does not have.
36    #[error("triangle {triangle} names vertex {vertex}, but the mesh has {vertices}")]
37    IndexOutOfRange {
38        /// The offending triangle.
39        triangle: usize,
40        /// The index it names.
41        vertex: u32,
42        /// How many positions the mesh has.
43        vertices: usize,
44    },
45    /// A triangle repeats a vertex, so it has no edges of its own.
46    #[error("triangle {triangle} repeats a vertex")]
47    DegenerateTriangle {
48        /// The offending triangle.
49        triangle: usize,
50    },
51    /// The mesh is not a two-manifold.
52    #[error(
53        "the mesh is not a two-manifold: {edges} edges on three or more \
54         triangles, {vertices} vertices whose triangles form several fans"
55    )]
56    NonManifold {
57        /// Edges used by three or more triangles.
58        edges: usize,
59        /// Vertices whose incident triangles form more than one fan.
60        vertices: usize,
61    },
62}
63
64/// Which closed surface a component is, with its boundary loops filled in.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66#[non_exhaustive]
67pub enum SurfaceKind {
68    /// An orientable surface of genus `genus`: a sphere is 0, a torus 1.
69    Orientable {
70        /// Number of handles.
71        genus: u32,
72    },
73    /// A non-orientable surface with `crosscaps` crosscaps: a projective
74    /// plane or Möbius strip is 1, a Klein bottle 2.
75    NonOrientable {
76        /// Number of crosscaps.
77        crosscaps: u32,
78    },
79}
80
81/// A closed loop of mesh edges, as its vertices in order: consecutive
82/// vertices, and the last and the first, are joined by an edge.
83pub type EdgeLoop = Vec<u32>;
84
85/// The topology of one connected component.
86#[derive(Debug, Clone, PartialEq, Eq)]
87#[non_exhaustive]
88pub struct ComponentTopology {
89    /// The component's triangles, as indices into the mesh's triangles.
90    pub triangles: Vec<usize>,
91    /// Vertices the component's triangles use.
92    pub vertices: usize,
93    /// Distinct edges of the component's triangles.
94    pub edges: usize,
95    /// `vertices - edges + triangles`.
96    pub euler_characteristic: i64,
97    /// Closed loops of boundary edges (edges on one triangle).
98    pub boundary_loops: usize,
99    /// Whether the triangles can be wound consistently.
100    pub orientable: bool,
101    /// Whether they are: every interior edge is used once in each
102    /// direction. Only an orientable component can be.
103    pub consistently_oriented: bool,
104    /// The surface, from `euler_characteristic = 2 - 2g - b` (orientable)
105    /// or `2 - k - b` (non-orientable).
106    pub surface: SurfaceKind,
107    /// A basis of the component's first homology with `Z2` coefficients:
108    /// `2g` closed edge loops for a closed orientable component of genus
109    /// `g`, `k` for a closed non-orientable one with `k` crosscaps, and
110    /// `2g + (b - 1)` or `k + (b - 1)` with `b >= 1` boundary loops.
111    /// Cutting along all of them leaves a disc (closed) or a disc with the
112    /// one boundary loop left out of the basis (with boundary). Always
113    /// `Some` for a classified component; kept optional for stability.
114    pub homology_basis: Option<Vec<EdgeLoop>>,
115}
116
117/// The topology of a two-manifold triangle mesh.
118#[derive(Debug, Clone, PartialEq, Eq)]
119#[non_exhaustive]
120pub struct MeshTopology {
121    /// Connected components (triangles joined through shared edges), in
122    /// order of their first triangle.
123    pub components: Vec<ComponentTopology>,
124}
125
126/// One use of an edge by a triangle.
127#[derive(Debug, Clone, Copy)]
128struct Use {
129    triangle: usize,
130    /// Whether the triangle runs the edge from its smaller vertex to its
131    /// larger one.
132    forward: bool,
133}
134
135/// Classify every connected component of `mesh`.
136///
137/// Positions not used by any triangle are ignored.
138///
139/// # Errors
140///
141/// A triangle naming a missing vertex or repeating one, and a mesh that is
142/// not a two-manifold ([`TopologyError::NonManifold`]).
143pub fn topology(mesh: &TriMesh) -> Result<MeshTopology, TopologyError> {
144    let triangles: Vec<[u32; 3]> = mesh
145        .indices
146        .as_chunks::<3>()
147        .0
148        .iter()
149        .map(|t| [t[0], t[1], t[2]])
150        .collect();
151    let vertex_count = mesh.positions.len();
152    for (triangle, t) in triangles.iter().enumerate() {
153        if let Some(&vertex) = t.iter().find(|&&v| v as usize >= vertex_count) {
154            return Err(TopologyError::IndexOutOfRange {
155                triangle,
156                vertex,
157                vertices: vertex_count,
158            });
159        }
160        if t[0] == t[1] || t[1] == t[2] || t[2] == t[0] {
161            return Err(TopologyError::DegenerateTriangle { triangle });
162        }
163    }
164
165    let mut edges: BTreeMap<(u32, u32), Vec<Use>> = BTreeMap::new();
166    for (triangle, t) in triangles.iter().enumerate() {
167        for k in 0..3 {
168            let (a, b) = (t[k], t[(k + 1) % 3]);
169            edges.entry((a.min(b), a.max(b))).or_default().push(Use {
170                triangle,
171                forward: a < b,
172            });
173        }
174    }
175    let non_manifold_edges = edges.values().filter(|uses| uses.len() > 2).count();
176    let non_manifold_vertices = count_split_vertices(&triangles, &edges, vertex_count);
177    if non_manifold_edges > 0 || non_manifold_vertices > 0 {
178        return Err(TopologyError::NonManifold {
179            edges: non_manifold_edges,
180            vertices: non_manifold_vertices,
181        });
182    }
183
184    // Components through shared edges, and a relative orientation of each
185    // triangle found on the way: `flip[t]` says whether `t` must be reversed
186    // to agree with the first triangle of its component.
187    let mut neighbours: Vec<Vec<(usize, bool)>> = vec![Vec::new(); triangles.len()];
188    for uses in edges.values() {
189        if let [a, b] = uses[..] {
190            // Consistent neighbours run their shared edge in opposite
191            // directions; the same direction means one must flip.
192            let flips = a.forward == b.forward;
193            neighbours[a.triangle].push((b.triangle, flips));
194            neighbours[b.triangle].push((a.triangle, flips));
195        }
196    }
197    let mut component_of = vec![usize::MAX; triangles.len()];
198    let mut flip = vec![false; triangles.len()];
199    let mut components = Vec::new();
200    for seed in 0..triangles.len() {
201        if component_of[seed] != usize::MAX {
202            continue;
203        }
204        let id = components.len();
205        component_of[seed] = id;
206        let mut members = vec![seed];
207        let mut orientable = true;
208        let mut consistent = true;
209        let mut next = 0;
210        while next < members.len() {
211            let t = members[next];
212            next += 1;
213            for &(n, flips) in &neighbours[t] {
214                consistent &= !flips;
215                let wanted = flip[t] ^ flips;
216                if component_of[n] == usize::MAX {
217                    component_of[n] = id;
218                    flip[n] = wanted;
219                    members.push(n);
220                } else if flip[n] != wanted {
221                    orientable = false;
222                }
223            }
224        }
225        members.sort_unstable();
226        components.push((members, orientable, consistent));
227    }
228
229    let mut out = Vec::with_capacity(components.len());
230    for (members, orientable, consistent) in components {
231        let component = component_of[members[0]];
232        let mut used: Vec<u32> = members.iter().flat_map(|&t| triangles[t]).collect();
233        used.sort_unstable();
234        used.dedup();
235        let own_edges: Vec<(&(u32, u32), &Vec<Use>)> = edges
236            .iter()
237            .filter(|(_, uses)| component_of[uses[0].triangle] == component)
238            .collect();
239        let boundary: Vec<(u32, u32)> = own_edges
240            .iter()
241            .filter(|(_, uses)| uses.len() == 1)
242            .map(|(&edge, _)| edge)
243            .collect();
244        let boundary_loops = count_loops(&boundary);
245        let characteristic = used.len() as i64 - own_edges.len() as i64 + members.len() as i64;
246        // chi = 2 - 2g - b or 2 - k - b; both deficits are non-negative on
247        // a connected two-manifold.
248        let deficit = 2 - characteristic - boundary_loops as i64;
249        let surface = if orientable {
250            SurfaceKind::Orientable {
251                genus: u32::try_from(deficit / 2).unwrap_or(0),
252            }
253        } else {
254            SurfaceKind::NonOrientable {
255                crosscaps: u32::try_from(deficit).unwrap_or(0),
256            }
257        };
258        let homology_basis = Some(homology_generators(&members, &own_edges, &boundary));
259        out.push(ComponentTopology {
260            triangles: members,
261            vertices: used.len(),
262            edges: own_edges.len(),
263            euler_characteristic: characteristic,
264            boundary_loops,
265            orientable,
266            consistently_oriented: consistent,
267            surface,
268            homology_basis,
269        });
270    }
271    Ok(MeshTopology { components: out })
272}
273
274/// Vertices whose incident triangles form more than one fan (joined
275/// through edges at the vertex), as at the tip where two cones touch.
276fn count_split_vertices(
277    triangles: &[[u32; 3]],
278    edges: &BTreeMap<(u32, u32), Vec<Use>>,
279    vertex_count: usize,
280) -> usize {
281    let mut incident: Vec<Vec<usize>> = vec![Vec::new(); vertex_count];
282    for (t, tri) in triangles.iter().enumerate() {
283        for &v in tri {
284            incident[v as usize].push(t);
285        }
286    }
287    let mut split = 0;
288    for (v, around) in incident.iter().enumerate() {
289        if around.len() < 2 {
290            continue;
291        }
292        let v = v as u32;
293        let slot = |t: usize| around.iter().position(|&u| u == t);
294        let mut parent: Vec<usize> = (0..around.len()).collect();
295        for (i, &t) in around.iter().enumerate() {
296            for &w in &triangles[t] {
297                if w == v {
298                    continue;
299                }
300                for other in &edges[&(v.min(w), v.max(w))] {
301                    if let Some(j) = slot(other.triangle) {
302                        union(&mut parent, i, j);
303                    }
304                }
305            }
306        }
307        let fans = (0..around.len())
308            .filter(|&i| find(&mut parent, i) == i)
309            .count();
310        if fans > 1 {
311            split += 1;
312        }
313    }
314    split
315}
316
317fn find(parent: &mut [usize], mut i: usize) -> usize {
318    while parent[i] != i {
319        parent[i] = parent[parent[i]];
320        i = parent[i];
321    }
322    i
323}
324
325fn union(parent: &mut [usize], a: usize, b: usize) {
326    let (a, b) = (find(parent, a), find(parent, b));
327    if a != b {
328        parent[a.max(b)] = a.min(b);
329    }
330}
331
332/// Boundary loops: on a two-manifold every boundary vertex has exactly two
333/// boundary edges, so the loops are the components of the boundary edges.
334fn count_loops(boundary: &[(u32, u32)]) -> usize {
335    let mut index: BTreeMap<u32, usize> = BTreeMap::new();
336    for &(a, b) in boundary {
337        let n = index.len();
338        index.entry(a).or_insert(n);
339        let n = index.len();
340        index.entry(b).or_insert(n);
341    }
342    let mut parent: Vec<usize> = (0..index.len()).collect();
343    for &(a, b) in boundary {
344        union(&mut parent, index[&a], index[&b]);
345    }
346    (0..parent.len())
347        .filter(|&i| find(&mut parent, i) == i)
348        .count()
349}
350
351/// A homology basis of one connected two-manifold component by tree-cotree:
352/// a spanning tree `T` of its vertices, a spanning tree of its dual graph
353/// (triangles, plus one virtual node per boundary loop that its boundary
354/// edges attach to) across edges not in `T`, and one loop for each edge in
355/// neither -- closed through `T`. Boundary edges give the virtual nodes
356/// somewhere to attach, so every edge has a well-defined dual side; this
357/// makes the leftover count exactly `E - (V - 1) - (F + b - 1) = 2 - chi -
358/// b`, which is `2g` (orientable) or `k` (non-orientable) regardless of
359/// boundary. `b - 1` of the `b` boundary loops (any one dropped; all `b`
360/// together are the boundary of the component's triangles taken at once,
361/// so they sum to zero) complete the basis.
362fn homology_generators(
363    members: &[usize],
364    own_edges: &[(&(u32, u32), &Vec<Use>)],
365    boundary: &[(u32, u32)],
366) -> Vec<EdgeLoop> {
367    // Primal spanning tree, breadth first from the smallest vertex, so the
368    // loops come out short and the result is deterministic.
369    let mut adjacent: BTreeMap<u32, Vec<u32>> = BTreeMap::new();
370    for (&(a, b), _) in own_edges {
371        adjacent.entry(a).or_default().push(b);
372        adjacent.entry(b).or_default().push(a);
373    }
374    let root = *adjacent.keys().next().expect("a component has vertices");
375    let mut parent: BTreeMap<u32, u32> = BTreeMap::new();
376    let mut depth: BTreeMap<u32, usize> = BTreeMap::new();
377    parent.insert(root, root);
378    depth.insert(root, 0);
379    let mut queue = std::collections::VecDeque::from([root]);
380    while let Some(v) = queue.pop_front() {
381        for &w in &adjacent[&v] {
382            if let std::collections::btree_map::Entry::Vacant(slot) = parent.entry(w) {
383                slot.insert(v);
384                depth.insert(w, depth[&v] + 1);
385                queue.push_back(w);
386            }
387        }
388    }
389    // The root is its own parent, and no edge joins a vertex to itself.
390    let in_tree = |a: u32, b: u32| parent[&a] == b || parent[&b] == a;
391
392    // The boundary's own loops, and which loop each boundary edge belongs
393    // to, so a boundary edge's dual side is that loop's virtual node.
394    let loops = boundary_edge_loops(boundary);
395    let mut loop_of: BTreeMap<(u32, u32), usize> = BTreeMap::new();
396    for (index, l) in loops.iter().enumerate() {
397        for k in 0..l.len() {
398            let (a, b) = (l[k], l[(k + 1) % l.len()]);
399            loop_of.insert((a.min(b), a.max(b)), index);
400        }
401    }
402
403    // Dual spanning tree over triangles and boundary-loop nodes, across
404    // edges left out of T: union-find on triangles (0..members.len()) and,
405    // after them, one node per boundary loop.
406    let slot: BTreeMap<usize, usize> = members.iter().enumerate().map(|(i, &t)| (t, i)).collect();
407    let mut dual: Vec<usize> = (0..members.len() + loops.len()).collect();
408    let mut leftover = Vec::new();
409    for (&(a, b), uses) in own_edges {
410        if in_tree(a, b) {
411            continue;
412        }
413        let (s, t) = if let [x, y] = uses[..] {
414            (slot[&x.triangle], slot[&y.triangle])
415        } else {
416            let node = members.len() + loop_of[&(a, b)];
417            (slot[&uses[0].triangle], node)
418        };
419        if find(&mut dual, s) == find(&mut dual, t) {
420            leftover.push((a, b));
421        } else {
422            union(&mut dual, s, t);
423        }
424    }
425    let mut basis: Vec<EdgeLoop> = leftover
426        .into_iter()
427        .map(|(a, b)| close_through_tree(a, b, &parent, &depth))
428        .collect();
429    let keep = loops.len().saturating_sub(1);
430    basis.extend(loops.into_iter().take(keep));
431    basis
432}
433
434/// The closed edge loop `a - b - ... - a` formed by the edge `(a, b)`
435/// closed through the spanning tree `parent`/`depth`: walk both ends up to
436/// their lowest common ancestor.
437fn close_through_tree(
438    a: u32,
439    b: u32,
440    parent: &BTreeMap<u32, u32>,
441    depth: &BTreeMap<u32, usize>,
442) -> EdgeLoop {
443    let (mut up_a, mut up_b) = (vec![a], vec![b]);
444    let (mut x, mut y) = (a, b);
445    while depth[&x] > depth[&y] {
446        x = parent[&x];
447        up_a.push(x);
448    }
449    while depth[&y] > depth[&x] {
450        y = parent[&y];
451        up_b.push(y);
452    }
453    while x != y {
454        x = parent[&x];
455        y = parent[&y];
456        up_a.push(x);
457        up_b.push(y);
458    }
459    // a .. lca, then back down to b; the loop closes over (b, a).
460    up_b.pop();
461    up_a.extend(up_b.into_iter().rev());
462    up_a
463}
464
465/// The boundary's closed loops, as ordered vertex cycles: on a
466/// two-manifold every boundary vertex has exactly two boundary edges, so
467/// each connected boundary component traces out to one simple cycle.
468fn boundary_edge_loops(boundary: &[(u32, u32)]) -> Vec<EdgeLoop> {
469    let mut adjacent: BTreeMap<u32, Vec<u32>> = BTreeMap::new();
470    for &(a, b) in boundary {
471        adjacent.entry(a).or_default().push(b);
472        adjacent.entry(b).or_default().push(a);
473    }
474    let mut used: BTreeSet<(u32, u32)> = BTreeSet::new();
475    let mut loops = Vec::new();
476    for start in adjacent.keys().copied().collect::<Vec<_>>() {
477        while let Some(&first) = adjacent[&start]
478            .iter()
479            .find(|&&n| !used.contains(&(start.min(n), start.max(n))))
480        {
481            let mut vertices = vec![start];
482            let (mut cur, mut next) = (start, first);
483            loop {
484                used.insert((cur.min(next), cur.max(next)));
485                if next == start {
486                    break;
487                }
488                vertices.push(next);
489                let after = adjacent[&next]
490                    .iter()
491                    .copied()
492                    .find(|&n| !used.contains(&(next.min(n), next.max(n))))
493                    .expect("a boundary loop closes on a two-manifold");
494                cur = next;
495                next = after;
496            }
497            loops.push(vertices);
498        }
499    }
500    loops
501}