all-is-cubes 0.5.0

Recursive voxel game engine. Can be used for voxel raytracing.
Documentation
//! Algorithms for collision detection with [`Space`](crate::space::Space)s.

use std::collections::HashSet;
use std::fmt;

use cgmath::{EuclideanSpace as _, InnerSpace as _, Point3, Vector3, Zero as _};

use super::POSITION_EPSILON;
use crate::block::{BlockCollision, EvaluatedBlock, Evoxel, Resolution, Resolution::R1};
use crate::math::{
    Aab, CubeFace, Face6, Face7, FreeCoordinate, Geometry, GridAab, GridArray, GridCoordinate,
    GridPoint, Rgba,
};
use crate::raycast::{Ray, Raycaster};
use crate::space::Space;
use crate::util::{ConciseDebug, CustomFormat, MapExtend};

/// An individual collision contact; something in a [`Space`] that a moving [`Aab`]
/// collided with.
///
/// This type is designed to be comparable/hashable to deduplicate contacts.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
#[allow(clippy::exhaustive_enums)] // any change will probably be breaking anyway
pub enum Contact {
    /// Contact with a fully solid block; the [`CubeFace`] specifies the block position
    /// and the side of it that was collided with (hence also the contact normal).
    Block(CubeFace),
    Voxel {
        /// The “outer” cube in the [`Space`].
        cube: GridPoint,
        /// The voxel resolution of the block; that is, the factor by which voxel
        /// coordinates are smaller than `cube` coordinates.
        resolution: Resolution,
        /// The voxel position in the block (each coordinate lies between `0` and
        /// `resolution - 1`) and the face of that voxel collided with, which is also
        /// the contact normal.
        voxel: CubeFace,
    },
}

impl Contact {
    pub fn cube(&self) -> GridPoint {
        match *self {
            Contact::Block(CubeFace { cube, .. }) => cube,
            Contact::Voxel { cube, .. } => cube,
        }
    }

    /// Returns the contact normal: the direction in which the colliding box should be
    /// pushed back.
    ///
    /// Note that this may be equal to [`Face7::Within`] in case the box was already
    /// intersecting before any movement.
    pub fn normal(&self) -> Face7 {
        match *self {
            Contact::Block(CubeFace { face, .. }) => face,
            Contact::Voxel {
                voxel: CubeFace { face, .. },
                ..
            } => face,
        }
    }

    pub fn resolution(&self) -> Resolution {
        match *self {
            Contact::Block(_) => R1,
            Contact::Voxel { resolution, .. } => resolution,
        }
    }

    /// Return a copy where the contact normal is replaced with [`Face7::Within`].
    fn without_normal(&self) -> Self {
        let mut result = *self;
        match result {
            Contact::Block(CubeFace { ref mut face, .. }) => *face = Face7::Within,
            Contact::Voxel {
                voxel: CubeFace { ref mut face, .. },
                ..
            } => *face = Face7::Within,
        }
        result
    }
}

impl fmt::Debug for Contact {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Block(CubeFace { cube, face }) => {
                write!(f, "{face:?} of {}", cube.custom_format(ConciseDebug))
            }
            Self::Voxel {
                cube,
                resolution,
                voxel: CubeFace { cube: voxel, face },
            } => write!(
                f,
                "{:?} of {} {}/{}",
                face,
                cube.custom_format(ConciseDebug),
                voxel.custom_format(ConciseDebug),
                resolution
            ),
        }
    }
}

impl Geometry for Contact {
    type Coord = GridCoordinate;

    fn translate(mut self, offset: Vector3<Self::Coord>) -> Self {
        match &mut self {
            Contact::Block(CubeFace { mut cube, .. }) => cube += offset,
            Contact::Voxel { mut cube, .. } => cube += offset,
        }
        self
    }

