all_is_cubes_base/raycast.rs
1use euclid::Vector3D;
2
3/// Acts as polyfill for float methods
4#[cfg(not(any(feature = "std", test)))]
5#[allow(
6    unused_imports,
7    reason = "unclear why this warns even though it is needed"
8)]
9use num_traits::float::Float as _;
10
11#[cfg(not(any(feature = "std", test)))]
12#[allow(
13    unused_imports,
14    reason = "unclear why this warns even though it is needed"
15)]
16use crate::math::Euclid as _;
17use crate::math::{
18    Axis, Cube, CubeFace, Face7, FreeCoordinate, FreePoint, FreeVector, GridAab, GridCoordinate,
19    GridPoint, GridVector,
20};
21use crate::resolution::Resolution;
22
23mod axis_aligned;
24pub use axis_aligned::AxisAlignedRaycaster;
25
26mod ray;
27pub use ray::{AaRay, Ray};
28
29#[cfg(test)]
30mod tests;
31
32/// Vector unit type for units of "t" (ray-length).
33enum Tc {}
34
35/// Iterator over grid positions that intersect a given ray.
36///
37/// The grid is of unit cubes which are identified by the integer coordinates of
38/// their most negative corners, the same definition used by [`Cube`].
39//
40//---
41//
42// Implementation notes:
43//
44// From "A Fast Voxel Traversal Algorithm for Ray Tracing"
45// by John Amanatides and Andrew Woo, 1987
46// <http://www.cse.yorku.ca/~amana/research/grid.pdf>
47// <https://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.42.3443>
48// Extensions to the described algorithm:
49//   • The face passed through to reach the current cube is reported.
50//
51// The foundation of this algorithm is a parameterized representation of
52// the provided ray,
53//                    origin + t * direction,
54// except that t is not actually stored; rather, at any given point in the
55// traversal, we keep track of the *greater* t values which we would have
56// if we took a step sufficient to cross a cube boundary along that axis
57// (i.e. change the integer part of the coordinate) in the components of
58// t_max.
59#[derive(Clone, Debug, PartialEq)]
60pub struct Raycaster {
61    /// The ray being cast.
62    ///
63    /// Note that this is not the *original* ray, in two cases:
64    ///
65    /// * [`Self::fast_forward()`] replaces it with a ray moved forward.
66    /// * If the original ray numbers are large and would break the algorithm,
67    ///   they are replaced with zero.
68    ///
69    /// TODO: Remove those cases so that we can provide a getter for the original ray,
70    /// which is useful in `intersection_point()` and `recursive_raycast()`.
71    ray: Ray,
72
73    /// Have we not yet produced the origin cube itself?
74    emit_current: bool,
75
76    /// Cube we're in; always the next cube to return from the iterator.
77    ///
78    /// This is stored as a [`GridPoint`], not a [`Cube`], for easier arithmetic on it.
79    cube: GridPoint,
80
81    /// Which way to increment `cube` when stepping; signum of `direction`.
82    step: Vector3D<GridCoordinate, Cube>,
83
84    /// `t_max` stores the t-value at which we would next cross a cube boundary,
85    /// for each axis in which we could move. Thus, the least element of `t_max`
86    /// is the next intersection between the grid and the ray.
87    t_max: Vector3D<FreeCoordinate, Tc>,
88
89    /// The change in t when taking a full grid step along a given axis.
90    /// Always positive; partially infinite if axis-aligned.
91    t_delta: Vector3D<FreeCoordinate, Tc>,
92
93    /// Last face we passed through.
94    last_face: Face7,
95
96    /// The `t_max` value used in the previous step; thus, the position along the
97    /// ray where we passed through `last_face`.
98    last_t_distance: FreeCoordinate,
99
100    /// Bounds to filter our outputs to within.
101    bounds: GridAab,
102}
103
104impl Raycaster {
105    /// Construct a [`Raycaster`] for a ray with the given `origin` and `direction` vector.
106    ///
107    /// The magnitude of `direction` has no effect on the sequence of cubes traversed
108    /// but may affect calculation precision, so should not be especially large or small.
109    /// It also appears as the scale of the output field [`RaycastStep::t_distance`].
110    ///
111    /// Note that this is an infinite iterator by default. Use [`.within()`](Self::within)
112    /// to restrict it.
113    ///
114    /// ```
115    /// # use all_is_cubes_base as all_is_cubes;
116    /// use all_is_cubes::math::Cube;
117    /// use all_is_cubes::raycast::Raycaster;
118    ///
119    /// let mut r = Raycaster::new([0.5, 0.5, 0.5], [1.0, 0.5, 0.0]);
120    /// let mut next = || r.next().unwrap();
121    ///
122    /// // The cube containing the origin point is always the first cube reported.
123    /// assert_eq!(next().cube_ahead(), Cube::new(0, 0, 0));
124    /// assert_eq!(next().cube_ahead(), Cube::new(1, 0, 0));
125    /// assert_eq!(next().cube_ahead(), Cube::new(1, 1, 0));
126    /// assert_eq!(next().cube_ahead(), Cube::new(2, 1, 0));
127    /// ```
128    #[must_use]
129    #[allow(clippy::missing_inline_in_public_items, reason = "is generic already")]
130    pub fn new(origin: impl Into<FreePoint>, direction: impl Into<FreeVector>) -> Self {
131        Self::new_impl(origin.into(), direction.into())
132    }
133
134    fn new_impl(origin: FreePoint, mut direction: FreeVector) -> Self {
135        // A ray whose direction vector is infinite — or very large — cannot be processed
136        // correctly because we rely on discriminating between different `t` values
137        // (distance in units of the direction vector) to choose the correct next cube.
138        // Therefore, treat it as no stepping -- this is too large to be practical anyway.
139        // (We cannot simply rescale the direction vector because that would change the
140        // reported `t` outputs.)
141        // TODO: Define better threshold value.
142        if !vec_iter(direction)
143            .all(|d| d.abs().partial_cmp(&1e100) == Some(core::cmp::Ordering::Less))
144        {
145            direction = Vector3D::zero();
146        }
147
148        // If there is no enclosing cube then the current cube is undefined so we cannot make
149        // meaningful progress. (In the event of within(), we could in theory have a
150        // suitably bounded interpretation, but that is not of practical interest.)
151        let cube = match Cube::containing(origin) {
152            Some(cube) => cube.lower_bounds(),
153            None => {
154                // Return a raycaster which emits no cubes.
155                return Self::new_empty();
156            }
157        };
158
159        Self {
160            ray: Ray::new(origin, direction),
161            emit_current: true,
162            cube,
163            step: direction.map(signum_101),
164            t_max: origin
165                .to_vector()
166                .zip(direction, scale_to_integer_step)
167                .cast_unit(),
168            t_delta: direction.map(|x| x.abs().recip()).cast_unit(),
169            last_face: Face7::Within,
170            last_t_distance: 0.0,
171            bounds: GridAab::EVERYWHERE,
172        }
173    }
174
175    /// Construct a [`Raycaster`] which will produce no items.
176    /// This is used when numeric overflow prevents success.
177    fn new_empty() -> Self {
178        Self {
179            ray: Ray::new(FreePoint::origin(), FreeVector::zero()),
180            emit_current: false,
181            cube: GridPoint::origin(),
182            step: Vector3D::zero(),
183            t_max: Vector3D::zero(),
184            t_delta: Vector3D::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
185            last_face: Face7::Within,
186            last_t_distance: 0.0,
187            bounds: GridAab::ORIGIN_EMPTY,
188        }
189    }
190
191    /// Restrict the cubes iterated over to those which lie within the given [`GridAab`].
192    ///
193    /// This makes the iterator finite: [`next()`](Self::next) will return [`None`]
194    /// forevermore once there are no more cubes intersecting the bounds to report.
195    ///
196    /// Calling this multiple times takes the intersection of all bounds.
197    #[must_use]
198    #[mutants::skip] // mutation testing will hang; thoroughly tested otherwise
199    #[inline]
200    pub fn within(mut self, bounds: GridAab) -> Self {
201        self.add_bounds(bounds);
202        self
203    }
204
205    /// Like [`Self::within`] but not moving self.
206    ///
207    /// TODO: This function was added for the needs of the raytracer. Think about API design more.
208    #[doc(hidden)]
209    #[allow(clippy::missing_inline_in_public_items)]
210    pub fn add_bounds(&mut self, bounds: GridAab) {
211        self.bounds = self
212            .bounds
213            .intersection_cubes(bounds)
214            .unwrap_or(GridAab::ORIGIN_EMPTY);
215        self.fast_forward();
216    }
217
218    /// Cancels a previous [`Raycaster::within`], allowing the raycast to proceed
219    /// an arbitrary distance (until `GridCoordinate` overflow).
220    ///
221    /// Note: The effect of calling `within()` and then `remove_bound()` without an
222    /// intervening `next()` is not currently guaranteed.
223    ///
224    /// TODO: This function was added for the needs of the raytracer. Think about API design more.
225    #[doc(hidden)]
226    #[inline]
227    pub fn remove_bounds(&mut self) {
228        self.bounds = GridAab::EVERYWHERE;
229    }
230
231    /// Determine the axis to step on and move in the appropriate direction along that axis.
232    ///
233    /// If this step would overflow the [`GridCoordinate`] range, returns [`Err`].
234    #[inline(always)]
235    #[mutants::skip] // mutation testing will hang; thoroughly tested otherwise
236    fn step(&mut self) -> Result<(), ()> {
237        // t_max stores the t-value at which we cross a cube boundary along the
238        // X axis, per component. Therefore, choosing the least t_max axis
239        // chooses the closest cube boundary.
240        let axis: Axis = if self.t_max.x < self.t_max.y {
241            if self.t_max.x < self.t_max.z {
242                Axis::X
243            } else {
244                Axis::Z
245            }
246        } else {
247            if self.t_max.y < self.t_max.z {
248                Axis::Y
249            } else {
250                Axis::Z
251            }
252        };
253
254        assert!(
255            self.step[axis] != 0,
256            "step on axis {axis:X} which is zero; state = {self:#?}"
257        );
258
259        // Save t position before we update it.
260        // We could back-compute this instead as
261        //     let axis = self.last_face.axis().unwrap();
262        //     self.t_max[axis] - self.t_delta[axis]
263        // but that seems an excessive computation to save a field.
264        self.last_t_distance = self.t_max[axis];
265
266        // Move into the new cube, checking for overflow.
267        self.cube[axis] = self.cube[axis].checked_add(self.step[axis]).ok_or(())?;
268
269        // Update t_max to reflect that we have crossed the previous t_max boundary.
270        self.t_max[axis] += self.t_delta[axis];
271
272        // Update face crossing info.
273        // Performance note: Using a match for this turns out to be just slightly slower.
274        const FACE_TABLE: [[Face7; 2]; 3] = [
275            [Face7::PX, Face7::NX],
276            [Face7::PY, Face7::NY],
277            [Face7::PZ, Face7::NZ],
278        ];
279        self.last_face = FACE_TABLE[axis][usize::from(self.step[axis] > 0)];
280
281        Ok(())
282    }
283
284    #[inline(always)]
285    fn valid_for_stepping(&self) -> bool {
286        // If all stepping directions are 0, then we cannot make progress.
287        self.step != Vector3D::zero()
288        // Also check if we had some kind of arithmetic problem in the state.
289        // But permit some positive infinity, because that's just an axis-aligned ray.
290        && !vec_iter(self.t_max).any(|t| t.is_nan())
291        && vec_iter(self.t_max).any(|t| t.is_finite())
292    }
293
294    /// Returns whether `self.cube` is outside of `self.bounds`.
295    ///
296    /// The first boolean is if the ray has _not yet entered_ the bounds,
297    /// and the second boolean is if it the ray has _left_ the bounds. If the ray does
298    /// not intersect the bounds, one or both might be true.
299    fn is_out_of_bounds(&self) -> (bool, bool) {
300        let mut oob_enter = false;
301        let mut oob_exit = false;
302        for axis in Axis::ALL {
303            let range = self.bounds.axis_range(axis);
304            let oob_low = self.cube[axis] < range.start;
305            let oob_high = self.cube[axis] >= range.end;
306            if self.step[axis] == 0 {
307                // Case where the ray has no motion on that axis.
308                oob_enter |= oob_low | oob_high;
309                oob_exit |= oob_low | oob_high;
310            } else {
311                if self.step[axis] > 0 {
312                    oob_enter |= oob_low;
313                    oob_exit |= oob_high;
314                } else {
315                    oob_enter |= oob_high;
316                    oob_exit |= oob_low;
317                }
318            }
319        }
320        (oob_enter, oob_exit)
321    }
322
323    /// In the case where the current position is outside the bounds but might intersect
324    /// the bounds later, attempt to move the position to intersect sooner.
325    #[mutants::skip] // an optimization not a behavior change
326    fn fast_forward(&mut self) {
327        // Find the point which is the origin of all three planes that we want to
328        // intersect with. (Strictly speaking, this could be combined with the next
329        // loop, but it seems more elegant to have a well-defined point.)
330        let plane_origin: GridPoint = {
331            let mut plane_origin = GridPoint::new(0, 0, 0);
332            for axis in Axis::ALL {
333                let which_bounds = if self.step[axis] < 0 {
334                    // Iff the ray is going negatively, then we must use the upper bound
335                    // for the plane origin in this axis. Otherwise, either it doesn't
336                    // matter (parallel) or should be lower bound.
337                    self.bounds.upper_bounds()
338                } else {
339                    self.bounds.lower_bounds()
340                };
341                plane_origin[axis] = which_bounds[axis];
342            }
343            plane_origin
344        };
345
346        // Perform intersections.
347        let mut max_t: FreeCoordinate = 0.0;
348        for axis in Axis::ALL {
349            let direction = self.step[axis];
350            if direction == 0 {
351                // Parallel ray; no intersection.
352                continue;
353            }
354            let mut plane_normal = Vector3D::zero();
355            plane_normal[axis] = direction;
356            let intersection_t = ray_plane_intersection(self.ray, plane_origin, plane_normal);
357            max_t = max_t.max(intersection_t);
358        }
359
360        // TODO: Right test?
361        if max_t > self.last_t_distance {
362            // Go forward to half a cube behind where we think we found the intersection point.
363            let t_start = max_t - 0.5 / self.ray.direction.length();
364            let t_start = if t_start.is_finite() { t_start } else { max_t };
365            let ff_ray = self.ray.advance(t_start);
366
367            let Some(cube) = Cube::containing(ff_ray.origin) else {
368                // Can't fast-forward if we would numeric overflow.
369                // But in that case, also, we almost certainly can't feasibly
370                // raycast either.
371                *self = Self::new_empty();
372                return;
373            };
374
375            *self = Self {
376                ray: ff_ray,
377                emit_current: false,
378                last_face: self.last_face,
379                cube: cube.lower_bounds(),
380
381                // t_max is done the same as in new(), except with an offset
382                t_max: ff_ray
383                    .origin
384                    .to_vector()
385                    .zip(ff_ray.direction, scale_to_integer_step)
386                    .cast_unit()
387                    .map(|t| t + t_start),
388                last_t_distance: t_start,
389
390                // These fields don't depend on position.
391                step: self.step,
392                t_delta: self.t_delta,
393                bounds: self.bounds,
394            };
395        }
396    }
397}
398
399impl Iterator for Raycaster {
400    type Item = RaycastStep;
401
402    /// Returns a [`RaycastStep`] describing the next cube face intersected by the ray.
403    #[inline]
404    #[mutants::skip] // thoroughly tested otherwise
405    fn next(&mut self) -> Option<RaycastStep> {
406        loop {
407            if self.emit_current {
408                self.emit_current = false;
409            } else {
410                if !self.valid_for_stepping() {
411                    // Can't make progress, and we already have done emit_current duty, so stop.
412                    return None;
413                }
414                self.step().ok()?;
415            }
416
417            let (oob_enter, oob_exit) = self.is_out_of_bounds();
418            if oob_exit {
419                // We are past the bounds. There will never again be a cube to report.
420                // Prevent extraneous next() calls from doing any stepping that could overflow
421                // by reusing the emit_current logic.
422                self.emit_current = true;
423                return None;
424            }
425            if oob_enter {
426                // We have not yet intersected the bounds.
427                continue;
428            }
429
430            return Some(RaycastStep {
431                cube_face: CubeFace {
432                    cube: Cube::from(self.cube),
433                    face: self.last_face,
434                },
435                t_distance: self.last_t_distance,
436                t_max: self.t_max,
437            });
438        }
439    }
440
441    // TODO: Implement optional methods:
442    // size_hint (can be determined by finding the far end and summing the offset in each axis)
443    // count, last (requires precise version of size_hint algorithm)
444}
445
446impl core::iter::FusedIterator for Raycaster {}
447
448/// Describes a ray crossing into a cube as defined by [`Raycaster`].
449#[derive(Clone, Copy, Debug, PartialEq)]
450#[expect(clippy::module_name_repetitions)] // TODO: rename to Step?
451pub struct RaycastStep {
452    // The fields of this structure are private to allow for future revision of which
453    // values are calculated versus stored.
454    /// The specific face that was crossed. If the ray's origin was within a cube,
455    /// the face will be [`Face7::Within`].
456    cube_face: CubeFace,
457    /// The distance traversed, as measured in multiples of the supplied direction vector.
458    t_distance: FreeCoordinate,
459    t_max: Vector3D<FreeCoordinate, Tc>,
460}
461
462impl RaycastStep {
463    /// Returns the cube which the raycaster has just found the ray to intersect.
464    ///
465    /// Note that the cube containing the origin of the ray, if any, will be included. In
466    /// that case and only that case, `self.cube_ahead() == self.cube_behind()`.
467    #[inline]
468    pub fn cube_ahead(&self) -> Cube {
469        self.cube_face.cube
470    }
471
472    /// Returns the cube which the raycaster has just found the ray to intersect
473    /// and the face of that cube crossed.
474    #[inline]
475    pub fn cube_face(&self) -> CubeFace {
476        self.cube_face
477    }
478
479    /// Returns the face of [`Self::cube_ahead()`] which is being crossed. The face's normal
480    /// vector points away from that cube and towards [`Self::cube_behind()`].
481    ///
482    /// If the ray starts within a cube, then the initial step will have a face of
483    /// [`Face7::Within`].
484    ///
485    /// ```
486    /// # use all_is_cubes_base as all_is_cubes;
487    /// use all_is_cubes::math::Face7;
488    /// use all_is_cubes::raycast::Raycaster;
489    ///
490    /// let mut r = Raycaster::new((0.5, 0.5, 0.5), (1.0, 0.0, 0.0));
491    /// let mut next = || r.next().unwrap();
492    ///
493    /// assert_eq!(next().face(), Face7::Within);  // started at (0, 0, 0)
494    /// assert_eq!(next().face(), Face7::NX);      // moved to (1, 0, 0)
495    /// assert_eq!(next().face(), Face7::NX);      // moved to (2, 0, 0)
496    /// ```
497    #[inline]
498    pub fn face(&self) -> Face7 {
499        self.cube_face.face
500    }
501
502    /// Returns the cube adjacent to `self.cube_ahead()` which the ray arrived from within.
503    ///
504    /// If the ray starts within a cube, then for that case and that case only,
505    /// `self.cube_ahead() == self.cube_behind()`.
506    ///
507    /// ```
508    /// # use all_is_cubes_base as all_is_cubes;
509    /// use all_is_cubes::math::Cube;
510    /// use all_is_cubes::raycast::Raycaster;
511    ///
512    /// let mut r = Raycaster::new([0.5, 0.5, 0.5], [1.0, 0.0, 0.0]);
513    /// let mut next = || r.next().unwrap();
514    ///
515    /// assert_eq!(next().cube_behind(), Cube::new(0, 0, 0));  // started here
516    /// assert_eq!(next().cube_behind(), Cube::new(0, 0, 0));  // moved to (1, 0, 0)
517    /// assert_eq!(next().cube_behind(), Cube::new(1, 0, 0));  // which is now behind...
518    /// assert_eq!(next().cube_behind(), Cube::new(2, 0, 0));
519    /// ```
520    #[inline]
521    pub fn cube_behind(&self) -> Cube {
522        self.cube_face.adjacent()
523    }
524
525    /// The distance traversed so far, as measured in multiples of the ray's direction vector.
526    #[inline]
527    #[mutants::skip] // trivial, but modifying it can cause test hangs
528    pub fn t_distance(&self) -> FreeCoordinate {
529        self.t_distance
530    }
531
532    /// Returns the specific point at which the ray intersected the face.
533    ///
534    /// The caller must provide the original ray; this is because remembering
535    /// the ray so as to perform a ray-plane intersection is unnecessary
536    /// overhead for all raycasts that don't need this information.
537    ///
538    /// The returned point is guaranteed to be within the face (a unit square):
539    /// the perpendicular axis's coordinate will have an integer value either equal to
540    /// [`Self::cube_ahead`]'s coordinate on that axis, or that plus 1 if the ray
541    /// entered from the positive direction, and the parallel axes will have coordinates
542    /// no more than +1.0 different.
543    ///
544    /// ```
545    /// # use all_is_cubes_base as all_is_cubes;
546    /// use all_is_cubes::euclid::point3;
547    /// use all_is_cubes::raycast::Ray;
548    ///
549    /// let ray = Ray::new([0.5, 0.5, 0.5], [1.0, 0.0, 0.0]);
550    /// let mut raycaster = ray.cast();
551    /// let mut next = || raycaster.next().unwrap().intersection_point(ray);
552    ///
553    /// // First intersection is the interior of the origin cube.
554    /// assert_eq!(next(), point3(0.5, 0.5, 0.5));
555    /// assert_eq!(next(), point3(1.0, 0.5, 0.5));
556    /// assert_eq!(next(), point3(2.0, 0.5, 0.5));
557    /// ```
558    #[allow(clippy::missing_inline_in_public_items)]
559    pub fn intersection_point(&self, ray: Ray) -> FreePoint {
560        let current_face_axis = self.cube_face.face.axis();
561        if current_face_axis.is_none() {
562            ray.origin
563        } else {
564            let mut intersection_point: FreePoint =
565                self.cube_face.cube.lower_bounds().map(FreeCoordinate::from);
566            for axis in Axis::ALL {
567                let step_direction = signum_101(ray.direction[axis]);
568                if Some(axis) == current_face_axis {
569                    // This is the plane we just hit.
570                    if step_direction < 0 {
571                        intersection_point[axis] += 1.0;
572                    }
573                } else if step_direction == 0 {
574                    // Ray is perpendicular to this axis, so it does not move from the origin.
575                    intersection_point[axis] = ray.origin[axis];
576                } else {
577                    // Normal cube face hit.
578                    let offset_inside_cube =
579                        (self.t_max[axis] - self.t_distance) * ray.direction[axis];
580                    intersection_point[axis] += if step_direction > 0 {
581                        1. - offset_inside_cube.clamp(0.0, 1.0)
582                    } else {
583                        (-offset_inside_cube).clamp(0.0, 1.0)
584                    };
585                }
586            }
587            intersection_point
588        }
589    }
590
591    /// Subdivide the [`cube_ahead`](Self::cube_ahead) into smaller cubes,
592    /// and start a raycast on that grid.
593    ///
594    /// `bounds` will constrain the raycaster exactly like [`Raycaster::within()`].
595    /// It should be no larger than `GridAab::for_block(resolution)`;
596    /// if it is, then whether the output includes those additional cubes is unspecified.
597    ///
598    /// The length of the adjusted ray is unaltered, so the `t` increments per cube will be
599    /// the same as they were for the original raycast.
600    // TODO: That length may not be ideal; review use cases.
601    //
602    // TODO: This doc comment could really use a diagram.
603    //
604    // TODO: Remove `Ray` from the return value, and instead make it possible to get the
605    // real original `Ray` from any `Raycaster`.
606    #[allow(clippy::missing_inline_in_public_items)]
607    #[doc(hidden)] // want to improve its robustness before making it public API
608    pub fn recursive_raycast(
609        &self,
610        ray: Ray,
611        resolution: Resolution,
612        bounds: GridAab,
613    ) -> (Raycaster, Ray) {
614        // TODO: Replace this with making use of the step and raycaster's information
615        // such that the produced raycaster is guaranteed to produce at least one cube
616        // on the face that `self` hits (rather than possibly missing everything due to
617        // rounding error).
618        let cube = self.cube_ahead();
619        let sub_ray = Ray {
620            origin: ((ray.origin - cube.lower_bounds().map(FreeCoordinate::from))
621                * FreeCoordinate::from(resolution))
622            .to_point(),
623            direction: ray.direction,
624        };
625        (sub_ray.cast().within(bounds), sub_ray)
626    }
627}
628
629fn vec_iter<T, U>(v: Vector3D<T, U>) -> impl Iterator<Item = T> {
630    Into::<[T; 3]>::into(v).into_iter()
631}
632
633/// 3-valued signum (zero produces zero) rather than the 2-valued one Rust gives,
634/// and with an integer result.
635fn signum_101(x: FreeCoordinate) -> GridCoordinate {
636    if x == 0.0 {
637        0
638    } else {
639        x.signum() as GridCoordinate
640    }
641}
642
643/// Find the smallest positive `t` such that `s + t * ds` is an integer.
644///
645/// If `ds` is zero, returns positive infinity; this is a useful answer because
646/// it means that the less-than comparisons in the raycast algorithm will never pick
647/// the corresponding axis. If any input is NaN, returns NaN.
648#[doc(hidden)] // public for GPU implementation comparison tests
649#[inline]
650pub fn scale_to_integer_step(mut s: FreeCoordinate, mut ds: FreeCoordinate) -> FreeCoordinate {
651    if ds == 0.0 && !s.is_nan() {
652        // Explicitly handle zero case.
653        // This almost could be implicit, but it is possible for the below division to
654        // return NaN instead of +inf, in the case where (1.0 - s) rounds down to zero.
655        return FreeCoordinate::INFINITY;
656    } else if ds < 0.0 {
657        // Simplify to positive case only.
658        // Note that the previous condition eliminated the case of negative zero.
659        s = -s;
660        ds = -ds;
661    }
662
663    let s = s.rem_euclid(1.0);
664    // problem is now s + t * ds = 1
665    let result = (1.0 - s) / ds;
666
667    debug_assert!(
668        result.signum() > 0.0 || ds.is_nan() || s.is_nan(),
669        "scale_to_integer_step failed ({s}, {ds}) => {result}"
670    );
671    result
672}
673
674fn ray_plane_intersection(
675    ray: Ray,
676    plane_origin: GridPoint,
677    plane_normal: GridVector,
678) -> FreeCoordinate {
679    let plane_origin: FreePoint = plane_origin.map(FreeCoordinate::from);
680    let plane_normal: FreeVector = plane_normal.map(FreeCoordinate::from);
681    let relative_position = plane_origin - ray.origin;
682
683    // Compute the intersection 't' value.
684    relative_position.dot(plane_normal) / ray.direction.dot(plane_normal)
685}