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.
24    pub normals: Vec<Vector>,
25    /// The surface parameters each vertex came from.
26    pub parameters: Vec<(f64, f64)>,
27    /// Triangles, as indices into the vertex arrays, wound counter-clockwise
28    /// about the outward normal.
29    pub triangles: Vec<[u32; 3]>,
30    /// Whether every face met its requested deflection.
31    pub deflection_met: bool,
32}
33
34impl Triangulation {
35    /// An empty mesh.
36    #[must_use]
37    pub fn new() -> Self {
38        Self {
39            deflection_met: true,
40            ..Self::default()
41        }
42    }
43
44    /// Number of vertices.
45    #[must_use]
46    pub fn vertex_count(&self) -> usize {
47        self.positions.len()
48    }
49
50    /// Number of triangles.
51    #[must_use]
52    pub fn triangle_count(&self) -> usize {
53        self.triangles.len()
54    }
55
56    /// Whether the mesh holds no triangles.
57    #[must_use]
58    pub fn is_empty(&self) -> bool {
59        self.triangles.is_empty()
60    }
61
62    /// The bounding box of the vertices.
63    #[must_use]
64    pub fn bounds(&self) -> Aabb {
65        Aabb::of_points(&self.positions)
66    }
67
68    /// The total area of the triangles.
69    ///
70    /// An *under*estimate of the surface's own area for a convex patch, since a
71    /// triangle chord-cuts the surface it spans. It converges from below as the
72    /// deflection tightens.
73    #[must_use]
74    pub fn area(&self) -> f64 {
75        self.triangles
76            .iter()
77            .map(|t| {
78                let [a, b, c] = t.map(|i| self.positions[i as usize]);
79                (b - a).cross(c - a).magnitude() * 0.5
80            })
81            .sum()
82    }
83
84    /// The signed volume enclosed, by the divergence theorem.
85    ///
86    /// Meaningful only for a mesh that is closed and consistently wound
87    /// outward: each triangle contributes the signed volume of the tetrahedron
88    /// it forms with the origin, and the contributions cancel except over the
89    /// enclosed region. An open mesh gives a number with no meaning, and a mesh
90    /// wound inward gives the negative, which is why
91    /// [`Triangulation::is_closed`] exists to be asked first.
92    #[must_use]
93    pub fn volume(&self) -> f64 {
94        self.triangles
95            .iter()
96            .map(|t| {
97                let [a, b, c] = t.map(|i| self.positions[i as usize].to_vector());
98                a.dot(b.cross(c)) / 6.0
99            })
100            .sum()
101    }
102
103    /// Whether every triangle edge is crossed as often one way as the other.
104    ///
105    /// The mesh equivalent of a closed shell, and the precondition for
106    /// [`Triangulation::volume`] meaning anything. It is exactly what the
107    /// divergence theorem needs: the surface has no boundary, and it is
108    /// wound consistently, so each triangle's contribution cancels against
109    /// its neighbours' except over the region enclosed.
110    ///
111    /// Counting *directed* edges rather than undirected ones is what makes
112    /// this the right question, and it is stricter and looser than the
113    /// obvious test in the two different ways that matter.
114    ///
115    /// Stricter: two triangles sharing an edge and winding the *same* way
116    /// round it traverse it twice in the same direction. The edge is used
117    /// twice, so a count of uses calls it closed, and the volume that comes
118    /// out is wrong because one of the two faces is inside out.
119    ///
120    /// Looser: an edge may legitimately carry four triangles. Where two
121    /// faces meet along a short edge that discretizes into several segments,
122    /// each can fill the sliver between the polyline and its own chord, and
123    /// the chord then belongs to both: four triangles round one edge, two
124    /// crossing each way. There is no hole there and the volume is right;
125    /// demanding exactly two refuses a mesh for being non-manifold when
126    /// nothing was asked about manifoldness. Sixty-four bodies of one real
127    /// assembly were refused that way, forty-four of them for this alone.
128    ///
129    /// This also agrees with the topology side at last:
130    /// [`is_shell_closed`](../../ogeom_algo/fn.is_shell_closed.html) counts an
131    /// edge's uses and accepts any even number, and the two halves of the
132    /// kernel should not mean different things by the same word.
133    #[must_use]
134    pub fn is_closed(&self) -> bool {
135        use std::collections::HashMap;
136        let mut balance: HashMap<(u32, u32), i64> = HashMap::new();
137        for t in &self.triangles {
138            for i in 0..3 {
139                let (a, b) = (t[i], t[(i + 1) % 3]);
140                // One key per undirected edge; the direction decides the sign.
141                let (key, step) = if a <= b { ((a, b), 1) } else { ((b, a), -1) };
142                *balance.entry(key).or_default() += step;
143            }
144        }
145        !balance.is_empty() && balance.values().all(|&n| n == 0)
146    }
147
148    /// Weld only the mesh's *border* vertices, within `reach`.
149    ///
150    /// The second pass after [`Triangulation::welded`]: interior edges are
151    /// already manifold, and touching them at a widened tolerance would eat
152    /// real features. Borders are where imported slop lives (an edge's curve
153    /// and its neighbour's disagree by the file's own tolerance, which the
154    /// model records on the edge), so only vertices on unmatched triangle
155    /// edges are candidates, merged to their nearest counterpart within
156    /// `reach`.
157    #[must_use]
158    pub fn border_welded(&self, reach: f64) -> Self {
159        use std::collections::HashMap;
160        if !reach.is_finite() || reach <= 0.0 {
161            return self.clone();
162        }
163        // Border vertices: endpoints of triangle edges used an odd number of
164        // times.
165        let mut uses: HashMap<(u32, u32), usize> = HashMap::new();
166        for t in &self.triangles {
167            for i in 0..3 {
168                let (a, b) = (t[i], t[(i + 1) % 3]);
169                *uses.entry((a.min(b), a.max(b))).or_default() += 1;
170            }
171        }
172        let mut border: Vec<u32> = uses
173            .iter()
174            .filter(|&(_, &n)| n % 2 == 1)
175            .flat_map(|(&(a, b), _)| [a, b])
176            .collect();
177        border.sort_unstable();
178        border.dedup();
179        if border.is_empty() {
180            return self.clone();
181        }
182
183        // Cluster border vertices within reach, first-seen wins, checked
184        // against the cluster representative so chains cannot creep.
185        let cell = reach.max(f64::MIN_POSITIVE);
186        let key = |p: Point| {
187            #[allow(clippy::cast_possible_truncation)]
188            (
189                (p.x / cell).round() as i64,
190                (p.y / cell).round() as i64,
191                (p.z / cell).round() as i64,
192            )
193        };
194        let mut buckets: HashMap<(i64, i64, i64), Vec<u32>> = HashMap::new();
195        #[allow(clippy::cast_possible_truncation)]
196        let mut remap: Vec<u32> = (0..self.positions.len() as u32).collect();
197        for &v in &border {
198            let p = self.positions[v as usize];
199            let (kx, ky, kz) = key(p);
200            let mut found = None;
201            'search: for dx in -1..=1 {
202                for dy in -1..=1 {
203                    for dz in -1..=1 {
204                        for &candidate in buckets
205                            .get(&(kx + dx, ky + dy, kz + dz))
206                            .map_or(&[][..], Vec::as_slice)
207                        {
208                            if self.positions[candidate as usize].distance(p) <= reach {
209                                found = Some(candidate);
210                                break 'search;
211                            }
212                        }
213                    }
214                }
215            }
216            match found {
217                Some(rep) => remap[v as usize] = rep,
218                None => buckets.entry((kx, ky, kz)).or_default().push(v),
219            }
220        }
221
222        let mut out = Self::new();
223        out.deflection_met = self.deflection_met;
224        // Compact: keep every vertex that survives as its own representative
225        // or is referenced; simplest is to keep all and let triangles remap.
226        out.positions = self.positions.clone();
227        out.normals = self.normals.clone();
228        out.parameters = self.parameters.clone();
229        for t in &self.triangles {
230            let mapped = t.map(|i| remap[i as usize]);
231            if mapped[0] != mapped[1] && mapped[1] != mapped[2] && mapped[2] != mapped[0] {
232                out.triangles.push(mapped);
233            }
234        }
235        out
236    }
237
238    /// Split border segments at border vertices that lie on them.
239    ///
240    /// The T-junction repair that follows [`Triangulation::border_welded`]:
241    /// after welding, two faces' border chains share their vertices but may
242    /// subdivide the same stretch differently: one face's segment spans two
243    /// of its neighbour's. Splitting the long segment *at the neighbour's own
244    /// vertex index* makes the chains segment-for-segment identical, which is
245    /// what closure counts. No positions move and none are added.
246    #[must_use]
247    pub fn border_stitched(&self, reach: f64) -> Self {
248        use std::collections::HashMap;
249        if !reach.is_finite() || reach <= 0.0 {
250            return self.clone();
251        }
252        let mut uses: HashMap<(u32, u32), usize> = HashMap::new();
253        for t in &self.triangles {
254            for i in 0..3 {
255                let (a, b) = (t[i], t[(i + 1) % 3]);
256                *uses.entry((a.min(b), a.max(b))).or_default() += 1;
257            }
258        }
259        let border_edges: Vec<(u32, u32)> = uses
260            .iter()
261            .filter(|&(_, &n)| n % 2 == 1)
262            .map(|(&e, _)| e)
263            .collect();
264        if border_edges.is_empty() {
265            return self.clone();
266        }
267        let mut border_vertices: Vec<u32> =
268            border_edges.iter().flat_map(|&(a, b)| [a, b]).collect();
269        border_vertices.sort_unstable();
270        border_vertices.dedup();
271
272        // For every border segment, the border vertices sitting on its
273        // interior, ordered along it.
274        let mut splits: HashMap<(u32, u32), Vec<u32>> = HashMap::new();
275        for &(a, b) in &border_edges {
276            let (pa, pb) = (self.positions[a as usize], self.positions[b as usize]);
277            let d = pb - pa;
278            let l2 = d.dot(d);
279            if l2 <= 0.0 {
280                continue;
281            }
282            let mut on: Vec<(f64, u32)> = border_vertices
283                .iter()
284                .filter(|&&v| v != a && v != b)
285                .filter_map(|&v| {
286                    let p = self.positions[v as usize];
287                    let t = (p - pa).dot(d) / l2;
288                    if !(0.001..=0.999).contains(&t) {
289                        return None;
290                    }
291                    ((pa + d * t).distance(p) <= reach).then_some((t, v))
292                })
293                .collect();
294            if on.is_empty() {
295                continue;
296            }
297            on.sort_by(|x, y| x.0.total_cmp(&y.0));
298            splits.insert((a, b), on.into_iter().map(|(_, v)| v).collect());
299        }
300        if splits.is_empty() {
301            return self.clone();
302        }
303
304        let mut out = Self::new();
305        out.deflection_met = self.deflection_met;
306        out.positions = self.positions.clone();
307        out.normals = self.normals.clone();
308        out.parameters = self.parameters.clone();
309        for t in &self.triangles {
310            // The triangle's ring with any split points inserted, fanned from
311            // its first corner.
312            let mut ring: Vec<u32> = Vec::with_capacity(6);
313            let mut any = false;
314            for i in 0..3 {
315                let (a, b) = (t[i], t[(i + 1) % 3]);
316                ring.push(a);
317                if let Some(vs) = splits.get(&(a.min(b), a.max(b))) {
318                    any = true;
319                    if a < b {
320                        ring.extend(vs.iter().copied());
321                    } else {
322                        ring.extend(vs.iter().rev().copied());
323                    }
324                }
325            }
326            if !any {
327                out.triangles.push(*t);
328                continue;
329            }
330            for i in 1..ring.len() - 1 {
331                let tri = [ring[0], ring[i], ring[i + 1]];
332                if tri[0] != tri[1] && tri[1] != tri[2] && tri[2] != tri[0] {
333                    out.triangles.push(tri);
334                }
335            }
336        }
337        out
338    }
339
340    /// This mesh with its folds cancelled and its cracks sealed: the last
341    /// pass over a mesh welded from faces that each met their edges.
342    ///
343    /// A fold is two triangles on the same three vertices facing opposite
344    /// ways, left where a sliver face collapses in the weld; they enclose
345    /// nothing, and both go. A crack is a loop of border edges whose mean
346    /// width (twice its area over its perimeter) is within `width`: two
347    /// faces sampling a shared corner differently leave one, narrower than
348    /// the chord they were drawn to. It is fanned shut from one of its
349    /// corners, each new triangle crossing a border edge the other way from
350    /// the triangle already on it. A loop wider than that, or a border
351    /// vertex with more than one way on, is a real opening and stays.
352    #[must_use]
353    pub fn sealed(&self, width: f64) -> Self {
354        use std::collections::HashMap;
355        let mut out = self.clone();
356        // Folds.
357        let mut seen: HashMap<[u32; 3], Vec<usize>> = HashMap::new();
358        for (i, t) in out.triangles.iter().enumerate() {
359            let mut key = *t;
360            key.sort_unstable();
361            seen.entry(key).or_default().push(i);
362        }
363        let mut drop = vec![false; out.triangles.len()];
364        for list in seen.values() {
365            let mut open: Vec<usize> = Vec::new();
366            for &i in list {
367                let t = out.triangles[i];
368                let reverse = open.iter().position(|&j| {
369                    let u = out.triangles[j];
370                    (0..3).any(|k| [u[k], u[(k + 2) % 3], u[(k + 1) % 3]] == t)
371                });
372                match reverse {
373                    Some(at) => {
374                        drop[open.remove(at)] = true;
375                        drop[i] = true;
376                    }
377                    None => open.push(i),
378                }
379            }
380        }
381        let mut index = 0;
382        out.triangles.retain(|_| {
383            let keep = !drop[index];
384            index += 1;
385            keep
386        });
387        if !width.is_finite() || width <= 0.0 {
388            return out;
389        }
390        // Cracks: each border edge walked the other way from its triangle.
391        let mut uses: HashMap<(u32, u32), usize> = HashMap::new();
392        for t in &out.triangles {
393            for k in 0..3 {
394                let (a, b) = (t[k], t[(k + 1) % 3]);
395                *uses.entry((a.min(b), a.max(b))).or_default() += 1;
396            }
397        }
398        let mut onward: HashMap<u32, Vec<u32>> = HashMap::new();
399        for t in &out.triangles {
400            for k in 0..3 {
401                let (a, b) = (t[k], t[(k + 1) % 3]);
402                if uses[&(a.min(b), a.max(b))] == 1 {
403                    onward.entry(b).or_default().push(a);
404                }
405            }
406        }
407        let mut done: std::collections::HashSet<u32> = std::collections::HashSet::new();
408        let mut starts: Vec<u32> = onward.keys().copied().collect();
409        starts.sort_unstable();
410        for start in starts {
411            if done.contains(&start) {
412                continue;
413            }
414            let mut ring = vec![start];
415            let mut at = start;
416            let closed = loop {
417                let Some(next) = onward.get(&at) else {
418                    break false;
419                };
420                let [next] = next[..] else {
421                    break false;
422                };
423                if next == start {
424                    break true;
425                }
426                if ring.contains(&next) || ring.len() > onward.len() {
427                    break false;
428                }
429                ring.push(next);
430                at = next;
431            };
432            for &v in &ring {
433                done.insert(v);
434            }
435            if !closed || ring.len() < 3 {
436                continue;
437            }
438            let points: Vec<Point> = ring.iter().map(|&v| out.positions[v as usize]).collect();
439            let mut normal = Vector::ZERO;
440            let mut perimeter = 0.0;
441            for (i, p) in points.iter().enumerate() {
442                let q = points[(i + 1) % points.len()];
443                normal += p.to_vector().cross(q.to_vector());
444                perimeter += p.distance(q);
445            }
446            let area = normal.magnitude() / 2.0;
447            if perimeter <= 0.0 || 2.0 * area / perimeter > width {
448                continue;
449            }
450            for i in 1..ring.len() - 1 {
451                out.triangles.push([ring[0], ring[i], ring[i + 1]]);
452            }
453        }
454        out
455    }
456
457    /// Append another mesh, shifting its indices.
458    pub fn append(&mut self, other: &Self) {
459        #[allow(clippy::cast_possible_truncation)]
460        let offset = self.positions.len() as u32;
461        self.positions.extend_from_slice(&other.positions);
462        self.normals.extend_from_slice(&other.normals);
463        self.parameters.extend_from_slice(&other.parameters);
464        self.triangles
465            .extend(other.triangles.iter().map(|t| t.map(|i| i + offset)));
466        self.deflection_met &= other.deflection_met;
467    }
468
469    /// Merge vertices that coincide within `tol`, rewiring the triangles.
470    ///
471    /// Faces are triangulated independently, so a shared edge produces two
472    /// copies of every boundary vertex, at identical positions, since both
473    /// came from the same edge discretization, but as separate entries. Merging
474    /// them is what turns a pile of face meshes into one closed surface, and
475    /// what lets [`Triangulation::is_closed`] answer truthfully.
476    #[must_use]
477    pub fn welded(&self, tol: Tolerances) -> Self {
478        use std::collections::HashMap;
479
480        // Quantize to a grid a good deal finer than the tolerance, then check
481        // the neighbourhood: hashing alone would separate two points that
482        // straddle a cell boundary however close they are.
483        let cell = tol.confusion().max(f64::MIN_POSITIVE);
484        let key = |p: Point| {
485            #[allow(clippy::cast_possible_truncation)]
486            (
487                (p.x / cell).round() as i64,
488                (p.y / cell).round() as i64,
489                (p.z / cell).round() as i64,
490            )
491        };
492
493        let mut buckets: HashMap<(i64, i64, i64), Vec<u32>> = HashMap::new();
494        let mut remap = vec![0_u32; self.positions.len()];
495        let mut out = Self::new();
496        out.deflection_met = self.deflection_met;
497
498        for (index, position) in self.positions.iter().enumerate() {
499            let (kx, ky, kz) = key(*position);
500            let mut found = None;
501            'search: for dx in -1..=1 {
502                for dy in -1..=1 {
503                    for dz in -1..=1 {
504                        for &candidate in buckets
505                            .get(&(kx + dx, ky + dy, kz + dz))
506                            .map_or(&[][..], Vec::as_slice)
507                        {
508                            if out.positions[candidate as usize].is_equal(*position, tol) {
509                                found = Some(candidate);
510                                break 'search;
511                            }
512                        }
513                    }
514                }
515            }
516
517            let target = found.unwrap_or_else(|| {
518                #[allow(clippy::cast_possible_truncation)]
519                let fresh = out.positions.len() as u32;
520                out.positions.push(*position);
521                out.normals.push(self.normals[index]);
522                out.parameters.push(self.parameters[index]);
523                buckets.entry((kx, ky, kz)).or_default().push(fresh);
524                fresh
525            });
526            remap[index] = target;
527        }
528
529        for t in &self.triangles {
530            let mapped = t.map(|i| remap[i as usize]);
531            // A triangle whose corners merged is degenerate and contributes
532            // nothing but trouble to anything that divides by its area.
533            if mapped[0] != mapped[1] && mapped[1] != mapped[2] && mapped[2] != mapped[0] {
534                out.triangles.push(mapped);
535            }
536        }
537        out
538    }
539}
540
541#[cfg(test)]
542#[allow(clippy::unwrap_used)]
543mod tests {
544    use super::*;
545    use approx::assert_relative_eq;
546
547    const T: Tolerances = Tolerances::millimetres();
548
549    #[test]
550    fn an_empty_mesh_answers_sensibly() {
551        let mesh = Triangulation::new();
552        assert!(mesh.is_empty());
553        assert_eq!(mesh.triangle_count(), 0);
554        assert_relative_eq!(mesh.area(), 0.0);
555        assert_relative_eq!(mesh.volume(), 0.0);
556        assert!(!mesh.is_closed(), "nothing is not closed");
557        assert!(mesh.bounds().is_empty());
558    }
559
560    /// A mesh of four corner positions, with whatever triangles are given.
561    fn over(points: &[Point], triangles: &[[u32; 3]]) -> Triangulation {
562        let mut mesh = Triangulation::new();
563        for p in points {
564            mesh.positions.push(*p);
565            mesh.normals.push(Vector::Z);
566            mesh.parameters.push((0.0, 0.0));
567        }
568        mesh.triangles.extend_from_slice(triangles);
569        mesh
570    }
571
572    /// Closure is a question about direction, not about how many.
573    ///
574    /// Counting an edge's uses answers the wrong question twice over: it
575    /// calls a mesh with one face inside out closed, and it calls a mesh
576    /// with four triangles round one edge open. Neither is what the
577    /// divergence theorem asks, which is only that the surface have no
578    /// boundary and wind one way.
579    #[test]
580    fn a_closed_mesh_is_one_crossed_as_often_each_way() {
581        let corners = [
582            Point::new(0.0, 0.0, 0.0),
583            Point::new(1.0, 0.0, 0.0),
584            Point::new(0.0, 1.0, 0.0),
585            Point::new(0.0, 0.0, 1.0),
586        ];
587        // A tetrahedron, every face wound outward.
588        let solid = over(&corners, &[[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]]);
589        assert!(solid.is_closed(), "a tetrahedron closes");
590        assert!(solid.volume() > 0.0, "and wound outward");
591
592        // One face turned over. Every edge is still used exactly twice, so
593        // counting uses calls this closed; two of its edges are now crossed
594        // the same way twice, and the volume it gives is wrong.
595        let mut flipped = solid.clone();
596        flipped.triangles[3] = [1, 3, 2];
597        assert!(
598            !flipped.is_closed(),
599            "a face inside out is not a closed mesh"
600        );
601
602        // A hole: one face dropped. Three edges are crossed once.
603        let mut holed = solid.clone();
604        holed.triangles.pop();
605        assert!(!holed.is_closed(), "three edges left dangling");
606
607        // Two tetrahedra sharing the edge 0-1, each closed and outward. The
608        // shared edge carries four triangles, two crossing each way. It is
609        // not a manifold and it is certainly closed, and its volume is both
610        // halves, which is the case a count of uses refuses and the one a
611        // real assembly produces where two faces fill a sliver with the same
612        // chord.
613        let mut pair = solid.clone();
614        let mirrored = Point::new(0.0, -1.0, 0.0);
615        #[allow(clippy::cast_possible_truncation)]
616        let m = pair.positions.len() as u32;
617        pair.positions.push(mirrored);
618        pair.normals.push(Vector::Z);
619        pair.parameters.push((0.0, 0.0));
620        let apex = 3;
621        pair.triangles
622            .extend_from_slice(&[[0, 1, m], [0, m, apex], [0, apex, 1], [1, apex, m]]);
623        let four = pair
624            .triangles
625            .iter()
626            .flat_map(|t| (0..3).map(move |i| (t[i], t[(i + 1) % 3])))
627            .filter(|(a, b)| (*a == 0 && *b == 1) || (*a == 1 && *b == 0))
628            .count();
629        assert_eq!(four, 4, "the shared edge carries four triangles");
630        assert!(pair.is_closed(), "and the pair is still closed");
631    }
632
633    #[test]
634    fn welding_drops_triangles_that_collapse() {
635        // Three corners that merge into one describe no area, and anything that
636        // divides by a triangle's area would divide by zero.
637        let mut mesh = Triangulation::new();
638        for _ in 0..3 {
639            mesh.positions.push(Point::ORIGIN);
640            mesh.normals.push(Vector::Z);
641            mesh.parameters.push((0.0, 0.0));
642        }
643        mesh.triangles.push([0, 1, 2]);
644        let welded = mesh.welded(T);
645        assert_eq!(welded.vertex_count(), 1);
646        assert_eq!(welded.triangle_count(), 0);
647    }
648
649    #[test]
650    fn appending_shifts_indices_rather_than_overlapping_them() {
651        let mut a = Triangulation::new();
652        a.positions.push(Point::ORIGIN);
653        a.normals.push(Vector::Z);
654        a.parameters.push((0.0, 0.0));
655
656        let mut b = Triangulation::new();
657        b.positions.push(Point::new(1.0, 0.0, 0.0));
658        b.normals.push(Vector::Z);
659        b.parameters.push((1.0, 0.0));
660        b.triangles.push([0, 0, 0]);
661
662        a.append(&b);
663        assert_eq!(a.vertex_count(), 2);
664        assert_eq!(
665            a.triangles[0],
666            [1, 1, 1],
667            "b's index moved past a's vertices"
668        );
669    }
670
671    #[test]
672    fn deflection_failure_propagates_through_the_whole_mesh() {
673        // One face that could not meet its tolerance makes the whole mesh's
674        // claim untrue, and the flag has to say so rather than being averaged
675        // away.
676        let mut a = Triangulation::new();
677        let mut b = Triangulation::new();
678        b.deflection_met = false;
679        a.append(&b);
680        assert!(!a.deflection_met);
681    }
682
683    /// A tetrahedron whose one face is a sliver a hundredth wide, dropped:
684    /// sealing at a width past the sliver's closes it again, and at a width
685    /// under it leaves it open. A triangle laid twice facing opposite ways
686    /// encloses nothing and is cancelled.
687    #[test]
688    fn sealing_closes_cracks_narrower_than_asked_and_cancels_folds() {
689        let corners = [
690            Point::new(0.0, 0.0, 0.0),
691            Point::new(1.0, 0.0, 0.0),
692            Point::new(0.5, 0.01, 0.0),
693            Point::new(0.5, 0.5, 1.0),
694        ];
695        let solid = over(&corners, &[[0, 2, 1], [0, 1, 3], [1, 2, 3], [2, 0, 3]]);
696        assert!(solid.is_closed());
697        assert!(solid.volume() > 0.0);
698        let mut cracked = solid.clone();
699        cracked.triangles.remove(0);
700        assert!(!cracked.is_closed());
701        let sealed = cracked.sealed(0.05);
702        assert!(sealed.is_closed(), "a crack under the width is sealed");
703        assert_relative_eq!(sealed.volume(), solid.volume(), epsilon = 1e-12);
704        assert!(
705            !cracked.sealed(1e-3).is_closed(),
706            "a crack wider than asked stays open"
707        );
708        let mut folded = solid.clone();
709        folded.triangles.push([0, 1, 3]);
710        folded.triangles.push([0, 3, 1]);
711        let unfolded = folded.sealed(0.0);
712        assert_eq!(
713            unfolded.triangle_count(),
714            solid.triangle_count(),
715            "the fold is cancelled"
716        );
717        assert!(unfolded.is_closed());
718    }
719}