    fn wireframe_points<E>(&self, output: &mut E)
    where
        E: Extend<(Point3<FreeCoordinate>, Option<Rgba>)>,
    {
        match self {
            Contact::Block(cube_face) => cube_face.wireframe_points(output),
            Contact::Voxel {
                cube,
                resolution,
                voxel,
            } => {
                let resolution: FreeCoordinate = (*resolution).into();
                voxel.wireframe_points(&mut MapExtend::new(
                    output,
                    |mut vert: (Point3<FreeCoordinate>, Option<Rgba>)| {
                        vert.0 = vert.0 / resolution + cube.to_vec().map(FreeCoordinate::from);
                        vert
                    },
                ))
            }
        }
    }
}

/// Result of [`collide_along_ray`] which specifies a collision point possibly inside the cube.
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct CollisionRayEnd {
    /// Non-colliding length of the provided ray.
    pub t_distance: FreeCoordinate,
    pub contact: Contact,
}

/// Specifies the ending condition for [`collide_along_ray()`]: what type of situation
/// it should stop prior to the end of the ray for.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(crate) enum StopAt {
    /// Stop on any collisions, including those at the starting position (already
    /// colliding).
    Anything,
    /// Stop when something new is collided with (excluding the starting position).
    NotAlreadyColliding,
    /// Stop at the first position where nothing is being collided with.
    #[allow(dead_code)] // TODO: write tests and use this in the implementation of push_out()
    EmptySpace,
}

impl StopAt {
    fn reversed(self) -> bool {
        match self {
            StopAt::Anything => false,
            StopAt::NotAlreadyColliding => false,
            StopAt::EmptySpace => true,
        }
    }
}

