Skip to main content

arcane_engine/physics/
narrowphase.rs

1use super::types::{Contact, RigidBody, Shape};
2
3/// Test collision between two rigid bodies. Returns a contact if overlapping.
4/// Contact normal always points from body_a toward body_b.
5pub 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
61/// Circle vs AABB. `swapped` means the original call had AABB as body_a.
62fn 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    // Transform circle center into AABB local space
73    let local_x = circle.x - aabb.x;
74    let local_y = circle.y - aabb.y;
75
76    // Closest point on AABB to circle center
77    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    // Check if circle center is inside the AABB
89    let inside = local_x.abs() < hw && local_y.abs() < hh;
90
91    let (nx, ny, penetration) = if inside {
92        // Push circle out along the axis of least penetration
93        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        // Original: AABB=a, Circle=b. Normal should point from a to b.
114        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        // Original: Circle=a, AABB=b. Normal should point from a to b (opposite).
127        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    // Contact point on the actual collision surface
169    let (cpx, cpy) = if overlap_x < overlap_y {
170        // Minimum separation on X axis — contact at A's X edge
171        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        // Minimum separation on Y axis — contact at A's Y edge
177        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
196/// Get world-space vertices for a polygon body.
197fn 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
215/// Get edge normals for a set of vertices.
216fn 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
232/// Project vertices onto an axis, returning (min, max).
233fn 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    // Ensure normal points from a to b
272    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    // Contact point: deepest penetrating vertex of B along -normal
279    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    // Find closest point on polygon to circle center
313    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    // Check if circle center is inside polygon
331    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        // Normal from closest point to circle center, inverted
341        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    // Ensure normal points from body_a to body_b
362    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    // Convert AABB to polygon and use polygon-polygon
387    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}