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