/// Move `aab`'s origin along the line segment from `ray.origin` to `ray.origin + ray.direction`,
/// and find the first point at which it collides with `space`'s collidable blocks.
///
/// The return value specifies the distance achieved and the normal (face) of the surface collided
///  with; if [`None`], then no obstacles were met along the full length of the line segment.
///
/// `collision_callback` is called once for each colliding cube — any one of them would have been
/// sufficient to stop the ray, but all are reported.
pub(crate) fn collide_along_ray<Sp, CC>(
    space: &Sp,
    ray: Ray,
    aab: Aab,
    mut collision_callback: CC,
    stop_at: StopAt,
) -> Option<CollisionRayEnd>
where
    Sp: CollisionSpace,
    CC: FnMut(Contact),
{
    let mut already_colliding: HashSet<Contact> = HashSet::new();

    debug_assert!(
        ray.direction.magnitude2() < super::body::VELOCITY_MAGNITUDE_LIMIT_SQUARED * 2.,
        "Attempting to collide_along_ray a very long distance: {:?}",
        ray
    );

    // Note: no `.within()` because that would not work when the leading corner is not
    // within the bounds. We could expand the bounds slightly (while considering overflow
    // cases), but this would be an optimization which only affects the unusual case of
    // being out of bounds, so it's not worth doing unless we specifically expect to have
    // many bodies outside a space and occasionally inside.
    for ray_step in aab_raycast(aab, ray, stop_at.reversed()) {
        let offset_segment = nudge_on_ray(
            aab,
            ray.scale_direction(ray_step.t_distance()),
            ray_step.face().opposite(),
            R1,
            stop_at.reversed(),
        );
        let step_aab = aab.translate(offset_segment.unit_endpoint().to_vec());
        if ray_step.t_distance() >= 1.0 {
            // Movement is unobstructed in this timestep.
            break;
        }

        // Compute the AAB of the potential intersection, excluding the exterior of the
        // space.
        let potential_intersection_bounds =
            match step_aab.round_up_to_grid().intersection(space.bounds()) {
                Some(aab) => aab,
                None => continue,
            };

        // Loop over all the cubes that our AAB is just now intersecting and check if
        // any of them are solid, and if so, how far into their volume is a hit.
        // TODO: Useful optimization for large AABs would be skipping all the interior
        // cubes that must have been detected in the _previous_ step.
        let mut something_hit = None;
        let mut nothing_hit = true;
        for cube in potential_intersection_bounds.interior_iter() {
            let cell = space.get_cell(cube);
            let full_cube_end = CollisionRayEnd {
                t_distance: ray_step.t_distance(),
                contact: Contact::Block(CubeFace {
                    cube,
                    face: ray_step.face(),
                }),
            };
            let found_end = match Sp::collision(cell) {
                BlockCollision::None => {
                    // No collision for this block
                    continue;
                }
                BlockCollision::Hard => full_cube_end,
                BlockCollision::Recur => {
                    if let Some(found_end) = Sp::recurse(
                        full_cube_end.clone(),
                        aab,
                        ray,
                        cell,
                        match (stop_at, ray_step.face()) {
                            // If we are checking the initial position (face == Face7::Within),
                            // then don't recursively ignore initial collisions, but report them
                            // so that we can add to already_collising.
                            (StopAt::NotAlreadyColliding, Face7::Within) => StopAt::Anything,
                            (stop_at, _) => stop_at,
                        },
                    ) {
                        found_end
                    } else {
                        // No collision
                        continue;
                    }
                }
            };

            // If we didn't continue then we hit something.
            nothing_hit = false;

            if stop_at == StopAt::NotAlreadyColliding {
                if found_end.contact.normal() == Face7::Within {
                    // If we start intersecting a block, we are allowed to leave it; pretend
                    // it doesn't exist. (Ideally, `push_out()` would have fixed this, but
                    // maybe there's no clear direction.)
                    already_colliding.insert(found_end.contact);
                    collision_callback(found_end.contact);
                    continue;
                } else if already_colliding.contains(&found_end.contact.without_normal()) {
                    continue;
                }
            }

            // TODO: We need to buffer contacts instead of calling this callback, in case something else is closer
            collision_callback(found_end.contact);

            let nearest_so_far = match something_hit {
                Some(CollisionRayEnd { t_distance, .. }) => t_distance,
                None => FreeCoordinate::INFINITY,
            };
            if found_end.t_distance < nearest_so_far {
                something_hit = Some(found_end);
            }
        }

        // Now that we've found _all_ the contacts for this step, report the collision.
        match stop_at {
            StopAt::Anything | StopAt::NotAlreadyColliding => {
                if let Some(end) = something_hit {
                    return Some(end);
                }
            }
            StopAt::EmptySpace => {
                if nothing_hit {
                    return Some(CollisionRayEnd {
                        t_distance: ray_step.t_distance(),
                        // TODO: incorrect result; this should arguably refer to the surface we're
                        // just *leaving*, and in any case should use recursion
                        contact: Contact::Block(ray_step.cube_face()),
                    });
                }
            }
        }
    }

    None
}

/// Returns an iterator over all blocks in `space` which intersect `aab`, accounting for
/// collision options.
pub(crate) fn find_colliding_cubes<Sp>(space: &Sp, aab: Aab) -> impl Iterator<Item = GridPoint> + '_
where
    Sp: CollisionSpace,
{
    // TODO: rework interfaces to avoid allocating a vector
    // TODO: don't throw out the contact details
    let mut points = Vec::new();
    collide_along_ray(
        space,
        Ray::new([0., 0., 0.], [0., 0., 0.]),
        aab,
        |contact| {
            points.push(contact.cube());
        },
        StopAt::Anything,
    );
    points.into_iter()
}

/// Abstraction over voxel arrays that the collision detection algorithm can use,
/// i.e. [`Space`] and `GridArray<Evoxel>`.
pub(crate) trait CollisionSpace {
    type Cell;

    /// Bounds outside of which there is definitely nothing that collides.
    fn bounds(&self) -> GridAab;

    /// Retrieve a cell value from the grid.
    /// Should return a non-colliding value if the point is out of bounds.
    fn get_cell(&self, cube: GridPoint) -> &Self::Cell;

    /// Retrieve a cell's collision behavior option.
    fn collision(cell: &Self::Cell) -> BlockCollision;

