1use super::{Aabb2d, BoundingCircle, IntersectsVolume};
2use crate::{ops::FloatPow, Dir2, Ray2d, Vec2};
3
4#[cfg(feature = "bevy_reflect")]
5use bevy_reflect::Reflect;
6
7#[derive(Clone, Debug)]
9#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
10pub struct RayCast2d {
11 pub ray: Ray2d,
13 pub max: f32,
15 direction_recip: Vec2,
17}
18
19impl RayCast2d {
20 pub fn new(origin: Vec2, direction: Dir2, max: f32) -> Self {
22 Self::from_ray(Ray2d { origin, direction }, max)
23 }
24
25 pub fn from_ray(ray: Ray2d, max: f32) -> Self {
27 Self {
28 ray,
29 direction_recip: ray.direction.recip(),
30 max,
31 }
32 }
33
34 pub fn direction_recip(&self) -> Vec2 {
36 self.direction_recip
37 }
38
39 pub fn aabb_intersection_at(&self, aabb: &Aabb2d) -> Option<f32> {
41 let (min_x, max_x) = if self.ray.direction.x.is_sign_positive() {
42 (aabb.min.x, aabb.max.x)
43 } else {
44 (aabb.max.x, aabb.min.x)
45 };
46 let (min_y, max_y) = if self.ray.direction.y.is_sign_positive() {
47 (aabb.min.y, aabb.max.y)
48 } else {
49 (aabb.max.y, aabb.min.y)
50 };
51
52 let tmin_x = (min_x - self.ray.origin.x) * self.direction_recip.x;
56 let tmin_y = (min_y - self.ray.origin.y) * self.direction_recip.y;
57 let tmax_x = (max_x - self.ray.origin.x) * self.direction_recip.x;
58 let tmax_y = (max_y - self.ray.origin.y) * self.direction_recip.y;
59
60 let tmin = tmin_x.max(tmin_y).max(0.);
65 let tmax = tmax_y.min(tmax_x).min(self.max);
66
67 if tmin <= tmax {
68 Some(tmin)
69 } else {
70 None
71 }
72 }
73
74 pub fn circle_intersection_at(&self, circle: &BoundingCircle) -> Option<f32> {
76 let offset = self.ray.origin - circle.center;
77 let projected = offset.dot(*self.ray.direction);
78 let closest_point = offset - projected * *self.ray.direction;
79 let distance_squared = circle.radius().squared() - closest_point.length_squared();
80 if distance_squared < 0. || projected.squared().copysign(-projected) < -distance_squared {
81 None
82 } else {
83 let toi = -projected - distance_squared.sqrt();
84 if toi > self.max {
85 None
86 } else {
87 Some(toi.max(0.))
88 }
89 }
90 }
91}
92
93impl IntersectsVolume<Aabb2d> for RayCast2d {
94 fn intersects(&self, volume: &Aabb2d) -> bool {
95 self.aabb_intersection_at(volume).is_some()
96 }
97}
98
99impl IntersectsVolume<BoundingCircle> for RayCast2d {
100 fn intersects(&self, volume: &BoundingCircle) -> bool {
101 self.circle_intersection_at(volume).is_some()
102 }
103}
104
105#[derive(Clone, Debug)]
107#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
108pub struct AabbCast2d {
109 pub ray: RayCast2d,
111 pub aabb: Aabb2d,
113}
114
115impl AabbCast2d {
116 pub fn new(aabb: Aabb2d, origin: Vec2, direction: Dir2, max: f32) -> Self {
118 Self::from_ray(aabb, Ray2d { origin, direction }, max)
119 }
120
121 pub fn from_ray(aabb: Aabb2d, ray: Ray2d, max: f32) -> Self {
123 Self {
124 ray: RayCast2d::from_ray(ray, max),
125 aabb,
126 }
127 }
128
129 pub fn aabb_collision_at(&self, mut aabb: Aabb2d) -> Option<f32> {
131 aabb.min -= self.aabb.max;
132 aabb.max -= self.aabb.min;
133 self.ray.aabb_intersection_at(&aabb)
134 }
135}
136
137impl IntersectsVolume<Aabb2d> for AabbCast2d {
138 fn intersects(&self, volume: &Aabb2d) -> bool {
139 self.aabb_collision_at(*volume).is_some()
140 }
141}
142
143#[derive(Clone, Debug)]
145#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
146pub struct BoundingCircleCast {
147 pub ray: RayCast2d,
149 pub circle: BoundingCircle,
151}
152
153impl BoundingCircleCast {
154 pub fn new(circle: BoundingCircle, origin: Vec2, direction: Dir2, max: f32) -> Self {
156 Self::from_ray(circle, Ray2d { origin, direction }, max)
157 }
158
159 pub fn from_ray(circle: BoundingCircle, ray: Ray2d, max: f32) -> Self {
161 Self {
162 ray: RayCast2d::from_ray(ray, max),
163 circle,
164 }
165 }
166
167 pub fn circle_collision_at(&self, mut circle: BoundingCircle) -> Option<f32> {
169 circle.center -= self.circle.center;
170 circle.circle.radius += self.circle.radius();
171 self.ray.circle_intersection_at(&circle)
172 }
173}
174
175impl IntersectsVolume<BoundingCircle> for BoundingCircleCast {
176 fn intersects(&self, volume: &BoundingCircle) -> bool {
177 self.circle_collision_at(*volume).is_some()
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 const EPSILON: f32 = 0.001;
186
187 #[test]
188 fn test_ray_intersection_circle_hits() {
189 for (test, volume, expected_distance) in &[
190 (
191 RayCast2d::new(Vec2::Y * -5., Dir2::Y, 90.),
193 BoundingCircle::new(Vec2::ZERO, 1.),
194 4.,
195 ),
196 (
197 RayCast2d::new(Vec2::Y * 5., -Dir2::Y, 90.),
199 BoundingCircle::new(Vec2::ZERO, 1.),
200 4.,
201 ),
202 (
203 RayCast2d::new(Vec2::ZERO, Dir2::Y, 90.),
205 BoundingCircle::new(Vec2::Y * 3., 2.),
206 1.,
207 ),
208 (
209 RayCast2d::new(Vec2::X, Dir2::Y, 1.),
211 BoundingCircle::new(Vec2::ONE, 0.01),
212 0.99,
213 ),
214 (
215 RayCast2d::new(Vec2::X, Dir2::Y, 90.),
217 BoundingCircle::new(Vec2::Y * 5., 2.),
218 3.268,
219 ),
220 (
221 RayCast2d::new(Vec2::X * 0.99999, Dir2::Y, 90.),
223 BoundingCircle::new(Vec2::Y * 5., 1.),
224 4.996,
225 ),
226 ] {
227 let case = format!(
228 "Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}",
229 test, volume, expected_distance
230 );
231 assert!(test.intersects(volume), "{}", case);
232 let actual_distance = test.circle_intersection_at(volume).unwrap();
233 assert!(
234 (actual_distance - expected_distance).abs() < EPSILON,
235 "{}\n Actual distance: {}",
236 case,
237 actual_distance
238 );
239
240 let inverted_ray = RayCast2d::new(test.ray.origin, -test.ray.direction, test.max);
241 assert!(!inverted_ray.intersects(volume), "{}", case);
242 }
243 }
244
245 #[test]
246 fn test_ray_intersection_circle_misses() {
247 for (test, volume) in &[
248 (
249 RayCast2d::new(Vec2::ZERO, Dir2::X, 90.),
251 BoundingCircle::new(Vec2::Y * 2., 1.),
252 ),
253 (
254 RayCast2d::new(Vec2::ZERO, Dir2::from_xy(1., 1.).unwrap(), 90.),
256 BoundingCircle::new(Vec2::Y * 2., 1.),
257 ),
258 (
259 RayCast2d::new(Vec2::ZERO, Dir2::Y, 0.5),
261 BoundingCircle::new(Vec2::Y * 2., 1.),
262 ),
263 ] {
264 assert!(
265 !test.intersects(volume),
266 "Case:\n Test: {:?}\n Volume: {:?}",
267 test,
268 volume,
269 );
270 }
271 }
272
273 #[test]
274 fn test_ray_intersection_circle_inside() {
275 let volume = BoundingCircle::new(Vec2::splat(0.5), 1.);
276 for origin in &[Vec2::X, Vec2::Y, Vec2::ONE, Vec2::ZERO] {
277 for direction in &[Dir2::X, Dir2::Y, -Dir2::X, -Dir2::Y] {
278 for max in &[0., 1., 900.] {
279 let test = RayCast2d::new(*origin, *direction, *max);
280
281 let case = format!(
282 "Case:\n origin: {:?}\n Direction: {:?}\n Max: {}",
283 origin, direction, max,
284 );
285 assert!(test.intersects(&volume), "{}", case);
286
287 let actual_distance = test.circle_intersection_at(&volume);
288 assert_eq!(actual_distance, Some(0.), "{}", case);
289 }
290 }
291 }
292 }
293
294 #[test]
295 fn test_ray_intersection_aabb_hits() {
296 for (test, volume, expected_distance) in &[
297 (
298 RayCast2d::new(Vec2::Y * -5., Dir2::Y, 90.),
300 Aabb2d::new(Vec2::ZERO, Vec2::ONE),
301 4.,
302 ),
303 (
304 RayCast2d::new(Vec2::Y * 5., -Dir2::Y, 90.),
306 Aabb2d::new(Vec2::ZERO, Vec2::ONE),
307 4.,
308 ),
309 (
310 RayCast2d::new(Vec2::ZERO, Dir2::Y, 90.),
312 Aabb2d::new(Vec2::Y * 3., Vec2::splat(2.)),
313 1.,
314 ),
315 (
316 RayCast2d::new(Vec2::X, Dir2::Y, 1.),
318 Aabb2d::new(Vec2::ONE, Vec2::splat(0.01)),
319 0.99,
320 ),
321 (
322 RayCast2d::new(Vec2::X, Dir2::Y, 90.),
324 Aabb2d::new(Vec2::Y * 5., Vec2::splat(2.)),
325 3.,
326 ),
327 (
328 RayCast2d::new(Vec2::X * -0.001, Dir2::from_xy(1., 1.).unwrap(), 90.),
330 Aabb2d::new(Vec2::Y * 2., Vec2::ONE),
331 1.414,
332 ),
333 ] {
334 let case = format!(
335 "Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}",
336 test, volume, expected_distance
337 );
338 assert!(test.intersects(volume), "{}", case);
339 let actual_distance = test.aabb_intersection_at(volume).unwrap();
340 assert!(
341 (actual_distance - expected_distance).abs() < EPSILON,
342 "{}\n Actual distance: {}",
343 case,
344 actual_distance
345 );
346
347 let inverted_ray = RayCast2d::new(test.ray.origin, -test.ray.direction, test.max);
348 assert!(!inverted_ray.intersects(volume), "{}", case);
349 }
350 }
351
352 #[test]
353 fn test_ray_intersection_aabb_misses() {
354 for (test, volume) in &[
355 (
356 RayCast2d::new(Vec2::ZERO, Dir2::X, 90.),
358 Aabb2d::new(Vec2::Y * 2., Vec2::ONE),
359 ),
360 (
361 RayCast2d::new(Vec2::ZERO, Dir2::from_xy(1., 0.99).unwrap(), 90.),
363 Aabb2d::new(Vec2::Y * 2., Vec2::ONE),
364 ),
365 (
366 RayCast2d::new(Vec2::ZERO, Dir2::Y, 0.5),
368 Aabb2d::new(Vec2::Y * 2., Vec2::ONE),
369 ),
370 ] {
371 assert!(
372 !test.intersects(volume),
373 "Case:\n Test: {:?}\n Volume: {:?}",
374 test,
375 volume,
376 );
377 }
378 }
379
380 #[test]
381 fn test_ray_intersection_aabb_inside() {
382 let volume = Aabb2d::new(Vec2::splat(0.5), Vec2::ONE);
383 for origin in &[Vec2::X, Vec2::Y, Vec2::ONE, Vec2::ZERO] {
384 for direction in &[Dir2::X, Dir2::Y, -Dir2::X, -Dir2::Y] {
385 for max in &[0., 1., 900.] {
386 let test = RayCast2d::new(*origin, *direction, *max);
387
388 let case = format!(
389 "Case:\n origin: {:?}\n Direction: {:?}\n Max: {}",
390 origin, direction, max,
391 );
392 assert!(test.intersects(&volume), "{}", case);
393
394 let actual_distance = test.aabb_intersection_at(&volume);
395 assert_eq!(actual_distance, Some(0.), "{}", case,);
396 }
397 }
398 }
399 }
400
401 #[test]
402 fn test_aabb_cast_hits() {
403 for (test, volume, expected_distance) in &[
404 (
405 AabbCast2d::new(Aabb2d::new(Vec2::ZERO, Vec2::ONE), Vec2::ZERO, Dir2::Y, 90.),
407 Aabb2d::new(Vec2::Y * 5., Vec2::ONE),
408 3.,
409 ),
410 (
411 AabbCast2d::new(
413 Aabb2d::new(Vec2::ZERO, Vec2::ONE),
414 Vec2::Y * 10.,
415 -Dir2::Y,
416 90.,
417 ),
418 Aabb2d::new(Vec2::Y * 5., Vec2::ONE),
419 3.,
420 ),
421 (
422 AabbCast2d::new(
424 Aabb2d::new(Vec2::ZERO, Vec2::ONE),
425 Vec2::X * 1.5,
426 Dir2::Y,
427 90.,
428 ),
429 Aabb2d::new(Vec2::Y * 5., Vec2::ONE),
430 3.,
431 ),
432 (
433 AabbCast2d::new(
435 Aabb2d::new(Vec2::X * -2., Vec2::ONE),
436 Vec2::X * 3.,
437 Dir2::Y,
438 90.,
439 ),
440 Aabb2d::new(Vec2::Y * 5., Vec2::ONE),
441 3.,
442 ),
443 ] {
444 let case = format!(
445 "Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}",
446 test, volume, expected_distance
447 );
448 assert!(test.intersects(volume), "{}", case);
449 let actual_distance = test.aabb_collision_at(*volume).unwrap();
450 assert!(
451 (actual_distance - expected_distance).abs() < EPSILON,
452 "{}\n Actual distance: {}",
453 case,
454 actual_distance
455 );
456
457 let inverted_ray =
458 RayCast2d::new(test.ray.ray.origin, -test.ray.ray.direction, test.ray.max);
459 assert!(!inverted_ray.intersects(volume), "{}", case);
460 }
461 }
462
463 #[test]
464 fn test_circle_cast_hits() {
465 for (test, volume, expected_distance) in &[
466 (
467 BoundingCircleCast::new(
469 BoundingCircle::new(Vec2::ZERO, 1.),
470 Vec2::ZERO,
471 Dir2::Y,
472 90.,
473 ),
474 BoundingCircle::new(Vec2::Y * 5., 1.),
475 3.,
476 ),
477 (
478 BoundingCircleCast::new(
480 BoundingCircle::new(Vec2::ZERO, 1.),
481 Vec2::Y * 10.,
482 -Dir2::Y,
483 90.,
484 ),
485 BoundingCircle::new(Vec2::Y * 5., 1.),
486 3.,
487 ),
488 (
489 BoundingCircleCast::new(
491 BoundingCircle::new(Vec2::ZERO, 1.),
492 Vec2::X * 1.5,
493 Dir2::Y,
494 90.,
495 ),
496 BoundingCircle::new(Vec2::Y * 5., 1.),
497 3.677,
498 ),
499 (
500 BoundingCircleCast::new(
502 BoundingCircle::new(Vec2::X * -1.5, 1.),
503 Vec2::X * 3.,
504 Dir2::Y,
505 90.,
506 ),
507 BoundingCircle::new(Vec2::Y * 5., 1.),
508 3.677,
509 ),
510 ] {
511 let case = format!(
512 "Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}",
513 test, volume, expected_distance
514 );
515 assert!(test.intersects(volume), "{}", case);
516 let actual_distance = test.circle_collision_at(*volume).unwrap();
517 assert!(
518 (actual_distance - expected_distance).abs() < EPSILON,
519 "{}\n Actual distance: {}",
520 case,
521 actual_distance
522 );
523
524 let inverted_ray =
525 RayCast2d::new(test.ray.ray.origin, -test.ray.ray.direction, test.ray.max);
526 assert!(!inverted_ray.intersects(volume), "{}", case);
527 }
528 }
529}