Skip to main content

fast_mesh_ops/
topology.rs

1//! Edge maps, connected components and boundary loops.
2//!
3//! Everything here is built from the triangles alone, never from the store's
4//! cached adjacency, which a transaction does not keep current. Triangles
5//! with a repeated vertex id are skipped throughout.
6
7use fast_mesh_store::{Mesh, TriangleId, VertexId};
8
9use crate::{OpsError, Result, filled, push, reserve};
10
11/// How many triangles share an edge. Re-exported from `fast_mesh_store`,
12/// whose corner table uses the same three kinds with the same meaning, so
13/// that callers of both crates see one type.
14pub use fast_mesh_store::EdgeKind;
15
16const NONE: u32 = u32::MAX;
17
18/// The undirected key of the edge `a`-`b`: `(min, max)`.
19fn key(a: VertexId, b: VertexId) -> (VertexId, VertexId) {
20    if a < b { (a, b) } else { (b, a) }
21}
22
23/// The vertices of triangle `t` when it is alive and has three distinct ones.
24pub(crate) fn proper_triangle(mesh: &Mesh, t: TriangleId) -> Option<[VertexId; 3]> {
25    let [a, b, c] = mesh.triangle(t)?;
26    (a != b && b != c && a != c).then_some([a, b, c])
27}
28
29/// The local position of `v` in `f64`, origin excluded.
30pub(crate) fn point(mesh: &Mesh, v: VertexId) -> [f64; 3] {
31    mesh.positions()[v as usize].map(f64::from)
32}
33
34pub(crate) fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
35    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
36}
37
38pub(crate) fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
39    [
40        a[1] * b[2] - a[2] * b[1],
41        a[2] * b[0] - a[0] * b[2],
42        a[0] * b[1] - a[1] * b[0],
43    ]
44}
45
46pub(crate) fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
47    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
48}
49
50fn norm(a: [f64; 3]) -> f64 {
51    dot(a, a).sqrt()
52}
53
54/// The area of triangle `[a, b, c]`.
55pub(crate) fn triangle_area(mesh: &Mesh, [a, b, c]: [VertexId; 3]) -> f64 {
56    let p = point(mesh, a);
57    0.5 * norm(cross(sub(point(mesh, b), p), sub(point(mesh, c), p)))
58}
59
60/// Every undirected edge of the proper triangles, sorted by key, with the
61/// triangles using it in ascending id order.
62pub(crate) struct EdgeMap {
63    keys: Vec<(VertexId, VertexId)>,
64    /// Edge `i` is used by `tris[starts[i]..starts[i + 1]]`.
65    starts: Vec<u32>,
66    tris: Vec<TriangleId>,
67}
68
69impl EdgeMap {
70    /// Builds the map from the alive triangles, skipping those with a
71    /// repeated vertex id.
72    pub(crate) fn build(mesh: &Mesh) -> Result<EdgeMap> {
73        let mut pairs: Vec<((VertexId, VertexId), TriangleId)> = Vec::new();
74        reserve(&mut pairs, 3 * mesh.triangle_count())?;
75        for t in 0..mesh.triangle_slots() as TriangleId {
76            if let Some([a, b, c]) = proper_triangle(mesh, t) {
77                for (u, v) in [(a, b), (b, c), (c, a)] {
78                    push(&mut pairs, (key(u, v), t))?;
79                }
80            }
81        }
82        if pairs.len() >= NONE as usize {
83            return Err(OpsError::InvalidInput("too many triangles for an edge map"));
84        }
85        pairs.sort_unstable();
86        let distinct = 1 + pairs.windows(2).filter(|w| w[0].0 != w[1].0).count();
87        let distinct = if pairs.is_empty() { 0 } else { distinct };
88        let mut map = EdgeMap {
89            keys: Vec::new(),
90            starts: Vec::new(),
91            tris: Vec::new(),
92        };
93        reserve(&mut map.keys, distinct)?;
94        reserve(&mut map.starts, distinct + 1)?;
95        reserve(&mut map.tris, pairs.len())?;
96        for (i, &(k, t)) in pairs.iter().enumerate() {
97            if map.keys.last() != Some(&k) {
98                map.keys.push(k);
99                map.starts.push(i as u32);
100            }
101            map.tris.push(t);
102        }
103        map.starts.push(pairs.len() as u32);
104        Ok(map)
105    }
106
107    /// The index of edge `a`-`b`, in either direction.
108    fn index(&self, a: VertexId, b: VertexId) -> Option<usize> {
109        self.keys.binary_search(&key(a, b)).ok()
110    }
111
112    fn at(&self, i: usize) -> &[TriangleId] {
113        &self.tris[self.starts[i] as usize..self.starts[i + 1] as usize]
114    }
115
116    /// The number of edges.
117    pub(crate) fn len(&self) -> usize {
118        self.keys.len()
119    }
120
121    /// The kind of edge `a`-`b`, or `None` when no proper triangle uses it.
122    pub(crate) fn kind(&self, a: VertexId, b: VertexId) -> Option<EdgeKind> {
123        self.index(a, b).map(|i| kind_of(self.at(i).len()))
124    }
125
126    /// The triangles using edge `a`-`b` in ascending id order; empty when
127    /// there are none.
128    pub(crate) fn triangles(&self, a: VertexId, b: VertexId) -> &[TriangleId] {
129        self.index(a, b).map_or(&[], |i| self.at(i))
130    }
131
132    /// Every edge in key order, with its triangles.
133    pub(crate) fn edges(&self) -> impl Iterator<Item = ((VertexId, VertexId), &[TriangleId])> {
134        (0..self.keys.len()).map(|i| (self.keys[i], self.at(i)))
135    }
136}
137
138fn kind_of(triangles: usize) -> EdgeKind {
139    match triangles {
140        1 => EdgeKind::Boundary,
141        2 => EdgeKind::Interior,
142        _ => EdgeKind::NonManifold,
143    }
144}
145
146/// For every vertex, the proper triangles using it, in ascending id order.
147pub(crate) struct VertexFans {
148    /// Vertex `v`'s fan is `tris[starts[v]..starts[v + 1]]`.
149    starts: Vec<u32>,
150    tris: Vec<TriangleId>,
151}
152
153impl VertexFans {
154    pub(crate) fn build(mesh: &Mesh) -> Result<VertexFans> {
155        let slots = mesh.vertex_slots();
156        let mut starts = filled(slots + 1, 0u32)?;
157        let mut corners = 0usize;
158        for t in 0..mesh.triangle_slots() as TriangleId {
159            if let Some(tri) = proper_triangle(mesh, t) {
160                for v in tri {
161                    starts[v as usize + 1] += 1;
162                }
163                corners += 3;
164            }
165        }
166        if corners >= NONE as usize {
167            return Err(OpsError::InvalidInput("too many triangles for vertex fans"));
168        }
169        // Prefix sums give each fan's end; filling backwards from the end with
170        // descending triangle ids leaves every fan ascending and `starts` at
171        // the fans' beginnings.
172        for v in 0..slots {
173            starts[v + 1] += starts[v];
174        }
175        let mut tris = filled(corners, 0)?;
176        for t in (0..mesh.triangle_slots() as TriangleId).rev() {
177            if let Some(tri) = proper_triangle(mesh, t) {
178                for v in tri {
179                    let end = &mut starts[v as usize + 1];
180                    *end -= 1;
181                    tris[*end as usize] = t;
182                }
183            }
184        }
185        // Each fan's end moved back to its start: shift the starts down one slot.
186        starts.copy_within(1.., 0);
187        starts[slots] = corners as u32;
188        Ok(VertexFans { starts, tris })
189    }
190
191    /// The proper triangles using `v`, ascending.
192    pub(crate) fn fan(&self, v: VertexId) -> &[TriangleId] {
193        &self.tris[self.starts[v as usize] as usize..self.starts[v as usize + 1] as usize]
194    }
195}
196
197/// A union-find over `0..n` whose roots are always the smallest member.
198pub(crate) struct UnionFind {
199    parent: Vec<u32>,
200}
201
202impl UnionFind {
203    pub(crate) fn new() -> Self {
204        UnionFind { parent: Vec::new() }
205    }
206
207    /// Resets to `n` singletons, reusing the storage.
208    pub(crate) fn reset(&mut self, n: usize) -> Result<()> {
209        self.parent.clear();
210        reserve(&mut self.parent, n)?;
211        self.parent.extend(0..n as u32);
212        Ok(())
213    }
214
215    pub(crate) fn find(&mut self, mut x: u32) -> u32 {
216        while self.parent[x as usize] != x {
217            let up = self.parent[self.parent[x as usize] as usize];
218            self.parent[x as usize] = up;
219            x = up;
220        }
221        x
222    }
223
224    /// Joins the sets of `a` and `b`; `true` when they were separate.
225    pub(crate) fn union(&mut self, a: u32, b: u32) -> bool {
226        let (ra, rb) = (self.find(a), self.find(b));
227        if ra == rb {
228            return false;
229        }
230        let (lo, hi) = if ra < rb { (ra, rb) } else { (rb, ra) };
231        self.parent[hi as usize] = lo;
232        true
233    }
234}
235
236/// A set of triangles connected through shared edges of any kind.
237#[derive(Debug, Clone, PartialEq)]
238pub struct Component {
239    /// The triangles, ascending.
240    pub triangles: Vec<TriangleId>,
241    /// The total triangle area, in local units squared.
242    pub area: f64,
243    /// `true` when the component has no boundary and no non-manifold edge.
244    pub closed: bool,
245}
246
247/// The connected components of the proper triangles, sorted by their
248/// smallest triangle id.
249pub(crate) fn components(mesh: &Mesh, edges: &EdgeMap) -> Result<Vec<Component>> {
250    let slots = mesh.triangle_slots();
251    let mut uf = UnionFind::new();
252    uf.reset(slots)?;
253    for (_, tris) in edges.edges() {
254        for &t in &tris[1..] {
255            uf.union(tris[0], t);
256        }
257    }
258    // The roots are the smallest triangle ids, so visiting triangles in
259    // ascending order numbers the components in the required order.
260    let mut index = filled(slots, NONE)?;
261    let mut out: Vec<Component> = Vec::new();
262    for t in 0..slots as TriangleId {
263        let Some(tri) = proper_triangle(mesh, t) else {
264            continue;
265        };
266        let root = uf.find(t) as usize;
267        if index[root] == NONE {
268            index[root] = out.len() as u32;
269            push(
270                &mut out,
271                Component {
272                    triangles: Vec::new(),
273                    area: 0.0,
274                    closed: true,
275                },
276            )?;
277        }
278        let c = &mut out[index[root] as usize];
279        push(&mut c.triangles, t)?;
280        c.area += triangle_area(mesh, tri);
281    }
282    for (_, tris) in edges.edges() {
283        if tris.len() != 2 {
284            let root = uf.find(tris[0]) as usize;
285            out[index[root] as usize].closed = false;
286        }
287    }
288    Ok(out)
289}
290
291/// The vertices whose fan of proper triangles splits into more than one
292/// piece, ascending. Triangles around a vertex `v` are in one piece when they
293/// are linked through edges at `v`, of any kind; a bowtie corner is a pinch,
294/// a fin's spine is not.
295pub(crate) fn pinch_vertices(mesh: &Mesh, edges: &EdgeMap) -> Result<Vec<VertexId>> {
296    let fans = VertexFans::build(mesh)?;
297    let mut uf = UnionFind::new();
298    let mut out = Vec::new();
299    for v in 0..mesh.vertex_slots() as VertexId {
300        let fan = fans.fan(v);
301        if fan.len() < 2 {
302            continue;
303        }
304        uf.reset(fan.len())?;
305        let mut pieces = fan.len();
306        for (i, &t) in fan.iter().enumerate() {
307            for x in mesh.triangles()[t as usize] {
308                if x == v {
309                    continue;
310                }
311                for &u in edges.triangles(v, x) {
312                    if u > t {
313                        let j = fan.binary_search(&u).expect("edge triangle in fan");
314                        if uf.union(i as u32, j as u32) {
315                            pieces -= 1;
316                        }
317                    }
318                }
319            }
320        }
321        if pieces > 1 {
322            push(&mut out, v)?;
323        }
324    }
325    Ok(out)
326}
327
328/// A content fingerprint of a boundary loop, valid across edits and
329/// transactions for as long as the loop's vertex sequence is unchanged.
330#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
331pub struct LoopId {
332    /// The smallest vertex id on the loop.
333    pub min_vertex: VertexId,
334    /// The number of vertices on the loop.
335    pub len: u32,
336    /// FNV-1a 64 over the little-endian bytes of the vertex ids in
337    /// [`BoundaryLoop::vertices`] order: for a closed loop, starting at
338    /// `min_vertex`; for an open chain, from its first end.
339    pub hash: u64,
340}
341
342/// A chain of boundary edges, closed unless the walk could not close it.
343#[derive(Debug, Clone, PartialEq)]
344pub struct BoundaryLoop {
345    /// The loop's content fingerprint, which callers pass back to pick this
346    /// loop (for example to fill it). It matches as long as the loop's vertex
347    /// sequence is unchanged, across edits and transactions.
348    pub id: LoopId,
349    /// The vertices in loop order. The loop runs along each boundary edge in
350    /// the direction its one triangle uses it, so a hole in an outward-facing
351    /// surface runs clockwise seen from outside. A closed loop starts at
352    /// `id.min_vertex` and its polygon closes from the last vertex back to the
353    /// first. An open chain runs from one end to the other, unrotated.
354    pub vertices: Vec<VertexId>,
355    /// The length of the polygon closed from the last vertex back to the
356    /// first, in local units (for an open chain, including that closing step).
357    pub perimeter: f64,
358    /// The vector area `|sum p_i x p_(i+1)| / 2` of the same closed polygon.
359    pub area: f64,
360    /// `true` when the walk came back to its first edge. `false` for an open
361    /// chain, which ends at a vertex where no boundary edge continues it (for
362    /// example at a non-manifold edge). An open chain is never clean, so it is
363    /// never filled.
364    pub closed: bool,
365    /// `false` when the edges at some vertex of the loop could not be ordered
366    /// by the fan walk: the vertex touches a non-manifold edge, or it is a
367    /// pinch whose fan pieces are wound against each other, so the walk fell
368    /// back to pairing by edge key. The loop may then be open or join openings
369    /// that are not one hole. Also `false` when the loop crosses an
370    /// orientation seam: it walks some boundary edge against its triangle's
371    /// direction, or it passes a vertex where outgoing and incoming boundary
372    /// edges do not balance (a reversed triangle on the rim), so it may run
373    /// backwards around its hole. This fails safe: unclean loops are skipped
374    /// by `fill_holes` unless allowed, and orienting first makes a
375    /// wound-against pinch or a flipped rim triangle clean.
376    pub clean: bool,
377}
378
379/// FNV-1a 64 over the little-endian bytes of `ids`.
380pub(crate) fn fnv1a(ids: &[VertexId]) -> u64 {
381    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
382    for id in ids {
383        for byte in id.to_le_bytes() {
384            h ^= u64::from(byte);
385            h = h.wrapping_mul(0x0000_0100_0000_01b3);
386        }
387    }
388    h
389}
390
391/// The boundary loops of `mesh`, sorted by smallest vertex, then by vertex
392/// sequence (for a closed loop, the vertex after the smallest).
393///
394/// Each loop follows the boundary edges (edges of exactly one proper
395/// triangle). At each vertex `b`, entered along `a -> b`, the walk rotates
396/// through `b`'s triangle fan from the incoming edge's triangle across
397/// interior edges to the far side of that fan piece. At an ordinary vertex
398/// that far side is the next edge. At a pinch vertex, where the fan splits
399/// into pieces, the far side borders the other opening, so the next edge is
400/// taken across the gap from another piece instead (by turning angle when
401/// there are several): two holes sharing one vertex are two loops. Where the
402/// fan reaches a non-manifold edge, the outgoing boundary edges at that
403/// vertex are paired in key order instead, and loops through a vertex on a
404/// non-manifold edge are marked unclean. So are loops that cross an
405/// orientation seam (see [`BoundaryLoop::clean`]).
406///
407/// Walks first start at chain heads, vertices with more outgoing than
408/// incoming boundary half-edges, so that an open chain is walked whole from
409/// its first end; the closed loops are walked after.
410pub fn boundary_loops(mesh: &Mesh) -> Result<Vec<BoundaryLoop>> {
411    let edges = EdgeMap::build(mesh)?;
412    loops_with(mesh, &edges)
413}
414
415/// A boundary edge traversed `from -> to`; `tri` is its one triangle and
416/// `edge` its index in the edge map.
417#[derive(Clone, Copy)]
418struct Step {
419    from: VertexId,
420    to: VertexId,
421    tri: TriangleId,
422    edge: usize,
423}
424
425/// [`boundary_loops`] with a prebuilt edge map.
426pub(crate) fn loops_with(mesh: &Mesh, edges: &EdgeMap) -> Result<Vec<BoundaryLoop>> {
427    let mut non_manifold = filled(mesh.vertex_slots(), false)?;
428    // Boundary half-edges, in the direction their triangle uses them, sorted
429    // by `(from, to)`.
430    let mut half: Vec<Step> = Vec::new();
431    for (i, ((a, b), tris)) in edges.edges().enumerate() {
432        match tris.len() {
433            1 => {
434                let t = tris[0];
435                let [x, y, z] = mesh.triangles()[t as usize];
436                let forward = (x, y) == (a, b) || (y, z) == (a, b) || (z, x) == (a, b);
437                let (from, to) = if forward { (a, b) } else { (b, a) };
438                push(
439                    &mut half,
440                    Step {
441                        from,
442                        to,
443                        tri: t,
444                        edge: i,
445                    },
446                )?;
447            }
448            2 => {}
449            _ => {
450                non_manifold[a as usize] = true;
451                non_manifold[b as usize] = true;
452            }
453        }
454    }
455    half.sort_unstable_by_key(|s| (s.from, s.to));
456    // Outgoing minus incoming boundary half-edges per vertex: chain heads are
457    // the vertices where it is positive.
458    let mut excess = filled(mesh.vertex_slots(), 0i64)?;
459    for s in &half {
460        excess[s.from as usize] += 1;
461        excess[s.to as usize] -= 1;
462    }
463    let mut visited = filled(edges.len(), false)?;
464    let mut loops: Vec<BoundaryLoop> = Vec::new();
465    let mut verts: Vec<VertexId> = Vec::new();
466    let order = (0..2).flat_map(|pass| (0..half.len()).map(move |h| (pass, h)));
467    for (pass, h) in order {
468        let start = half[h];
469        if visited[start.edge] || (pass == 0 && excess[start.from as usize] <= 0) {
470            continue;
471        }
472        let mut closed = true;
473        verts.clear();
474        let mut clean = true;
475        let mut cur = start;
476        loop {
477            visited[cur.edge] = true;
478            push(&mut verts, cur.from)?;
479            // An edge walked against its triangle's direction lies past an
480            // orientation seam: the loop may run backwards around its hole.
481            if !runs_forward(mesh, cur) {
482                clean = false;
483            }
484            let b = cur.to;
485            let lo = half.partition_point(|s| s.from < b);
486            let hi = lo + half[lo..].iter().take_while(|s| s.from == b).count();
487            let leaving = &half[lo..hi];
488            // The fan walk crosses the incoming edge's fan piece to its far
489            // side. At a pinch the next edge of the same hole lies across the
490            // gap, in another piece; otherwise it is the far side itself.
491            let walked = fan_walk(mesh, edges, cur.tri, cur.from, b).and_then(|(c, t)| {
492                let e = edges.index(b, c).expect("walked edge in map");
493                let s = across_gap(mesh, leaving, e, cur).unwrap_or(Step {
494                    from: b,
495                    to: c,
496                    tri: t,
497                    edge: e,
498                });
499                (s.edge == start.edge || !visited[s.edge]).then_some(s)
500            });
501            let next = match walked {
502                Some(s) => Some(s),
503                None => {
504                    // Pair by key order: the first outgoing boundary half-edge at
505                    // `b` that is unvisited or closes this loop.
506                    clean = false;
507                    leaving
508                        .iter()
509                        .find(|s| s.edge == start.edge || !visited[s.edge])
510                        .copied()
511                }
512            };
513            match next {
514                Some(s) if s.edge == start.edge => break,
515                Some(s) => cur = s,
516                None => {
517                    // Nothing leaves `b`: an open chain ending at `b`. Only the
518                    // key-order fallback can get here, so it is already unclean.
519                    push(&mut verts, b)?;
520                    closed = false;
521                    clean = false;
522                    break;
523                }
524            }
525        }
526        // A vertex with unbalanced boundary half-edges is where a reversed
527        // edge meets the loop; a closed loop through it crosses a seam.
528        if verts
529            .iter()
530            .any(|&v| non_manifold[v as usize] || (closed && excess[v as usize] != 0))
531        {
532            clean = false;
533        }
534        push(&mut loops, make_loop(mesh, &verts, closed, clean)?)?;
535    }
536    loops.sort_unstable_by(|a, b| {
537        (a.id.min_vertex, &a.vertices).cmp(&(b.id.min_vertex, &b.vertices))
538    });
539    Ok(loops)
540}
541
542/// Whether `s.tri` uses its edge in the direction `s.from -> s.to`.
543fn runs_forward(mesh: &Mesh, s: Step) -> bool {
544    let [x, y, z] = mesh.triangles()[s.tri as usize];
545    [(x, y), (y, z), (z, x)].contains(&(s.from, s.to))
546}
547
548/// At a pinch vertex `b`, the boundary half-edge that continues the hole
549/// entered along `cur` (`a -> b`): one leaving `b` in another fan piece than
550/// `cur`'s, whose far side at `b` is the edge `walked`. With one such edge it
551/// is the answer. With several, it is the first met turning from `b -> a`
552/// counter-clockwise around `b`'s local normal (into the gap beside `cur`'s
553/// piece), ties to edge-key order. `None` when no other piece has an
554/// outgoing boundary edge, as at an ordinary vertex.
555fn across_gap(mesh: &Mesh, leaving: &[Step], walked: usize, cur: Step) -> Option<Step> {
556    // `cur` itself leaves `b` when the walk runs against its triangle's
557    // direction, past an orientation flip.
558    let other = |s: &&Step| s.edge != walked && s.edge != cur.edge;
559    let mut others = leaving.iter().filter(other);
560    let first = *others.next()?;
561    if others.clone().next().is_none() {
562        return Some(first);
563    }
564    let normal = |t: TriangleId| {
565        let [x, y, z] = mesh.triangles()[t as usize].map(|v| point(mesh, v));
566        cross(sub(y, x), sub(z, x))
567    };
568    let mut n = normal(cur.tri);
569    for s in leaving.iter().filter(other) {
570        let m = normal(s.tri);
571        n = [n[0] + m[0], n[1] + m[1], n[2] + m[2]];
572    }
573    let pb = point(mesh, cur.to);
574    let nn = dot(n, n);
575    let flat = |v: VertexId| {
576        let d = sub(point(mesh, v), pb);
577        let k = dot(n, d) / nn;
578        [d[0] - k * n[0], d[1] - k * n[1], d[2] - k * n[2]]
579    };
580    let d0 = flat(cur.from);
581    let turn = |s: &Step| {
582        let d = flat(s.to);
583        let a = dot(n, cross(d0, d)).atan2(dot(d0, d) * norm(n));
584        let a = if a <= 0.0 {
585            a + std::f64::consts::TAU
586        } else {
587            a
588        };
589        if a.is_finite() { a } else { f64::INFINITY }
590    };
591    let mut best = (turn(&first), first);
592    for s in others {
593        let a = turn(s);
594        if a < best.0 {
595            best = (a, *s);
596        }
597    }
598    Some(best.1)
599}
600
601/// Rotates around vertex `b` from triangle `t`, entered across its edge
602/// `a`-`b`, through interior edges until a boundary edge `b`-`c` is reached.
603/// Returns `c` and that edge's triangle, or `None` at a non-manifold edge or
604/// when the walk takes more steps than `b` has triangles.
605fn fan_walk(
606    mesh: &Mesh,
607    edges: &EdgeMap,
608    t: TriangleId,
609    a: VertexId,
610    b: VertexId,
611) -> Option<(VertexId, TriangleId)> {
612    let (mut tri, mut prev) = (t, a);
613    for _ in 0..=mesh.corner_count(b) {
614        let c = mesh.triangles()[tri as usize]
615            .into_iter()
616            .find(|&v| v != b && v != prev)?;
617        match edges.triangles(b, c) {
618            [_] => return Some((c, tri)),
619            &[x, y] => {
620                tri = if x == tri { y } else { x };
621                prev = c;
622            }
623            _ => return None,
624        }
625    }
626    None
627}
628
629/// The loop through `verts`: a closed one rotated to its canonical start, an
630/// open chain kept as walked so that its ends stay first and last.
631fn make_loop(mesh: &Mesh, verts: &[VertexId], closed: bool, clean: bool) -> Result<BoundaryLoop> {
632    let n = verts.len();
633    let min = *verts.iter().min().expect("a loop has vertices");
634    // Among the occurrences of the smallest vertex, start at the one giving
635    // the lexicographically smallest rotation, so the start does not depend
636    // on where the walk began.
637    let rotation = |i: usize| verts[i..].iter().chain(&verts[..i]);
638    let mut best = 0;
639    if closed {
640        best = verts
641            .iter()
642            .position(|&v| v == min)
643            .expect("min is present");
644        for (i, &v) in verts.iter().enumerate().skip(best + 1) {
645            if v == min && rotation(i).cmp(rotation(best)).is_lt() {
646                best = i;
647            }
648        }
649    }
650    let mut vertices = Vec::new();
651    reserve(&mut vertices, n)?;
652    vertices.extend(rotation(best));
653    let (mut perimeter, mut vector) = (0.0, [0.0; 3]);
654    for i in 0..n {
655        let (p, q) = (point(mesh, vertices[i]), point(mesh, vertices[(i + 1) % n]));
656        perimeter += norm(sub(q, p));
657        let c = cross(p, q);
658        vector = [vector[0] + c[0], vector[1] + c[1], vector[2] + c[2]];
659    }
660    Ok(BoundaryLoop {
661        id: LoopId {
662            min_vertex: min,
663            // Fewer than `u32::MAX` edges exist (checked by `EdgeMap::build`).
664            len: n as u32,
665            hash: fnv1a(&vertices),
666        },
667        vertices,
668        perimeter,
669        area: 0.5 * norm(vector),
670        closed,
671        clean,
672    })
673}
674
675#[cfg(test)]
676mod tests {
677    use super::*;
678    use crate::test_util::*;
679    use crate::{OpsError, fail_allocations_after};
680    use fast_mesh_store::{Mesh, StoreError};
681
682    fn kinds(mesh: &Mesh) -> Vec<((VertexId, VertexId), EdgeKind)> {
683        let map = EdgeMap::build(mesh).unwrap();
684        map.edges()
685            .map(|(k, _)| (k, map.kind(k.0, k.1).unwrap()))
686            .collect()
687    }
688
689    fn count(mesh: &Mesh, kind: EdgeKind) -> usize {
690        kinds(mesh).iter().filter(|(_, k)| *k == kind).count()
691    }
692
693    #[test]
694    fn edge_kinds_on_fixtures() {
695        let sphere = uv_sphere(8, 12, 1.0);
696        assert_eq!(count(&sphere, EdgeKind::Boundary), 0);
697        assert_eq!(count(&sphere, EdgeKind::NonManifold), 0);
698        assert_eq!(
699            count(&sphere, EdgeKind::Interior),
700            3 * sphere.triangle_count() / 2
701        );
702        assert_eq!(count(&grid(4), EdgeKind::Boundary), 16);
703        assert_eq!(count(&grid(4), EdgeKind::NonManifold), 0);
704        let fin = fin();
705        assert_eq!(count(&fin, EdgeKind::NonManifold), 1);
706        let map = EdgeMap::build(&fin).unwrap();
707        assert_eq!(map.kind(1, 0), Some(EdgeKind::NonManifold));
708        assert_eq!(map.triangles(1, 0), &[1, 3, 5]);
709        assert_eq!(map.kind(0, 3), Some(EdgeKind::Interior));
710        assert_eq!(map.triangles(3, 0), &[0, 1]);
711        assert_eq!(map.kind(2, 4), None);
712        assert_eq!(map.triangles(2, 4), &[] as &[TriangleId]);
713    }
714
715    #[test]
716    fn components_count_and_closed() {
717        let two = combine(&[&uv_sphere(8, 12, 1.0), &uv_sphere(6, 8, 0.25)]);
718        let map = EdgeMap::build(&two).unwrap();
719        let comps = components(&two, &map).unwrap();
720        assert_eq!(comps.len(), 2);
721        assert!(comps.iter().all(|c| c.closed));
722        assert_eq!(comps[0].triangles.len(), 2 * 12 * 7);
723        assert_eq!(comps[1].triangles.len(), 2 * 8 * 5);
724        assert_eq!(comps[1].triangles[0], 2 * 12 * 7);
725        assert!(comps[0].area > comps[1].area);
726        // A UV sphere's area approaches 4 pi r^2 from below.
727        assert!(comps[0].area < 4.0 * std::f64::consts::PI && comps[0].area > 3.5);
728
729        let hemi = hemisphere(4, 16, 1.0);
730        let map = EdgeMap::build(&hemi).unwrap();
731        let comps = components(&hemi, &map).unwrap();
732        assert_eq!(comps.len(), 1);
733        assert!(!comps[0].closed);
734        assert_eq!(comps[0].triangles.len(), hemi.triangle_count());
735
736        // The fin is one component (shared edges of any kind connect), not closed.
737        let fin = fin();
738        let comps = components(&fin, &EdgeMap::build(&fin).unwrap()).unwrap();
739        assert_eq!(comps.len(), 1);
740        assert!(!comps[0].closed);
741    }
742
743    #[test]
744    fn hemisphere_has_one_loop() {
745        let r = 2.0;
746        let loops = boundary_loops(&hemisphere(8, 64, r)).unwrap();
747        assert_eq!(loops.len(), 1);
748        let l = &loops[0];
749        assert_eq!(l.vertices.len(), 64);
750        assert_eq!(l.id.len, 64);
751        assert!(l.clean);
752        let circumference = 2.0 * std::f64::consts::PI * f64::from(r);
753        assert!((l.perimeter - circumference).abs() < 0.01 * circumference);
754        // The rim is a flat circle: vector area is about pi r^2.
755        let disc = std::f64::consts::PI * f64::from(r * r);
756        assert!((l.area - disc).abs() < 0.01 * disc);
757        // Ring 8 starts at vertex 1 + 7 * 64. With outward winding the rim
758        // runs counter-clockwise from +z, in ring order.
759        let first = 1 + 7 * 64;
760        assert_eq!(l.id.min_vertex, first);
761        assert_eq!(l.vertices[0], first);
762        assert_eq!(l.vertices[1], first + 1);
763    }
764
765    #[test]
766    fn bowtie_holes_are_two_clean_loops() {
767        let mesh = bowtie_holes();
768        let loops = boundary_loops(&mesh).unwrap();
769        let seqs: Vec<&[VertexId]> = loops.iter().map(|l| l.vertices.as_slice()).collect();
770        assert_eq!(
771            seqs,
772            [
773                // The grid border, counter-clockwise from +z.
774                &[0, 1, 2, 3, 4, 9, 14, 19, 24, 23, 22, 21, 20, 15, 10, 5][..],
775                // Cell (1, 1): its corners 6, 7, 12, 11 walked clockwise.
776                &[6, 11, 12, 7][..],
777                // Cell (2, 2): its corners 12, 13, 18, 17 walked clockwise.
778                &[12, 17, 18, 13][..],
779            ]
780        );
781        assert!(loops.iter().all(|l| l.clean && l.closed));
782        assert_eq!(loops[1].id.len, 4);
783        assert_eq!(loops[2].id.len, 4);
784        assert_eq!(loops[1].id.min_vertex, 6);
785        assert_eq!(loops[2].id.min_vertex, 12);
786        for l in &loops[1..] {
787            assert!((l.perimeter - 4.0).abs() < 1e-12);
788            assert!((l.area - 1.0).abs() < 1e-12);
789        }
790        assert!((loops[0].perimeter - 16.0).abs() < 1e-12);
791        assert!((loops[0].area - 16.0).abs() < 1e-12);
792        let map = EdgeMap::build(&mesh).unwrap();
793        assert_eq!(pinch_vertices(&mesh, &map).unwrap(), vec![12]);
794        // A plain grid has no pinch.
795        let g = grid(4);
796        assert!(
797            pinch_vertices(&g, &EdgeMap::build(&g).unwrap())
798                .unwrap()
799                .is_empty()
800        );
801    }
802
803    #[test]
804    fn three_holes_at_one_vertex_are_three_loops() {
805        // Three wedges meet at vertex 0, so each hole's next edge there is
806        // chosen by turning into the gap; key order alone would mix them.
807        let mesh = tri_pinch();
808        let loops = boundary_loops(&mesh).unwrap();
809        let seqs: Vec<&[VertexId]> = loops.iter().map(|l| l.vertices.as_slice()).collect();
810        assert_eq!(
811            seqs,
812            [
813                &[0, 1, 6][..],
814                &[0, 3, 2][..],
815                &[0, 5, 4][..],
816                &[7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18][..],
817            ]
818        );
819        assert!(loops.iter().all(|l| l.clean));
820        let map = EdgeMap::build(&mesh).unwrap();
821        assert_eq!(pinch_vertices(&mesh, &map).unwrap(), vec![0]);
822    }
823
824    #[test]
825    fn fin_loops_unclean() {
826        let mesh = fin();
827        let loops = boundary_loops(&mesh).unwrap();
828        // Each quad's boundary runs 0 -> p -> q -> 1 and would close over the
829        // non-manifold edge (1, 0); vertex 1 has no outgoing boundary edge, so
830        // each comes out as an open chain, unclean.
831        let seqs: Vec<&[VertexId]> = loops.iter().map(|l| l.vertices.as_slice()).collect();
832        assert_eq!(
833            seqs,
834            [&[0, 2, 3, 1][..], &[0, 4, 5, 1][..], &[0, 6, 7, 1][..]]
835        );
836        assert!(loops.iter().all(|l| !l.clean && !l.closed));
837        assert_eq!(loops[0].id.hash, fnv1a(&[0, 2, 3, 1]));
838        // The non-manifold edge does not make the fin's vertices pinches.
839        let map = EdgeMap::build(&mesh).unwrap();
840        assert!(pinch_vertices(&mesh, &map).unwrap().is_empty());
841    }
842
843    #[test]
844    fn fin_chains_whole_whatever_the_numbering() {
845        // The fin with the spine relabelled (1, 5): every chain's first edge
846        // no longer sorts first, so walks must start at chain heads to keep
847        // each chain whole, and the chains must keep their ends.
848        let (old, _) = parts(&fin());
849        // New id of each old vertex 0..8; new vertex 0 is unused.
850        let new = [5, 1, 7, 3, 2, 4, 6, 8];
851        let mut positions = vec![0.0f32; 27];
852        for (o, &n) in new.iter().enumerate() {
853            positions[3 * n..3 * n + 3].copy_from_slice(&old[3 * o..3 * o + 3]);
854        }
855        let tris = [
856            [5, 7, 3],
857            [5, 3, 1],
858            [5, 2, 4],
859            [5, 4, 1],
860            [5, 6, 8],
861            [5, 8, 1],
862        ];
863        let indices: Vec<u32> = tris.iter().flatten().copied().collect();
864        let mesh = Mesh::from_buffers(&positions, &indices).unwrap();
865        let loops = boundary_loops(&mesh).unwrap();
866        let seqs: Vec<&[VertexId]> = loops.iter().map(|l| l.vertices.as_slice()).collect();
867        assert_eq!(
868            seqs,
869            [&[5, 2, 4, 1][..], &[5, 6, 8, 1][..], &[5, 7, 3, 1][..]]
870        );
871        assert!(loops.iter().all(|l| !l.clean && !l.closed));
872        assert!(loops.iter().all(|l| l.id.min_vertex == 1 && l.id.len == 4));
873        assert_eq!(loops[0].id.hash, fnv1a(&[5, 2, 4, 1]));
874    }
875
876    #[test]
877    fn three_holes_at_one_vertex_out_of_plane() {
878        // The tri_pinch wedges curved into a bowl, twisted alternately up and
879        // down (a monkey saddle), and folded 39 degrees along the y axis
880        // through vertex 0; all well under 90 degrees.
881        let bowl: fn(f64, f64) -> f64 = |r, _| 0.3 * r * r;
882        let saddle: fn(f64, f64) -> f64 = |r, deg| 0.7 * r * (3.0 * deg.to_radians()).cos();
883        let fold: fn(f64, f64) -> f64 = |r, deg| (0.8 * r * deg.to_radians().cos()).max(0.0);
884        for lift in [bowl, saddle, fold] {
885            let mesh = tri_pinch_lifted(lift);
886            let loops = boundary_loops(&mesh).unwrap();
887            let seqs: Vec<&[VertexId]> = loops.iter().map(|l| l.vertices.as_slice()).collect();
888            assert_eq!(
889                seqs,
890                [
891                    &[0, 1, 6][..],
892                    &[0, 3, 2][..],
893                    &[0, 5, 4][..],
894                    &[7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18][..],
895                ]
896            );
897            assert!(loops.iter().all(|l| l.clean && l.closed));
898        }
899    }
900
901    #[test]
902    fn loop_id_is_rotation_invariant_and_stable() {
903        let mesh = bowtie_holes();
904        let before = boundary_loops(&mesh).unwrap();
905        // Re-add the same vertices with the triangles in another order and each
906        // triangle's corners rotated, so the walk starts elsewhere.
907        let (positions, triangles) = parts(&mesh);
908        let mut shuffled: Vec<u32> = Vec::new();
909        for (i, t) in triangles.iter().enumerate().rev() {
910            let r = i % 3;
911            shuffled.extend([t[r], t[(r + 1) % 3], t[(r + 2) % 3]]);
912        }
913        let again = Mesh::from_buffers(&positions, &shuffled).unwrap();
914        let after = boundary_loops(&again).unwrap();
915        let ids = |ls: &[BoundaryLoop]| ls.iter().map(|l| l.id).collect::<Vec<_>>();
916        assert_eq!(ids(&before), ids(&after));
917        assert_eq!(before[1].vertices, after[1].vertices);
918        // Different loops, different hashes.
919        let mut hashes: Vec<u64> = before.iter().map(|l| l.id.hash).collect();
920        hashes.sort_unstable();
921        hashes.dedup();
922        assert_eq!(hashes.len(), 3);
923        // The hash is FNV-1a 64 over the rotated sequence.
924        assert_eq!(before[1].id.hash, fnv1a(&[6, 11, 12, 7]));
925        assert_ne!(fnv1a(&[6, 11, 12, 7]), fnv1a(&[6, 7, 12, 11]));
926    }
927
928    #[test]
929    fn degenerate_triangles_ignored_by_maps() {
930        for mesh in [bowtie_holes(), fin(), hemisphere(3, 8, 1.0)] {
931            let (_, tris) = parts(&mesh);
932            let extra: Vec<[u32; 3]> = tris
933                .iter()
934                .step_by(3)
935                .flat_map(|&[a, b, c]| [[a, a, b], [b, c, c], [c, c, c]])
936                .collect();
937            let dirty = with_triangles(&mesh, &extra);
938            assert_eq!(kinds(&mesh), kinds(&dirty));
939            let (l0, l1) = (
940                boundary_loops(&mesh).unwrap(),
941                boundary_loops(&dirty).unwrap(),
942            );
943            assert_eq!(l0, l1);
944            let m0 = EdgeMap::build(&mesh).unwrap();
945            let m1 = EdgeMap::build(&dirty).unwrap();
946            assert_eq!(
947                pinch_vertices(&mesh, &m0).unwrap(),
948                pinch_vertices(&dirty, &m1).unwrap()
949            );
950            let c1 = components(&dirty, &m1).unwrap();
951            assert_eq!(components(&mesh, &m0).unwrap(), c1);
952        }
953    }
954
955    #[test]
956    fn fixtures_sanity() {
957        // Möbius: one component, one boundary curve of 2n edges, walked across
958        // the orientation seam, so it is unclean.
959        let m = mobius(12);
960        let map = EdgeMap::build(&m).unwrap();
961        assert_eq!(components(&m, &map).unwrap().len(), 1);
962        let loops = boundary_loops(&m).unwrap();
963        assert_eq!(loops.len(), 1);
964        assert_eq!(loops[0].vertices.len(), 24);
965        assert!(loops[0].closed && !loops[0].clean);
966        // Two punched holes in a sphere give two loops in one component.
967        let punched = punch_holes(
968            &uv_sphere(16, 16, 1.0),
969            &[[0.0, 0.0, 1.0], [0.0, 0.0, -1.0]],
970            0.3,
971        );
972        let map = EdgeMap::build(&punched).unwrap();
973        assert_eq!(components(&punched, &map).unwrap().len(), 1);
974        assert_eq!(boundary_loops(&punched).unwrap().len(), 2);
975        // Flipping a triangle changes no edge kind and keeps one border loop.
976        let g = grid(3);
977        let flipped = flip(&g, &[4]);
978        assert_eq!(kinds(&g), kinds(&flipped));
979        let loops = boundary_loops(&flipped).unwrap();
980        assert_eq!(loops.len(), 1);
981        assert_eq!(loops[0].vertices.len(), 12);
982        // Crossed boxes: two closed components.
983        let b = crossed_boxes();
984        let comps = components(&b, &EdgeMap::build(&b).unwrap()).unwrap();
985        assert_eq!(comps.len(), 2);
986        assert!(comps.iter().all(|c| c.closed && c.triangles.len() == 12));
987        assert!((comps[0].area - 24.0).abs() < 1e-9);
988    }
989
990    #[test]
991    fn boundary_loops_oom_is_clean() {
992        let mesh = bowtie_holes();
993        let expected = boundary_loops(&mesh).unwrap();
994        let mut n = 0;
995        loop {
996            fail_allocations_after(Some(n));
997            let got = boundary_loops(&mesh);
998            fail_allocations_after(None);
999            match got {
1000                Ok(loops) => {
1001                    assert_eq!(loops, expected);
1002                    break;
1003                }
1004                Err(e) => assert_eq!(e, OpsError::Store(StoreError::OutOfMemory)),
1005            }
1006            n += 1;
1007        }
1008        assert!(n > 5, "only {n} fallible reservations");
1009    }
1010}