phys-raycast 2.0.0

Ray casting functionality for 3D physics shapes
Documentation
// Copyright (C) 2020-2025 phys-raycast authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use phys_geom::math::*;
use phys_geom::shape::InfinitePlane;

use crate::{Raycast, RaycastHitResult};

/// Extension trait for InfinitePlane providing additional utility methods.
pub trait InfinitePlaneExt {
    /// Calculate the signed distance from a point to the plane.
    ///
    /// Positive distance means the point is above the plane (in the direction of the normal),
    /// negative distance means the point is below the plane.
    fn distance(&self, p: Point3) -> Real;
}

fn normal() -> UnitVec3 {
    Vec3::y_axis()
}

impl InfinitePlaneExt for InfinitePlane {
    #[inline]
    fn distance(&self, p: Point3) -> Real {
        p.coords.dot(&normal().into_inner())
    }
}

impl Raycast for InfinitePlane {
    fn raycast(
        &self,
        local_ray: phys_geom::Ray,
        max_distance: Real,
        discard_inside_hit: bool,
    ) -> Option<RaycastHitResult> {
        let origin_to_plane_distance = self.distance(local_ray.origin);

        // ray start on the plane
        if origin_to_plane_distance.abs() <= Real::EPSILON && !discard_inside_hit {
            return Some(RaycastHitResult {
                distance: 0.0,
                normal: normal(),
            });
        }

        let dn = local_ray.direction.dot(&normal().into_inner());

        // parallel ray, not on the plane
        if (-Real::EPSILON < dn) && (dn < Real::EPSILON) {
            return None;
        }

        let hit_distance = -origin_to_plane_distance / dn;

        // ray cast away from plane
        if hit_distance < 0.0 {
            return None;
        }

        if hit_distance > 0.0 && hit_distance <= max_distance {
            // above plane
            if origin_to_plane_distance > 0.0 {
                Some(RaycastHitResult {
                    distance: hit_distance,
                    normal: normal(),
                })
            // under plane
            } else if discard_inside_hit {
                None
            } else {
                Some(RaycastHitResult {
                    distance: hit_distance,
                    normal: -normal(),
                })
            }
        } else {
            None
        }
    }
}

#[cfg(test)]
mod tests {
    use approx::assert_relative_eq;

    use super::*;

    #[test]
    fn test_raycast_infinite_plane() {
        let plane = InfinitePlane::default();

        // Direct hit from above
        let ray = phys_geom::Ray::new(
            Point3::new(0.0, 2.0, 0.0),
            UnitVec3::new_normalize(Vec3::new(0.0, -1.0, 0.0)),
        );

        let hit = plane.raycast(ray, 10.0, false).unwrap();
        assert_relative_eq!(hit.distance, 2.0);
        assert_relative_eq!(hit.normal, Vec3::y_axis());
    }

    #[test]
    fn test_raycast_infinite_plane_from_below() {
        let plane = InfinitePlane::default();

        // Hit from below
        let ray = phys_geom::Ray::new(
            Point3::new(0.0, -2.0, 0.0),
            UnitVec3::new_normalize(Vec3::new(0.0, 1.0, 0.0)),
        );

        let hit = plane.raycast(ray, 10.0, false).unwrap();
        assert_relative_eq!(hit.distance, 2.0);
        assert_relative_eq!(
            hit.normal,
            UnitVec3::new_normalize(Vec3::new(0.0, -1.0, 0.0))
        );
    }

    #[test]
    fn test_raycast_infinite_plane_from_below_discarded() {
        let plane = InfinitePlane::default();

        // Hit from below but discard inside hits
        let ray = phys_geom::Ray::new(
            Point3::new(0.0, -2.0, 0.0),
            UnitVec3::new_normalize(Vec3::new(0.0, 1.0, 0.0)),
        );

        assert_eq!(plane.raycast(ray, 10.0, true), None);
    }

    #[test]
    fn test_raycast_infinite_plane_parallel() {
        let plane = InfinitePlane::default();

        // Parallel ray
        let ray = phys_geom::Ray::new(
            Point3::new(0.0, 2.0, 0.0),
            UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
        );

        assert_eq!(plane.raycast(ray, 10.0, false), None);
    }

    #[test]
    fn test_raycast_infinite_plane_on_plane() {
        let plane = InfinitePlane::default();

        // Ray starting on the plane
        let ray = phys_geom::Ray::new(
            Point3::new(0.0, 0.0, 0.0),
            UnitVec3::new_normalize(Vec3::new(0.0, 1.0, 0.0)),
        );

        let hit = plane.raycast(ray, 10.0, false).unwrap();
        assert_relative_eq!(hit.distance, 0.0);
        assert_relative_eq!(hit.normal, Vec3::y_axis());
    }

    #[test]
    fn test_raycast_infinite_plane_on_plane_discarded() {
        let plane = InfinitePlane::default();

        // Ray starting on the plane but discard inside hits
        let ray = phys_geom::Ray::new(
            Point3::new(0.0, 0.0, 0.0),
            UnitVec3::new_normalize(Vec3::new(0.0, 1.0, 0.0)),
        );

        assert_eq!(plane.raycast(ray, 10.0, true), None);
    }

    #[test]
    fn test_raycast_infinite_plane_away_from_plane() {
        let plane = InfinitePlane::default();

        // Ray pointing away from plane
        let ray = phys_geom::Ray::new(
            Point3::new(0.0, 2.0, 0.0),
            UnitVec3::new_normalize(Vec3::new(0.0, 1.0, 0.0)),
        );

        assert_eq!(plane.raycast(ray, 10.0, false), None);
    }

    #[test]
    fn test_raycast_infinite_plane_max_distance() {
        let plane = InfinitePlane::default();

        let ray = phys_geom::Ray::new(
            Point3::new(0.0, 5.0, 0.0),
            UnitVec3::new_normalize(Vec3::new(0.0, -1.0, 0.0)),
        );

        // Max distance too short
        assert_eq!(plane.raycast(ray, 1.0, false), None);

        // Max distance sufficient
        let hit = plane.raycast(ray, 10.0, false).unwrap();
        assert_relative_eq!(hit.distance, 5.0);
    }

    #[test]
    fn test_infinite_plane_distance() {
        let plane = InfinitePlane::default();

        // Point above plane
        assert_relative_eq!(plane.distance(Point3::new(0.0, 3.0, 0.0)), 3.0);

        // Point on plane
        assert_relative_eq!(plane.distance(Point3::new(0.0, 0.0, 0.0)), 0.0);

        // Point below plane
        assert_relative_eq!(plane.distance(Point3::new(0.0, -3.0, 0.0)), -3.0);
    }
}