    /// TODO: document
    fn get_voxels(cell: &Self::Cell) -> Option<(Resolution, &GridArray<Evoxel>)>;

    /// TODO: document
    ///
    /// * `entry_end`: the endpoint we would return if the recursion stopped here — entering the given cell.
    fn recurse(
        entry_end: CollisionRayEnd,
        aab: Aab,
        ray: Ray,
        cell: &Self::Cell,
        stop_at: StopAt,
    ) -> Option<CollisionRayEnd>;
}

impl CollisionSpace for Space {
    type Cell = EvaluatedBlock;

    fn bounds(&self) -> GridAab {
        Space::bounds(self)
    }

    #[inline]
    fn get_cell(&self, cube: GridPoint) -> &Self::Cell {
        self.get_evaluated(cube)
    }

    #[inline]
    fn collision(cell: &Self::Cell) -> BlockCollision {
        cell.attributes.collision
    }

    #[inline]
    fn get_voxels(evaluated: &EvaluatedBlock) -> Option<(Resolution, &GridArray<Evoxel>)> {
        evaluated.voxels.as_ref().map(|v| (evaluated.resolution, v))
    }

    #[inline]
    fn recurse(
        entry_end: CollisionRayEnd,
        space_aab: Aab,
        space_ray: Ray,
        evaluated: &EvaluatedBlock,
        stop_at: StopAt,
    ) -> Option<CollisionRayEnd> {
        match &evaluated.voxels {
            // Plain non-recursive collision
            None => Some(entry_end),
            Some(voxels) => {
                let cube_translation = entry_end
                    .contact
                    .cube()
                    .to_vec()
                    .map(|s| -FreeCoordinate::from(s));
                let scale = FreeCoordinate::from(evaluated.resolution);
                // Transform our original AAB and ray so that it is in the coordinate system of the block voxels.
                // Note: aab is not translated since it's relative to the ray anyway.
                let voxel_aab = space_aab.scale(scale);
                let voxel_ray = space_ray.translate(cube_translation).scale_all(scale);
                if let Some(hit_voxel) =
                    collide_along_ray(voxels, voxel_ray, voxel_aab, |_| {}, stop_at)
                {
                    let CollisionRayEnd {
                        t_distance: voxel_t_distance,
                        contact,
                    } = hit_voxel;

                    match contact {
                        Contact::Block(voxel) => Some(CollisionRayEnd {
                            t_distance: voxel_t_distance,
                            contact: Contact::Voxel {
                                cube: entry_end.contact.cube(),
                                resolution: evaluated.resolution,
                                voxel,
                            },
                        }),
                        Contact::Voxel { .. } => panic!("encountered 3-level voxel recursion"),
                    }
                } else {
                    // Didn't hit anything within this block.
                    None
                }
            }
        }
    }
}

// TODO: This impl is not yet used; it will be used for voxel block collision.
// It exists now as a piece of incremental progress and to prove that `CollisionSpace`
// *can* be implemented for EvaluatedBlock.
impl CollisionSpace for GridArray<Evoxel> {
    type Cell = Evoxel;

    fn bounds(&self) -> GridAab {
        GridArray::bounds(self)
    }

    #[inline]
    fn get_cell(&self, cube: GridPoint) -> &Self::Cell {
        self.get(cube).unwrap_or(&Evoxel::AIR)
    }

    #[inline]
    fn collision(cell: &Self::Cell) -> BlockCollision {
        cell.collision
    }

    #[inline]
    fn get_voxels(_cell: &Self::Cell) -> Option<(Resolution, &GridArray<Evoxel>)> {
        None
    }
    #[inline(always)]
    fn recurse(
        entry_end: CollisionRayEnd,
        _aab: Aab,
        _local_ray: Ray,
        _cell: &Self::Cell,
        _stop_at: StopAt,
    ) -> Option<CollisionRayEnd> {
        Some(entry_end)
    }
}

