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}