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