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