1use super::raycast::Box2;
10
11const CONTACT_MANIFOLD_RATIO: f32 = 0.15;
19
20#[derive(Clone, Copy, Debug)]
23pub struct Contact {
24 pub nx: f32,
25 pub ny: f32,
26 pub depth: f32,
27 pub cx: f32,
28 pub cy: f32,
29}
30
31#[derive(Clone, Copy, Debug)]
34pub struct TileContact {
35 pub contact: Contact,
36 pub tile_x: f32,
37 pub tile_y: f32,
38}
39
40pub fn box_center_from_origin(x: f32, y: f32, angle: f32, half_w: f32, half_h: f32) -> [f32; 2] {
44 let (sin, cos) = angle.sin_cos();
45
46 [
47 x + cos * half_w - sin * half_h,
48 y + sin * half_w + cos * half_h,
49 ]
50}
51
52fn obb_corners(b: &Box2) -> [[f32; 2]; 4] {
54 let (sin, cos) = b.angle.sin_cos();
55 let mut corners = [[0.0f32; 2]; 4];
56 let mut i = 0;
57
58 for sx in [-1.0f32, 1.0] {
59 for sy in [-1.0f32, 1.0] {
60 let lx = sx * b.half_w;
61 let ly = sy * b.half_h;
62
63 corners[i] = [b.x + cos * lx - sin * ly, b.y + sin * lx + cos * ly];
64 i += 1;
65 }
66 }
67
68 corners
69}
70
71fn contact_point(corners: &[[f32; 2]; 4], nx: f32, ny: f32, tolerance: f32) -> [f32; 2] {
76 let mut best = f32::NEG_INFINITY;
77
78 for p in corners {
79 best = best.max(p[0] * nx + p[1] * ny);
80 }
81
82 let mut sum_x = 0.0;
83 let mut sum_y = 0.0;
84 let mut sum_weight = 0.0;
85
86 for p in corners {
87 let weight = 1.0 - (best - (p[0] * nx + p[1] * ny)) / tolerance;
88
89 if weight > 0.0 {
90 sum_x += p[0] * weight;
91 sum_y += p[1] * weight;
92 sum_weight += weight;
93 }
94 }
95
96 [sum_x / sum_weight, sum_y / sum_weight]
97}
98
99pub fn obb_vs_obb(a: &Box2, b: &Box2) -> Option<Contact> {
102 let (a_sin, a_cos) = a.angle.sin_cos();
103 let (b_sin, b_cos) = b.angle.sin_cos();
104 let axes = [
105 [a_cos, a_sin],
106 [-a_sin, a_cos],
107 [b_cos, b_sin],
108 [-b_sin, b_cos],
109 ];
110
111 let corners_a = obb_corners(a);
112 let corners_b = obb_corners(b);
113
114 let mut min_overlap = f32::INFINITY;
115 let mut normal_x = 0.0;
116 let mut normal_y = 0.0;
117
118 for [ax, ay] in axes {
119 let mut min_a = f32::INFINITY;
120 let mut max_a = f32::NEG_INFINITY;
121 let mut min_b = f32::INFINITY;
122 let mut max_b = f32::NEG_INFINITY;
123
124 for p in &corners_a {
125 let proj = p[0] * ax + p[1] * ay;
126
127 min_a = min_a.min(proj);
128 max_a = max_a.max(proj);
129 }
130
131 for p in &corners_b {
132 let proj = p[0] * ax + p[1] * ay;
133
134 min_b = min_b.min(proj);
135 max_b = max_b.max(proj);
136 }
137
138 let overlap = max_a.min(max_b) - min_a.max(min_b);
139
140 if overlap <= 0.0 {
141 return None;
142 }
143
144 if overlap < min_overlap {
145 min_overlap = overlap;
146
147 let cx = b.x - a.x;
149 let cy = b.y - a.y;
150 let sign = if cx * ax + cy * ay < 0.0 { -1.0 } else { 1.0 };
151
152 normal_x = ax * sign;
153 normal_y = ay * sign;
154 }
155 }
156
157 let contact = contact_point(
158 &corners_a,
159 normal_x,
160 normal_y,
161 CONTACT_MANIFOLD_RATIO * (a.half_w + a.half_h),
162 );
163
164 Some(Contact {
165 nx: normal_x,
166 ny: normal_y,
167 depth: min_overlap,
168 cx: contact[0],
169 cy: contact[1],
170 })
171}
172
173pub fn collect_tile_contacts(
179 obb: &Box2,
180 map: &[Vec<i32>],
181 solid_tiles: &[i32],
182 tile_size: f32,
183) -> Vec<TileContact> {
184 let rows = map.len();
185 let cols = map.first().map(|row| row.len()).unwrap_or(0);
186 let mut contacts = Vec::new();
187
188 if rows == 0 || cols == 0 || solid_tiles.is_empty() {
189 return contacts;
190 }
191
192 let (sin, cos) = obb.angle.sin_cos();
194 let (sin, cos) = (sin.abs(), cos.abs());
195 let extent_x = obb.half_w * cos + obb.half_h * sin;
196 let extent_y = obb.half_w * sin + obb.half_h * cos;
197
198 let min_cell_x = (((obb.x - extent_x) / tile_size).floor() as i64).max(0);
199 let max_cell_x = (((obb.x + extent_x) / tile_size).floor() as i64).min(cols as i64 - 1);
200 let min_cell_y = (((obb.y - extent_y) / tile_size).floor() as i64).max(0);
201 let max_cell_y = (((obb.y + extent_y) / tile_size).floor() as i64).min(rows as i64 - 1);
202
203 for cell_y in min_cell_y..=max_cell_y {
204 for cell_x in min_cell_x..=max_cell_x {
205 if !solid_tiles.contains(&map[cell_y as usize][cell_x as usize]) {
206 continue;
207 }
208
209 let tile_x = cell_x as f32 * tile_size + tile_size / 2.0;
210 let tile_y = cell_y as f32 * tile_size + tile_size / 2.0;
211 let tile = Box2 {
212 x: tile_x,
213 y: tile_y,
214 angle: 0.0,
215 half_w: tile_size / 2.0,
216 half_h: tile_size / 2.0,
217 };
218
219 if let Some(contact) = obb_vs_obb(obb, &tile) {
220 contacts.push(TileContact {
221 contact,
222 tile_x,
223 tile_y,
224 });
225 }
226 }
227 }
228
229 contacts
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 fn box2(x: f32, y: f32, angle: f32, half_w: f32, half_h: f32) -> Box2 {
237 Box2 {
238 x,
239 y,
240 angle,
241 half_w,
242 half_h,
243 }
244 }
245
246 #[test]
247 fn origin_to_center_without_rotation_is_a_plain_offset() {
248 assert_eq!(
249 box_center_from_origin(10.0, 20.0, 0.0, 5.0, 3.0),
250 [15.0, 23.0]
251 );
252 }
253
254 #[test]
255 fn origin_to_center_follows_rotation() {
256 let center = box_center_from_origin(10.0, 20.0, core::f32::consts::FRAC_PI_2, 5.0, 3.0);
257
258 assert!((center[0] - 7.0).abs() < 1e-5);
259 assert!((center[1] - 25.0).abs() < 1e-5);
260 }
261
262 #[test]
263 fn separated_boxes_do_not_touch() {
264 let a = box2(0.0, 0.0, 0.0, 5.0, 5.0);
265 let b = box2(20.0, 0.0, 0.0, 5.0, 5.0);
266
267 assert!(obb_vs_obb(&a, &b).is_none());
268 }
269
270 #[test]
271 fn flush_touch_is_a_miss() {
272 let a = box2(0.0, 0.0, 0.0, 5.0, 5.0);
273 let b = box2(10.0, 0.0, 0.0, 5.0, 5.0);
274
275 assert!(obb_vs_obb(&a, &b).is_none());
276 }
277
278 #[test]
279 fn overlap_normal_points_from_a_to_b() {
280 let a = box2(0.0, 0.0, 0.0, 5.0, 5.0);
281 let b = box2(8.0, 0.0, 0.0, 5.0, 5.0);
282 let contact = obb_vs_obb(&a, &b).expect("контакт");
283
284 assert!((contact.nx - 1.0).abs() < 1e-5);
285 assert!(contact.ny.abs() < 1e-5);
286 assert!((contact.depth - 2.0).abs() < 1e-5);
287 }
288
289 #[test]
290 fn overlap_on_y_axis_keeps_direction() {
291 let a = box2(0.0, 0.0, 0.0, 5.0, 5.0);
292 let b = box2(0.0, -8.0, 0.0, 5.0, 5.0);
293 let contact = obb_vs_obb(&a, &b).expect("контакт");
294
295 assert!(contact.nx.abs() < 1e-5);
296 assert!((contact.ny + 1.0).abs() < 1e-5);
297 assert!((contact.depth - 2.0).abs() < 1e-5);
298 }
299
300 #[test]
301 fn face_contact_lands_in_the_middle_of_the_face() {
302 let a = box2(0.0, 0.0, 0.0, 5.0, 5.0);
303 let b = box2(8.0, 0.0, 0.0, 5.0, 5.0);
304 let contact = obb_vs_obb(&a, &b).expect("контакт");
305
306 assert!((contact.cx - 5.0).abs() < 1e-5);
307 assert!(contact.cy.abs() < 1e-5);
308 }
309
310 #[test]
311 fn corner_contact_keeps_the_lever() {
312 let a = box2(0.0, 0.0, core::f32::consts::FRAC_PI_4, 5.0, 5.0);
313 let b = box2(9.0, 0.0, 0.0, 3.0, 3.0);
314 let contact = obb_vs_obb(&a, &b).expect("контакт");
315
316 let distance = contact.cx.hypot(contact.cy);
318
319 assert!((distance - 5.0 * core::f32::consts::SQRT_2).abs() < 1e-4);
320 }
321
322 #[test]
323 fn returned_vector_actually_separates_rotated_boxes() {
324 let a = box2(0.0, 0.0, 0.0, 10.0, 2.0);
326 let b = box2(0.0, 5.0, core::f32::consts::FRAC_PI_4, 6.0, 1.0);
327 let contact = obb_vs_obb(&a, &b).expect("контакт");
328
329 let epsilon = 1e-4;
332 let separated = Box2 {
333 x: b.x + contact.nx * (contact.depth + epsilon),
334 y: b.y + contact.ny * (contact.depth + epsilon),
335 ..b
336 };
337
338 assert!(obb_vs_obb(&a, &separated).is_none());
339 }
340
341 #[test]
344 fn degenerate_box_is_a_miss() {
345 let degenerate = box2(0.0, 0.0, 0.0, 0.0, 0.0);
346 let b = box2(0.0, 0.0, 0.0, 5.0, 5.0);
347
348 assert!(obb_vs_obb(°enerate, &b).is_none());
349 assert!(obb_vs_obb(&b, °enerate).is_none());
350 }
351
352 #[test]
353 fn no_solid_tiles_means_no_contacts() {
354 let map = vec![vec![0, 0], vec![0, 0]];
355 let obb = box2(5.0, 5.0, 0.0, 3.0, 3.0);
356
357 assert!(collect_tile_contacts(&obb, &map, &[], 10.0).is_empty());
358 }
359
360 #[test]
361 fn box_away_from_the_wall_has_no_contacts() {
362 let map = vec![vec![0, 0], vec![0, 1]];
363 let obb = box2(100.0, 100.0, 0.0, 3.0, 3.0);
364
365 assert!(collect_tile_contacts(&obb, &map, &[1], 10.0).is_empty());
366 }
367
368 #[test]
369 fn single_wall_gives_one_contact_along_the_shortest_axis() {
370 let map = vec![vec![0, 0], vec![0, 1]];
372 let obb = box2(15.0, 22.0, 0.0, 3.0, 4.0);
374 let contacts = collect_tile_contacts(&obb, &map, &[1], 10.0);
375
376 assert_eq!(contacts.len(), 1);
377
378 let hit = contacts[0];
379
380 assert!(hit.contact.nx.abs() < 1e-5);
382 assert!((hit.contact.ny + 1.0).abs() < 1e-5);
383 assert!((hit.contact.depth - 2.0).abs() < 1e-5);
384 assert_eq!(hit.tile_x, 15.0);
385 assert_eq!(hit.tile_y, 15.0);
386
387 let resolved = Box2 {
389 x: obb.x - hit.contact.nx * hit.contact.depth,
390 y: obb.y - hit.contact.ny * hit.contact.depth,
391 ..obb
392 };
393 let tile = box2(15.0, 15.0, 0.0, 5.0, 5.0);
394
395 assert!(obb_vs_obb(&resolved, &tile).is_none());
396 }
397
398 #[test]
399 fn inner_corner_gives_a_contact_per_touched_tile() {
400 let map = vec![vec![1, 1], vec![1, 0]];
402 let obb = box2(12.0, 12.0, 0.0, 4.0, 4.0);
404 let contacts = collect_tile_contacts(&obb, &map, &[1], 10.0);
405
406 assert_eq!(contacts.len(), 3);
407
408 let mut tiles: Vec<(i32, i32)> = contacts
409 .iter()
410 .map(|c| (c.tile_x as i32, c.tile_y as i32))
411 .collect();
412
413 tiles.sort();
414
415 assert_eq!(tiles, vec![(5, 5), (5, 15), (15, 5)]);
416 assert!(contacts.iter().all(|c| c.contact.depth > 0.0));
417 }
418
419 #[test]
420 fn rotated_box_picks_candidate_cells_by_conservative_aabb() {
421 let map = vec![vec![1, 0], vec![0, 0]];
422 let obb = box2(13.0, 13.0, core::f32::consts::FRAC_PI_4, 5.0, 2.0);
424 let contacts = collect_tile_contacts(&obb, &map, &[1], 10.0);
425
426 assert_eq!(contacts.len(), 1);
427 assert!(contacts[0].contact.depth > 0.0);
428 }
429
430 #[test]
433 fn ray_and_contact_agree_on_the_same_wall() {
434 let map = vec![vec![0, 0], vec![0, 1]];
435 let obb = box2(15.0, 22.0, 0.0, 3.0, 4.0);
436 let contacts = collect_tile_contacts(&obb, &map, &[1], 10.0);
437 let hit = super::super::raycast::ray_vs_grid(
438 [obb.x, obb.y],
439 [contacts[0].contact.nx, contacts[0].contact.ny],
440 50.0,
441 &map,
442 &[1],
443 10.0,
444 );
445
446 assert!(hit.is_some());
448 assert!((hit.unwrap() - 2.0).abs() < 1e-4);
449 }
450}