/// Given a ray describing movement of the origin of an AAB, perform a raycast to find
/// the positions where the AAB moves into new cubes. The returned ray steps' `t_distance`
/// values report how far to move the AAB to meet the edge.
///
/// If `reversed` is true, find positions where it leaves cubes.
///
/// Note that due to the nature of floating-point arithmetic, it is uncertain whether the
/// resulting new AAB position will have the AAB's forward face land before, after, or
/// exactly on the boundary. The caller must compute an appropriate nudge (using TODO:
/// provide a function for this) to serve its needs.
pub(crate) fn aab_raycast(aab: Aab, origin_ray: Ray, reversed: bool) -> Raycaster {
    let leading_corner = aab.leading_corner(if reversed {
        -origin_ray.direction
    } else {
        origin_ray.direction
    });
    let leading_ray = origin_ray.translate(leading_corner);
    leading_ray.cast()
}

/// Given an [`Aab`] and a [`Ray`] such that the given face of
/// `aab.translate(segment.unit_endpoint().to_vec())`
/// lands almost but not quite on a unit cell boundary along the `plane` axis,
/// nudge the endpoint of the ray
/// infinitesimally so that it lands definitely beyond (or before if `backward`)
/// the cell boundary.
///
/// Note that the required `face` is the opposite of the face produced by a raycast.
/// (This may be confusing but we feel that it would be more confusing to use a face
/// other than the relevant face of the [`Aab`]).
pub(crate) fn nudge_on_ray(
    aab: Aab,
    segment: Ray,
    face: Face7,
    subdivision: Resolution,
    backward: bool,
) -> Ray {
    if segment.direction.is_zero() {
        return segment;
    }
    let face: Face6 = match face.try_into() {
        Ok(f) => f,
        Err(_) => return segment,
    };

    // This is the depth by which the un-nudged face penetrates the plane, which we are going to subtract.
    let penetration_depth = {
        let subdivision = FreeCoordinate::from(subdivision);
        let fc_scaled = aab
            .translate(segment.unit_endpoint().to_vec())
            .face_coordinate(face)
            * subdivision;
        (fc_scaled - fc_scaled.round()) / subdivision
    };

    // This is the length of the direction vector projected along the face normal — thus, the reciprocal of the scale factor by which to adjust the distance.
    let direction_projection = face.dot(segment.direction);

    debug_assert!(direction_projection != 0.0);

    // `translation` is how far we want to translate the AAB linearly along the face normal.
    let epsilon_nudge = if backward {
        -POSITION_EPSILON
    } else {
        POSITION_EPSILON
    };
    let translation = epsilon_nudge - penetration_depth;

    if false {
        dbg!(
            face,
            aab.face_coordinate(face),
            penetration_depth,
            translation,
            direction_projection,
        );
    }
    segment.scale_direction(1.0 + translation / direction_projection)
}

#[cfg(test)]
mod tests {
    use crate::block::Resolution::*;
    use crate::block::{Block, AIR};
    use crate::content::{make_slab, make_some_blocks};
    use crate::math::{point_to_enclosing_cube, GridAab};
    use crate::raytracer::print_space;
    use crate::universe::Universe;

    use super::*;
    use rand::{Rng, SeedableRng as _};
    #[test]
    fn collide_along_ray_with_opaque_block() {
        collide_along_ray_tester(
            1.5,
            |_u| {
                let [block] = make_some_blocks();
                [AIR, block]
            },
            Some(CollisionRayEnd {
                t_distance: 0.25, // half of a unit cube, quarter of a ray with magnitude 2
                contact: Contact::Block(CubeFace::new([1, 0, 0], Face7::PY)),
            }),
        );
    }

    #[test]
    fn collide_along_ray_recursive_from_outside() {
        collide_along_ray_tester(
            1.5,
            |u| [AIR, make_slab(u, 1, R2)],
            Some(CollisionRayEnd {
                t_distance: 0.5, // half of a ray with magnitude 2
                contact: Contact::Voxel {
                    cube: GridPoint::new(1, 0, 0),
                    resolution: R2,
                    // TODO: the voxel reported here is arbitrary, so this test is fragile
                    voxel: CubeFace::new([0, 0, 0], Face7::PY),
                },
            }),
        );
    }

