Skip to main content

ogeom_topo/
tessellation.rs

1//! Cached tessellation: the polyline and triangle forms of exact geometry.
2//!
3//! A triangulation is a *representation* of a face, not a replacement for it
4//! (`docs/DATA_MODEL.md` ยง6). It lives here, beside the entity data, because
5//! that is what it belongs to: a face holds one the way an edge holds a
6//! pcurve, and the algorithms that build it live a layer up in `ogeom-mesh`.
7//!
8//! Everything here is plain data with the queries that read it. Nothing here
9//! decides how finely to sample anything.
10
11use ogeom_core::Tolerances;
12use ogeom_math::{Aabb, Point, Vector};
13
14/// A triangulated surface.
15///
16/// Vertices carry their parameters as well as their positions, so a caller can
17/// ask the exact surface about a triangulated point rather than only the
18/// approximation.
19#[derive(Debug, Clone, PartialEq, Default)]
20pub struct Triangulation {
21    /// Vertex positions.
22    pub positions: Vec<Point>,
23    /// Outward unit normals, one per vertex, or none for a mesh read
24    /// without them.
25    pub normals: Vec<Vector>,
26    /// The surface parameters each vertex came from, or none for a mesh
27    /// that did not come from a surface.
28    pub parameters: Vec<(f64, f64)>,
29    /// Triangles, as indices into the vertex arrays, wound counter-clockwise
30    /// about the outward normal.
31    pub triangles: Vec<[u32; 3]>,
32    /// Whether every face met its requested deflection.
33    pub deflection_met: bool,
34}
35
36impl Triangulation {
37    /// An empty mesh.
38    #[must_use]
39    pub fn new() -> Self {
40        Self {
41            deflection_met: true,
42            ..Self::default()
43        }
44    }
45
46    /// Number of vertices.
47    #[must_use]
48    pub fn vertex_count(&self) -> usize {
49        self.positions.len()
50    }
51
52    /// Number of triangles.
53    #[must_use]
54    pub fn triangle_count(&self) -> usize {
55        self.triangles.len()
56    }
57
58    /// Whether the mesh holds no triangles.
59    #[must_use]
60    pub fn is_empty(&self) -> bool {
61        self.triangles.is_empty()
62    }
63
64    /// The bounding box of the vertices.
65    #[must_use]
66    pub fn bounds(&self) -> Aabb {
67        Aabb::of_points(&self.positions)
68    }
69
70    /// The total area of the triangles.
71    ///
72    /// An *under*estimate of the surface's own area for a convex patch, since a
73    /// triangle chord-cuts the surface it spans. It converges from below as the
74    /// deflection tightens.
75    #[must_use]
76    pub fn area(&self) -> f64 {
77        self.triangles
78            .iter()
79            .map(|t| {
80                let [a, b, c] = t.map(|i| self.positions[i as usize]);
81                (b - a).cross(c - a).magnitude() * 0.5
82            })
83            .sum()
84    }
85
86    /// The signed volume enclosed, by the divergence theorem.
87    ///
88    /// Meaningful only for a mesh that is closed and consistently wound
89    /// outward: each triangle contributes the signed volume of the tetrahedron
90    /// it forms with the mesh's first position, and the contributions cancel
91    /// except over the enclosed region. An open mesh gives a number with no
92    /// meaning, and a mesh wound inward gives the negative, which is why
93    /// [`Triangulation::is_closed`] exists to be asked first.
94    ///
95    /// The apex is a point of the mesh rather than the origin so a mesh far
96    /// from the origin sums tetrahedra of its own size, not slivers of huge
97    /// ones whose volumes cancel to rounding.
98    #[must_use]
99    pub fn volume(&self) -> f64 {
100        let Some(&apex) = self.positions.first() else {
101            return 0.0;
102        };
103        self.triangles
104            .iter()
105            .map(|t| {
106                let [a, b, c] = t.map(|i| self.positions[i as usize] - apex);
107                a.dot(b.cross(c)) / 6.0
108            })
109            .sum()
110    }
111
112    /// Whether every triangle edge is crossed as often one way as the other.
113    ///
114    /// The mesh equivalent of a closed shell, and the precondition for
115    /// [`Triangulation::volume`] meaning anything. It is exactly what the
116    /// divergence theorem needs: the surface has no boundary, and it is
117    /// wound consistently, so each triangle's contribution cancels against
118    /// its neighbours' except over the region enclosed.
119    ///
120    /// Counting *directed* edges rather than undirected ones is what makes
121    /// this the right question, and it is stricter and looser than the
122    /// obvious test in the two different ways that matter.
123    ///
124    /// Stricter: two triangles sharing an edge and winding the *same* way
125    /// round it traverse it twice in the same direction. The edge is used
126    /// twice, so a count of uses calls it closed, and the volume that comes
127    /// out is wrong because one of the two faces is inside out.
128    ///
129    /// Looser: an edge may legitimately carry four triangles. Where two
130    /// faces meet along a short edge that discretizes into several segments,
131    /// each can fill the sliver between the polyline and its own chord, and
132    /// the chord then belongs to both: four triangles round one edge, two
133    /// crossing each way. There is no hole there and the volume is right.
134    /// Demanding exactly two refuses a mesh for being non-manifold when
135    /// nothing was asked about manifoldness.
136    ///
137    /// This agrees with the topology side:
138    /// [`is_shell_closed`](../../ogeom_algo/fn.is_shell_closed.html) counts an
139    /// edge's uses and accepts any even number, and the two halves of the
140    /// kernel should not mean different things by the same word.
141    #[must_use]
142    pub fn is_closed(&self) -> bool {
143        use ogeom_core::FastMap;
144        let mut balance: FastMap<(u32, u32), i64> = FastMap::default();
145        for t in &self.triangles {
146            for i in 0..3 {
147                let (a, b) = (t[i], t[(i + 1) % 3]);
148                // One key per undirected edge; the direction decides the sign.
149                let (key, step) = if a <= b { ((a, b), 1) } else { ((b, a), -1) };
150                *balance.entry(key).or_default() += step;
151            }
152        }
153        !balance.is_empty() && balance.values().all(|&n| n == 0)
154    }
155
156    /// Weld only the mesh's *border* vertices, within `reach`.
157    ///
158    /// The second pass after [`Triangulation::welded`]: interior edges are
159    /// already manifold, and touching them at a widened tolerance would eat
160    /// real features. Borders are where imported slop lives (an edge's curve
161    /// and its neighbour's disagree by the file's own tolerance, which the
162    /// model records on the edge), so only vertices on unmatched triangle
163    /// edges are candidates, merged to their nearest counterpart within
164    /// `reach`.
165    #[must_use]
166    pub fn border_welded(&self, reach: f64) -> Self {
167        use ogeom_core::FastMap;
168        if !reach.is_finite() || reach <= 0.0 {
169            return self.clone();
170        }
171        // Border vertices: endpoints of triangle edges used an odd number of
172        // times.
173        let uses = edge_uses(&self.triangles);
174        let mut border: Vec<u32> = uses
175            .iter()
176            .filter(|&(_, &n)| n % 2 == 1)
177            .flat_map(|(&(a, b), _)| [a, b])
178            .collect();
179        border.sort_unstable();
180        border.dedup();
181        if border.is_empty() {
182            return self.clone();
183        }
184
185        // Cluster border vertices within reach, first-seen wins, checked
186        // against the cluster representative so chains cannot creep.
187        let cell = reach.max(f64::MIN_POSITIVE);
188        let key = |p: Point| {
189            #[allow(clippy::cast_possible_truncation)]
190            (
191                (p.x / cell).round() as i64,
192                (p.y / cell).round() as i64,
193                (p.z / cell).round() as i64,
194            )
195        };
196        let mut buckets: FastMap<(i64, i64, i64), Vec<u32>> = FastMap::default();
197        #[allow(clippy::cast_possible_truncation)]
198        let mut remap: Vec<u32> = (0..self.positions.len() as u32).collect();
199        for &v in &border {
200            let p = self.positions[v as usize];
201            let (kx, ky, kz) = key(p);
202            let mut found = None;
203            'search: for dx in -1..=1 {
204                for dy in -1..=1 {
205                    for dz in -1..=1 {
206                        for &candidate in buckets
207                            .get(&(
208                                kx.saturating_add(dx),
209                                ky.saturating_add(dy),
210                                kz.saturating_add(dz),
211                            ))
212                            .map_or(&[][..], Vec::as_slice)
213                        {
214                            if self.positions[candidate as usize].distance(p) <= reach {
215                                found = Some(candidate);
216                                break 'search;
217                            }
218                        }
219                    }
220                }
221            }
222            match found {
223                Some(rep) => remap[v as usize] = rep,
224                None => buckets.entry((kx, ky, kz)).or_default().push(v),
225            }
226        }
227
228        let mut out = Self::new();
229        out.deflection_met = self.deflection_met;
230        // Every vertex is kept, merged ones included, and the triangles are
231        // remapped onto the representatives.
232        out.positions = self.positions.clone();
233        out.normals = self.normals.clone();
234        out.parameters = self.parameters.clone();
235        for t in &self.triangles {
236            let mapped = t.map(|i| remap[i as usize]);
237            if mapped[0] != mapped[1] && mapped[1] != mapped[2] && mapped[2] != mapped[0] {
238                out.triangles.push(mapped);
239            }
240        }
241        out
242    }
243
244    /// Split border segments at border vertices that lie on them.
245    ///
246    /// The T-junction repair that follows [`Triangulation::border_welded`]:
247    /// after welding, two faces' border chains share their vertices but may
248    /// subdivide the same stretch differently: one face's segment spans two
249    /// of its neighbour's. Splitting the long segment *at the neighbour's own
250    /// vertex index* makes the chains segment-for-segment identical, which is
251    /// what closure counts. No positions move and none are added.
252    #[must_use]
253    pub fn border_stitched(&self, reach: f64) -> Self {
254        self.border_stitched_with_sources(reach).0
255    }
256
257    /// As [`Self::border_stitched`], retaining the source triangle of every output triangle.
258    #[must_use]
259    pub fn border_stitched_with_sources(&self, reach: f64) -> (Self, Vec<usize>) {
260        use ogeom_core::FastMap;
261        if !reach.is_finite() || reach <= 0.0 {
262            return (self.clone(), (0..self.triangles.len()).collect());
263        }
264        let uses = edge_uses(&self.triangles);
265        let border_edges: Vec<(u32, u32)> = uses
266            .iter()
267            .filter(|&(_, &n)| n % 2 == 1)
268            .map(|(&e, _)| e)
269            .collect();
270        if border_edges.is_empty() {
271            return (self.clone(), (0..self.triangles.len()).collect());
272        }
273        let mut border_vertices: Vec<u32> =
274            border_edges.iter().flat_map(|&(a, b)| [a, b]).collect();
275        border_vertices.sort_unstable();
276        border_vertices.dedup();
277        // Cells about a border segment long, so a segment's box spans few.
278        #[allow(clippy::cast_precision_loss, reason = "a mean over a count")]
279        let spacing = border_edges
280            .iter()
281            .map(|&(a, b)| self.positions[a as usize].distance(self.positions[b as usize]))
282            .sum::<f64>()
283            / border_edges.len() as f64;
284        let grid = PointGrid::over(&self.positions, &border_vertices, spacing, reach);
285
286        // For every border segment, the border vertices sitting on its
287        // interior, ordered along it (by vertex where two stand level).
288        let mut splits: FastMap<(u32, u32), Vec<u32>> = FastMap::default();
289        let mut near: Vec<u32> = Vec::new();
290        for &(a, b) in &border_edges {
291            let (pa, pb) = (self.positions[a as usize], self.positions[b as usize]);
292            let d = pb - pa;
293            let l2 = d.dot(d);
294            if l2 <= 0.0 {
295                continue;
296            }
297            near.clear();
298            match &grid {
299                Some(grid) => grid.within(pa, pb, reach, &mut near),
300                None => near.extend_from_slice(&border_vertices),
301            }
302            let mut on: Vec<(f64, u32)> = near
303                .iter()
304                .filter(|&&v| v != a && v != b)
305                .filter_map(|&v| {
306                    let p = self.positions[v as usize];
307                    let t = (p - pa).dot(d) / l2;
308                    if !(0.001..=0.999).contains(&t) {
309                        return None;
310                    }
311                    ((pa + d * t).distance(p) <= reach).then_some((t, v))
312                })
313                .collect();
314            if on.is_empty() {
315                continue;
316            }
317            on.sort_by(|x, y| x.0.total_cmp(&y.0).then(x.1.cmp(&y.1)));
318            splits.insert((a, b), on.into_iter().map(|(_, v)| v).collect());
319        }
320        if splits.is_empty() {
321            return (self.clone(), (0..self.triangles.len()).collect());
322        }
323
324        let mut out = Self::new();
325        out.deflection_met = self.deflection_met;
326        out.positions = self.positions.clone();
327        out.normals = self.normals.clone();
328        out.parameters = self.parameters.clone();
329        let mut sources = Vec::new();
330        for (source, t) in self.triangles.iter().enumerate() {
331            // Split each original side from the opposite corner. A fan from
332            // the ring's first vertex would leave its incident sides unsplit
333            // and emit collinear triangles along them.
334            let sides = std::array::from_fn(|i| {
335                let (a, b) = (t[i], t[(i + 1) % 3]);
336                splits
337                    .get(&(a.min(b), a.max(b)))
338                    .map_or(&[][..], Vec::as_slice)
339            });
340            let nearest = nearest_split_sides(t, sides, |i, v| {
341                let (a, b) = (t[i], t[(i + 1) % 3]);
342                let (pa, pb, p) = (
343                    self.positions[a as usize],
344                    self.positions[b as usize],
345                    self.positions[v as usize],
346                );
347                let d = pb - pa;
348                let along = (p - pa).dot(d) / d.dot(d);
349                (pa + d * along).distance(p)
350            });
351            let mut pieces = vec![*t];
352            for i in 0..3 {
353                let (a, b) = (t[i], t[(i + 1) % 3]);
354                if let Some(vs) = splits.get(&(a.min(b), a.max(b))) {
355                    // At a narrow corner a vertex can be within reach of two
356                    // sides. Insert it only on the nearest: inserting it on
357                    // both turns the polygon back through the same vertex.
358                    let vs: Vec<_> = vs
359                        .iter()
360                        .copied()
361                        .filter(|v| nearest.get(v) == Some(&i))
362                        .collect();
363                    if vs.is_empty() {
364                        continue;
365                    }
366                    let mut chain = vec![a];
367                    if a < b {
368                        chain.extend(vs.iter().copied());
369                    } else {
370                        chain.extend(vs.iter().rev().copied());
371                    }
372                    chain.push(b);
373                    let Some(at) = pieces
374                        .iter()
375                        .position(|piece| (0..3).any(|k| piece[k] == a && piece[(k + 1) % 3] == b))
376                    else {
377                        panic!(
378                            "source triangle {source}: original side {a}->{b} is missing from current pieces {pieces:?}"
379                        );
380                    };
381                    let piece = pieces.remove(at);
382                    let Some(&opposite) = piece.iter().find(|&&v| v != a && v != b) else {
383                        panic!(
384                            "source triangle {source}: piece {piece:?} has no corner opposite side {a}->{b}"
385                        );
386                    };
387                    pieces.extend(chain.windows(2).map(|side| [side[0], side[1], opposite]));
388                }
389            }
390            for tri in pieces {
391                out.triangles.push(tri);
392                sources.push(source);
393            }
394        }
395        (out, sources)
396    }
397
398    /// This mesh with its folds cancelled and its cracks sealed: the last
399    /// pass over a mesh welded from faces that each met their edges.
400    ///
401    /// A fold is two triangles on the same three vertices facing opposite
402    /// ways, left where a sliver face collapses in the weld; they enclose
403    /// nothing, and both go. A crack is a loop of border edges whose mean
404    /// width (twice its area over its perimeter) is within `width`: two
405    /// faces sampling a shared corner differently leave one, narrower than
406    /// the chord they were drawn to. It is fanned shut from one of its
407    /// corners, each new triangle crossing a border edge the other way from
408    /// the triangle already on it. A loop wider than that, or a border
409    /// vertex with more than one way on, is a real opening and stays.
410    #[must_use]
411    pub fn sealed(&self, width: f64) -> Self {
412        use ogeom_core::FastMap;
413        let mut out = self.without_folds_with_sources().0;
414        if !width.is_finite() || width <= 0.0 {
415            return out;
416        }
417        // Cracks: each border edge walked the other way from its triangle.
418        let uses = edge_uses(&out.triangles);
419        let mut onward: FastMap<u32, Vec<u32>> = FastMap::default();
420        for t in &out.triangles {
421            for k in 0..3 {
422                let (a, b) = (t[k], t[(k + 1) % 3]);
423                if uses[&(a.min(b), a.max(b))] == 1 {
424                    onward.entry(b).or_default().push(a);
425                }
426            }
427        }
428        let mut done: ogeom_core::FastSet<u32> = ogeom_core::FastSet::default();
429        let mut starts: Vec<u32> = onward.keys().copied().collect();
430        starts.sort_unstable();
431        // The walk each vertex was last put on a ring by, plus one.
432        let mut on_walk: Vec<u32> = vec![0; out.positions.len()];
433        #[allow(clippy::cast_possible_truncation)]
434        for (walk, start) in starts.into_iter().enumerate() {
435            let walk = walk as u32 + 1;
436            if done.contains(&start) {
437                continue;
438            }
439            let mut ring = vec![start];
440            if let Some(mark) = on_walk.get_mut(start as usize) {
441                *mark = walk;
442            }
443            let mut at = start;
444            let closed = loop {
445                let Some(next) = onward.get(&at) else {
446                    break false;
447                };
448                let [next] = next[..] else {
449                    break false;
450                };
451                if next == start {
452                    break true;
453                }
454                let seen = match on_walk.get(next as usize) {
455                    Some(&mark) => mark == walk,
456                    None => ring.contains(&next),
457                };
458                if seen || ring.len() > onward.len() {
459                    break false;
460                }
461                ring.push(next);
462                if let Some(mark) = on_walk.get_mut(next as usize) {
463                    *mark = walk;
464                }
465                at = next;
466            };
467            for &v in &ring {
468                done.insert(v);
469            }
470            if !closed || ring.len() < 3 {
471                continue;
472            }
473            let points: Vec<Point> = ring.iter().map(|&v| out.positions[v as usize]).collect();
474            // About the ring's first point: the sum is the same about any
475            // point, and about a far one it is a difference of huge
476            // products.
477            let anchor = points[0];
478            let mut normal = Vector::ZERO;
479            let mut perimeter = 0.0;
480            for (i, p) in points.iter().enumerate() {
481                let q = points[(i + 1) % points.len()];
482                normal += (*p - anchor).cross(q - anchor);
483                perimeter += p.distance(q);
484            }
485            let area = normal.magnitude() / 2.0;
486            if perimeter <= 0.0 || 2.0 * area / perimeter > width {
487                continue;
488            }
489            for i in 1..ring.len() - 1 {
490                out.triangles.push([ring[0], ring[i], ring[i + 1]]);
491            }
492        }
493        out
494    }
495
496    /// Cancel opposite triangles on the same vertices, retaining each survivor's source.
497    /// No positions move and no crack is filled.
498    #[must_use]
499    pub fn without_folds_with_sources(&self) -> (Self, Vec<usize>) {
500        let mut keyed: Vec<([u32; 3], usize)> = self
501            .triangles
502            .iter()
503            .enumerate()
504            .map(|(i, t)| {
505                let mut key = *t;
506                key.sort_unstable();
507                (key, i)
508            })
509            .collect();
510        keyed.sort_unstable();
511        let mut drop = vec![false; self.triangles.len()];
512        let mut open: Vec<usize> = Vec::new();
513        for group in keyed.chunk_by(|x, y| x.0 == y.0) {
514            if group.len() < 2 {
515                continue;
516            }
517            open.clear();
518            for &(_, i) in group {
519                let t = self.triangles[i];
520                let reverse = open.iter().position(|&j| {
521                    let u = self.triangles[j];
522                    (0..3).any(|k| [u[k], u[(k + 2) % 3], u[(k + 1) % 3]] == t)
523                });
524                match reverse {
525                    Some(at) => {
526                        drop[open.remove(at)] = true;
527                        drop[i] = true;
528                    }
529                    None => open.push(i),
530                }
531            }
532        }
533        let sources: Vec<_> = (0..self.triangles.len()).filter(|&i| !drop[i]).collect();
534        let mut out = self.clone();
535        out.triangles = sources.iter().map(|&i| self.triangles[i]).collect();
536        (out, sources)
537    }
538
539    /// Append another mesh, shifting its indices.
540    pub fn append(&mut self, other: &Self) {
541        #[allow(clippy::cast_possible_truncation)]
542        let offset = self.positions.len() as u32;
543        self.positions.extend_from_slice(&other.positions);
544        self.normals.extend_from_slice(&other.normals);
545        self.parameters.extend_from_slice(&other.parameters);
546        self.triangles
547            .extend(other.triangles.iter().map(|t| t.map(|i| i + offset)));
548        self.deflection_met &= other.deflection_met;
549    }
550
551    /// Merge vertices that coincide within `tol`, rewiring the triangles.
552    ///
553    /// Faces are triangulated independently, so a shared edge produces two
554    /// copies of every boundary vertex, at identical positions, since both
555    /// came from the same edge discretization, but as separate entries. Merging
556    /// them is what turns a pile of face meshes into one closed surface, and
557    /// what lets [`Triangulation::is_closed`] answer truthfully.
558    #[must_use]
559    pub fn welded(&self, tol: Tolerances) -> Self {
560        use ogeom_core::FastMap;
561
562        // Quantize to a grid a good deal finer than the tolerance, then check
563        // the neighbourhood: hashing alone would separate two points that
564        // straddle a cell boundary however close they are.
565        let cell = tol.confusion().max(f64::MIN_POSITIVE);
566        let key = |p: Point| {
567            #[allow(clippy::cast_possible_truncation)]
568            (
569                (p.x / cell).round() as i64,
570                (p.y / cell).round() as i64,
571                (p.z / cell).round() as i64,
572            )
573        };
574
575        // Each cell's first and last vertex, and each vertex's successor in
576        // its cell: the cells read back in the order they were filled.
577        let mut buckets: FastMap<(i64, i64, i64), (u32, u32)> = FastMap::default();
578        let mut next_in_cell: Vec<u32> = Vec::with_capacity(self.positions.len());
579        let mut remap = vec![0_u32; self.positions.len()];
580        let mut out = Self::new();
581        out.deflection_met = self.deflection_met;
582
583        for (index, position) in self.positions.iter().enumerate() {
584            let (kx, ky, kz) = key(*position);
585            let mut found = None;
586            'search: for dx in -1..=1 {
587                for dy in -1..=1 {
588                    for dz in -1..=1 {
589                        let Some(&(first, _)) = buckets.get(&(
590                            kx.saturating_add(dx),
591                            ky.saturating_add(dy),
592                            kz.saturating_add(dz),
593                        )) else {
594                            continue;
595                        };
596                        let mut candidate = first;
597                        while candidate != u32::MAX {
598                            if out.positions[candidate as usize].is_equal(*position, tol) {
599                                found = Some(candidate);
600                                break 'search;
601                            }
602                            candidate = next_in_cell[candidate as usize];
603                        }
604                    }
605                }
606            }
607
608            let target = found.unwrap_or_else(|| {
609                #[allow(clippy::cast_possible_truncation)]
610                let fresh = out.positions.len() as u32;
611                out.positions.push(*position);
612                // A mesh read from a file may carry positions alone; the
613                // optional per-vertex arrays follow only where they are full.
614                if let Some(&normal) = self.normals.get(index)
615                    && self.normals.len() == self.positions.len()
616                {
617                    out.normals.push(normal);
618                }
619                if let Some(&uv) = self.parameters.get(index)
620                    && self.parameters.len() == self.positions.len()
621                {
622                    out.parameters.push(uv);
623                }
624                next_in_cell.push(u32::MAX);
625                if let Some((_, last)) = buckets.get_mut(&(kx, ky, kz)) {
626                    next_in_cell[*last as usize] = fresh;
627                    *last = fresh;
628                } else {
629                    buckets.insert((kx, ky, kz), (fresh, fresh));
630                }
631                fresh
632            });
633            remap[index] = target;
634        }
635
636        for t in &self.triangles {
637            let mapped = t.map(|i| remap[i as usize]);
638            // A triangle whose corners merged is degenerate and contributes
639            // nothing but trouble to anything that divides by its area.
640            if mapped[0] != mapped[1] && mapped[1] != mapped[2] && mapped[2] != mapped[0] {
641                out.triangles.push(mapped);
642            }
643        }
644        out
645    }
646}
647
648/// Assign each candidate once per incident side, with stable ties at narrow
649/// corners. Hash lookups keep a densely subdivided side linear after sorting.
650fn nearest_split_sides<V: Copy + Eq + std::hash::Hash>(
651    corners: &[V; 3],
652    sides: [&[V]; 3],
653    mut distance: impl FnMut(usize, V) -> f64,
654) -> ogeom_core::FastMap<V, usize> {
655    let mut nearest: ogeom_core::FastMap<V, (usize, f64)> = ogeom_core::FastMap::default();
656    for (side, vertices) in sides.into_iter().enumerate() {
657        for &v in vertices {
658            if corners.contains(&v) {
659                continue;
660            }
661            let d = distance(side, v);
662            let entry = nearest.entry(v).or_insert((side, d));
663            if d.total_cmp(&entry.1).is_lt() {
664                *entry = (side, d);
665            }
666        }
667    }
668    nearest
669        .into_iter()
670        .map(|(v, (side, _))| (v, side))
671        .collect()
672}
673
674/// How many triangles use each undirected edge, keyed (smaller, larger).
675fn edge_uses(triangles: &[[u32; 3]]) -> ogeom_core::FastMap<(u32, u32), usize> {
676    let mut uses: ogeom_core::FastMap<(u32, u32), usize> = ogeom_core::FastMap::default();
677    // A closed mesh has three edges to every two triangles; sized for that,
678    // the table is never grown while it is filled.
679    uses.reserve(triangles.len() * 3 / 2 + 3);
680    for t in triangles {
681        for i in 0..3 {
682            let (a, b) = (t[i], t[(i + 1) % 3]);
683            *uses.entry((a.min(b), a.max(b))).or_default() += 1;
684        }
685    }
686    uses
687}
688
689/// Some of a mesh's vertices bucketed by a cubic grid, for the ones near a
690/// segment.
691struct PointGrid {
692    cell: f64,
693    /// Each occupied cell's vertices.
694    cells: ogeom_core::FastMap<(i64, i64, i64), Vec<u32>>,
695    /// The vertices, for a query whose box spans more cells than there are
696    /// vertices to look at.
697    all: Vec<u32>,
698}
699
700impl PointGrid {
701    /// `vertices` of `positions` in cells of about `spacing`, never finer
702    /// than `reach`; `None` where no finite cell size fits.
703    fn over(positions: &[Point], vertices: &[u32], spacing: f64, reach: f64) -> Option<Self> {
704        let cell = spacing.max(reach);
705        if !cell.is_finite() || cell <= 0.0 {
706            return None;
707        }
708        let mut cells: ogeom_core::FastMap<(i64, i64, i64), Vec<u32>> =
709            ogeom_core::FastMap::default();
710        for &v in vertices {
711            cells
712                .entry(Self::key(positions[v as usize], cell))
713                .or_default()
714                .push(v);
715        }
716        Some(Self {
717            cell,
718            cells,
719            all: vertices.to_vec(),
720        })
721    }
722
723    #[allow(clippy::cast_possible_truncation)]
724    fn key(p: Point, cell: f64) -> (i64, i64, i64) {
725        (
726            (p.x / cell).floor() as i64,
727            (p.y / cell).floor() as i64,
728            (p.z / cell).floor() as i64,
729        )
730    }
731
732    /// Every vertex that may lie within `reach` of the segment from `a` to
733    /// `b`, and some that do not: those in the cells round the segment's
734    /// box widened by `reach`, with a cell to spare each way for rounding.
735    fn within(&self, a: Point, b: Point, reach: f64, out: &mut Vec<u32>) {
736        let low = Point::new(
737            a.x.min(b.x) - reach,
738            a.y.min(b.y) - reach,
739            a.z.min(b.z) - reach,
740        );
741        let high = Point::new(
742            a.x.max(b.x) + reach,
743            a.y.max(b.y) + reach,
744            a.z.max(b.z) + reach,
745        );
746        let (lo, hi) = (Self::key(low, self.cell), Self::key(high, self.cell));
747        let span = |l: i64, h: i64| h.saturating_sub(l).saturating_add(3).unsigned_abs();
748        let boxed = span(lo.0, hi.0)
749            .saturating_mul(span(lo.1, hi.1))
750            .saturating_mul(span(lo.2, hi.2));
751        if boxed > self.cells.len() as u64 {
752            out.extend_from_slice(&self.all);
753            return;
754        }
755        for x in lo.0.saturating_sub(1)..=hi.0.saturating_add(1) {
756            for y in lo.1.saturating_sub(1)..=hi.1.saturating_add(1) {
757                for z in lo.2.saturating_sub(1)..=hi.2.saturating_add(1) {
758                    if let Some(found) = self.cells.get(&(x, y, z)) {
759                        out.extend_from_slice(found);
760                    }
761                }
762            }
763        }
764    }
765}
766
767#[cfg(test)]
768#[allow(clippy::unwrap_used)]
769mod tests {
770    use super::*;
771    use approx::assert_relative_eq;
772
773    const T: Tolerances = Tolerances::millimetres();
774
775    #[test]
776    fn a_mesh_of_positions_alone_welds() {
777        let mut mesh = Triangulation::new();
778        mesh.positions = vec![
779            Point::new(0.0, 0.0, 0.0),
780            Point::new(1.0, 0.0, 0.0),
781            Point::new(0.0, 1.0, 0.0),
782            Point::new(1.0, 0.0, 0.0),
783            Point::new(1.0, 1.0, 0.0),
784            Point::new(0.0, 1.0, 0.0),
785        ];
786        mesh.triangles = vec![[0, 1, 2], [3, 4, 5]];
787        let welded = mesh.welded(T);
788        assert_eq!(welded.positions.len(), 4);
789        assert!(welded.normals.is_empty() && welded.parameters.is_empty());
790        assert_eq!(welded.triangles, vec![[0, 1, 2], [1, 3, 2]]);
791    }
792
793    #[test]
794    fn an_empty_mesh_answers_sensibly() {
795        let mesh = Triangulation::new();
796        assert!(mesh.is_empty());
797        assert_eq!(mesh.triangle_count(), 0);
798        assert_relative_eq!(mesh.area(), 0.0);
799        assert_relative_eq!(mesh.volume(), 0.0);
800        assert!(!mesh.is_closed(), "nothing is not closed");
801        assert!(mesh.bounds().is_empty());
802    }
803
804    /// A closed mesh encloses the same volume wherever it stands. A unit
805    /// cube up to 1e8 from the origin has vertices exact in `f64` (spacing
806    /// there is 1.5e-8), so any error is the sum's own rounding.
807    #[test]
808    fn a_closed_mesh_far_from_the_origin_encloses_its_own_volume() {
809        for offset in [0.0, 1.0e6, 1.0e7, 1.0e8] {
810            let o = Vector::new(offset, -offset, offset);
811            let mut mesh = Triangulation::new();
812            for k in 0..8 {
813                let corner =
814                    Point::new(f64::from(k & 1), f64::from((k >> 1) & 1), f64::from(k >> 2));
815                mesh.positions.push(corner + o);
816            }
817            // Corner k sits at (k & 1, k >> 1 & 1, k >> 2); two triangles a
818            // side, each wound counter-clockwise seen from outside.
819            mesh.triangles = vec![
820                [0, 2, 1],
821                [1, 2, 3], // z = 0
822                [4, 5, 6],
823                [5, 7, 6], // z = 1
824                [0, 1, 4],
825                [1, 5, 4], // y = 0
826                [2, 6, 3],
827                [3, 6, 7], // y = 1
828                [0, 4, 2],
829                [2, 4, 6], // x = 0
830                [1, 3, 5],
831                [3, 7, 5], // x = 1
832            ];
833            assert!(mesh.is_closed(), "the cube closes at {offset:e}");
834            // The cube's own rounding is nil, so 1e-9 leaves room only for
835            // summing twelve terms of order one.
836            assert_relative_eq!(mesh.volume(), 1.0, epsilon = 1e-9);
837        }
838    }
839
840    /// A mesh of four corner positions, with whatever triangles are given.
841    fn over(points: &[Point], triangles: &[[u32; 3]]) -> Triangulation {
842        let mut mesh = Triangulation::new();
843        for p in points {
844            mesh.positions.push(*p);
845            mesh.normals.push(Vector::Z);
846            mesh.parameters.push((0.0, 0.0));
847        }
848        mesh.triangles.extend_from_slice(triangles);
849        mesh
850    }
851
852    /// Closure is a question about direction, not about how many.
853    ///
854    /// Counting an edge's uses answers the wrong question twice over: it
855    /// calls a mesh with one face inside out closed, and it calls a mesh
856    /// with four triangles round one edge open. Neither is what the
857    /// divergence theorem asks, which is only that the surface have no
858    /// boundary and wind one way.
859    #[test]
860    fn a_closed_mesh_is_one_crossed_as_often_each_way() {
861        let corners = [
862            Point::new(0.0, 0.0, 0.0),
863            Point::new(1.0, 0.0, 0.0),
864            Point::new(0.0, 1.0, 0.0),
865            Point::new(0.0, 0.0, 1.0),
866        ];
867        // A tetrahedron, every face wound outward.
868        let solid = over(&corners, &[[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]]);
869        assert!(solid.is_closed(), "a tetrahedron closes");
870        assert!(solid.volume() > 0.0, "and wound outward");
871
872        // One face turned over. Every edge is still used exactly twice, so
873        // counting uses calls this closed; two of its edges are now crossed
874        // the same way twice, and the volume it gives is wrong.
875        let mut flipped = solid.clone();
876        flipped.triangles[3] = [1, 3, 2];
877        assert!(
878            !flipped.is_closed(),
879            "a face inside out is not a closed mesh"
880        );
881
882        // A hole: one face dropped. Three edges are crossed once.
883        let mut holed = solid.clone();
884        holed.triangles.pop();
885        assert!(!holed.is_closed(), "three edges left dangling");
886
887        // Two tetrahedra sharing the edge 0-1, each closed and outward. The
888        // shared edge carries four triangles, two crossing each way. It is
889        // not a manifold and it is certainly closed, and its volume is both
890        // halves, which is the case a count of uses refuses and the one a
891        // real assembly produces where two faces fill a sliver with the same
892        // chord.
893        let mut pair = solid.clone();
894        let mirrored = Point::new(0.0, -1.0, 0.0);
895        #[allow(clippy::cast_possible_truncation)]
896        let m = pair.positions.len() as u32;
897        pair.positions.push(mirrored);
898        pair.normals.push(Vector::Z);
899        pair.parameters.push((0.0, 0.0));
900        let apex = 3;
901        pair.triangles
902            .extend_from_slice(&[[0, 1, m], [0, m, apex], [0, apex, 1], [1, apex, m]]);
903        let four = pair
904            .triangles
905            .iter()
906            .flat_map(|t| (0..3).map(move |i| (t[i], t[(i + 1) % 3])))
907            .filter(|(a, b)| (*a == 0 && *b == 1) || (*a == 1 && *b == 0))
908            .count();
909        assert_eq!(four, 4, "the shared edge carries four triangles");
910        assert!(pair.is_closed(), "and the pair is still closed");
911    }
912
913    #[test]
914    fn welding_drops_triangles_that_collapse() {
915        // Three corners that merge into one describe no area, and anything that
916        // divides by a triangle's area would divide by zero.
917        let mut mesh = Triangulation::new();
918        for _ in 0..3 {
919            mesh.positions.push(Point::ORIGIN);
920            mesh.normals.push(Vector::Z);
921            mesh.parameters.push((0.0, 0.0));
922        }
923        mesh.triangles.push([0, 1, 2]);
924        let welded = mesh.welded(T);
925        assert_eq!(welded.vertex_count(), 1);
926        assert_eq!(welded.triangle_count(), 0);
927    }
928
929    #[test]
930    fn appending_shifts_indices_rather_than_overlapping_them() {
931        let mut a = Triangulation::new();
932        a.positions.push(Point::ORIGIN);
933        a.normals.push(Vector::Z);
934        a.parameters.push((0.0, 0.0));
935
936        let mut b = Triangulation::new();
937        b.positions.push(Point::new(1.0, 0.0, 0.0));
938        b.normals.push(Vector::Z);
939        b.parameters.push((1.0, 0.0));
940        b.triangles.push([0, 0, 0]);
941
942        a.append(&b);
943        assert_eq!(a.vertex_count(), 2);
944        assert_eq!(
945            a.triangles[0],
946            [1, 1, 1],
947            "b's index moved past a's vertices"
948        );
949    }
950
951    #[test]
952    fn deflection_failure_propagates_through_the_whole_mesh() {
953        // One face that could not meet its tolerance makes the whole mesh's
954        // claim untrue, and the flag has to say so rather than being averaged
955        // away.
956        let mut a = Triangulation::new();
957        let mut b = Triangulation::new();
958        b.deflection_met = false;
959        a.append(&b);
960        assert!(!a.deflection_met);
961    }
962
963    /// A tetrahedron whose one face is a sliver a hundredth wide, dropped:
964    /// sealing at a width past the sliver's closes it again, and at a width
965    /// under it leaves it open. A triangle laid twice facing opposite ways
966    /// encloses nothing and is cancelled.
967    #[test]
968    fn sealing_closes_cracks_narrower_than_asked_and_cancels_folds() {
969        let corners = [
970            Point::new(0.0, 0.0, 0.0),
971            Point::new(1.0, 0.0, 0.0),
972            Point::new(0.5, 0.01, 0.0),
973            Point::new(0.5, 0.5, 1.0),
974        ];
975        let solid = over(&corners, &[[0, 2, 1], [0, 1, 3], [1, 2, 3], [2, 0, 3]]);
976        assert!(solid.is_closed());
977        assert!(solid.volume() > 0.0);
978        let mut cracked = solid.clone();
979        cracked.triangles.remove(0);
980        assert!(!cracked.is_closed());
981        let sealed = cracked.sealed(0.05);
982        assert!(sealed.is_closed(), "a crack under the width is sealed");
983        assert_relative_eq!(sealed.volume(), solid.volume(), epsilon = 1e-12);
984        assert!(
985            !cracked.sealed(1e-3).is_closed(),
986            "a crack wider than asked stays open"
987        );
988        let mut folded = solid.clone();
989        folded.triangles.push([0, 1, 3]);
990        folded.triangles.push([0, 3, 1]);
991        let unfolded = folded.sealed(0.0);
992        assert_eq!(
993            unfolded.triangle_count(),
994            solid.triangle_count(),
995            "the fold is cancelled"
996        );
997        assert!(unfolded.is_closed());
998        let (unfolded, sources) = folded.without_folds_with_sources();
999        assert_eq!(sources.len(), unfolded.triangles.len());
1000        for (triangle, source) in unfolded.triangles.iter().zip(sources) {
1001            assert_eq!(*triangle, folded.triangles[source]);
1002        }
1003    }
1004
1005    #[test]
1006    fn dense_border_assigns_each_candidate_once_per_side() {
1007        use std::cell::Cell;
1008        use std::hash::{Hash, Hasher};
1009
1010        #[derive(Clone, Copy)]
1011        struct Vertex<'a>(u32, &'a Cell<usize>);
1012        impl PartialEq for Vertex<'_> {
1013            fn eq(&self, other: &Self) -> bool {
1014                self.1.set(self.1.get() + 1);
1015                self.0 == other.0
1016            }
1017        }
1018        impl Eq for Vertex<'_> {}
1019        impl Hash for Vertex<'_> {
1020            fn hash<H: Hasher>(&self, state: &mut H) {
1021                self.0.hash(state);
1022            }
1023        }
1024
1025        let comparisons = Cell::new(0);
1026        let vertices: Vec<_> = (3..10_003).map(|v| Vertex(v, &comparisons)).collect();
1027        let corners = [
1028            Vertex(0, &comparisons),
1029            Vertex(1, &comparisons),
1030            Vertex(2, &comparisons),
1031        ];
1032        let mut visits = 0;
1033        let nearest =
1034            nearest_split_sides(&corners, [&vertices, &vertices[..20], &[]], |side, v| {
1035                visits += 1;
1036                // Twenty vertices also reach the adjacent side, which is nearer.
1037                if side == 1 && v.0 < 23 { 0.0 } else { 1.0 }
1038            });
1039        // Three corner checks per candidate and hash-table probes fit this
1040        // linear budget. A split-list scan alone takes over 50 million checks.
1041        assert!(
1042            comparisons.get() < 8 * vertices.len(),
1043            "{} comparisons",
1044            comparisons.get()
1045        );
1046        assert_eq!(visits, vertices.len() + 20);
1047        assert_eq!(nearest.len(), vertices.len());
1048        for v in vertices {
1049            assert_eq!(nearest[&v], usize::from(v.0 < 23));
1050        }
1051    }
1052
1053    /// A split on any of the three sides must create two nonzero triangles,
1054    /// including the sides incident to the first corner.
1055    #[test]
1056    fn stitching_splits_each_side_and_retains_triangle_sources() {
1057        for [a, b, c] in [[0, 1, 2], [1, 2, 0], [2, 0, 1]] {
1058            let mut mesh = over(
1059                &[
1060                    Point::new(0.0, 0.0, 0.0),
1061                    Point::new(2.0, 0.0, 0.0),
1062                    Point::new(0.0, 2.0, 0.0),
1063                ],
1064                &[[0, 1, 2]],
1065            );
1066            mesh.positions.push(Point::midpoint(
1067                mesh.positions[a as usize],
1068                mesh.positions[b as usize],
1069            ));
1070            // The neighbour is in another plane, already split at position 3.
1071            mesh.positions.push(Point::new(0.0, 0.0, 2.0));
1072            mesh.triangles.extend([[b, 3, 4], [3, a, 4]]);
1073            let (split, sources) = mesh.border_stitched_with_sources(1e-9);
1074            assert_eq!(split.triangles.len(), 4);
1075            assert_eq!(sources.iter().filter(|&&i| i == 0).count(), 2);
1076            for (t, source) in split.triangles.iter().zip(sources) {
1077                let [pa, pb, pc] = t.map(|i| split.positions[i as usize]);
1078                assert!((pb - pa).cross(pc - pa).magnitude() > 0.0);
1079                if source == 0 {
1080                    assert!(t.contains(&3) && t.contains(&c));
1081                    assert!((pb - pa).cross(pc - pa).z > 0.0);
1082                }
1083            }
1084        }
1085    }
1086
1087    /// The sliver crack of the tetrahedron above, moved up to 2e7 from the
1088    /// origin along a tilted direction: a crack's width is its own, so the
1089    /// same widths seal it and leave it open there as at the origin.
1090    #[test]
1091    fn sealing_far_from_the_origin_reads_a_crack_by_its_own_width() {
1092        for offset in [0.0, 1.0e6, 5.0e6, 1.0e7, 2.0e7] {
1093            let o = Vector::new(offset, -offset, offset);
1094            let corners = [
1095                Point::new(0.0, 0.0, 0.0) + o,
1096                Point::new(1.0, 0.0, 0.0) + o,
1097                Point::new(0.5, 0.01, 0.0) + o,
1098                Point::new(0.5, 0.5, 1.0) + o,
1099            ];
1100            let cracked = over(&corners, &[[0, 1, 3], [1, 2, 3], [2, 0, 3]]);
1101            assert!(
1102                cracked.sealed(0.05).is_closed(),
1103                "a crack under the width is sealed at {offset:e}"
1104            );
1105            assert!(
1106                !cracked.sealed(1e-3).is_closed(),
1107                "a crack wider than asked stays open at {offset:e}"
1108            );
1109        }
1110    }
1111
1112    /// Two strips meeting along a line, one drawn to twice the other's
1113    /// spacing there: every segment of the coarse side has a vertex of the
1114    /// fine side at its middle, and stitching splits each at it.
1115    #[test]
1116    fn a_coarse_border_is_split_at_its_neighbours_vertices() {
1117        const N: u32 = 60;
1118        let mut mesh = Triangulation::new();
1119        // The fine strip: a bottom row and a shared row at y = 1.
1120        for y in [0.0, 1.0] {
1121            for x in 0..=N {
1122                mesh.positions.push(Point::new(f64::from(x), y, 0.0));
1123            }
1124        }
1125        let top = |x: u32| N + 1 + x;
1126        for x in 0..N {
1127            mesh.triangles.push([x, x + 1, top(x + 1)]);
1128            mesh.triangles.push([x, top(x + 1), top(x)]);
1129        }
1130        // The coarse strip above, on every other vertex of the shared row.
1131        let apex = top(N) + 1;
1132        for x in (0..=N).step_by(2) {
1133            mesh.positions.push(Point::new(f64::from(x), 2.0, 0.0));
1134        }
1135        for (i, x) in (0..N).step_by(2).enumerate() {
1136            let i = u32::try_from(i).unwrap();
1137            let (a, b) = (apex + i, apex + i + 1);
1138            mesh.triangles.push([top(x), top(x + 2), b]);
1139            mesh.triangles.push([top(x), b, a]);
1140        }
1141
1142        let stitched = mesh.border_stitched(1e-6);
1143        let uses = edge_uses(&stitched.triangles);
1144        for x in 0..N {
1145            assert_eq!(
1146                uses.get(&(top(x), top(x + 1))),
1147                Some(&2),
1148                "the shared row's segment at {x} is used from both sides"
1149            );
1150        }
1151        assert_eq!(stitched.positions, mesh.positions, "no vertex moves");
1152    }
1153}