1#[derive(Clone, Copy, Debug)]
9pub struct Box2 {
10 pub x: f32,
11 pub y: f32,
12 pub angle: f32,
13 pub half_w: f32,
14 pub half_h: f32,
15}
16
17pub fn walk_ray_cells(
26 origin: [f32; 2],
27 dir: [f32; 2],
28 range: f32,
29 rows: usize,
30 cols: usize,
31 tile_size: f32,
32 mut visit: impl FnMut(i64, i64, f32) -> bool,
33) {
34 if rows == 0 || cols == 0 {
35 return;
36 }
37
38 let mut cell_x = (origin[0] / tile_size).floor() as i64;
39 let mut cell_y = (origin[1] / tile_size).floor() as i64;
40
41 if !visit(cell_x, cell_y, 0.0) {
42 return;
43 }
44
45 let step_x: i64 = if dir[0] > 0.0 { 1 } else { -1 };
46 let step_y: i64 = if dir[1] > 0.0 { 1 } else { -1 };
47
48 let delta_x = if dir[0] != 0.0 {
50 (tile_size / dir[0]).abs()
51 } else {
52 f32::INFINITY
53 };
54 let delta_y = if dir[1] != 0.0 {
55 (tile_size / dir[1]).abs()
56 } else {
57 f32::INFINITY
58 };
59
60 let mut max_x = if dir[0] != 0.0 {
62 let edge = if dir[0] > 0.0 {
63 (cell_x + 1) as f32 * tile_size - origin[0]
64 } else {
65 origin[0] - cell_x as f32 * tile_size
66 };
67
68 edge / dir[0].abs()
69 } else {
70 f32::INFINITY
71 };
72 let mut max_y = if dir[1] != 0.0 {
73 let edge = if dir[1] > 0.0 {
74 (cell_y + 1) as f32 * tile_size - origin[1]
75 } else {
76 origin[1] - cell_y as f32 * tile_size
77 };
78
79 edge / dir[1].abs()
80 } else {
81 f32::INFINITY
82 };
83
84 let mut traveled = 0.0f32;
85
86 while traveled <= range {
87 if max_x < max_y {
88 traveled = max_x;
89 max_x += delta_x;
90 cell_x += step_x;
91 } else {
92 traveled = max_y;
93 max_y += delta_y;
94 cell_y += step_y;
95 }
96
97 if traveled > range {
98 return;
99 }
100
101 let gone = (step_x < 0 && cell_x < 0)
104 || (step_x > 0 && cell_x >= cols as i64)
105 || (step_y < 0 && cell_y < 0)
106 || (step_y > 0 && cell_y >= rows as i64);
107
108 if gone {
109 return;
110 }
111
112 if !visit(cell_x, cell_y, traveled) {
113 return;
114 }
115 }
116}
117
118pub fn ray_vs_grid(
121 origin: [f32; 2],
122 dir: [f32; 2],
123 range: f32,
124 map: &[Vec<i32>],
125 solid_tiles: &[i32],
126 tile_size: f32,
127) -> Option<f32> {
128 let rows = map.len();
129 let cols = map.first().map(|row| row.len()).unwrap_or(0);
130
131 if rows == 0 || cols == 0 || solid_tiles.is_empty() {
132 return None;
133 }
134
135 let mut hit = None;
136
137 walk_ray_cells(origin, dir, range, rows, cols, tile_size, |cx, cy, t| {
138 let solid = cy >= 0
139 && (cy as usize) < rows
140 && cx >= 0
141 && (cx as usize) < cols
142 && solid_tiles.contains(&map[cy as usize][cx as usize]);
143
144 if solid {
145 hit = Some(t);
146
147 return false;
148 }
149
150 true
151 });
152
153 hit
154}
155
156pub fn ray_vs_box(origin: [f32; 2], dir: [f32; 2], range: f32, b: &Box2) -> Option<f32> {
159 let (sin, cos) = (-b.angle).sin_cos();
161 let rel_x = origin[0] - b.x;
162 let rel_y = origin[1] - b.y;
163
164 let local_origin = [cos * rel_x - sin * rel_y, sin * rel_x + cos * rel_y];
165 let local_dir = [cos * dir[0] - sin * dir[1], sin * dir[0] + cos * dir[1]];
166
167 let mut t_min = 0.0f32;
168 let mut t_max = range;
169
170 let slabs = [
172 (local_origin[0], local_dir[0], b.half_w),
173 (local_origin[1], local_dir[1], b.half_h),
174 ];
175
176 for (o, d, half) in slabs {
177 if d == 0.0 {
178 if o < -half || o > half {
179 return None;
180 }
181
182 continue;
183 }
184
185 let mut t1 = (-half - o) / d;
186 let mut t2 = (half - o) / d;
187
188 if t1 > t2 {
189 std::mem::swap(&mut t1, &mut t2);
190 }
191
192 t_min = t_min.max(t1);
193 t_max = t_max.min(t2);
194
195 if t_min > t_max {
196 return None;
197 }
198 }
199
200 Some(t_min)
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 fn wall_grid() -> Vec<Vec<i32>> {
209 let mut grid = vec![vec![0; 5]; 5];
210
211 for row in &mut grid {
212 row[3] = 1;
213 }
214
215 grid
216 }
217
218 #[test]
219 fn grid_ray_hits_wall_to_the_right() {
220 let grid = wall_grid();
221 let hit = ray_vs_grid([15.0, 15.0], [1.0, 0.0], 100.0, &grid, &[1], 10.0);
223
224 assert_eq!(hit, Some(15.0));
225 }
226
227 #[test]
228 fn grid_ray_misses_within_range() {
229 let grid = wall_grid();
230 let hit = ray_vs_grid([15.0, 15.0], [1.0, 0.0], 10.0, &grid, &[1], 10.0);
231
232 assert_eq!(hit, None);
233 }
234
235 #[test]
236 fn grid_ray_away_from_wall_misses() {
237 let grid = wall_grid();
238 let hit = ray_vs_grid([15.0, 15.0], [-1.0, 0.0], 100.0, &grid, &[1], 10.0);
239
240 assert_eq!(hit, None);
241 }
242
243 #[test]
244 fn grid_start_inside_wall_hits_at_zero() {
245 let grid = wall_grid();
246 let hit = ray_vs_grid([35.0, 15.0], [1.0, 0.0], 100.0, &grid, &[1], 10.0);
247
248 assert_eq!(hit, Some(0.0));
249 }
250
251 #[test]
252 fn grid_diagonal_and_vertical_rays() {
253 let mut grid = vec![vec![0; 5]; 5];
254
255 grid[3] = vec![1; 5]; let dir = [std::f32::consts::FRAC_1_SQRT_2, std::f32::consts::FRAC_1_SQRT_2];
258 let diagonal = ray_vs_grid([5.0, 5.0], dir, 100.0, &grid, &[1], 10.0).unwrap();
259
260 assert!((diagonal - 25.0 * std::f32::consts::SQRT_2).abs() < 0.01);
262
263 let vertical = ray_vs_grid([5.0, 5.0], [0.0, 1.0], 100.0, &grid, &[1], 10.0);
264
265 assert_eq!(vertical, Some(25.0));
266 }
267
268 #[test]
269 fn grid_without_solid_tiles_never_hits() {
270 let grid = wall_grid();
271 let hit = ray_vs_grid([15.0, 15.0], [1.0, 0.0], 100.0, &grid, &[], 10.0);
272
273 assert_eq!(hit, None);
274 }
275
276 #[test]
277 fn walk_visits_start_cell_at_zero() {
278 let mut visited: Vec<(i64, i64, f32)> = Vec::new();
279
280 walk_ray_cells([15.0, 15.0], [1.0, 0.0], 25.0, 5, 5, 10.0, |cx, cy, t| {
281 visited.push((cx, cy, t));
282
283 true
284 });
285
286 assert_eq!(visited[0], (1, 1, 0.0));
287 assert_eq!(visited[1], (2, 1, 5.0));
288 assert!(visited.iter().all(|(_, _, t)| *t <= 25.0));
290 }
291
292 #[test]
293 fn walk_stops_on_false() {
294 let mut count = 0;
295
296 walk_ray_cells([15.0, 15.0], [1.0, 0.0], 100.0, 5, 5, 10.0, |_, _, _| {
297 count += 1;
298
299 count < 3
300 });
301
302 assert_eq!(count, 3);
303 }
304
305 #[test]
306 fn walk_matches_ray_vs_grid() {
307 let grid = wall_grid();
308 let dirs = [
309 [1.0, 0.0],
310 [-1.0, 0.0],
311 [0.0, 1.0],
312 [0.0, -1.0],
313 [
314 std::f32::consts::FRAC_1_SQRT_2,
315 std::f32::consts::FRAC_1_SQRT_2,
316 ],
317 [0.6, -0.8],
318 ];
319
320 for dir in dirs {
321 let expected = ray_vs_grid([15.0, 15.0], dir, 100.0, &grid, &[1], 10.0);
322 let mut manual = None;
323
324 walk_ray_cells([15.0, 15.0], dir, 100.0, 5, 5, 10.0, |cx, cy, t| {
325 let solid = (0..5).contains(&cy)
326 && (0..5).contains(&cx)
327 && grid[cy as usize][cx as usize] == 1;
328
329 if solid {
330 manual = Some(t);
331
332 return false;
333 }
334
335 true
336 });
337
338 assert_eq!(expected, manual, "dir {dir:?}");
339 }
340 }
341
342 #[test]
343 fn box_head_on_hit() {
344 let b = Box2 {
345 x: 50.0,
346 y: 0.0,
347 angle: 0.0,
348 half_w: 10.0,
349 half_h: 5.0,
350 };
351 let hit = ray_vs_box([0.0, 0.0], [1.0, 0.0], 100.0, &b);
352
353 assert_eq!(hit, Some(40.0));
354 }
355
356 #[test]
357 fn box_miss_and_out_of_range() {
358 let b = Box2 {
359 x: 50.0,
360 y: 20.0,
361 angle: 0.0,
362 half_w: 10.0,
363 half_h: 5.0,
364 };
365
366 assert_eq!(ray_vs_box([0.0, 0.0], [1.0, 0.0], 100.0, &b), None);
367
368 let near = Box2 { y: 0.0, ..b };
369
370 assert_eq!(ray_vs_box([0.0, 0.0], [1.0, 0.0], 30.0, &near), None);
371 }
372
373 #[test]
374 fn box_start_inside_hits_at_zero() {
375 let b = Box2 {
376 x: 0.0,
377 y: 0.0,
378 angle: 0.0,
379 half_w: 10.0,
380 half_h: 10.0,
381 };
382
383 assert_eq!(ray_vs_box([0.0, 0.0], [1.0, 0.0], 100.0, &b), Some(0.0));
384 }
385
386 #[test]
387 fn box_rotation_changes_hit_distance() {
388 let b = Box2 {
390 x: 50.0,
391 y: 0.0,
392 angle: std::f32::consts::FRAC_PI_2,
393 half_w: 10.0,
394 half_h: 1.0,
395 };
396 let hit = ray_vs_box([0.0, 0.0], [1.0, 0.0], 100.0, &b).unwrap();
397
398 assert!((hit - 49.0).abs() < 0.001);
399 }
400
401 #[test]
402 fn box_behind_ray_misses() {
403 let b = Box2 {
404 x: -50.0,
405 y: 0.0,
406 angle: 0.0,
407 half_w: 10.0,
408 half_h: 5.0,
409 };
410
411 assert_eq!(ray_vs_box([0.0, 0.0], [1.0, 0.0], 100.0, &b), None);
412 }
413}