    #[test]
    fn collide_along_ray_recursive_from_inside() {
        collide_along_ray_tester(
            0.75,
            |u| [AIR, make_slab(u, 1, R2)],
            Some(CollisionRayEnd {
                t_distance: 0.125,
                contact: Contact::Voxel {
                    cube: GridPoint::new(1, 0, 0),
                    resolution: R2,
                    // TODO: the voxel reported here is arbitrary, so this test is fragile
                    voxel: CubeFace::new([0, 0, 0], Face7::PY),
                },
            }),
        );
    }

    /// Check that colliding against two recursive blocks correctly picks the taller one,
    /// in either ordering.
    #[test]
    fn collide_along_ray_two_recursive() {
        collide_along_ray_tester(
            0.75,
            |u| [make_slab(u, 1, R4), make_slab(u, 1, R2)],
            Some(CollisionRayEnd {
                t_distance: 0.125,
                contact: Contact::Voxel {
                    cube: GridPoint::new(1, 0, 0), // second of 2 blocks is taller
                    resolution: R2,
                    voxel: CubeFace::new([0, 0, 0], Face7::PY),
                },
            }),
        );
        collide_along_ray_tester(
            0.75,
            // Opposite ordering of the other test
            |u| [make_slab(u, 1, R2), make_slab(u, 1, R4)],
            Some(CollisionRayEnd {
                t_distance: 0.125,
                contact: Contact::Voxel {
                    cube: GridPoint::new(0, 0, 0), // first of 2 blocks is taller
                    resolution: R2,
                    voxel: CubeFace::new([1, 0, 0], Face7::PY),
                },
            }),
        );
    }

    fn collide_along_ray_tester(
        initial_y: FreeCoordinate,
        block_gen: fn(&mut Universe) -> [Block; 2],
        expected_end: Option<CollisionRayEnd>,
    ) {
        let u = &mut Universe::new();
        let blocks = block_gen(u);
        let mut space = Space::empty_positive(2, 1, 1);
        space.set([0, 0, 0], &blocks[0]).unwrap();
        space.set([1, 0, 0], &blocks[1]).unwrap();
        print_space(&space, [1., 1., 1.]);

        // Set up to collide with the block such that the ray doesn't pass through it, to
        // make sure the right cube is returned.
        // The block is at [1, 0, 0] and we're "dropping" a block-shaped AAB onto it
        // with lower corner [0.5, 1.5, 0] so it should contact the "left" half of
        // the `block` after moving a distance of 0.5 plus whatever penetration
        // depth into a recursive block applies.
        let aab = Aab::from_cube(GridPoint::new(0, 0, 0));
        let ray = Ray::new([0.5, initial_y, 0.], [0., -2., 0.]);

        let result = collide_along_ray(&space, ray, aab, |_| {}, StopAt::NotAlreadyColliding);
        assert_eq!(result, expected_end);
    }

    /// Test reporting of being already inside a block at the start of the ray,
    /// particularly with a trailing AAB bigger than a block.
    #[test]
    fn already_colliding() {
        let mut u = Universe::new();
        let mut space = Space::empty_positive(2, 1, 1);
        let [block] = make_some_blocks();
        let half_block = make_slab(&mut u, 1, R2);
        space.set([0, 0, 0], &block).unwrap();
        space.set([1, 0, 0], &half_block).unwrap();

        let aab = Aab::from_lower_upper([-1., -1., -1.], [2., 1., 1.]);
        let ray = Ray::new([0.5, 0.0, 0.5], [1., 0., 0.]);

        let mut contacts = Vec::new();
        let result = collide_along_ray(
            &space,
            ray,
            aab,
            |c| contacts.push(c),
            StopAt::NotAlreadyColliding,
        );
        assert_eq!(
            (result, contacts),
            (
                None,
                vec![
                    Contact::Block(CubeFace::new([0, 0, 0], Face7::Within)),
                    Contact::Voxel {
                        cube: GridPoint::new(1, 0, 0),
                        resolution: R2,
                        // TODO: the voxel reported here is arbitrary, so this test is fragile
                        voxel: CubeFace::new([0, 0, 0], Face7::Within),
                    }
                ]
            )
        )
    }

