Skip to main content

brepkit_math/nurbs/intersection/
ray.rs

1//! Every crossing of a ray with a NURBS surface.
2
3use crate::frame::Frame3;
4use crate::nurbs::decompose::surface_to_bezier_patches;
5use crate::nurbs::surface::NurbsSurface;
6use crate::vec::{Point3, Vec3};
7
8/// Deepest subdivision of one Bezier patch.
9const MAX_DEPTH: u32 = 48;
10
11/// Most patches one ray may visit before it is reported as grazing.
12const MAX_VISITS: usize = 20_000;
13
14/// A leaf's size against the surface's: below it, Newton from the leaf's
15/// middle finds the crossing the leaf holds.
16const LEAF: f64 = 1e-3;
17
18/// The least slant a hit is read at, however flat the surface.
19const GRAZE_COS: f64 = 1e-9;
20
21/// How many leaves apart a hit's second crossing may lie and still share a
22/// leaf's Newton with it.
23const SEPARATION: f64 = 4.0;
24
25/// A NURBS surface made ready for ray crossings.
26///
27/// It holds the surface's Bezier patches, each a grid of homogeneous control
28/// points. A surface whose weights are all positive lies in the hull of each
29/// patch's control points, which the search relies on to pass over the
30/// patches a ray misses.
31#[derive(Debug, Clone)]
32pub struct RaySurface {
33    surface: NurbsSurface,
34    patches: Vec<Patch>,
35    scale: f64,
36}
37
38#[derive(Debug, Clone)]
39struct Patch {
40    rows: usize,
41    cols: usize,
42    /// `rows` rows of `cols` homogeneous points `[w x, w y, w z, w]`, a row
43    /// per `u` index.
44    pts: Vec<[f64; 4]>,
45    u: (f64, f64),
46    v: (f64, f64),
47}
48
49/// Where a ray meets a surface.
50#[derive(Debug, Clone, Copy)]
51pub struct RayHit {
52    /// The surface's `u` there.
53    pub u: f64,
54    /// The surface's `v` there.
55    pub v: f64,
56    /// The distance along the ray's unit direction.
57    pub t: f64,
58    /// The point.
59    pub point: Point3,
60}
61
62/// A ray's hits on a surface, and whether any of them, or a place the ray
63/// passes, is too near tangent for the count of hits to be trusted.
64#[derive(Debug, Clone, Default)]
65pub struct RayHits {
66    /// The hits, each once.
67    pub hits: Vec<RayHit>,
68    /// The ray touches the surface tangentially, runs in it, or passes
69    /// within the reach of it without a crossing that settles.
70    pub grazed: bool,
71}
72
73impl Patch {
74    fn point(&self, i: usize, j: usize) -> Point3 {
75        let [x, y, z, w] = self.pts[i * self.cols + j];
76        Point3::new(x / w, y / w, z / w)
77    }
78
79    /// The patch split at the middle of its `u` range (`along_u`) or its
80    /// `v` range, by de Casteljau on the homogeneous points.
81    fn split(&self, along_u: bool) -> (Self, Self) {
82        let (n, m) = if along_u {
83            (self.rows, self.cols)
84        } else {
85            (self.cols, self.rows)
86        };
87        let at = |k: usize, l: usize| {
88            if along_u {
89                k * self.cols + l
90            } else {
91                l * self.cols + k
92            }
93        };
94        let mut low = self.pts.clone();
95        let mut high = self.pts.clone();
96        let mut work = vec![[0.0; 4]; n];
97        for l in 0..m {
98            for (k, slot) in work.iter_mut().enumerate() {
99                *slot = self.pts[at(k, l)];
100            }
101            low[at(0, l)] = work[0];
102            high[at(n - 1, l)] = work[n - 1];
103            for r in 1..n {
104                for k in 0..n - r {
105                    for c in 0..4 {
106                        work[k][c] = 0.5 * (work[k][c] + work[k + 1][c]);
107                    }
108                }
109                low[at(r, l)] = work[0];
110                high[at(n - 1 - r, l)] = work[n - 1 - r];
111            }
112        }
113        let (mut a, mut b) = (self.clone(), self.clone());
114        a.pts = low;
115        b.pts = high;
116        if along_u {
117            let mid = f64::midpoint(self.u.0, self.u.1);
118            (a.u, b.u) = ((self.u.0, mid), (mid, self.u.1));
119        } else {
120            let mid = f64::midpoint(self.v.0, self.v.1);
121            (a.v, b.v) = ((self.v.0, mid), (mid, self.v.1));
122        }
123        (a, b)
124    }
125
126    /// The longest control polygon along `u` and along `v`: each bounds the
127    /// length of the patch's curves that way, however far a bulge between
128    /// its ends rises.
129    fn spans(&self) -> (f64, f64) {
130        let mut along_u = 0.0_f64;
131        for j in 0..self.cols {
132            let length: f64 = (1..self.rows)
133                .map(|i| (self.point(i, j) - self.point(i - 1, j)).length())
134                .sum();
135            along_u = along_u.max(length);
136        }
137        let mut along_v = 0.0_f64;
138        for i in 0..self.rows {
139            let length: f64 = (1..self.cols)
140                .map(|j| (self.point(i, j) - self.point(i, j - 1)).length())
141                .sum();
142            along_v = along_v.max(length);
143        }
144        (along_u, along_v)
145    }
146}
147
148impl RaySurface {
149    /// `surface` made ready, or `None` when a weight is not positive, a knot
150    /// vector is not clamped, or the surface cannot be cut into Bezier
151    /// patches.
152    #[must_use]
153    pub fn new(surface: &NurbsSurface) -> Option<Self> {
154        // An unclamped knot vector's end spans are not Bezier patches of the
155        // surface as decomposed, so the search would cross a different one.
156        let clamped = |knots: &[f64], degree: usize| {
157            let n = knots.len();
158            n > 2 * degree + 1
159                && knots[..=degree]
160                    .iter()
161                    .all(|&k| (k - knots[0]).abs() <= 0.0)
162                && knots[n - degree - 1..]
163                    .iter()
164                    .all(|&k| (k - knots[n - 1]).abs() <= 0.0)
165        };
166        if surface
167            .weights()
168            .iter()
169            .flatten()
170            .any(|&w| !w.is_finite() || w <= 0.0)
171            || !clamped(surface.knots_u(), surface.degree_u())
172            || !clamped(surface.knots_v(), surface.degree_v())
173        {
174            return None;
175        }
176        let patches = surface_to_bezier_patches(surface)
177            .ok()?
178            .into_iter()
179            .map(|p| {
180                let cps = p.surface.control_points();
181                let ws = p.surface.weights();
182                let rows = cps.len();
183                let cols = cps.first().map_or(0, Vec::len);
184                let pts = cps
185                    .iter()
186                    .zip(ws)
187                    .flat_map(|(row, wrow)| {
188                        row.iter()
189                            .zip(wrow)
190                            .map(|(c, &w)| [c.x() * w, c.y() * w, c.z() * w, w])
191                    })
192                    .collect();
193                Patch {
194                    rows,
195                    cols,
196                    pts,
197                    u: p.u_range,
198                    v: p.v_range,
199                }
200            })
201            .filter(|p| p.rows > 0 && p.cols > 0)
202            .collect();
203        let b = surface.aabb();
204        let scale = (b.max - b.min).length().max(1e-9);
205        Some(Self {
206            surface: surface.clone(),
207            patches,
208            scale,
209        })
210    }
211
212    /// The surface.
213    #[must_use]
214    pub const fn surface(&self) -> &NurbsSurface {
215        &self.surface
216    }
217
218    /// Every point where the line through `origin` along `dir` meets the
219    /// surface no farther than `reach` behind `origin`, with `t` measured
220    /// along `dir` made unit. A hit within `reach` of another is the same
221    /// hit.
222    #[must_use]
223    pub fn hits(&self, origin: Point3, dir: Vec3, reach: f64) -> RayHits {
224        let mut out = RayHits::default();
225        // How near another hit must lie to each hit to be it: a hit settled
226        // on its parameters' spacing is known no closer than that.
227        let mut radii: Vec<f64> = Vec::new();
228        out = self.collect_hits(origin, dir, reach, (out, &mut radii));
229        coalesce(&mut out.hits, &radii);
230        out
231    }
232
233    /// Every hit [`Self::hits`] finds, before those of one crossing are
234    /// joined, each with its radius in `radii`.
235    fn collect_hits(
236        &self,
237        origin: Point3,
238        dir: Vec3,
239        reach: f64,
240        (mut out, radii): (RayHits, &mut Vec<f64>),
241    ) -> RayHits {
242        let Ok(frame) = Frame3::from_normal(origin, dir) else {
243            out.grazed = true;
244            return out;
245        };
246        let leaf = LEAF * self.scale;
247        let mut stack: Vec<(Patch, u32)> = self.patches.iter().map(|p| (p.clone(), 0)).collect();
248        let mut visits = 0_usize;
249        while let Some((patch, depth)) = stack.pop() {
250            visits += 1;
251            if visits > MAX_VISITS {
252                out.grazed = true;
253                return out;
254            }
255            let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
256            for i in 0..patch.rows {
257                for j in 0..patch.cols {
258                    let d = patch.point(i, j) - origin;
259                    for (k, x) in [frame.x.dot(d), frame.y.dot(d), frame.z.dot(d)]
260                        .into_iter()
261                        .enumerate()
262                    {
263                        lo[k] = lo[k].min(x);
264                        hi[k] = hi[k].max(x);
265                    }
266                }
267            }
268            if lo[0] > reach || hi[0] < -reach || lo[1] > reach || hi[1] < -reach || hi[2] < -reach
269            {
270                continue;
271            }
272            let (along_u, along_v) = patch.spans();
273            if along_u.max(along_v) <= leaf || depth >= MAX_DEPTH {
274                // A leaf the solve cannot settle is cut further, and at the
275                // deepest level the ray is taken to graze it.
276                if self.solve(&patch, origin, frame.z, reach, (&mut out, radii)) {
277                    continue;
278                }
279                if depth >= MAX_DEPTH {
280                    out.grazed = true;
281                    continue;
282                }
283            }
284            let (a, b) = patch.split(along_u >= along_v);
285            stack.push((a, depth + 1));
286            stack.push((b, depth + 1));
287        }
288        out
289    }
290
291    /// Whether a ray along `dir` through the surface at `(u, v)` meets it
292    /// again too near to be found apart: bending by its normal curvature
293    /// `k` along the ray's tangential part `w`, the surface comes back to
294    /// the ray `2 (n . dir) / (k |w|^2)` along it. A ray at a tangent point
295    /// meets it there twice.
296    fn second_crossing_near(&self, u: f64, v: f64, dir: Vec3) -> bool {
297        let d = self.surface.derivatives(u, v, 2);
298        let (su, sv) = (d[1][0], d[0][1]);
299        let Ok(n) = su.cross(sv).normalize() else {
300            return true;
301        };
302        let slant = n.dot(dir);
303        if slant.abs() <= GRAZE_COS {
304            return true;
305        }
306        let w = dir - n * slant;
307        let (guu, guv, gvv) = (su.dot(su), su.dot(sv), sv.dot(sv));
308        let det = guu.mul_add(gvv, -guv * guv);
309        if det <= 0.0 || w.length() <= GRAZE_COS {
310            return false;
311        }
312        let (wu, wv) = (su.dot(w), sv.dot(w));
313        let a = gvv.mul_add(wu, -guv * wv) / det;
314        let b = guu.mul_add(wv, -guv * wu) / det;
315        let bend = n.dot(d[2][0] * (a * a) + d[1][1] * (2.0 * a * b) + d[0][2] * (b * b));
316        2.0 * slant.abs() <= SEPARATION * LEAF * self.scale * bend.abs()
317    }
318
319    /// Newton on `S(u, v) = origin + t dir` from the middle of `leaf`,
320    /// recording the hit when it lands in the leaf. Whether it converged,
321    /// in the leaf or out of it: a stall (the ray along the tangent plane
322    /// at a step) or no convergence leaves the leaf unsettled.
323    ///
324    /// Where the surface runs fast in its parameters, a unit in the last
325    /// place of `u` or `v` moves the point farther than 1e-12 of the
326    /// surface's size, and Newton stalls short of that. A hit it stalls on
327    /// within four such units is settled there, after Newton has run its
328    /// course; it is one hit with any other that near, and when that is
329    /// farther than `reach` the ray is flagged, its count in doubt.
330    fn solve(
331        &self,
332        leaf: &Patch,
333        origin: Point3,
334        dir: Vec3,
335        reach: f64,
336        (out, radii): (&mut RayHits, &mut Vec<f64>),
337    ) -> bool {
338        let (mut u, mut v) = (
339            f64::midpoint(leaf.u.0, leaf.u.1),
340            f64::midpoint(leaf.v.0, leaf.v.1),
341        );
342        let (du, dv) = (self.surface.domain_u(), self.surface.domain_v());
343        let mut t = dir.dot(self.surface.evaluate(u, v) - origin);
344        let mut stalled = None;
345        for _ in 0..32 {
346            let d = self.surface.derivatives(u, v, 1);
347            let (s, su, sv) = (d[0][0], d[1][0], d[0][1]);
348            let f = s - Vec3::new(origin.x(), origin.y(), origin.z()) - dir * t;
349            if f.length() <= 1e-12 * self.scale {
350                return self.record(leaf, (u, v, t), origin, dir, reach, reach, (out, radii));
351            }
352            let spacing = 4.0 * f64::EPSILON * su.length().mul_add(u.abs(), sv.length() * v.abs());
353            stalled = (f.length() <= spacing).then_some((u, v, t, spacing));
354            let normal = su.cross(sv);
355            let det = -normal.dot(dir);
356            if det.abs() <= 1e-300 {
357                break;
358            }
359            // Cramer's rule on [su sv -dir] x = -f.
360            let neg_dir = -dir;
361            let rhs = -f;
362            let x_u = rhs.cross(sv).dot(neg_dir) / det;
363            let x_v = su.cross(rhs).dot(neg_dir) / det;
364            let x_t = normal.dot(rhs) / det;
365            u = (u + x_u).clamp(du.0, du.1);
366            v = (v + x_v).clamp(dv.0, dv.1);
367            t += x_t;
368        }
369        let Some((u, v, t, spacing)) = stalled else {
370            return false;
371        };
372        out.grazed |= spacing > reach;
373        self.record(
374            leaf,
375            (u, v, t),
376            origin,
377            dir,
378            reach,
379            spacing.max(reach),
380            (out, radii),
381        )
382    }
383
384    /// Record the hit at `(u, v)`, `t` along the ray, when it lies in `leaf`
385    /// and ahead of `origin` by no less than `-reach`, with `merge` in
386    /// `radii`: the distance within which another hit is the same one.
387    /// Always settles the leaf.
388    #[allow(clippy::too_many_arguments)]
389    fn record(
390        &self,
391        leaf: &Patch,
392        (u, v, t): (f64, f64, f64),
393        origin: Point3,
394        dir: Vec3,
395        reach: f64,
396        merge: f64,
397        (out, radii): (&mut RayHits, &mut Vec<f64>),
398    ) -> bool {
399        let (du, dv) = (self.surface.domain_u(), self.surface.domain_v());
400        let slack_u = 1e-9 * (du.1 - du.0);
401        let slack_v = 1e-9 * (dv.1 - dv.0);
402        if u < leaf.u.0 - slack_u
403            || u > leaf.u.1 + slack_u
404            || v < leaf.v.0 - slack_v
405            || v > leaf.v.1 + slack_v
406            || t < -reach
407        {
408            return true;
409        }
410        out.grazed |= self.second_crossing_near(u, v, dir);
411        let point = origin + dir * t;
412        out.hits.push(RayHit { u, v, t, point });
413        radii.push(merge);
414        true
415    }
416}
417
418/// Join the hits of one crossing: two lie on one when they lie within the
419/// wider of their radii, and so does any chain of such pairs. A chain does
420/// not hang on the order the hits were found in, so neither does the count.
421/// Each crossing keeps its most precise hit, the nearest along the ray among
422/// those as precise, and the hits are left in order along the ray.
423///
424/// Every hit lies on the ray, `t` along it, so two lie `|t - t'|` apart.
425/// Taken in order of `t`, a hit joins the last crossing when it lies within
426/// the farthest that crossing's hits reach, or its own radius reaches back to
427/// that crossing's last hit, and its radius can reach on back to join the
428/// crossings before it into one. A crossing before the last whose own reach
429/// takes the hit in would have taken the last one in already.
430fn coalesce(hits: &mut Vec<RayHit>, radii: &[f64]) {
431    /// A crossing's hits so far: the last along the ray, the one whose radius
432    /// reaches farthest past it, and the most precise.
433    struct Crossing {
434        last: f64,
435        reacher: (f64, f64),
436        best: usize,
437    }
438    // Separations are taken between `t`s and set against radii, never as
439    // sums: at `t` = 1e10 a unit in the last place is 1.9e-6, and `t + r`
440    // would round a radius of 1e-6 away. A hit at `(t, r)` reaches past one
441    // at `(s, q)` when `t - s` exceeds `q - r`.
442    let farther = |a: (f64, f64), b: (f64, f64)| a.0 - b.0 > b.1 - a.1;
443    let mut order: Vec<usize> = (0..hits.len()).collect();
444    order.sort_by(|&a, &b| hits[a].t.total_cmp(&hits[b].t));
445    let precise = |a: usize, b: usize| {
446        radii[a]
447            .total_cmp(&radii[b])
448            .then(hits[a].t.total_cmp(&hits[b].t))
449            .is_lt()
450    };
451    let mut crossings: Vec<Crossing> = Vec::new();
452    for i in order {
453        let (t, r) = (hits[i].t, radii[i]);
454        match crossings.last_mut() {
455            Some(c) if t - c.reacher.0 <= c.reacher.1 || t - c.last <= r => {
456                c.last = t;
457                if farther((t, r), c.reacher) {
458                    c.reacher = (t, r);
459                }
460                if precise(i, c.best) {
461                    c.best = i;
462                }
463            }
464            _ => crossings.push(Crossing {
465                last: t,
466                reacher: (t, r),
467                best: i,
468            }),
469        }
470        while crossings.len() >= 2 && t - crossings[crossings.len() - 2].last <= r {
471            let Some(later) = crossings.pop() else { break };
472            if let Some(c) = crossings.last_mut() {
473                c.last = later.last;
474                if farther(later.reacher, c.reacher) {
475                    c.reacher = later.reacher;
476                }
477                if precise(later.best, c.best) {
478                    c.best = later.best;
479                }
480            }
481        }
482    }
483    *hits = crossings.into_iter().map(|c| hits[c.best]).collect();
484}
485
486#[cfg(test)]
487mod tests {
488    #![allow(clippy::unwrap_used)]
489
490    use super::*;
491
492    /// The lid `z = 100 u (1 - u)^4 v (1 - v)` over the unit square, flat
493    /// along its rim and peaking at 2.048 at `(0.2, 0.5)`.
494    fn lid() -> NurbsSurface {
495        let rows = (0..6)
496            .map(|i| {
497                (0..3)
498                    .map(|j| {
499                        let z = if (i, j) == (1, 1) { 10.0 } else { 0.0 };
500                        Point3::new(f64::from(i) / 5.0, f64::from(j) / 2.0, z)
501                    })
502                    .collect()
503            })
504            .collect();
505        NurbsSurface::new(
506            5,
507            2,
508            [vec![0.0; 6], vec![1.0; 6]].concat(),
509            vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
510            rows,
511            vec![vec![1.0; 3]; 6],
512        )
513        .unwrap()
514    }
515
516    #[test]
517    fn a_ray_up_through_the_lids_peak_meets_it_there() {
518        let lid = RaySurface::new(&lid()).unwrap();
519        let up = Vec3::new(0.0, 0.0, 1.0);
520        let at_peak = lid.hits(Point3::new(0.2, 0.5, -1.0), up, 1e-9);
521        assert!(!at_peak.grazed);
522        assert_eq!(at_peak.hits.len(), 1);
523        assert!((at_peak.hits[0].point.z() - 2.048).abs() < 1e-12);
524        assert!((at_peak.hits[0].t - 3.048).abs() < 1e-12);
525        assert!((at_peak.hits[0].u - 0.2).abs() < 1e-9 && (at_peak.hits[0].v - 0.5).abs() < 1e-9);
526
527        let beside = lid.hits(Point3::new(1.5, 0.5, -1.0), up, 1e-9);
528        assert!(beside.hits.is_empty() && !beside.grazed);
529        let behind = lid.hits(Point3::new(0.2, 0.5, 3.0), up, 1e-9);
530        assert!(behind.hits.is_empty());
531    }
532
533    /// A level ray at `z = 1.5` along `u` through `v = 0.5` crosses the
534    /// lid's bulge twice, where `100 u (1 - u)^4 / 4 = 1.5`.
535    #[test]
536    fn a_level_ray_crosses_the_bulge_twice() {
537        let lid = RaySurface::new(&lid()).unwrap();
538        let across = lid.hits(Point3::new(-1.0, 0.5, 1.5), Vec3::new(1.0, 0.0, 0.0), 1e-9);
539        assert!(!across.grazed);
540        assert_eq!(across.hits.len(), 2, "{:?}", across.hits);
541        for h in &across.hits {
542            let u = h.point.x();
543            assert!((25.0 * u * (1.0 - u).powi(4) - 1.5).abs() < 1e-9, "{h:?}");
544        }
545    }
546
547    #[test]
548    fn a_ray_touching_the_peak_grazes() {
549        let lid = RaySurface::new(&lid()).unwrap();
550        let level = lid.hits(
551            Point3::new(-1.0, 0.5, 2.048),
552            Vec3::new(1.0, 0.0, 0.0),
553            1e-9,
554        );
555        assert!(level.grazed, "{level:?}");
556    }
557
558    /// A unit biquadratic patch with only its middle control point raised,
559    /// to 4000: a level ray at 500 along u through v = 0.5 crosses its
560    /// spike twice, where 4000 u (1 - u) = 500 at v = 0.5, so at
561    /// u = (1 ± 1/√2) / 2.
562    #[test]
563    fn a_level_ray_crosses_a_tall_spike() {
564        let rows = (0..3)
565            .map(|i| {
566                (0..3)
567                    .map(|j| {
568                        let z = if (i, j) == (1, 1) { 4000.0 } else { 0.0 };
569                        Point3::new(f64::from(i) / 2.0, f64::from(j) / 2.0, z)
570                    })
571                    .collect()
572            })
573            .collect();
574        let knots = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
575        let spike =
576            NurbsSurface::new(2, 2, knots.clone(), knots, rows, vec![vec![1.0; 3]; 3]).unwrap();
577        let ray = RaySurface::new(&spike).unwrap();
578        let across = ray.hits(
579            Point3::new(-1.0, 0.5, 500.0),
580            Vec3::new(1.0, 0.0, 0.0),
581            1e-9,
582        );
583        assert!(!across.grazed);
584        let mut xs: Vec<f64> = across.hits.iter().map(|h| h.point.x()).collect();
585        xs.sort_by(f64::total_cmp);
586        let root = std::f64::consts::FRAC_1_SQRT_2;
587        assert_eq!(xs.len(), 2, "{xs:?}");
588        assert!((xs[0] - (1.0 - root) / 2.0).abs() < 1e-9, "{xs:?}");
589        assert!((xs[1] - f64::midpoint(1.0, root)).abs() < 1e-9, "{xs:?}");
590    }
591
592    /// The saddle `(u, v, (u - 1/2)(v - 1/2))` over the unit square, beside
593    /// a span reaching to `x = 2000` that makes the whole saddle one leaf.
594    /// A level ray from its middle at height 0.04 along `(1, 1, 0)` crosses
595    /// it at `(0.7, 0.7)`, though at the leaf's middle the ray runs along
596    /// the tangent plane and a solve from there stalls.
597    #[test]
598    fn a_ray_level_with_a_saddles_middle_crosses_it() {
599        let p = Point3::new;
600        let rows = vec![
601            vec![p(0.0, 0.0, 0.25), p(0.0, 1.0, -0.25)],
602            vec![p(1.0, 0.0, -0.25), p(1.0, 1.0, 0.25)],
603            vec![p(2000.0, 0.0, 0.0), p(2000.0, 1.0, 0.0)],
604        ];
605        let s = NurbsSurface::new(
606            1,
607            1,
608            vec![0.0, 0.0, 1.0, 2.0, 2.0],
609            vec![0.0, 0.0, 1.0, 1.0],
610            rows,
611            vec![vec![1.0; 2]; 3],
612        )
613        .unwrap();
614        let ray = RaySurface::new(&s).unwrap();
615        let dir = Vec3::new(1.0, 1.0, 0.0).normalize().unwrap();
616        let found = ray.hits(p(0.5, 0.5, 0.04), dir, 1e-9);
617        assert_eq!(found.hits.len(), 1, "{:?}", found.grazed);
618        assert!((found.hits[0].point - p(0.7, 0.7, 0.04)).length() < 1e-9);
619    }
620
621    /// A surface whose `u` knots run `[-2, -1, 0, 1, 2, 3]`, unclamped, is
622    /// not taken: its end patches as decomposed are not the surface.
623    #[test]
624    fn an_unclamped_surface_is_not_ready() {
625        let rows = (0..3)
626            .map(|i| {
627                (0..2)
628                    .map(|j| Point3::new(0.5 * f64::from(i), f64::from(j), 0.0))
629                    .collect()
630            })
631            .collect();
632        let s = NurbsSurface::new(
633            2,
634            1,
635            vec![-2.0, -1.0, 0.0, 1.0, 2.0, 3.0],
636            vec![0.0, 0.0, 1.0, 1.0],
637            rows,
638            vec![vec![1.0; 2]; 3],
639        )
640        .unwrap();
641        assert!(RaySurface::new(&s).is_none());
642    }
643
644    #[test]
645    fn a_surface_with_a_negative_weight_is_not_ready() {
646        let mut weights = vec![vec![1.0; 3]; 6];
647        weights[2][1] = -1.0;
648        let s = lid();
649        let s = NurbsSurface::new(
650            5,
651            2,
652            s.knots_u().to_vec(),
653            s.knots_v().to_vec(),
654            s.control_points().to_vec(),
655            weights,
656        )
657        .unwrap();
658        assert!(RaySurface::new(&s).is_none());
659    }
660
661    /// A flat unit patch crossed slant: one hit over [0, 1] in each
662    /// parameter, over [0, 1e-12], and over [2^40, 2^40 + 1], each through
663    /// its middle, where every leaf of its subdivision has a corner. Over
664    /// [2^40, 2^40 + 1] a unit in the last place of a parameter is 2^-12:
665    /// the middle lies on that grid, but (0.50003, 0.50007) does not, and
666    /// its hit, settled no finer, is flagged.
667    #[test]
668    fn a_patch_on_a_tiny_or_far_knot_domain_is_crossed_once() {
669        let p = Point3::new;
670        let corners = vec![
671            vec![p(0.0, 0.0, 0.0), p(0.0, 1.0, 0.0)],
672            vec![p(1.0, 0.0, 0.0), p(1.0, 1.0, 0.0)],
673        ];
674        let dir = Vec3::new(0.3, 0.2, -1.0);
675        let far = 2.0_f64.powi(40);
676        let middle = p(0.5, 0.5, 0.0);
677        for (lo, hi, through, flagged) in [
678            (0.0, 1.0, middle, false),
679            (0.0, 1e-12, middle, false),
680            (far, far + 1.0, middle, false),
681            (far, far + 1.0, p(0.500_03, 0.500_07, 0.0), true),
682        ] {
683            let knots = vec![lo, lo, hi, hi];
684            let s = NurbsSurface::new(
685                1,
686                1,
687                knots.clone(),
688                knots,
689                corners.clone(),
690                vec![vec![1.0; 2]; 2],
691            )
692            .unwrap();
693            let found = RaySurface::new(&s).unwrap().hits(through - dir, dir, 1e-6);
694            let case = format!("knots {lo} to {hi} through {through:?}");
695            assert_eq!(found.hits.len(), 1, "{case}: {:?}", found.hits);
696            assert_eq!(found.grazed, flagged, "{case}");
697        }
698    }
699
700    /// Three hits along a ray at 1, 1.0001 and 1.0002, the middle one
701    /// settled on a spacing of 1e-3 and the others found to 1e-6, are one
702    /// crossing in every order they are found in, and so are three a wide
703    /// radius chains from the last hit back past the middle one. Two hits
704    /// 1.9e-6 apart at 1e10 along the ray, each found to 1e-6, are two.
705    #[test]
706    fn hits_merge_on_the_wider_radius_in_any_order() {
707        let p = Point3::new;
708        let knots = vec![0.0, 0.0, 1.0, 1.0];
709        let flat = NurbsSurface::new(
710            1,
711            1,
712            knots.clone(),
713            knots,
714            vec![
715                vec![p(0.0, 0.0, 0.0), p(0.0, 1.0, 0.0)],
716                vec![p(1.0, 0.0, 0.0), p(1.0, 1.0, 0.0)],
717            ],
718            vec![vec![1.0; 2]; 2],
719        )
720        .unwrap();
721        let ray = RaySurface::new(&flat).unwrap();
722        let leaf = ray.patches[0].clone();
723        let (origin, dir) = (p(0.5, 0.5, 1.0), Vec3::new(0.0, 0.0, -1.0));
724        // The middle hit reaches both others; and a hit at 1.0021, reaching
725        // back 3e-3, joins one at 1 reaching 1e-3 with one at 1.002 that
726        // neither reaches.
727        let chains = [
728            [
729                ((0.5, 0.5, 1.0), 1e-6),
730                ((0.5, 0.5, 1.0001), 1e-3),
731                ((0.5, 0.5, 1.0002), 1e-6),
732            ],
733            [
734                ((0.5, 0.5, 1.0), 1e-3),
735                ((0.5, 0.5, 1.002), 1e-6),
736                ((0.5, 0.5, 1.0021), 3e-3),
737            ],
738        ];
739        let orders = [
740            [0, 1, 2],
741            [0, 2, 1],
742            [1, 0, 2],
743            [1, 2, 0],
744            [2, 0, 1],
745            [2, 1, 0],
746        ];
747        // Far along the ray a unit in the last place of `t` is 1.9e-6: two hits
748        // that far apart, each found to 1e-6, are two crossings.
749        let far = 1e10_f64;
750        let next = f64::from_bits(far.to_bits() + 1);
751        for order in [[0, 1], [1, 0]] {
752            let (mut out, mut radii) = (RayHits::default(), Vec::new());
753            for k in order {
754                let t = [far, next][k];
755                assert!(ray.record(
756                    &leaf,
757                    (0.5, 0.5, t),
758                    origin,
759                    dir,
760                    1e-6,
761                    1e-6,
762                    (&mut out, &mut radii)
763                ));
764            }
765            coalesce(&mut out.hits, &radii);
766            assert_eq!(out.hits.len(), 2, "far along the ray in {order:?}");
767        }
768        for found in chains {
769            for order in orders {
770                let (mut out, mut radii) = (RayHits::default(), Vec::new());
771                for k in order {
772                    let (hit, merge) = found[k];
773                    assert!(ray.record(
774                        &leaf,
775                        hit,
776                        origin,
777                        dir,
778                        1e-6,
779                        merge,
780                        (&mut out, &mut radii)
781                    ));
782                }
783                coalesce(&mut out.hits, &radii);
784                assert_eq!(out.hits.len(), 1, "{found:?} in {order:?}: {:?}", out.hits);
785            }
786        }
787    }
788}