phys_raycast/shapes/
infinite_plane.rs

1// Copyright (C) 2020-2025 phys-raycast authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use phys_geom::math::*;
16use phys_geom::shape::InfinitePlane;
17
18use crate::{Raycast, RaycastHitResult};
19
20/// Extension trait for InfinitePlane providing additional utility methods.
21pub trait InfinitePlaneExt {
22    /// Calculate the signed distance from a point to the plane.
23    ///
24    /// Positive distance means the point is above the plane (in the direction of the normal),
25    /// negative distance means the point is below the plane.
26    fn distance(&self, p: Point3) -> Real;
27}
28
29fn normal() -> UnitVec3 {
30    Vec3::y_axis()
31}
32
33impl InfinitePlaneExt for InfinitePlane {
34    #[inline]
35    fn distance(&self, p: Point3) -> Real {
36        p.coords.dot(&normal().into_inner())
37    }
38}
39
40impl Raycast for InfinitePlane {
41    fn raycast(
42        &self,
43        local_ray: phys_geom::Ray,
44        max_distance: Real,
45        discard_inside_hit: bool,
46    ) -> Option<RaycastHitResult> {
47        let origin_to_plane_distance = self.distance(local_ray.origin);
48
49        // ray start on the plane
50        if origin_to_plane_distance.abs() <= Real::EPSILON && !discard_inside_hit {
51            return Some(RaycastHitResult {
52                distance: 0.0,
53                normal: normal(),
54            });
55        }
56
57        let dn = local_ray.direction.dot(&normal().into_inner());
58
59        // parallel ray, not on the plane
60        if (-Real::EPSILON < dn) && (dn < Real::EPSILON) {
61            return None;
62        }
63
64        let hit_distance = -origin_to_plane_distance / dn;
65
66        // ray cast away from plane
67        if hit_distance < 0.0 {
68            return None;
69        }
70
71        if hit_distance > 0.0 && hit_distance <= max_distance {
72            // above plane
73            if origin_to_plane_distance > 0.0 {
74                Some(RaycastHitResult {
75                    distance: hit_distance,
76                    normal: normal(),
77                })
78            // under plane
79            } else if discard_inside_hit {
80                None
81            } else {
82                Some(RaycastHitResult {
83                    distance: hit_distance,
84                    normal: -normal(),
85                })
86            }
87        } else {
88            None
89        }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use approx::assert_relative_eq;
96
97    use super::*;
98
99    #[test]
100    fn test_raycast_infinite_plane() {
101        let plane = InfinitePlane::default();
102
103        // Direct hit from above
104        let ray = phys_geom::Ray::new(
105            Point3::new(0.0, 2.0, 0.0),
106            UnitVec3::new_normalize(Vec3::new(0.0, -1.0, 0.0)),
107        );
108
109        let hit = plane.raycast(ray, 10.0, false).unwrap();
110        assert_relative_eq!(hit.distance, 2.0);
111        assert_relative_eq!(hit.normal, Vec3::y_axis());
112    }
113
114    #[test]
115    fn test_raycast_infinite_plane_from_below() {
116        let plane = InfinitePlane::default();
117
118        // Hit from below
119        let ray = phys_geom::Ray::new(
120            Point3::new(0.0, -2.0, 0.0),
121            UnitVec3::new_normalize(Vec3::new(0.0, 1.0, 0.0)),
122        );
123
124        let hit = plane.raycast(ray, 10.0, false).unwrap();
125        assert_relative_eq!(hit.distance, 2.0);
126        assert_relative_eq!(
127            hit.normal,
128            UnitVec3::new_normalize(Vec3::new(0.0, -1.0, 0.0))
129        );
130    }
131
132    #[test]
133    fn test_raycast_infinite_plane_from_below_discarded() {
134        let plane = InfinitePlane::default();
135
136        // Hit from below but discard inside hits
137        let ray = phys_geom::Ray::new(
138            Point3::new(0.0, -2.0, 0.0),
139            UnitVec3::new_normalize(Vec3::new(0.0, 1.0, 0.0)),
140        );
141
142        assert_eq!(plane.raycast(ray, 10.0, true), None);
143    }
144
145    #[test]
146    fn test_raycast_infinite_plane_parallel() {
147        let plane = InfinitePlane::default();
148
149        // Parallel ray
150        let ray = phys_geom::Ray::new(
151            Point3::new(0.0, 2.0, 0.0),
152            UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
153        );
154
155        assert_eq!(plane.raycast(ray, 10.0, false), None);
156    }
157
158    #[test]
159    fn test_raycast_infinite_plane_on_plane() {
160        let plane = InfinitePlane::default();
161
162        // Ray starting on the plane
163        let ray = phys_geom::Ray::new(
164            Point3::new(0.0, 0.0, 0.0),
165            UnitVec3::new_normalize(Vec3::new(0.0, 1.0, 0.0)),
166        );
167
168        let hit = plane.raycast(ray, 10.0, false).unwrap();
169        assert_relative_eq!(hit.distance, 0.0);
170        assert_relative_eq!(hit.normal, Vec3::y_axis());
171    }
172
173    #[test]
174    fn test_raycast_infinite_plane_on_plane_discarded() {
175        let plane = InfinitePlane::default();
176
177        // Ray starting on the plane but discard inside hits
178        let ray = phys_geom::Ray::new(
179            Point3::new(0.0, 0.0, 0.0),
180            UnitVec3::new_normalize(Vec3::new(0.0, 1.0, 0.0)),
181        );
182
183        assert_eq!(plane.raycast(ray, 10.0, true), None);
184    }
185
186    #[test]
187    fn test_raycast_infinite_plane_away_from_plane() {
188        let plane = InfinitePlane::default();
189
190        // Ray pointing away from plane
191        let ray = phys_geom::Ray::new(
192            Point3::new(0.0, 2.0, 0.0),
193            UnitVec3::new_normalize(Vec3::new(0.0, 1.0, 0.0)),
194        );
195
196        assert_eq!(plane.raycast(ray, 10.0, false), None);
197    }
198
199    #[test]
200    fn test_raycast_infinite_plane_max_distance() {
201        let plane = InfinitePlane::default();
202
203        let ray = phys_geom::Ray::new(
204            Point3::new(0.0, 5.0, 0.0),
205            UnitVec3::new_normalize(Vec3::new(0.0, -1.0, 0.0)),
206        );
207
208        // Max distance too short
209        assert_eq!(plane.raycast(ray, 1.0, false), None);
210
211        // Max distance sufficient
212        let hit = plane.raycast(ray, 10.0, false).unwrap();
213        assert_relative_eq!(hit.distance, 5.0);
214    }
215
216    #[test]
217    fn test_infinite_plane_distance() {
218        let plane = InfinitePlane::default();
219
220        // Point above plane
221        assert_relative_eq!(plane.distance(Point3::new(0.0, 3.0, 0.0)), 3.0);
222
223        // Point on plane
224        assert_relative_eq!(plane.distance(Point3::new(0.0, 0.0, 0.0)), 0.0);
225
226        // Point below plane
227        assert_relative_eq!(plane.distance(Point3::new(0.0, -3.0, 0.0)), -3.0);
228    }
229}