    #[test]
    fn nudge_random_test() {
        let moving_aab = Aab::new(-0.345, 0.489, -0.118, 0.0325, -0.319, 0.2252);
        let mut rng = rand_xoshiro::Xoshiro256Plus::seed_from_u64(0);
        for case_number in 0..1000 {
            // Prepare test data
            let ray = Ray::new(
                Aab::new(-2., 2., -2., 2., -2., 2.).random_point(&mut rng),
                Aab::new(-1., 1., -1., 1., -1., 1.)
                    .random_point(&mut rng)
                    .to_vec(),
            );
            let step = aab_raycast(moving_aab, ray, false)
                .nth(rng.gen_range(1..10))
                .unwrap();
            let axis = step.face().axis_number().expect("should have an axis");
            let segment = ray.scale_direction(step.t_distance()); // TODO: this should be a function? Should aab_raycast return a special step type with these features?
            let unnudged_aab = moving_aab.translate(segment.unit_endpoint().to_vec());
            let face_to_nudge: Face6 = Face6::try_from(step.face().opposite()).unwrap();

            eprintln!("\n#{case_number} with inputs:");
            eprintln!("  ray: {ray:?}");
            eprintln!("  step: {step:?}");
            eprintln!("  segment: {segment:?}");
            eprintln!("  face_to_nudge: {face_to_nudge:?}");

            for backward in [true, false] {
                // Perform nudge
                // TODO: test non-1 resolution
                let adjusted_segment =
                    nudge_on_ray(moving_aab, segment, face_to_nudge.into(), R1, backward);
                let nudged_aab = moving_aab.translate(adjusted_segment.unit_endpoint().to_vec());

                // Check that the nudge was not too big
                let nudge_length = (adjusted_segment.direction - segment.direction).magnitude();
                assert!(
                    nudge_length <= 0.001,
                    "nudge moved too far\nfrom {:?}\n  to {:?}\ndistance of {}",
                    segment,
                    adjusted_segment,
                    nudge_length
                );

                // Check that the nudge was not backwards of the ray
                assert!(
                    adjusted_segment.direction.dot(segment.direction) >= 0.0,
                    "nudge went backwards to {:?} from starting position {:?}",
                    adjusted_segment,
                    segment,
                );

                // Check expected position properties
                let position_on_axis = nudged_aab.face_coordinate(face_to_nudge);
                let fraction = position_on_axis - position_on_axis.round();
                assert!(
                    fraction.abs() > POSITION_EPSILON / 2.,
                    "{:?} coord {:?} shouldn't be at surface",
                    face_to_nudge,
                    position_on_axis,
                );
                assert!(
                    fraction.abs() < POSITION_EPSILON * 2.,
                    "{:?} coord {:?} shouldn't be so large",
                    face_to_nudge,
                    position_on_axis,
                );

                let enclosing = nudged_aab.round_up_to_grid();
                if backward {
                    let expected_enclosing = GridAab::single_cube(
                        point_to_enclosing_cube(segment.unit_endpoint()).unwrap(),
                    );
                    assert_eq!(
                        enclosing.axis_range(axis),
                        expected_enclosing.axis_range(axis),
                        "\ncase {:?}\nface {:?}\nsegment {:?}\nunnudged_aab {:#?}\nnudged {:#?}\n",
                        case_number,
                        face_to_nudge,
                        segment,
                        unnudged_aab,
                        nudged_aab,
                    );
                } else {
                    // TODO check the forward cases
                }
            }
        }
    }
}