Skip to main content

pantometry_shape/
voxels.rs

1//! A mesh rasterised into cells, and an account of what the cells could not hold.
2
3use crate::mesh::Mesh;
4use glam::DVec3;
5use pantometry_units::{Length, LengthVec, Volume};
6
7/// What a rasterisation lost, reported rather than left to be discovered.
8///
9/// A missing feature has no symptom. It does not make a solver fail, produce a `NaN` or trip the
10/// conservation audit — it produces a smooth, plausible answer about a different object, which is the
11/// failure this workspace is organised around not having. So every [`Voxels`] carries one of these and
12/// [`Loss::is_clean`] says in one call whether anything here needs reading.
13#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct Loss {
15    /// `voxel volume / mesh volume − 1`.
16    ///
17    /// The aggregate of what the grid kept and did not, and it is **signed**, because the cells the
18    /// surface bulges out of and the ones it cuts into partly cancel.
19    ///
20    /// That cancellation is why this is *not* a discretisation error with an order, and it is worth
21    /// saying plainly because the expectation is so natural. Rasterising a sphere of radius 10 mm — a
22    /// 32×64 tessellation, and the numbers move a few tenths of a point with that — at 2.5, 2.0 and
23    /// 1.5 mm gives `+4.9%, +5.8%, −2.3%`: the first refinement makes it **worse** and the second changes
24    /// its sign. Sliding the mesh relative to the grid changes none of it. What is left after the
25    /// cancellation is a lattice-point count, and its error is erratic by nature.
26    ///
27    /// So use it as a *result* and not as a bound on the next resolution.
28    pub volume_error: f64,
29    /// The share of the voxel volume in cells that have a face on the outside.
30    ///
31    /// **The rasterisation's uncertainty rather than its error**: the volume that sat close enough to the
32    /// surface for the answer to have gone either way.
33    ///
34    /// Unlike the volume error this is clean, because it is a surface area rather than a cancellation:
35    /// the layer is one cell thick over an area `A`, so the fraction is `A·dx/V` — **first order, with a
36    /// coefficient**. For a sphere that is `c·3dx/R`, and the tests measure `c ≈ 0.82`.
37    ///
38    /// It is the number to put in front of someone choosing a cell size. *Forty-three percent of your
39    /// object's volume is in cells that could have gone either way* says what 2 mm on a 20 mm ball means
40    /// in a way that a step count does not.
41    ///
42    /// # It does **not** bound the volume error, and an earlier version of this sentence said it did
43    ///
44    /// The argument is seductive and wrong: "only cells the surface passes through can be
45    /// misclassified, and those are these". They are not. This counts exposed cells that came out
46    /// **filled**; a cell the surface passes through whose centre landed outside is misclassified too and
47    /// appears in neither the numerator nor the denominator. The two are also fractions of different
48    /// volumes — this of the voxel volume, the error of the mesh's.
49    ///
50    /// The 0.4 mm plate in `a_designed_shape.rs` is the counterexample and it was in the suite the whole
51    /// time: at a 2 mm cell it reports a volume error of `+4.0` against a boundary fraction of `1.0`.
52    pub boundary_fraction: f64,
53    /// Solid runs one or two cells thick, counted along all three axes.
54    ///
55    /// A feature two cells across is not resolved by any scheme here — a seven-point stencil has no
56    /// interior in it, a trilinear element has one element — and a feature *one* cell across exists only
57    /// by luck of where the surface fell relative to the cell centres. Move the mesh half a cell and it
58    /// may not be there at all.
59    pub thin_runs: usize,
60    /// Triangles smaller than one face of a cell. See [`Mesh::triangles_below`].
61    pub small_triangles: usize,
62    /// Scanlines whose first ray was degenerate and which a perturbed one decided.
63    ///
64    /// Not a loss — these rows came out right — but the count says the mesh has geometry lined up with
65    /// the grid, which is the common case rather than the exotic one: a cube on cell boundaries sends
66    /// **every** row through a face diagonal, and this reads in the hundreds.
67    ///
68    /// It is here because the alternative is a mechanism nothing can see working. [`Loss::ambiguous_rows`]
69    /// counts only the rows where *all* the perturbations failed, and no mesh in the test suite has ever
70    /// produced one — so without this field the whole retry path would be exercised only by inference
71    /// from a cell count, and a broken retry would look exactly like a mesh that never needed it.
72    pub retried_rows: usize,
73    /// Scanlines no ray could decide, after every perturbation was tried.
74    ///
75    /// A ray through a closed surface must cross it an even number of times, and must not pass through
76    /// an edge. A row that fails both tests on all four rays cannot be filled by parity. **These rows are
77    /// left empty**, which is visible as a slot missing from the shape rather than as a subtly wrong
78    /// fill — and the count is here so it is not only visible in a picture.
79    ///
80    /// **No mesh in the test suite has produced one**, which is stated rather than left to be assumed:
81    /// the branch is written and reasoned about and is not covered by a measurement, so a caller who sees
82    /// a nonzero value here is in territory this crate has not walked. [`Loss::retried_rows`] is the
83    /// nearby thing that *is* exercised.
84    pub ambiguous_rows: usize,
85}
86
87impl Loss {
88    /// The volume error [`Loss::is_clean`] tolerates: **2%**.
89    ///
90    /// A constant rather than a literal inside the comparison, because a caller that reports *why* a
91    /// rasterisation is unclean has to name the bar, and a second copy of `0.02` in that message is a
92    /// number that can drift away from the one actually applied. `pantometry-world`'s verify battery
93    /// is that caller.
94    pub const CLEAN_VOLUME_ERROR: f64 = 0.02;
95
96    /// Whether anything was lost that a caller should look at.
97    ///
98    /// The thresholds are deliberately loose and deliberately stated: [`Loss::CLEAN_VOLUME_ERROR`], and *any* thin run
99    /// or ambiguous row at all. Volume error is a smooth thing that a caller trades against cost, so it
100    /// gets a number; a feature one cell thick and a row that could not be filled are not trade-offs, they
101    /// are things that either happened or did not.
102    ///
103    /// Three fields are deliberately **not** here, for two different reasons.
104    ///
105    /// [`Loss::boundary_fraction`] and [`Loss::small_triangles`] are left out because no threshold on
106    /// either is right for more than one physics: a diffusion problem run to steady state barely notices
107    /// a boundary layer that would move a stress concentration by a factor of two, and a large flat face
108    /// tessellated into a thousand slivers loses nothing at all. Putting a number on those would be this
109    /// library guessing, which is the one thing it does not do — so they are reported and the judgement is
110    /// the caller's.
111    ///
112    /// [`Loss::retried_rows`] is left out because it is not a loss. Those rows came out right.
113    pub fn is_clean(&self) -> bool {
114        self.volume_error.abs() < Self::CLEAN_VOLUME_ERROR
115            && self.thin_runs == 0
116            && self.ambiguous_rows == 0
117    }
118}
119
120/// A mesh rasterised onto a grid of cubes.
121///
122/// The grid is the mesh's own bounding box, grown to whole cells, with the shape sitting inside it. Cells
123/// are **inside or outside** — there is no partial cell, because the domains this feeds have no partial
124/// cell either.
125#[derive(Clone, Debug)]
126pub struct Voxels {
127    counts: (usize, usize, usize),
128    dx: f64,
129    origin: DVec3,
130    inside: Vec<bool>,
131    loss: Loss,
132}
133
134impl Voxels {
135    /// Rasterise `mesh` onto cubes of side `cell`.
136    ///
137    /// # By scanline, and why the crossings are counted a row at a time
138    ///
139    /// A point-in-mesh test per cell would cast one ray per cell and cost `cells × triangles`. Casting one
140    /// ray along `x` for each `(j, k)` row and filling between sorted pairs of crossings costs
141    /// `rows × triangles` — the same answer for a factor of `nx` less work, and it is not only faster: a
142    /// whole row decided from one sorted list of crossings **cannot** disagree with itself about parity,
143    /// where per-cell tests can and do near a surface.
144    ///
145    /// # Degenerate rays are detected, perturbed deterministically, and then admitted
146    ///
147    /// Detected, and that word is doing the work. **Parity is not enough to find them.** A ray through
148    /// the edge two triangles share hits both, so the count stays even and the two crossings coincide;
149    /// the row pairs them against each other, fills nothing, and reports success. A cube loses a whole
150    /// diagonal plane of rows to this at every resolution — 64 cells of 512 on an eight-cell cube — with
151    /// no error anywhere. So a hit on an edge is a case of its own, distinct from both a miss and a
152    /// clean crossing, and a row that produces one is retried whatever its parity says.
153    ///
154    /// The retry moves the ray, and the offsets are **fixed** — a short list of small multiples of the
155    /// cell — so the same mesh and cell size give bit-for-bit the same voxels on every platform and at
156    /// every optimisation level, which is a promise the whole workspace makes.
157    ///
158    /// A row still degenerate or still odd after all of them is left **empty** and counted in
159    /// [`Loss::ambiguous_rows`]. Empty is the honest failure: a hole is visible, and a row filled by
160    /// guessing which crossing to drop is a wrong shape that looks right.
161    ///
162    /// Refuses an open mesh, because parity has no meaning through a surface with a hole in it.
163    pub fn of(mesh: &Mesh, cell: Length) -> Result<Voxels, String> {
164        let dx = cell.to_si();
165        if !(dx.is_finite() && dx > 0.0) {
166            return Err(format!("a cell size must be finite and positive, is {dx}"));
167        }
168        if !mesh.is_closed() {
169            return Err(
170                "the mesh is not closed: some edge is not shared by exactly two triangles, so a ray \
171                 can pass through the surface and parity cannot say what is inside. STL stores no \
172                 topology, so this is matched on the vertices as written — see `Mesh::is_closed`"
173                    .to_string(),
174            );
175        }
176        let (low, high) = mesh
177            .bounds()
178            .ok_or_else(|| "an empty mesh has no bounds to rasterise".to_string())?;
179        let (low, high) = (low.to_si(), high.to_si());
180
181        // Whole cells, with the shape centred in them. The half cell of margin means the surface never
182        // lands exactly on the outer boundary, where a crossing is hardest to count.
183        let span = high - low;
184        let counts = (
185            ((span.x / dx).ceil() as usize + 2).max(1),
186            ((span.y / dx).ceil() as usize + 2).max(1),
187            ((span.z / dx).ceil() as usize + 2).max(1),
188        );
189        let grid = DVec3::new(
190            counts.0 as f64 * dx,
191            counts.1 as f64 * dx,
192            counts.2 as f64 * dx,
193        );
194        let origin = low - (grid - span) * 0.5;
195        Ok(Voxels::rasterise(mesh, origin, counts, dx))
196    }
197
198    /// Rasterise `mesh` onto a grid somebody else chose: a stated origin, cell count and cell.
199    ///
200    /// **This is what assembly needs.** [`Voxels::of`] gives every mesh its own box and its own
201    /// origin, so two parts come back on two grids that share no cell and cannot be adjacent to
202    /// each other — `ARCHITECTURE.md` names that as the gap that arrives first in practice.
203    /// Rasterised onto one grid, two parts occupy neighbouring cells of the same array, and a
204    /// domain that fills from both gets a conducting interface between them for free, because
205    /// its stencil already crosses a face between cells of different materials.
206    ///
207    /// The grid is the caller's and the mesh's coordinates are read as they are written: an STL
208    /// carries absolute positions, so where two parts sit relative to each other is what their
209    /// files already say. No pose is applied here — `Pose` places a *domain*, and both parts are
210    /// inside one domain now.
211    ///
212    /// **A mesh that does not fit is refused, with both boxes named.** Cropping it silently is
213    /// the failure this workspace keeps finding: a part with its corner cut off runs, audits,
214    /// renders and answers a question about a different shape.
215    pub fn onto(
216        mesh: &Mesh,
217        origin: LengthVec,
218        counts: (usize, usize, usize),
219        cell: Length,
220    ) -> Result<Voxels, String> {
221        let dx = cell.to_si();
222        if !(dx.is_finite() && dx > 0.0) {
223            return Err(format!("a cell size must be finite and positive, is {dx}"));
224        }
225        if counts.0 == 0 || counts.1 == 0 || counts.2 == 0 {
226            return Err(format!("a grid of {counts:?} cells holds nothing"));
227        }
228        if !mesh.is_closed() {
229            return Err(
230                "the mesh is not closed: some edge is not shared by exactly two triangles, so a \
231                 ray can pass through the surface and parity cannot say what is inside"
232                    .to_string(),
233            );
234        }
235        let (low, high) = mesh
236            .bounds()
237            .ok_or_else(|| "an empty mesh has no bounds to rasterise".to_string())?;
238        let (low, high) = (low.to_si(), high.to_si());
239        let o = origin.to_si();
240        let far = o + DVec3::new(
241            counts.0 as f64 * dx,
242            counts.1 as f64 * dx,
243            counts.2 as f64 * dx,
244        );
245        let fits = low.x >= o.x
246            && low.y >= o.y
247            && low.z >= o.z
248            && high.x <= far.x
249            && high.y <= far.y
250            && high.z <= far.z;
251        if !fits {
252            return Err(format!(
253                "the mesh spans ({:.4}, {:.4}, {:.4}) to ({:.4}, {:.4}, {:.4}) m and the grid \
254                 covers ({:.4}, {:.4}, {:.4}) to ({:.4}, {:.4}, {:.4}) m, so part of it would be \
255                 cut off — a part with its corner missing runs and audits and answers about a \
256                 different shape, so it is refused rather than cropped",
257                low.x, low.y, low.z, high.x, high.y, high.z, o.x, o.y, o.z, far.x, far.y, far.z
258            ));
259        }
260        Ok(Voxels::rasterise(mesh, o, counts, dx))
261    }
262
263    /// The scanline itself, on whatever grid it is handed. Shared by [`Voxels::of`] and
264    /// [`Voxels::onto`] so there is one rasteriser and not two that agree until they do not.
265    fn rasterise(mesh: &Mesh, origin: DVec3, counts: (usize, usize, usize), dx: f64) -> Voxels {
266        let mut inside = vec![false; counts.0 * counts.1 * counts.2];
267        let mut ambiguous_rows = 0;
268        let mut retried_rows = 0;
269        // Fixed offsets, tried in order. Irregular multiples so a second attempt does not land on the
270        // same symmetry the first one did, and constant so the result is reproducible.
271        const NUDGE: [(f64, f64); 4] = [(0.0, 0.0), (0.19, 0.07), (-0.11, 0.23), (0.31, -0.29)];
272
273        let mut crossings: Vec<f64> = Vec::new();
274        for k in 0..counts.2 {
275            for j in 0..counts.1 {
276                let mut filled = false;
277                for (attempt, (dy, dz)) in NUDGE.into_iter().enumerate() {
278                    let y = origin.y + (j as f64 + 0.5 + dy) * dx;
279                    let z = origin.z + (k as f64 + 0.5 + dz) * dx;
280                    crossings.clear();
281                    let mut degenerate = false;
282                    for t in mesh.triangles() {
283                        match hit_x(t.a, t.b, t.c, y, z) {
284                            Hit::Miss => {}
285                            Hit::At(x) => crossings.push(x),
286                            // One is enough to spoil the row, and the rest of the triangles cannot
287                            // un-spoil it.
288                            Hit::Degenerate => {
289                                degenerate = true;
290                                break;
291                            }
292                        }
293                    }
294                    // Clippy on current stable suggests `usize::is_multiple_of`, stabilised in 1.87 —
295                    // later than the 1.78 this workspace declares and its CI verifies. A declared MSRV
296                    // is a promise to a consumer and a lint suggestion is not.
297                    #[allow(clippy::manual_is_multiple_of)]
298                    let odd = crossings.len() % 2 != 0;
299                    if degenerate || odd {
300                        continue;
301                    }
302                    crossings.sort_by(|a, b| a.partial_cmp(b).expect("finite"));
303                    // And the same again, one lint later: clippy suggests `as_chunks::<2>()`,
304                    // stabilised in 1.88. The MSRV job builds this with 1.78 and would not compile
305                    // it. Two `#[allow]`s in five lines for the same reason is what a declared floor
306                    // costs while stable keeps moving — and the alternative, taking the suggestion,
307                    // breaks a promise to a consumer to satisfy a suggestion to a maintainer.
308                    #[allow(clippy::chunks_exact_to_as_chunks)]
309                    for pair in crossings.chunks_exact(2) {
310                        for i in 0..counts.0 {
311                            let x = origin.x + (i as f64 + 0.5) * dx;
312                            if x > pair[0] && x < pair[1] {
313                                inside[i + counts.0 * (j + counts.1 * k)] = true;
314                            }
315                        }
316                    }
317                    if attempt > 0 {
318                        retried_rows += 1;
319                    }
320                    filled = true;
321                    break;
322                }
323                if !filled {
324                    ambiguous_rows += 1;
325                }
326            }
327        }
328
329        let mut voxels = Voxels {
330            counts,
331            dx,
332            origin,
333            inside,
334            loss: Loss {
335                volume_error: 0.0,
336                boundary_fraction: 0.0,
337                thin_runs: 0,
338                small_triangles: mesh.triangles_below(Length::from_si(dx)),
339                retried_rows,
340                ambiguous_rows,
341            },
342        };
343        let meshed = mesh.volume().to_si();
344        voxels.loss.volume_error = if meshed != 0.0 {
345            voxels.volume().to_si() / meshed - 1.0
346        } else {
347            f64::NAN
348        };
349        voxels.loss.boundary_fraction = voxels.boundary_share();
350        voxels.loss.thin_runs = voxels.count_thin_runs();
351        voxels
352    }
353
354    /// Cells along each axis.
355    pub fn counts(&self) -> (usize, usize, usize) {
356        self.counts
357    }
358
359    /// The cell side.
360    pub fn cell(&self) -> Length {
361        Length::from_si(self.dx)
362    }
363
364    /// The low corner of cell `(0, 0, 0)`.
365    pub fn origin(&self) -> LengthVec {
366        LengthVec::from_si(self.origin)
367    }
368
369    /// Whether a cell is inside the surface. Out-of-range indices are outside.
370    ///
371    /// This is the predicate a domain's `fill` takes, and the whole coupling between this crate and the
372    /// physics:
373    ///
374    /// ```no_run
375    /// # use pantometry_shape::Voxels;
376    /// # fn go(voxels: &Voxels, block: &mut impl FnMut(&dyn Fn(usize, usize, usize) -> bool)) {
377    /// block(&|i, j, k| voxels.contains(i, j, k));
378    /// # }
379    /// ```
380    pub fn contains(&self, i: usize, j: usize, k: usize) -> bool {
381        if i >= self.counts.0 || j >= self.counts.1 || k >= self.counts.2 {
382            return false;
383        }
384        self.inside[i + self.counts.0 * (j + self.counts.1 * k)]
385    }
386
387    /// How many cells are inside.
388    pub fn filled(&self) -> usize {
389        self.inside.iter().filter(|b| **b).count()
390    }
391
392    /// The volume those cells occupy.
393    pub fn volume(&self) -> Volume {
394        Volume::from_si(self.filled() as f64 * self.dx.powi(3))
395    }
396
397    /// What the rasterisation lost. See [`Loss`].
398    pub fn loss(&self) -> Loss {
399        self.loss
400    }
401
402    /// The share of filled cells with a face on the outside.
403    ///
404    /// Six neighbours, not twenty-six: a cell touching the outside only at an edge or a corner is not one
405    /// the seven-point stencils here exchange anything through, and counting it would make the layer
406    /// thicker than the physics sees it.
407    fn boundary_share(&self) -> f64 {
408        let filled = self.filled();
409        if filled == 0 {
410            return 0.0;
411        }
412        let (nx, ny, nz) = self.counts;
413        let mut on_surface = 0;
414        for k in 0..nz {
415            for j in 0..ny {
416                for i in 0..nx {
417                    if !self.contains(i, j, k) {
418                        continue;
419                    }
420                    let exposed = i == 0
421                        || j == 0
422                        || k == 0
423                        || !self.contains(i - 1, j, k)
424                        || !self.contains(i + 1, j, k)
425                        || !self.contains(i, j - 1, k)
426                        || !self.contains(i, j + 1, k)
427                        || !self.contains(i, j, k - 1)
428                        || !self.contains(i, j, k + 1);
429                    if exposed {
430                        on_surface += 1;
431                    }
432                }
433            }
434        }
435        on_surface as f64 / filled as f64
436    }
437
438    /// Solid runs one or two cells long, along all three axes.
439    fn count_thin_runs(&self) -> usize {
440        let (nx, ny, nz) = self.counts;
441        let mut thin = 0;
442        let mut tally = |run: usize| {
443            if run == 1 || run == 2 {
444                thin += 1;
445            }
446        };
447        for k in 0..nz {
448            for j in 0..ny {
449                let mut run = 0;
450                for i in 0..nx {
451                    if self.contains(i, j, k) {
452                        run += 1;
453                    } else {
454                        tally(run);
455                        run = 0;
456                    }
457                }
458                tally(run);
459            }
460        }
461        for k in 0..nz {
462            for i in 0..nx {
463                let mut run = 0;
464                for j in 0..ny {
465                    if self.contains(i, j, k) {
466                        run += 1;
467                    } else {
468                        tally(run);
469                        run = 0;
470                    }
471                }
472                tally(run);
473            }
474        }
475        for j in 0..ny {
476            for i in 0..nx {
477                let mut run = 0;
478                for k in 0..nz {
479                    if self.contains(i, j, k) {
480                        run += 1;
481                    } else {
482                        tally(run);
483                        run = 0;
484                    }
485                }
486                tally(run);
487            }
488        }
489        thin
490    }
491}
492
493/// What a ray along `+x` did to one triangle.
494enum Hit {
495    /// It missed, or ran parallel to the triangle's plane. Either way the triangle is not on this row.
496    Miss,
497    /// It crossed cleanly, at this `x`.
498    At(f64),
499    /// It went through an edge, or grazed the plane, so this row's parity cannot be trusted.
500    ///
501    /// **Parity does not detect this, which is why it has its own variant.** A ray through the edge two
502    /// triangles share hits *both*, and the count stays even — so a box whose face diagonal passes
503    /// through a cell centre yields crossings `[0, 0, 24, 24]`, pairs them as `(0, 0)` and `(24, 24)`,
504    /// fills nothing, and reports no error. A cube at any resolution loses a whole diagonal plane of
505    /// rows that way, silently. The row has to be retried on a moved ray instead.
506    Degenerate,
507}
508
509/// Where a ray along `+x` through `(y, z)` meets a triangle.
510///
511/// Möller–Trumbore reduced to a fixed direction, with the degenerate cases separated out rather than
512/// rounded into an answer. Both thresholds are on **scale-free** quantities so a mesh in metres and the
513/// same mesh in millimetres are judged alike:
514///
515/// - `u`, `v` and `1 − u − v` are barycentric, and near zero means the hit is on an edge;
516/// - `det / |e1 × e2|` is exactly `−n·x̂`, the triangle normal's component along the ray, so near zero
517///   means edge-on. A triangle *at* zero contributes nothing and is a miss; one merely close gives a
518///   crossing position divided by that small number, which is a position not worth having.
519fn hit_x(a: DVec3, b: DVec3, c: DVec3, y: f64, z: f64) -> Hit {
520    // Both are six orders above the rounding and four below anything a real mesh does on purpose, and
521    // that gap is the whole justification — neither is a measurement.
522    //
523    // `u` and `v` come out of two divisions and a handful of products, so a well-conditioned triangle
524    // carries a few ulps and a sliver a few thousand; `1e-9` leaves six orders over the worst of that. In
525    // the other direction, a *deliberate* feature 1e-9 of a facet across is below any manufacturing
526    // tolerance and below `f32`, which is what the file it came from stores. So the band between "this is
527    // rounding" and "this is geometry" is wide, and where in it the threshold sits does not matter.
528    //
529    // What is *not* claimed: the tests do not distinguish `1e-9` from any value between about `1e-16` and
530    // `1e-4`, because the case they exercise — a cube's face diagonal — is degenerate exactly rather than
531    // nearly. Finding a mesh that lands in between is possible and has not been done.
532    const ON_EDGE: f64 = 1e-9;
533    const GRAZING: f64 = 1e-9;
534
535    let origin = DVec3::new(0.0, y, z);
536    let direction = DVec3::X;
537    let (e1, e2) = (b - a, c - a);
538    let twice_area = e1.cross(e2).length();
539    if twice_area == 0.0 {
540        // A degenerate triangle: three collinear vertices, which exporters do produce. It has no
541        // inside, so it is on no row.
542        return Hit::Miss;
543    }
544    let h = direction.cross(e2);
545    let det = e1.dot(h);
546    let along = det / twice_area;
547    if along == 0.0 {
548        return Hit::Miss;
549    }
550    if along.abs() < GRAZING {
551        return Hit::Degenerate;
552    }
553    let inv = 1.0 / det;
554    let s = origin - a;
555    let u = inv * s.dot(h);
556    let q = s.cross(e1);
557    let v = inv * direction.dot(q);
558    let w = 1.0 - u - v;
559    if u < -ON_EDGE || v < -ON_EDGE || w < -ON_EDGE {
560        return Hit::Miss;
561    }
562    if u < ON_EDGE || v < ON_EDGE || w < ON_EDGE {
563        return Hit::Degenerate;
564    }
565    Hit::At(inv * e2.dot(q))
566}