1use super::types::{Contact, RigidBody, Shape};
2
3pub fn test_collision(a: &RigidBody, b: &RigidBody) -> Option<Contact> {
6 match (&a.shape, &b.shape) {
7 (Shape::Circle { .. }, Shape::Circle { .. }) => circle_vs_circle(a, b),
8 (Shape::Circle { .. }, Shape::AABB { .. }) => circle_vs_aabb(a, b, false),
9 (Shape::AABB { .. }, Shape::Circle { .. }) => circle_vs_aabb(b, a, true),
10 (Shape::AABB { .. }, Shape::AABB { .. }) => aabb_vs_aabb(a, b),
11 (Shape::Polygon { .. }, Shape::Polygon { .. }) => polygon_vs_polygon(a, b),
12 (Shape::Circle { .. }, Shape::Polygon { .. }) => circle_vs_polygon(a, b, false),
13 (Shape::Polygon { .. }, Shape::Circle { .. }) => circle_vs_polygon(b, a, true),
14 (Shape::AABB { .. }, Shape::Polygon { .. }) => aabb_vs_polygon(a, b, false),
15 (Shape::Polygon { .. }, Shape::AABB { .. }) => aabb_vs_polygon(b, a, true),
16 }
17}
18
19fn circle_vs_circle(a: &RigidBody, b: &RigidBody) -> Option<Contact> {
20 let ra = match a.shape {
21 Shape::Circle { radius } => radius,
22 _ => return None,
23 };
24 let rb = match b.shape {
25 Shape::Circle { radius } => radius,
26 _ => return None,
27 };
28
29 let dx = b.x - a.x;
30 let dy = b.y - a.y;
31 let dist_sq = dx * dx + dy * dy;
32 let sum_r = ra + rb;
33
34 if dist_sq >= sum_r * sum_r {
35 return None;
36 }
37
38 let dist = dist_sq.sqrt();
39 let (nx, ny) = if dist > 1e-8 {
40 (dx / dist, dy / dist)
41 } else {
42 (1.0, 0.0)
43 };
44
45 Some(Contact {
46 body_a: a.id,
47 body_b: b.id,
48 normal: (nx, ny),
49 penetration: sum_r - dist,
50 contact_point: (
51 a.x + nx * (ra - (sum_r - dist) * 0.5),
52 a.y + ny * (ra - (sum_r - dist) * 0.5),
53 ),
54 accumulated_jn: 0.0,
55 accumulated_jt: 0.0,
56 velocity_bias: 0.0,
57 tangent: (0.0, 0.0),
58 })
59}
60
61fn circle_vs_aabb(circle: &RigidBody, aabb: &RigidBody, swapped: bool) -> Option<Contact> {
63 let radius = match circle.shape {
64 Shape::Circle { radius } => radius,
65 _ => return None,
66 };
67 let (hw, hh) = match aabb.shape {
68 Shape::AABB { half_w, half_h } => (half_w, half_h),
69 _ => return None,
70 };
71
72 let local_x = circle.x - aabb.x;
74 let local_y = circle.y - aabb.y;
75
76 let closest_x = local_x.clamp(-hw, hw);
78 let closest_y = local_y.clamp(-hh, hh);
79
80 let dx = local_x - closest_x;
81 let dy = local_y - closest_y;
82 let dist_sq = dx * dx + dy * dy;
83
84 if dist_sq >= radius * radius {
85 return None;
86 }
87
88 let inside = local_x.abs() < hw && local_y.abs() < hh;
90
91 let (nx, ny, penetration) = if inside {
92 let overlap_x = hw - local_x.abs();
94 let overlap_y = hh - local_y.abs();
95 if overlap_x < overlap_y {
96 let nx = if local_x >= 0.0 { 1.0 } else { -1.0 };
97 (nx, 0.0, overlap_x + radius)
98 } else {
99 let ny = if local_y >= 0.0 { 1.0 } else { -1.0 };
100 (0.0, ny, overlap_y + radius)
101 }
102 } else {
103 let dist = dist_sq.sqrt();
104 let nx = if dist > 1e-8 { dx / dist } else { 1.0 };
105 let ny = if dist > 1e-8 { dy / dist } else { 0.0 };
106 (nx, ny, radius - dist)
107 };
108
109 let contact_x = aabb.x + closest_x;
110 let contact_y = aabb.y + closest_y;
111
112 if swapped {
113 Some(Contact {
115 body_a: aabb.id,
116 body_b: circle.id,
117 normal: (nx, ny),
118 penetration,
119 contact_point: (contact_x, contact_y),
120 accumulated_jn: 0.0,
121 accumulated_jt: 0.0,
122 velocity_bias: 0.0,
123 tangent: (0.0, 0.0),
124 })
125 } else {
126 Some(Contact {
128 body_a: circle.id,
129 body_b: aabb.id,
130 normal: (-nx, -ny),
131 penetration,
132 contact_point: (contact_x, contact_y),
133 accumulated_jn: 0.0,
134 accumulated_jt: 0.0,
135 velocity_bias: 0.0,
136 tangent: (0.0, 0.0),
137 })
138 }
139}
140
141fn aabb_vs_aabb(a: &RigidBody, b: &RigidBody) -> Option<Contact> {
142 let (ahw, ahh) = match a.shape {
143 Shape::AABB { half_w, half_h } => (half_w, half_h),
144 _ => return None,
145 };
146 let (bhw, bhh) = match b.shape {
147 Shape::AABB { half_w, half_h } => (half_w, half_h),
148 _ => return None,
149 };
150
151 let dx = b.x - a.x;
152 let dy = b.y - a.y;
153 let overlap_x = (ahw + bhw) - dx.abs();
154 let overlap_y = (ahh + bhh) - dy.abs();
155
156 if overlap_x <= 0.0 || overlap_y <= 0.0 {
157 return None;
158 }
159
160 let (nx, ny, penetration) = if overlap_x < overlap_y {
161 let nx = if dx >= 0.0 { 1.0 } else { -1.0 };
162 (nx, 0.0, overlap_x)
163 } else {
164 let ny = if dy >= 0.0 { 1.0 } else { -1.0 };
165 (0.0, ny, overlap_y)
166 };
167
168 let (cpx, cpy) = if overlap_x < overlap_y {
170 let cx = if dx >= 0.0 { a.x + ahw } else { a.x - ahw };
172 let y_min = (a.y - ahh).max(b.y - bhh);
173 let y_max = (a.y + ahh).min(b.y + bhh);
174 (cx, (y_min + y_max) * 0.5)
175 } else {
176 let cy = if dy >= 0.0 { a.y + ahh } else { a.y - ahh };
178 let x_min = (a.x - ahw).max(b.x - bhw);
179 let x_max = (a.x + ahw).min(b.x + bhw);
180 ((x_min + x_max) * 0.5, cy)
181 };
182
183 Some(Contact {
184 body_a: a.id,
185 body_b: b.id,
186 normal: (nx, ny),
187 penetration,
188 contact_point: (cpx, cpy),
189 accumulated_jn: 0.0,
190 accumulated_jt: 0.0,
191 velocity_bias: 0.0,
192 tangent: (0.0, 0.0),
193 })
194}
195
196fn get_world_vertices(body: &RigidBody) -> Vec<(f32, f32)> {
198 let verts = match &body.shape {
199 Shape::Polygon { vertices } => vertices,
200 _ => return Vec::new(),
201 };
202 let cos = body.angle.cos();
203 let sin = body.angle.sin();
204 verts
205 .iter()
206 .map(|&(vx, vy)| {
207 (
208 vx * cos - vy * sin + body.x,
209 vx * sin + vy * cos + body.y,
210 )
211 })
212 .collect()
213}
214
215fn get_edge_normals(vertices: &[(f32, f32)]) -> Vec<(f32, f32)> {
217 let n = vertices.len();
218 let mut normals = Vec::with_capacity(n);
219 for i in 0..n {
220 let (x0, y0) = vertices[i];
221 let (x1, y1) = vertices[(i + 1) % n];
222 let ex = x1 - x0;
223 let ey = y1 - y0;
224 let len = (ex * ex + ey * ey).sqrt();
225 if len > 1e-8 {
226 normals.push((ey / len, -ex / len));
227 }
228 }
229 normals
230}
231
232fn project_vertices(vertices: &[(f32, f32)], axis: (f32, f32)) -> (f32, f32) {
234 let mut min = f32::MAX;
235 let mut max = f32::MIN;
236 for &(vx, vy) in vertices {
237 let p = vx * axis.0 + vy * axis.1;
238 min = min.min(p);
239 max = max.max(p);
240 }
241 (min, max)
242}
243
244fn polygon_vs_polygon(a: &RigidBody, b: &RigidBody) -> Option<Contact> {
245 let verts_a = get_world_vertices(a);
246 let verts_b = get_world_vertices(b);
247 if verts_a.len() < 3 || verts_b.len() < 3 {
248 return None;
249 }
250
251 let normals_a = get_edge_normals(&verts_a);
252 let normals_b = get_edge_normals(&verts_b);
253
254 let mut min_overlap = f32::MAX;
255 let mut min_axis = (0.0f32, 0.0f32);
256
257 for &axis in normals_a.iter().chain(normals_b.iter()) {
258 let (min_a, max_a) = project_vertices(&verts_a, axis);
259 let (min_b, max_b) = project_vertices(&verts_b, axis);
260
261 let overlap = (max_a.min(max_b)) - (min_a.max(min_b));
262 if overlap <= 0.0 {
263 return None;
264 }
265 if overlap < min_overlap {
266 min_overlap = overlap;
267 min_axis = axis;
268 }
269 }
270
271 let dx = b.x - a.x;
273 let dy = b.y - a.y;
274 if dx * min_axis.0 + dy * min_axis.1 < 0.0 {
275 min_axis = (-min_axis.0, -min_axis.1);
276 }
277
278 let mut best_dot = f32::MAX;
280 let mut best_point = ((a.x + b.x) * 0.5, (a.y + b.y) * 0.5);
281 for &(vx, vy) in &verts_b {
282 let d = vx * min_axis.0 + vy * min_axis.1;
283 if d < best_dot {
284 best_dot = d;
285 best_point = (vx, vy);
286 }
287 }
288
289 Some(Contact {
290 body_a: a.id,
291 body_b: b.id,
292 normal: min_axis,
293 penetration: min_overlap,
294 contact_point: best_point,
295 accumulated_jn: 0.0,
296 accumulated_jt: 0.0,
297 velocity_bias: 0.0,
298 tangent: (0.0, 0.0),
299 })
300}
301
302fn circle_vs_polygon(circle: &RigidBody, poly: &RigidBody, swapped: bool) -> Option<Contact> {
303 let radius = match circle.shape {
304 Shape::Circle { radius } => radius,
305 _ => return None,
306 };
307 let verts = get_world_vertices(poly);
308 if verts.len() < 3 {
309 return None;
310 }
311
312 let mut closest_dist_sq = f32::MAX;
314 let mut closest_point = (0.0f32, 0.0f32);
315
316 let n = verts.len();
317 for i in 0..n {
318 let (ax, ay) = verts[i];
319 let (bx, by) = verts[(i + 1) % n];
320 let (cx, cy) = closest_point_on_segment(circle.x, circle.y, ax, ay, bx, by);
321 let dx = circle.x - cx;
322 let dy = circle.y - cy;
323 let d2 = dx * dx + dy * dy;
324 if d2 < closest_dist_sq {
325 closest_dist_sq = d2;
326 closest_point = (cx, cy);
327 }
328 }
329
330 let inside = point_in_polygon(circle.x, circle.y, &verts);
332
333 let dist = closest_dist_sq.sqrt();
334
335 if !inside && dist >= radius {
336 return None;
337 }
338
339 let (nx, ny, penetration) = if inside {
340 let dx = circle.x - closest_point.0;
342 let dy = circle.y - closest_point.1;
343 let len = (dx * dx + dy * dy).sqrt();
344 if len > 1e-8 {
345 (-dx / len, -dy / len, radius + dist)
346 } else {
347 (1.0, 0.0, radius)
348 }
349 } else {
350 let dx = circle.x - closest_point.0;
351 let dy = circle.y - closest_point.1;
352 (dx / dist, dy / dist, radius - dist)
353 };
354
355 let (ba, bb, fnx, fny) = if swapped {
356 (poly.id, circle.id, -nx, -ny)
357 } else {
358 (circle.id, poly.id, nx, ny)
359 };
360
361 let dir_x = if swapped { circle.x - poly.x } else { poly.x - circle.x };
363 let dir_y = if swapped { circle.y - poly.y } else { poly.y - circle.y };
364 let dot = fnx * dir_x + fny * dir_y;
365 let (fnx, fny) = if dot < 0.0 { (-fnx, -fny) } else { (fnx, fny) };
366
367 Some(Contact {
368 body_a: ba,
369 body_b: bb,
370 normal: (fnx, fny),
371 penetration,
372 contact_point: closest_point,
373 accumulated_jn: 0.0,
374 accumulated_jt: 0.0,
375 velocity_bias: 0.0,
376 tangent: (0.0, 0.0),
377 })
378}
379
380fn aabb_vs_polygon(aabb: &RigidBody, poly: &RigidBody, swapped: bool) -> Option<Contact> {
381 let (hw, hh) = match aabb.shape {
382 Shape::AABB { half_w, half_h } => (half_w, half_h),
383 _ => return None,
384 };
385
386 let aabb_as_poly = RigidBody {
388 shape: Shape::Polygon {
389 vertices: vec![(-hw, -hh), (hw, -hh), (hw, hh), (-hw, hh)],
390 },
391 ..aabb.clone()
392 };
393
394 let result = polygon_vs_polygon(&aabb_as_poly, poly)?;
395
396 if swapped {
397 Some(Contact {
398 body_a: poly.id,
399 body_b: aabb.id,
400 normal: (-result.normal.0, -result.normal.1),
401 penetration: result.penetration,
402 contact_point: result.contact_point,
403 accumulated_jn: 0.0,
404 accumulated_jt: 0.0,
405 velocity_bias: 0.0,
406 tangent: (0.0, 0.0),
407 })
408 } else {
409 Some(Contact {
410 body_a: aabb.id,
411 body_b: poly.id,
412 ..result
413 })
414 }
415}
416
417fn closest_point_on_segment(
418 px: f32, py: f32,
419 ax: f32, ay: f32,
420 bx: f32, by: f32,
421) -> (f32, f32) {
422 let abx = bx - ax;
423 let aby = by - ay;
424 let apx = px - ax;
425 let apy = py - ay;
426 let ab_sq = abx * abx + aby * aby;
427 if ab_sq < 1e-12 {
428 return (ax, ay);
429 }
430 let t = ((apx * abx + apy * aby) / ab_sq).clamp(0.0, 1.0);
431 (ax + abx * t, ay + aby * t)
432}
433
434fn point_in_polygon(px: f32, py: f32, verts: &[(f32, f32)]) -> bool {
435 let n = verts.len();
436 let mut inside = false;
437 let mut j = n - 1;
438 for i in 0..n {
439 let (xi, yi) = verts[i];
440 let (xj, yj) = verts[j];
441 if ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
442 inside = !inside;
443 }
444 j = i;
445 }
446 inside
447}