1use crate::math::Vec2;
4
5#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct Aabb {
8 pub center: Vec2,
10 pub half: Vec2,
12}
13
14impl Aabb {
15 #[must_use]
17 pub const fn new(center: Vec2, half: Vec2) -> Self {
18 Self { center, half }
19 }
20
21 #[must_use]
26 pub fn from_min_max(min: Vec2, max: Vec2) -> Self {
27 Self {
28 center: (min + max) * 0.5,
29 half: (max - min) * 0.5,
30 }
31 }
32
33 #[must_use]
35 pub fn min(&self) -> Vec2 {
36 self.center - self.half
37 }
38
39 #[must_use]
41 pub fn max(&self) -> Vec2 {
42 self.center + self.half
43 }
44
45 #[must_use]
47 pub fn translated(&self, delta: Vec2) -> Self {
48 Self {
49 center: self.center + delta,
50 half: self.half,
51 }
52 }
53
54 #[must_use]
58 pub fn overlaps(&self, other: &Self) -> bool {
59 let (a_min, a_max) = (self.min(), self.max());
60 let (b_min, b_max) = (other.min(), other.max());
61 a_min.x < b_max.x && a_max.x > b_min.x && a_min.y < b_max.y && a_max.y > b_min.y
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq)]
67pub struct Hit {
68 pub t: f32,
72 pub normal: Vec2,
75}
76
77#[must_use]
101pub fn sweep_aabb(body: &Aabb, delta: Vec2, target: &Aabb) -> Option<Hit> {
102 if delta == Vec2::ZERO || !delta.is_finite() {
103 return None;
104 }
105
106 let (b_min, b_max) = (body.min(), body.max());
107 let (t_min, t_max) = (target.min(), target.max());
108
109 let (entry_x, exit_x) = slab(b_min.x, b_max.x, delta.x, t_min.x, t_max.x)?;
110 let (entry_y, exit_y) = slab(b_min.y, b_max.y, delta.y, t_min.y, t_max.y)?;
111
112 let entry = entry_x.max(entry_y);
113 let exit = exit_x.min(exit_y);
114
115 if entry > exit || !(0.0..=1.0).contains(&entry) {
120 return None;
121 }
122
123 let normal = if entry_x > entry_y {
124 Vec2::new(if delta.x > 0.0 { -1.0 } else { 1.0 }, 0.0)
126 } else {
127 Vec2::new(0.0, if delta.y > 0.0 { -1.0 } else { 1.0 })
129 };
130
131 Some(Hit {
134 t: entry + 0.0,
135 normal,
136 })
137}
138
139fn slab(b_lo: f32, b_hi: f32, d: f32, t_lo: f32, t_hi: f32) -> Option<(f32, f32)> {
149 if d == 0.0 {
150 if b_hi > t_lo && b_lo < t_hi {
151 Some((f32::NEG_INFINITY, f32::INFINITY))
152 } else {
153 None
154 }
155 } else if d > 0.0 {
156 Some(((t_lo - b_hi) / d, (t_hi - b_lo) / d))
157 } else {
158 Some(((t_hi - b_lo) / d, (t_lo - b_hi) / d))
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use wasm_bindgen_test::wasm_bindgen_test;
166
167 #[wasm_bindgen_test(unsupported = test)]
171 fn touching_on_every_side_is_not_overlap_but_a_nudge_is() {
172 let body = Aabb::new(Vec2::ZERO, Vec2::splat(1.0));
173 for (dx, dy) in [(2.0, 0.0), (-2.0, 0.0), (0.0, 2.0), (0.0, -2.0), (2.0, 2.0)] {
174 let touching = Aabb::new(Vec2::new(dx, dy), Vec2::splat(1.0));
175 assert!(!body.overlaps(&touching), "touching at {dx},{dy}");
176 let nudged = Aabb::new(Vec2::new(dx * 0.999, dy * 0.999), Vec2::splat(1.0));
177 assert!(body.overlaps(&nudged), "nudged at {dx},{dy}");
178 }
179 }
180
181 #[wasm_bindgen_test(unsupported = test)]
186 fn corner_grazes_hit_and_edge_slides_do_not() {
187 let target = Aabb::new(Vec2::new(4.0, 4.0), Vec2::splat(1.0));
188 let graze = sweep_aabb(
189 &Aabb::new(Vec2::new(0.0, 4.0), Vec2::splat(1.0)),
190 Vec2::new(4.0, 4.0),
191 &target,
192 )
193 .expect("grazing the corner is a contact");
194 assert_eq!(graze.t.to_bits(), 0.5f32.to_bits());
195 assert_eq!(graze.normal, Vec2::NEG_X);
196
197 let slide = sweep_aabb(
200 &Aabb::new(Vec2::new(0.0, 6.0), Vec2::splat(1.0)),
201 Vec2::new(8.0, 0.0),
202 &target,
203 );
204 assert_eq!(slide, None);
205 let scrape = sweep_aabb(
207 &Aabb::new(Vec2::new(0.0, 5.999), Vec2::splat(1.0)),
208 Vec2::new(8.0, 0.0),
209 &target,
210 );
211 assert!(scrape.is_some());
212 }
213
214 #[wasm_bindgen_test(unsupported = test)]
218 fn a_body_exactly_touching_at_rounding_coordinates_hits_at_once() {
219 let body = aabb(1.090_340_4, 4.711_776_3, 0.4, 0.249_400_84);
220 let floor = aabb(1.090_340_4, 4.337_375_6, 50.0, 0.125);
221 assert_eq!(body.min().y.to_bits(), floor.max().y.to_bits());
222 assert!(!body.overlaps(&floor));
223 let hit = sweep_aabb(&body, Vec2::new(0.0, -3.0), &floor).expect("touching is a contact");
224 assert_eq!(hit.t.to_bits(), 0.0f32.to_bits());
225 assert_eq!(hit.normal, Vec2::Y);
226 }
227
228 fn aabb(cx: f32, cy: f32, hx: f32, hy: f32) -> Aabb {
229 Aabb::new(Vec2::new(cx, cy), Vec2::new(hx, hy))
230 }
231
232 #[wasm_bindgen_test(unsupported = test)]
237 fn from_min_max_recovers_geometry_and_touching_is_not_overlap() {
238 let a = Aabb::from_min_max(Vec2::new(1.0, 2.0), Vec2::new(4.0, 6.0));
239 assert_eq!(a.center, Vec2::new(2.5, 4.0));
240 assert_eq!(a.half, Vec2::new(1.5, 2.0));
241 assert_eq!(a.min(), Vec2::new(1.0, 2.0));
242 assert_eq!(a.max(), Vec2::new(4.0, 6.0));
243
244 let touching = Aabb::from_min_max(Vec2::new(4.0, 2.0), Vec2::new(5.0, 6.0));
245 assert!(!a.overlaps(&touching), "shared edge is not an overlap");
246 assert!(a.overlaps(&touching.translated(Vec2::new(-0.5, 0.0))));
247 }
248
249 #[wasm_bindgen_test(unsupported = test)]
253 fn sweep_wall_reports_geometric_entry_time_and_facing_normal() {
254 let body = aabb(-5.0, 0.0, 0.5, 0.5);
255 let wall = aabb(0.0, 0.0, 0.5, 0.5);
256 let hit = sweep_aabb(&body, Vec2::new(10.0, 0.0), &wall).expect("hit");
257 assert_eq!(hit.normal, Vec2::new(-1.0, 0.0));
258 assert!((hit.t - 0.4).abs() < 1e-6, "t was {}", hit.t);
259 }
260
261 #[wasm_bindgen_test(unsupported = test)]
264 fn sweep_vertical_normals_are_signed_against_the_motion() {
265 let surface = aabb(0.0, 0.0, 5.0, 0.5);
266 let above = aabb(0.0, 5.0, 0.5, 0.5);
267 let below = aabb(0.0, -5.0, 0.5, 0.5);
268 let landing = sweep_aabb(&above, Vec2::new(0.0, -10.0), &surface).expect("hit");
269 let bump = sweep_aabb(&below, Vec2::new(0.0, 10.0), &surface).expect("hit");
270 assert_eq!(landing.normal, Vec2::new(0.0, 1.0));
271 assert_eq!(bump.normal, Vec2::new(0.0, -1.0));
272 }
273
274 #[wasm_bindgen_test(unsupported = test)]
277 fn sweep_miss_and_end_of_motion_boundaries() {
278 let body = aabb(-5.0, 0.0, 0.5, 0.5);
279 let wall = aabb(0.0, 0.0, 0.5, 0.5);
280 let beside = aabb(-5.0, 5.0, 0.5, 0.5);
281
282 assert!(sweep_aabb(&beside, Vec2::new(10.0, 0.0), &wall).is_none());
283 assert!(sweep_aabb(&body, Vec2::new(1.0, 0.0), &wall).is_none());
285 let hit = sweep_aabb(&body, Vec2::new(4.0, 0.0), &wall).expect("hit at t=1");
286 assert!((hit.t - 1.0).abs() < 1e-6, "t was {}", hit.t);
287 }
288
289 #[wasm_bindgen_test(unsupported = test)]
293 fn sweep_corner_tie_resolves_to_the_horizontal_face() {
294 let body = aabb(-2.0, -2.0, 0.5, 0.5);
296 let block = aabb(0.0, 0.0, 0.5, 0.5);
297 let hit = sweep_aabb(&body, Vec2::new(4.0, 4.0), &block).expect("hit");
298 assert!((hit.t - 0.25).abs() < 1e-6, "t was {}", hit.t);
299 assert_eq!(hit.normal, Vec2::new(0.0, -1.0));
300 }
301
302 #[wasm_bindgen_test(unsupported = test)]
306 fn sweep_embedded_body_reports_no_hit_in_any_direction() {
307 let block = aabb(0.0, 0.0, 1.0, 1.0);
308 let body = aabb(0.2, 0.0, 0.5, 0.5);
309 for delta in [
310 Vec2::new(3.0, 0.0),
311 Vec2::new(-3.0, 0.0),
312 Vec2::new(0.0, 3.0),
313 Vec2::new(0.0, -3.0),
314 ] {
315 assert!(
316 sweep_aabb(&body, delta, &block).is_none(),
317 "embedded body must be free to move by {delta}"
318 );
319 }
320 }
321
322 #[wasm_bindgen_test(unsupported = test)]
326 fn sweep_zero_and_non_finite_delta_report_no_hit() {
327 let body = aabb(-5.0, 0.0, 0.5, 0.5);
328 let wall = aabb(0.0, 0.0, 0.5, 0.5);
329 assert!(sweep_aabb(&body, Vec2::ZERO, &wall).is_none());
330 assert!(sweep_aabb(&body, Vec2::new(f32::NAN, 0.0), &wall).is_none());
331 assert!(sweep_aabb(&body, Vec2::new(f32::INFINITY, 0.0), &wall).is_none());
332 assert!(sweep_aabb(&body, Vec2::new(0.0, f32::NEG_INFINITY), &wall).is_none());
333 }
334
335 #[wasm_bindgen_test(unsupported = test)]
339 fn sweep_sub_epsilon_delta_cannot_reach_a_distant_target() {
340 let body = aabb(-5.0, 0.0, 0.5, 0.5);
341 let wall = aabb(0.0, 0.0, 0.5, 0.5);
342 assert!(sweep_aabb(&body, Vec2::new(1e-9, 0.0), &wall).is_none());
343 assert!(sweep_aabb(&body, Vec2::new(1e-3, 0.0), &wall).is_none());
344 }
345
346 #[wasm_bindgen_test(unsupported = test)]
349 fn sweep_zero_extent_body_behaves_like_a_ray() {
350 let point = aabb(-4.0, 0.0, 0.0, 0.0);
351 let block = aabb(0.0, 0.0, 2.0, 2.0);
352 let hit = sweep_aabb(&point, Vec2::new(8.0, 0.0), &block).expect("hit");
354 assert!((hit.t - 0.25).abs() < 1e-6, "t was {}", hit.t);
355 assert_eq!(hit.normal, Vec2::new(-1.0, 0.0));
356 }
357}