Skip to main content

arcane_core/physics/
narrowphase.rs

1use super::types::{Contact, ContactID, ContactManifold, ManifoldPoint, 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}
448
449// ============================================================================
450// Contact Manifold Generation (TGS Soft Phase 1)
451// ============================================================================
452
453/// Test collision between two bodies and return a contact manifold.
454/// This is the new entry point that generates proper 2-point manifolds using
455/// Sutherland-Hodgman clipping for polygon-polygon collisions.
456pub fn test_collision_manifold(a: &RigidBody, b: &RigidBody) -> Option<ContactManifold> {
457    match (&a.shape, &b.shape) {
458        (Shape::Circle { .. }, Shape::Circle { .. }) => circle_vs_circle_manifold(a, b),
459        (Shape::Circle { .. }, Shape::AABB { .. }) => circle_vs_aabb_manifold(a, b, false),
460        (Shape::AABB { .. }, Shape::Circle { .. }) => circle_vs_aabb_manifold(b, a, true),
461        (Shape::AABB { .. }, Shape::AABB { .. }) => aabb_vs_aabb_manifold(a, b),
462        (Shape::Polygon { .. }, Shape::Polygon { .. }) => polygon_vs_polygon_manifold(a, b),
463        (Shape::Circle { .. }, Shape::Polygon { .. }) => circle_vs_polygon_manifold(a, b, false),
464        (Shape::Polygon { .. }, Shape::Circle { .. }) => circle_vs_polygon_manifold(b, a, true),
465        (Shape::AABB { .. }, Shape::Polygon { .. }) => aabb_vs_polygon_manifold(a, b, false),
466        (Shape::Polygon { .. }, Shape::AABB { .. }) => aabb_vs_polygon_manifold(b, a, true),
467    }
468}
469
470/// Transform a world-space point to body-local space
471fn world_to_local(body: &RigidBody, wx: f32, wy: f32) -> (f32, f32) {
472    let dx = wx - body.x;
473    let dy = wy - body.y;
474    let cos = body.angle.cos();
475    let sin = body.angle.sin();
476    // Inverse rotation
477    (dx * cos + dy * sin, -dx * sin + dy * cos)
478}
479
480fn circle_vs_circle_manifold(a: &RigidBody, b: &RigidBody) -> Option<ContactManifold> {
481    let ra = match a.shape {
482        Shape::Circle { radius } => radius,
483        _ => return None,
484    };
485    let rb = match b.shape {
486        Shape::Circle { radius } => radius,
487        _ => return None,
488    };
489
490    let dx = b.x - a.x;
491    let dy = b.y - a.y;
492    let dist_sq = dx * dx + dy * dy;
493    let sum_r = ra + rb;
494
495    if dist_sq >= sum_r * sum_r {
496        return None;
497    }
498
499    let dist = dist_sq.sqrt();
500    let (nx, ny) = if dist > 1e-8 {
501        (dx / dist, dy / dist)
502    } else {
503        (1.0, 0.0)
504    };
505
506    let penetration = sum_r - dist;
507
508    // World-space contact point
509    let cpx = a.x + nx * (ra - penetration * 0.5);
510    let cpy = a.y + ny * (ra - penetration * 0.5);
511
512    // Convert to body-local anchors
513    let local_a = world_to_local(a, cpx, cpy);
514    let local_b = world_to_local(b, cpx, cpy);
515
516    Some(ContactManifold {
517        body_a: a.id,
518        body_b: b.id,
519        normal: (nx, ny),
520        points: vec![ManifoldPoint::new(local_a, local_b, penetration, ContactID::circle())],
521        tangent: (-ny, nx),
522        velocity_bias: 0.0,
523    })
524}
525
526fn circle_vs_aabb_manifold(circle: &RigidBody, aabb: &RigidBody, swapped: bool) -> Option<ContactManifold> {
527    let radius = match circle.shape {
528        Shape::Circle { radius } => radius,
529        _ => return None,
530    };
531    let (hw, hh) = match aabb.shape {
532        Shape::AABB { half_w, half_h } => (half_w, half_h),
533        _ => return None,
534    };
535
536    let local_x = circle.x - aabb.x;
537    let local_y = circle.y - aabb.y;
538
539    let closest_x = local_x.clamp(-hw, hw);
540    let closest_y = local_y.clamp(-hh, hh);
541
542    let dx = local_x - closest_x;
543    let dy = local_y - closest_y;
544    let dist_sq = dx * dx + dy * dy;
545
546    if dist_sq >= radius * radius {
547        return None;
548    }
549
550    let inside = local_x.abs() < hw && local_y.abs() < hh;
551
552    let (nx, ny, penetration) = if inside {
553        let overlap_x = hw - local_x.abs();
554        let overlap_y = hh - local_y.abs();
555        if overlap_x < overlap_y {
556            let nx = if local_x >= 0.0 { 1.0 } else { -1.0 };
557            (nx, 0.0, overlap_x + radius)
558        } else {
559            let ny = if local_y >= 0.0 { 1.0 } else { -1.0 };
560            (0.0, ny, overlap_y + radius)
561        }
562    } else {
563        let dist = dist_sq.sqrt();
564        let nx = if dist > 1e-8 { dx / dist } else { 1.0 };
565        let ny = if dist > 1e-8 { dy / dist } else { 0.0 };
566        (nx, ny, radius - dist)
567    };
568
569    let cpx = aabb.x + closest_x;
570    let cpy = aabb.y + closest_y;
571
572    let (body_a, body_b, fnx, fny) = if swapped {
573        (aabb, circle, nx, ny)
574    } else {
575        (circle, aabb, -nx, -ny)
576    };
577
578    let local_a = world_to_local(body_a, cpx, cpy);
579    let local_b = world_to_local(body_b, cpx, cpy);
580
581    Some(ContactManifold {
582        body_a: body_a.id,
583        body_b: body_b.id,
584        normal: (fnx, fny),
585        points: vec![ManifoldPoint::new(local_a, local_b, penetration, ContactID::circle())],
586        tangent: (-fny, fnx),
587        velocity_bias: 0.0,
588    })
589}
590
591fn aabb_vs_aabb_manifold(a: &RigidBody, b: &RigidBody) -> Option<ContactManifold> {
592    let (ahw, ahh) = match a.shape {
593        Shape::AABB { half_w, half_h } => (half_w, half_h),
594        _ => return None,
595    };
596    let (bhw, bhh) = match b.shape {
597        Shape::AABB { half_w, half_h } => (half_w, half_h),
598        _ => return None,
599    };
600
601    let dx = b.x - a.x;
602    let dy = b.y - a.y;
603    let overlap_x = (ahw + bhw) - dx.abs();
604    let overlap_y = (ahh + bhh) - dy.abs();
605
606    if overlap_x <= 0.0 || overlap_y <= 0.0 {
607        return None;
608    }
609
610    // Choose axis of minimum penetration
611    if overlap_x < overlap_y {
612        // X-axis separation (vertical edge contact)
613        let nx = if dx >= 0.0 { 1.0 } else { -1.0 };
614
615        // Contact edge X position
616        let cx = if dx >= 0.0 { a.x + ahw } else { a.x - ahw };
617
618        // Y range of overlap
619        let y_min = (a.y - ahh).max(b.y - bhh);
620        let y_max = (a.y + ahh).min(b.y + bhh);
621
622        // Two contact points at overlap corners
623        let mut points = Vec::with_capacity(2);
624
625        let cp1 = (cx, y_min);
626        let local_a1 = world_to_local(a, cp1.0, cp1.1);
627        let local_b1 = world_to_local(b, cp1.0, cp1.1);
628        points.push(ManifoldPoint::new(local_a1, local_b1, overlap_x, ContactID::new(0, 0, 0)));
629
630        let cp2 = (cx, y_max);
631        let local_a2 = world_to_local(a, cp2.0, cp2.1);
632        let local_b2 = world_to_local(b, cp2.0, cp2.1);
633        points.push(ManifoldPoint::new(local_a2, local_b2, overlap_x, ContactID::new(0, 0, 1)));
634
635        Some(ContactManifold {
636            body_a: a.id,
637            body_b: b.id,
638            normal: (nx, 0.0),
639            points,
640            tangent: (0.0, 1.0),
641            velocity_bias: 0.0,
642        })
643    } else {
644        // Y-axis separation (horizontal edge contact)
645        let ny = if dy >= 0.0 { 1.0 } else { -1.0 };
646
647        // Contact edge Y position
648        let cy = if dy >= 0.0 { a.y + ahh } else { a.y - ahh };
649
650        // X range of overlap
651        let x_min = (a.x - ahw).max(b.x - bhw);
652        let x_max = (a.x + ahw).min(b.x + bhw);
653
654        // Two contact points at overlap corners
655        let mut points = Vec::with_capacity(2);
656
657        let cp1 = (x_min, cy);
658        let local_a1 = world_to_local(a, cp1.0, cp1.1);
659        let local_b1 = world_to_local(b, cp1.0, cp1.1);
660        points.push(ManifoldPoint::new(local_a1, local_b1, overlap_y, ContactID::new(1, 1, 0)));
661
662        let cp2 = (x_max, cy);
663        let local_a2 = world_to_local(a, cp2.0, cp2.1);
664        let local_b2 = world_to_local(b, cp2.0, cp2.1);
665        points.push(ManifoldPoint::new(local_a2, local_b2, overlap_y, ContactID::new(1, 1, 1)));
666
667        Some(ContactManifold {
668            body_a: a.id,
669            body_b: b.id,
670            normal: (0.0, ny),
671            points,
672            tangent: (1.0, 0.0),
673            velocity_bias: 0.0,
674        })
675    }
676}
677
678/// Find the edge with maximum separation between two polygons (SAT).
679/// Returns (separation, edge_index) where edge_index is on polygon `a`.
680fn find_max_separation(
681    a_verts: &[(f32, f32)],
682    b_verts: &[(f32, f32)],
683) -> (f32, usize) {
684    let mut max_sep = f32::MIN;
685    let mut best_edge = 0;
686
687    let n = a_verts.len();
688    for i in 0..n {
689        let v0 = a_verts[i];
690        let v1 = a_verts[(i + 1) % n];
691
692        // Outward edge normal
693        let ex = v1.0 - v0.0;
694        let ey = v1.1 - v0.1;
695        let len = (ex * ex + ey * ey).sqrt();
696        if len < 1e-8 {
697            continue;
698        }
699        let nx = ey / len;
700        let ny = -ex / len;
701
702        // Find support point on B in direction -n
703        let mut min_dot = f32::MAX;
704        for &bv in b_verts {
705            let d = (bv.0 - v0.0) * nx + (bv.1 - v0.1) * ny;
706            min_dot = min_dot.min(d);
707        }
708
709        // min_dot is the separation along this axis (negative = overlap)
710        if min_dot > max_sep {
711            max_sep = min_dot;
712            best_edge = i;
713        }
714    }
715
716    (max_sep, best_edge)
717}
718
719/// Find the edge on the incident polygon that is most anti-parallel to the reference normal.
720fn find_incident_edge(
721    inc_verts: &[(f32, f32)],
722    ref_normal: (f32, f32),
723) -> usize {
724    let n = inc_verts.len();
725    let mut min_dot = f32::MAX;
726    let mut best_edge = 0;
727
728    for i in 0..n {
729        let v0 = inc_verts[i];
730        let v1 = inc_verts[(i + 1) % n];
731
732        // Edge normal (outward)
733        let ex = v1.0 - v0.0;
734        let ey = v1.1 - v0.1;
735        let len = (ex * ex + ey * ey).sqrt();
736        if len < 1e-8 {
737            continue;
738        }
739        let nx = ey / len;
740        let ny = -ex / len;
741
742        // Most anti-parallel to reference normal
743        let dot = nx * ref_normal.0 + ny * ref_normal.1;
744        if dot < min_dot {
745            min_dot = dot;
746            best_edge = i;
747        }
748    }
749
750    best_edge
751}
752
753/// Clip a line segment against a half-plane defined by the line passing through
754/// `line_point` with normal `normal`. Points are kept if they're on the positive side.
755/// Returns up to 2 clipped points.
756fn clip_segment_to_line(
757    v0: (f32, f32),
758    v1: (f32, f32),
759    line_point: (f32, f32),
760    normal: (f32, f32),
761) -> Vec<(f32, f32)> {
762    let mut result = Vec::with_capacity(2);
763
764    // Distance from line (positive = inside)
765    let d0 = (v0.0 - line_point.0) * normal.0 + (v0.1 - line_point.1) * normal.1;
766    let d1 = (v1.0 - line_point.0) * normal.0 + (v1.1 - line_point.1) * normal.1;
767
768    // Keep points inside
769    if d0 >= 0.0 {
770        result.push(v0);
771    }
772    if d1 >= 0.0 {
773        result.push(v1);
774    }
775
776    // If they're on opposite sides, compute intersection
777    if d0 * d1 < 0.0 {
778        let t = d0 / (d0 - d1);
779        let cx = v0.0 + t * (v1.0 - v0.0);
780        let cy = v0.1 + t * (v1.1 - v0.1);
781        result.push((cx, cy));
782    }
783
784    result
785}
786
787/// Sutherland-Hodgman polygon clipping for polygon-polygon collision.
788/// Generates a proper 2-point contact manifold.
789fn polygon_vs_polygon_manifold(a: &RigidBody, b: &RigidBody) -> Option<ContactManifold> {
790    let verts_a = get_world_vertices(a);
791    let verts_b = get_world_vertices(b);
792
793    if verts_a.len() < 3 || verts_b.len() < 3 {
794        return None;
795    }
796
797    // Find axes of minimum penetration for both polygons
798    let (sep_a, edge_a) = find_max_separation(&verts_a, &verts_b);
799    let (sep_b, edge_b) = find_max_separation(&verts_b, &verts_a);
800
801    // If either is positive, no collision
802    if sep_a > 0.0 || sep_b > 0.0 {
803        return None;
804    }
805
806    // Choose reference face (the one with smaller penetration = larger separation)
807    // Use a small bias to prefer A when close to equal
808    let (ref_verts, inc_verts, ref_edge, ref_body, inc_body, flip) = if sep_a > sep_b - 0.001 {
809        (&verts_a, &verts_b, edge_a, a, b, false)
810    } else {
811        (&verts_b, &verts_a, edge_b, b, a, true)
812    };
813
814    let n = ref_verts.len();
815    let ref_v0 = ref_verts[ref_edge];
816    let ref_v1 = ref_verts[(ref_edge + 1) % n];
817
818    // Reference face normal (outward)
819    let ref_ex = ref_v1.0 - ref_v0.0;
820    let ref_ey = ref_v1.1 - ref_v0.1;
821    let ref_len = (ref_ex * ref_ex + ref_ey * ref_ey).sqrt();
822    if ref_len < 1e-8 {
823        return None;
824    }
825    let ref_nx = ref_ey / ref_len;
826    let ref_ny = -ref_ex / ref_len;
827
828    // Reference face tangent (along edge)
829    let ref_tx = ref_ex / ref_len;
830    let ref_ty = ref_ey / ref_len;
831
832    // Find incident edge
833    let inc_edge = find_incident_edge(inc_verts, (ref_nx, ref_ny));
834    let inc_n = inc_verts.len();
835    let inc_v0 = inc_verts[inc_edge];
836    let inc_v1 = inc_verts[(inc_edge + 1) % inc_n];
837
838    // Clip incident edge against side planes of reference face
839    // The side planes are perpendicular to the reference edge at its endpoints.
840    // We want to keep points BETWEEN the two endpoints, so:
841    // - At ref_v0: keep points in the +tangent direction (toward ref_v1)
842    // - At ref_v1: keep points in the -tangent direction (toward ref_v0)
843
844    // Side plane 1: at ref_v0, normal = +tangent (keeps points toward ref_v1)
845    let mut clipped = clip_segment_to_line(inc_v0, inc_v1, ref_v0, (ref_tx, ref_ty));
846
847    if clipped.len() < 2 {
848        // Degenerate case - use single point
849        if clipped.is_empty() {
850            return None;
851        }
852    }
853
854    // Side plane 2: at ref_v1, normal = -tangent (keeps points toward ref_v0)
855    if clipped.len() >= 2 {
856        clipped = clip_segment_to_line(clipped[0], clipped[1], ref_v1, (-ref_tx, -ref_ty));
857    }
858
859    // Keep only points behind (or very close to) reference face
860    // Tolerance prevents losing contacts due to floating-point precision
861    const CLIP_TOLERANCE: f32 = 0.02;
862    let mut points = Vec::with_capacity(2);
863    for (i, &cp) in clipped.iter().enumerate() {
864        // Distance behind reference face (negative = behind)
865        let sep = (cp.0 - ref_v0.0) * ref_nx + (cp.1 - ref_v0.1) * ref_ny;
866
867        if sep <= CLIP_TOLERANCE {
868            // Penetration = -sep
869            let penetration = -sep;
870
871            let (local_a, local_b) = if flip {
872                (world_to_local(inc_body, cp.0, cp.1), world_to_local(ref_body, cp.0, cp.1))
873            } else {
874                (world_to_local(ref_body, cp.0, cp.1), world_to_local(inc_body, cp.0, cp.1))
875            };
876
877            let id = ContactID::new(ref_edge as u8, inc_edge as u8, i as u8);
878            points.push(ManifoldPoint::new(local_a, local_b, penetration, id));
879        }
880    }
881
882    if points.is_empty() {
883        return None;
884    }
885
886    // Build final normal pointing from A to B
887    let (final_nx, final_ny) = if flip {
888        (-ref_nx, -ref_ny)
889    } else {
890        (ref_nx, ref_ny)
891    };
892
893    // Ensure normal points from A toward B
894    let dir_x = b.x - a.x;
895    let dir_y = b.y - a.y;
896    let (final_nx, final_ny) = if dir_x * final_nx + dir_y * final_ny < 0.0 {
897        (-final_nx, -final_ny)
898    } else {
899        (final_nx, final_ny)
900    };
901
902    Some(ContactManifold {
903        body_a: a.id,
904        body_b: b.id,
905        normal: (final_nx, final_ny),
906        points,
907        tangent: (-final_ny, final_nx),
908        velocity_bias: 0.0,
909    })
910}
911
912fn circle_vs_polygon_manifold(circle: &RigidBody, poly: &RigidBody, swapped: bool) -> Option<ContactManifold> {
913    let radius = match circle.shape {
914        Shape::Circle { radius } => radius,
915        _ => return None,
916    };
917    let verts = get_world_vertices(poly);
918    if verts.len() < 3 {
919        return None;
920    }
921
922    // Find closest point on polygon to circle center
923    let mut closest_dist_sq = f32::MAX;
924    let mut closest_point = (0.0f32, 0.0f32);
925
926    let n = verts.len();
927    for i in 0..n {
928        let (ax, ay) = verts[i];
929        let (bx, by) = verts[(i + 1) % n];
930        let (cx, cy) = closest_point_on_segment(circle.x, circle.y, ax, ay, bx, by);
931        let dx = circle.x - cx;
932        let dy = circle.y - cy;
933        let d2 = dx * dx + dy * dy;
934        if d2 < closest_dist_sq {
935            closest_dist_sq = d2;
936            closest_point = (cx, cy);
937        }
938    }
939
940    let inside = point_in_polygon(circle.x, circle.y, &verts);
941    let dist = closest_dist_sq.sqrt();
942
943    if !inside && dist >= radius {
944        return None;
945    }
946
947    let (nx, ny, penetration) = if inside {
948        let dx = circle.x - closest_point.0;
949        let dy = circle.y - closest_point.1;
950        let len = (dx * dx + dy * dy).sqrt();
951        if len > 1e-8 {
952            (-dx / len, -dy / len, radius + dist)
953        } else {
954            (1.0, 0.0, radius)
955        }
956    } else {
957        let dx = circle.x - closest_point.0;
958        let dy = circle.y - closest_point.1;
959        (dx / dist, dy / dist, radius - dist)
960    };
961
962    let (body_a, body_b, fnx, fny) = if swapped {
963        (poly, circle, -nx, -ny)
964    } else {
965        (circle, poly, nx, ny)
966    };
967
968    // Ensure normal points from body_a to body_b
969    let dir_x = body_b.x - body_a.x;
970    let dir_y = body_b.y - body_a.y;
971    let (fnx, fny) = if fnx * dir_x + fny * dir_y < 0.0 {
972        (-fnx, -fny)
973    } else {
974        (fnx, fny)
975    };
976
977    let local_a = world_to_local(body_a, closest_point.0, closest_point.1);
978    let local_b = world_to_local(body_b, closest_point.0, closest_point.1);
979
980    Some(ContactManifold {
981        body_a: body_a.id,
982        body_b: body_b.id,
983        normal: (fnx, fny),
984        points: vec![ManifoldPoint::new(local_a, local_b, penetration, ContactID::circle())],
985        tangent: (-fny, fnx),
986        velocity_bias: 0.0,
987    })
988}
989
990fn aabb_vs_polygon_manifold(aabb: &RigidBody, poly: &RigidBody, swapped: bool) -> Option<ContactManifold> {
991    let (hw, hh) = match aabb.shape {
992        Shape::AABB { half_w, half_h } => (half_w, half_h),
993        _ => return None,
994    };
995
996    // Convert AABB to polygon and use polygon-polygon collision
997    let aabb_as_poly = RigidBody {
998        shape: Shape::Polygon {
999            vertices: vec![(-hw, -hh), (hw, -hh), (hw, hh), (-hw, hh)],
1000        },
1001        ..aabb.clone()
1002    };
1003
1004    let mut manifold = polygon_vs_polygon_manifold(&aabb_as_poly, poly)?;
1005
1006    if swapped {
1007        // Swap body IDs and flip normal
1008        std::mem::swap(&mut manifold.body_a, &mut manifold.body_b);
1009        manifold.normal = (-manifold.normal.0, -manifold.normal.1);
1010        manifold.tangent = (-manifold.tangent.0, -manifold.tangent.1);
1011
1012        // Swap local anchors in each point
1013        for point in &mut manifold.points {
1014            std::mem::swap(&mut point.local_a, &mut point.local_b);
1015        }
1016    } else {
1017        // Fix body IDs (we used aabb_as_poly which has same id as aabb)
1018        manifold.body_a = aabb.id;
1019    }
1020
1021    Some(manifold)
1022}
1023
1024// ============================================================================
1025// Speculative Contact Detection (TGS Soft Phase 3)
1026// ============================================================================
1027
1028/// Test collision between two bodies with speculative contact detection.
1029/// If bodies are separated but within the speculative margin, generates a
1030/// contact with negative penetration (representing the separation distance).
1031/// This prevents tunneling for fast-moving objects.
1032pub fn test_collision_manifold_speculative(
1033    a: &RigidBody,
1034    b: &RigidBody,
1035    margin: f32,
1036) -> Option<ContactManifold> {
1037    // First try normal collision detection
1038    if let Some(manifold) = test_collision_manifold(a, b) {
1039        return Some(manifold);
1040    }
1041
1042    // If no collision, check if bodies are close enough for speculative contact
1043    // Use shape-specific separation distance calculation
1044    match (&a.shape, &b.shape) {
1045        (Shape::Circle { .. }, Shape::Circle { .. }) => {
1046            circle_vs_circle_speculative(a, b, margin)
1047        }
1048        (Shape::Circle { .. }, Shape::AABB { .. }) => {
1049            circle_vs_aabb_speculative(a, b, margin, false)
1050        }
1051        (Shape::AABB { .. }, Shape::Circle { .. }) => {
1052            circle_vs_aabb_speculative(b, a, margin, true)
1053        }
1054        (Shape::AABB { .. }, Shape::AABB { .. }) => {
1055            aabb_vs_aabb_speculative(a, b, margin)
1056        }
1057        (Shape::Polygon { .. }, Shape::Polygon { .. }) => {
1058            polygon_vs_polygon_speculative(a, b, margin)
1059        }
1060        (Shape::Circle { .. }, Shape::Polygon { .. }) => {
1061            circle_vs_polygon_speculative(a, b, margin, false)
1062        }
1063        (Shape::Polygon { .. }, Shape::Circle { .. }) => {
1064            circle_vs_polygon_speculative(b, a, margin, true)
1065        }
1066        // AABB vs Polygon: convert AABB and use polygon-polygon
1067        (Shape::AABB { half_w, half_h }, Shape::Polygon { .. }) => {
1068            let aabb_as_poly = RigidBody {
1069                shape: Shape::Polygon {
1070                    vertices: vec![(-half_w, -half_h), (*half_w, -half_h), (*half_w, *half_h), (-half_w, *half_h)],
1071                },
1072                ..a.clone()
1073            };
1074            let mut result = polygon_vs_polygon_speculative(&aabb_as_poly, b, margin)?;
1075            result.body_a = a.id;
1076            Some(result)
1077        }
1078        (Shape::Polygon { .. }, Shape::AABB { half_w, half_h }) => {
1079            let aabb_as_poly = RigidBody {
1080                shape: Shape::Polygon {
1081                    vertices: vec![(-half_w, -half_h), (*half_w, -half_h), (*half_w, *half_h), (-half_w, *half_h)],
1082                },
1083                ..b.clone()
1084            };
1085            let mut result = polygon_vs_polygon_speculative(a, &aabb_as_poly, margin)?;
1086            result.body_b = b.id;
1087            Some(result)
1088        }
1089    }
1090}
1091
1092/// Speculative circle vs circle: compute separation and create contact if within margin.
1093fn circle_vs_circle_speculative(a: &RigidBody, b: &RigidBody, margin: f32) -> Option<ContactManifold> {
1094    let ra = match a.shape {
1095        Shape::Circle { radius } => radius,
1096        _ => return None,
1097    };
1098    let rb = match b.shape {
1099        Shape::Circle { radius } => radius,
1100        _ => return None,
1101    };
1102
1103    let dx = b.x - a.x;
1104    let dy = b.y - a.y;
1105    let dist = (dx * dx + dy * dy).sqrt();
1106    let sum_r = ra + rb;
1107    let separation = dist - sum_r;
1108
1109    // Only create speculative contact if separated but within margin
1110    if separation <= 0.0 || separation > margin {
1111        return None;
1112    }
1113
1114    // Normal from a to b
1115    let (nx, ny) = if dist > 1e-8 {
1116        (dx / dist, dy / dist)
1117    } else {
1118        (1.0, 0.0)
1119    };
1120
1121    // Contact point: midpoint between closest surface points
1122    let cpx = a.x + nx * ra;
1123    let cpy = a.y + ny * ra;
1124
1125    let local_a = world_to_local(a, cpx, cpy);
1126    let local_b = world_to_local(b, cpx, cpy);
1127
1128    // Negative penetration = separation distance
1129    Some(ContactManifold {
1130        body_a: a.id,
1131        body_b: b.id,
1132        normal: (nx, ny),
1133        points: vec![ManifoldPoint::new(local_a, local_b, -separation, ContactID::circle())],
1134        tangent: (-ny, nx),
1135        velocity_bias: 0.0,
1136    })
1137}
1138
1139/// Speculative circle vs AABB.
1140fn circle_vs_aabb_speculative(
1141    circle: &RigidBody,
1142    aabb: &RigidBody,
1143    margin: f32,
1144    swapped: bool,
1145) -> Option<ContactManifold> {
1146    let radius = match circle.shape {
1147        Shape::Circle { radius } => radius,
1148        _ => return None,
1149    };
1150    let (hw, hh) = match aabb.shape {
1151        Shape::AABB { half_w, half_h } => (half_w, half_h),
1152        _ => return None,
1153    };
1154
1155    // Circle center in AABB local space
1156    let local_x = circle.x - aabb.x;
1157    let local_y = circle.y - aabb.y;
1158
1159    // Closest point on AABB to circle center
1160    let closest_x = local_x.clamp(-hw, hw);
1161    let closest_y = local_y.clamp(-hh, hh);
1162
1163    let dx = local_x - closest_x;
1164    let dy = local_y - closest_y;
1165    let dist_sq = dx * dx + dy * dy;
1166    let dist = dist_sq.sqrt();
1167
1168    // Separation = distance from closest point to circle surface
1169    let separation = dist - radius;
1170
1171    // Only speculative if separated but within margin
1172    if separation <= 0.0 || separation > margin {
1173        return None;
1174    }
1175
1176    // Normal from AABB toward circle
1177    let (nx, ny) = if dist > 1e-8 {
1178        (dx / dist, dy / dist)
1179    } else {
1180        (1.0, 0.0)
1181    };
1182
1183    let cpx = aabb.x + closest_x;
1184    let cpy = aabb.y + closest_y;
1185
1186    let (body_a, body_b, fnx, fny) = if swapped {
1187        (aabb, circle, nx, ny)
1188    } else {
1189        (circle, aabb, -nx, -ny)
1190    };
1191
1192    let local_a = world_to_local(body_a, cpx, cpy);
1193    let local_b = world_to_local(body_b, cpx, cpy);
1194
1195    Some(ContactManifold {
1196        body_a: body_a.id,
1197        body_b: body_b.id,
1198        normal: (fnx, fny),
1199        points: vec![ManifoldPoint::new(local_a, local_b, -separation, ContactID::circle())],
1200        tangent: (-fny, fnx),
1201        velocity_bias: 0.0,
1202    })
1203}
1204
1205/// Speculative AABB vs AABB.
1206fn aabb_vs_aabb_speculative(a: &RigidBody, b: &RigidBody, margin: f32) -> Option<ContactManifold> {
1207    let (ahw, ahh) = match a.shape {
1208        Shape::AABB { half_w, half_h } => (half_w, half_h),
1209        _ => return None,
1210    };
1211    let (bhw, bhh) = match b.shape {
1212        Shape::AABB { half_w, half_h } => (half_w, half_h),
1213        _ => return None,
1214    };
1215
1216    let dx = b.x - a.x;
1217    let dy = b.y - a.y;
1218
1219    // Compute separation on each axis
1220    let sep_x = dx.abs() - (ahw + bhw);
1221    let sep_y = dy.abs() - (ahh + bhh);
1222
1223    // Both must be separated, and the smaller separation must be within margin
1224    let separation = sep_x.max(sep_y);
1225
1226    if separation <= 0.0 || separation > margin {
1227        return None;
1228    }
1229
1230    // Choose axis of minimum separation for the contact normal
1231    let (nx, ny, sep) = if sep_x > sep_y {
1232        // X-axis separation
1233        let nx = if dx >= 0.0 { 1.0 } else { -1.0 };
1234        (nx, 0.0, sep_x)
1235    } else {
1236        // Y-axis separation
1237        let ny = if dy >= 0.0 { 1.0 } else { -1.0 };
1238        (0.0, ny, sep_y)
1239    };
1240
1241    // Contact point: surface of A closest to B
1242    let cpx = a.x + nx * ahw;
1243    let cpy = a.y + ny * ahh;
1244
1245    let local_a = world_to_local(a, cpx, cpy);
1246    let local_b = world_to_local(b, cpx, cpy);
1247
1248    Some(ContactManifold {
1249        body_a: a.id,
1250        body_b: b.id,
1251        normal: (nx, ny),
1252        points: vec![ManifoldPoint::new(local_a, local_b, -sep, ContactID::new(0, 0, 0))],
1253        tangent: (-ny, nx),
1254        velocity_bias: 0.0,
1255    })
1256}
1257
1258/// Speculative polygon vs polygon using SAT separation.
1259fn polygon_vs_polygon_speculative(a: &RigidBody, b: &RigidBody, margin: f32) -> Option<ContactManifold> {
1260    let verts_a = get_world_vertices(a);
1261    let verts_b = get_world_vertices(b);
1262
1263    if verts_a.len() < 3 || verts_b.len() < 3 {
1264        return None;
1265    }
1266
1267    // Find minimum separation using SAT
1268    let (sep_a, edge_a) = find_max_separation(&verts_a, &verts_b);
1269    let (sep_b, edge_b) = find_max_separation(&verts_b, &verts_a);
1270
1271    // Take the larger separation (both should be positive for separated polygons)
1272    let separation = sep_a.max(sep_b);
1273
1274    // Only speculative if separated but within margin
1275    if separation <= 0.0 || separation > margin {
1276        return None;
1277    }
1278
1279    // Use the edge with larger separation as reference
1280    let (ref_verts, inc_verts, ref_edge, _ref_body, _inc_body, flip) = if sep_a >= sep_b {
1281        (&verts_a, &verts_b, edge_a, a, b, false)
1282    } else {
1283        (&verts_b, &verts_a, edge_b, b, a, true)
1284    };
1285
1286    // Get reference edge normal
1287    let n = ref_verts.len();
1288    let ref_v0 = ref_verts[ref_edge];
1289    let ref_v1 = ref_verts[(ref_edge + 1) % n];
1290
1291    let ref_ex = ref_v1.0 - ref_v0.0;
1292    let ref_ey = ref_v1.1 - ref_v0.1;
1293    let ref_len = (ref_ex * ref_ex + ref_ey * ref_ey).sqrt();
1294    if ref_len < 1e-8 {
1295        return None;
1296    }
1297    let ref_nx = ref_ey / ref_len;
1298    let ref_ny = -ref_ex / ref_len;
1299
1300    // Find closest vertex on incident polygon
1301    let mut min_proj = f32::MAX;
1302    let mut closest_point = inc_verts[0];
1303    for &v in inc_verts {
1304        let proj = (v.0 - ref_v0.0) * ref_nx + (v.1 - ref_v0.1) * ref_ny;
1305        if proj < min_proj {
1306            min_proj = proj;
1307            closest_point = v;
1308        }
1309    }
1310
1311    // Build normal pointing from A to B
1312    let (final_nx, final_ny) = if flip {
1313        (-ref_nx, -ref_ny)
1314    } else {
1315        (ref_nx, ref_ny)
1316    };
1317
1318    // Ensure normal points from A toward B
1319    let dir_x = b.x - a.x;
1320    let dir_y = b.y - a.y;
1321    let (final_nx, final_ny) = if dir_x * final_nx + dir_y * final_ny < 0.0 {
1322        (-final_nx, -final_ny)
1323    } else {
1324        (final_nx, final_ny)
1325    };
1326
1327    let local_a = world_to_local(a, closest_point.0, closest_point.1);
1328    let local_b = world_to_local(b, closest_point.0, closest_point.1);
1329
1330    Some(ContactManifold {
1331        body_a: a.id,
1332        body_b: b.id,
1333        normal: (final_nx, final_ny),
1334        points: vec![ManifoldPoint::new(local_a, local_b, -separation, ContactID::new(ref_edge as u8, 0, 0))],
1335        tangent: (-final_ny, final_nx),
1336        velocity_bias: 0.0,
1337    })
1338}
1339
1340/// Speculative circle vs polygon.
1341fn circle_vs_polygon_speculative(
1342    circle: &RigidBody,
1343    poly: &RigidBody,
1344    margin: f32,
1345    swapped: bool,
1346) -> Option<ContactManifold> {
1347    let radius = match circle.shape {
1348        Shape::Circle { radius } => radius,
1349        _ => return None,
1350    };
1351    let verts = get_world_vertices(poly);
1352    if verts.len() < 3 {
1353        return None;
1354    }
1355
1356    // Find closest point on polygon to circle center
1357    let mut closest_dist_sq = f32::MAX;
1358    let mut closest_point = (0.0f32, 0.0f32);
1359
1360    let n = verts.len();
1361    for i in 0..n {
1362        let (ax, ay) = verts[i];
1363        let (bx, by) = verts[(i + 1) % n];
1364        let (cx, cy) = closest_point_on_segment(circle.x, circle.y, ax, ay, bx, by);
1365        let dx = circle.x - cx;
1366        let dy = circle.y - cy;
1367        let d2 = dx * dx + dy * dy;
1368        if d2 < closest_dist_sq {
1369            closest_dist_sq = d2;
1370            closest_point = (cx, cy);
1371        }
1372    }
1373
1374    // Check if inside polygon (would be handled by normal collision)
1375    if point_in_polygon(circle.x, circle.y, &verts) {
1376        return None;
1377    }
1378
1379    let dist = closest_dist_sq.sqrt();
1380    let separation = dist - radius;
1381
1382    // Only speculative if separated but within margin
1383    if separation <= 0.0 || separation > margin {
1384        return None;
1385    }
1386
1387    // Normal from polygon toward circle
1388    let dx = circle.x - closest_point.0;
1389    let dy = circle.y - closest_point.1;
1390    let (nx, ny) = if dist > 1e-8 {
1391        (dx / dist, dy / dist)
1392    } else {
1393        (1.0, 0.0)
1394    };
1395
1396    let (body_a, body_b, fnx, fny) = if swapped {
1397        (poly, circle, -nx, -ny)
1398    } else {
1399        (circle, poly, nx, ny)
1400    };
1401
1402    // Ensure normal points from body_a to body_b
1403    let dir_x = body_b.x - body_a.x;
1404    let dir_y = body_b.y - body_a.y;
1405    let (fnx, fny) = if fnx * dir_x + fny * dir_y < 0.0 {
1406        (-fnx, -fny)
1407    } else {
1408        (fnx, fny)
1409    };
1410
1411    let local_a = world_to_local(body_a, closest_point.0, closest_point.1);
1412    let local_b = world_to_local(body_b, closest_point.0, closest_point.1);
1413
1414    Some(ContactManifold {
1415        body_a: body_a.id,
1416        body_b: body_b.id,
1417        normal: (fnx, fny),
1418        points: vec![ManifoldPoint::new(local_a, local_b, -separation, ContactID::circle())],
1419        tangent: (-fny, fnx),
1420        velocity_bias: 0.0,
1421    })
1422}