Skip to main content

arcane_core/physics/
world.rs

1use std::collections::{HashMap, HashSet};
2
3use super::broadphase::SpatialHash;
4use super::constraints::{solve_constraints, solve_constraints_position};
5use super::integrate::integrate;
6use super::broadphase::SPECULATIVE_MARGIN;
7use super::narrowphase::{test_collision, test_collision_manifold_speculative};
8use super::resolve::{
9    initialize_contacts, initialize_manifolds, resolve_contacts_position,
10    resolve_contacts_velocity_iteration, resolve_manifolds_position,
11    resolve_manifolds_velocity_iteration, warm_start_contacts, warm_start_manifolds,
12};
13use super::sleep::update_sleep;
14use super::types::*;
15
16pub struct PhysicsWorld {
17    bodies: Vec<Option<RigidBody>>,
18    free_ids: Vec<BodyId>,
19    next_id: BodyId,
20    constraints: Vec<Constraint>,
21    next_constraint_id: ConstraintId,
22    gravity: (f32, f32),
23    fixed_dt: f32,
24    accumulator: f32,
25    contacts: Vec<Contact>,
26    /// Contact manifolds (TGS Soft): proper 2-point contacts with local anchors
27    manifolds: Vec<ContactManifold>,
28    /// Contacts accumulated across all sub-steps within a frame.
29    /// Game code reads this via get_contacts() to see every collision that
30    /// occurred during the frame, not just the last sub-step's contacts.
31    /// De-duplicated by body pair (first contact per pair wins).
32    frame_contacts: Vec<Contact>,
33    /// Tracks which body pairs already have a contact in frame_contacts.
34    frame_contact_pairs: HashSet<(BodyId, BodyId)>,
35    broadphase: SpatialHash,
36    solver_iterations: usize,
37    /// Warm-start cache: maps (body_a, body_b) → (normal_impulse, friction_impulse)
38    /// Legacy cache for single-contact mode
39    warm_cache: HashMap<(BodyId, BodyId), (f32, f32)>,
40    /// Warm-start cache for manifolds: maps (body_a, body_b, ContactID) → (jn, jt)
41    manifold_warm_cache: HashMap<(BodyId, BodyId, ContactID), (f32, f32)>,
42    /// Whether to use the new manifold-based solver (TGS Soft)
43    use_manifolds: bool,
44}
45
46impl PhysicsWorld {
47    pub fn new(gravity_x: f32, gravity_y: f32) -> Self {
48        Self {
49            bodies: Vec::new(),
50            free_ids: Vec::new(),
51            next_id: 0,
52            constraints: Vec::new(),
53            next_constraint_id: 0,
54            gravity: (gravity_x, gravity_y),
55            fixed_dt: 1.0 / 60.0,
56            accumulator: 0.0,
57            contacts: Vec::new(),
58            manifolds: Vec::new(),
59            frame_contacts: Vec::new(),
60            frame_contact_pairs: HashSet::new(),
61            broadphase: SpatialHash::new(64.0),
62            // Increased from 6 to 10 for better constraint convergence (ropes/chains).
63            // Box2D uses 4 velocity + 2 position with sub-stepping; we use more iterations.
64            solver_iterations: 10,
65            warm_cache: HashMap::new(),
66            manifold_warm_cache: HashMap::new(),
67            // Enable manifold solver (TGS Soft Phase 1)
68            use_manifolds: true,
69        }
70    }
71
72    /// Enable or disable the manifold-based solver (TGS Soft).
73    /// When disabled, falls back to the legacy single-contact solver.
74    pub fn set_use_manifolds(&mut self, use_manifolds: bool) {
75        self.use_manifolds = use_manifolds;
76    }
77
78    /// Fixed-timestep physics step. Accumulates dt and runs sub-steps as needed.
79    /// Uses 4 sub-steps per fixed step (Box2D v3 approach) for improved stack
80    /// stability: sub-stepping is more effective than extra solver iterations.
81    ///
82    /// TGS Soft Phase 4: Narrowphase runs ONCE per frame, sub-steps use
83    /// analytical contact updating (O(contacts) vs O(contacts × geometry)).
84    ///
85    /// Contacts are accumulated across all sub-steps (de-duplicated by body pair)
86    /// so that game code can see every collision via get_contacts().
87    pub fn step(&mut self, dt: f32) {
88        self.accumulator += dt;
89
90        // Clear frame-level contact accumulator at the start of each step call
91        self.frame_contacts.clear();
92        self.frame_contact_pairs.clear();
93
94        while self.accumulator >= self.fixed_dt {
95            if self.use_manifolds {
96                // TGS Soft: narrowphase once per frame, analytical updating in sub-steps
97                self.step_manifolds(self.fixed_dt);
98            } else {
99                // Legacy: full narrowphase per sub-step
100                let sub_dt = self.fixed_dt / 4.0;
101                for _ in 0..4 {
102                    self.sub_step_legacy(sub_dt);
103
104                    // Accumulate contacts from this sub-step (de-duplicate by pair)
105                    for contact in &self.contacts {
106                        let key = (contact.body_a.min(contact.body_b), contact.body_a.max(contact.body_b));
107                        if self.frame_contact_pairs.insert(key) {
108                            self.frame_contacts.push(contact.clone());
109                        }
110                    }
111                }
112            }
113            self.accumulator -= self.fixed_dt;
114        }
115    }
116
117    /// TGS Soft Phase 4: Run narrowphase once per sub-step, but use analytical updating
118    /// for position correction phase. This reduces narrowphase calls from 16x to 4x per frame.
119    fn step_manifolds(&mut self, fixed_dt: f32) {
120        let sub_dt = fixed_dt / 4.0;
121
122        for sub_step in 0..4 {
123            // 1. Integrate
124            for body in self.bodies.iter_mut().flatten() {
125                integrate(body, self.gravity.0, self.gravity.1, sub_dt);
126            }
127
128            // 2. Broadphase with speculative expansion
129            self.broadphase.clear();
130            for body in self.bodies.iter().flatten() {
131                let (min_x, min_y, max_x, max_y) = get_shape_aabb(body);
132                self.broadphase.insert_speculative(
133                    body.id, min_x, min_y, max_x, max_y,
134                    body.vx, body.vy, sub_dt,
135                );
136            }
137            let pairs = self.broadphase.get_pairs();
138
139            // 3. Narrowphase - generate contact manifolds
140            self.manifolds.clear();
141            self.contacts.clear();
142            for (id_a, id_b) in &pairs {
143                let a_idx = *id_a as usize;
144                let b_idx = *id_b as usize;
145
146                let (layer_a, mask_a, sleeping_a) = match &self.bodies[a_idx] {
147                    Some(b) => (b.layer, b.mask, b.sleeping),
148                    None => continue,
149                };
150                let (layer_b, mask_b, sleeping_b) = match &self.bodies[b_idx] {
151                    Some(b) => (b.layer, b.mask, b.sleeping),
152                    None => continue,
153                };
154
155                if (layer_a & mask_b) == 0 || (layer_b & mask_a) == 0 {
156                    continue;
157                }
158
159                if sleeping_a && sleeping_b {
160                    continue;
161                }
162
163                let body_a = self.bodies[a_idx].as_ref().unwrap();
164                let body_b = self.bodies[b_idx].as_ref().unwrap();
165
166                let speculative_margin = SPECULATIVE_MARGIN + (body_a.vx.abs() + body_a.vy.abs() + body_b.vx.abs() + body_b.vy.abs()) * sub_dt;
167                if let Some(manifold) = test_collision_manifold_speculative(body_a, body_b, speculative_margin) {
168                    if !manifold.points.is_empty() {
169                        let point = &manifold.points[0];
170                        let cos_a = body_a.angle.cos();
171                        let sin_a = body_a.angle.sin();
172                        let cpx = point.local_a.0 * cos_a - point.local_a.1 * sin_a + body_a.x;
173                        let cpy = point.local_a.0 * sin_a + point.local_a.1 * cos_a + body_a.y;
174                        self.contacts.push(Contact {
175                            body_a: manifold.body_a,
176                            body_b: manifold.body_b,
177                            normal: manifold.normal,
178                            penetration: point.penetration,
179                            contact_point: (cpx, cpy),
180                            accumulated_jn: 0.0,
181                            accumulated_jt: 0.0,
182                            velocity_bias: 0.0,
183                            tangent: manifold.tangent,
184                        });
185                    }
186                    self.manifolds.push(manifold);
187                }
188            }
189
190            // Accumulate contacts to frame_contacts (first sub-step only to avoid duplicates)
191            if sub_step == 0 {
192                for contact in &self.contacts {
193                    let key = (contact.body_a.min(contact.body_b), contact.body_a.max(contact.body_b));
194                    if self.frame_contact_pairs.insert(key) {
195                        self.frame_contacts.push(contact.clone());
196                    }
197                }
198            }
199
200            // 3b. Sort manifolds bottom-up for stack stability
201            self.manifolds.sort_by(|a, b| {
202                let ay = self.bodies[a.body_a as usize]
203                    .as_ref()
204                    .map_or(0.0f32, |body| body.y)
205                    .max(
206                        self.bodies[a.body_b as usize]
207                            .as_ref()
208                            .map_or(0.0f32, |body| body.y),
209                    );
210                let by = self.bodies[b.body_a as usize]
211                    .as_ref()
212                    .map_or(0.0f32, |body| body.y)
213                    .max(
214                        self.bodies[b.body_b as usize]
215                            .as_ref()
216                            .map_or(0.0f32, |body| body.y),
217                    );
218                by.partial_cmp(&ay).unwrap_or(std::cmp::Ordering::Equal)
219            });
220
221            // 3c. Pre-compute velocity bias
222            let gravity_mag = (self.gravity.0 * self.gravity.0 + self.gravity.1 * self.gravity.1).sqrt();
223            let restitution_threshold = gravity_mag * sub_dt * 1.5;
224            initialize_manifolds(&self.bodies, &mut self.manifolds, restitution_threshold);
225
226            // 3d. Warm start from cache using ContactID
227            for manifold in &mut self.manifolds {
228                let pair_key = (
229                    manifold.body_a.min(manifold.body_b),
230                    manifold.body_a.max(manifold.body_b),
231                );
232                for point in &mut manifold.points {
233                    let key = (pair_key.0, pair_key.1, point.id);
234                    if let Some(&(jn, jt)) = self.manifold_warm_cache.get(&key) {
235                        point.accumulated_jn = jn * 0.95;
236                        point.accumulated_jt = jt * 0.95;
237                    }
238                }
239            }
240            warm_start_manifolds(&mut self.bodies, &self.manifolds);
241
242            // 3e. Reset soft constraint accumulated impulses
243            for constraint in &mut self.constraints {
244                match constraint {
245                    Constraint::Distance { soft: Some(_), accumulated_impulse, .. } => {
246                        *accumulated_impulse = 0.0;
247                    }
248                    Constraint::Revolute { soft: Some(_), accumulated_impulse, .. } => {
249                        *accumulated_impulse = (0.0, 0.0);
250                    }
251                    _ => {}
252                }
253            }
254
255            // 4. Velocity solve
256            for i in 0..self.solver_iterations {
257                let reverse = i % 2 == 1;
258                resolve_manifolds_velocity_iteration(&mut self.bodies, &mut self.manifolds, reverse, sub_dt);
259                solve_constraints(&mut self.bodies, &mut self.constraints, sub_dt);
260            }
261
262            // 4b. Save accumulated impulses to warm cache
263            self.manifold_warm_cache.clear();
264            for manifold in &self.manifolds {
265                let pair_key = (
266                    manifold.body_a.min(manifold.body_b),
267                    manifold.body_a.max(manifold.body_b),
268                );
269                for point in &manifold.points {
270                    let key = (pair_key.0, pair_key.1, point.id);
271                    self.manifold_warm_cache.insert(key, (point.accumulated_jn, point.accumulated_jt));
272                }
273            }
274
275            // 5. Position correction - re-run narrowphase for accurate penetration
276            // Note: We tried analytical updating but it doesn't converge well for
277            // position correction. Keeping narrowphase here maintains correctness.
278            for i in 0..3 {
279                for manifold in &mut self.manifolds {
280                    let a = &self.bodies[manifold.body_a as usize];
281                    let b = &self.bodies[manifold.body_b as usize];
282                    if let (Some(a), Some(b)) = (a, b) {
283                        if let Some(fresh) = test_collision_manifold_speculative(a, b, SPECULATIVE_MARGIN) {
284                            manifold.normal = fresh.normal;
285                            manifold.tangent = fresh.tangent;
286                            for (point, fresh_point) in manifold.points.iter_mut().zip(fresh.points.iter()) {
287                                point.penetration = fresh_point.penetration;
288                                point.local_a = fresh_point.local_a;
289                                point.local_b = fresh_point.local_b;
290                            }
291                            if manifold.points.len() != fresh.points.len() {
292                                manifold.points = fresh.points;
293                            }
294                        } else {
295                            for point in &mut manifold.points {
296                                point.penetration = 0.0;
297                            }
298                        }
299                    }
300                }
301                resolve_manifolds_position(&mut self.bodies, &self.manifolds, i % 2 == 1);
302                solve_constraints_position(&mut self.bodies, &self.constraints);
303            }
304
305            // 6. Post-correction velocity clamping
306            for manifold in &self.manifolds {
307                let a_idx = manifold.body_a as usize;
308                let b_idx = manifold.body_b as usize;
309
310                let still_penetrating = manifold.points.iter().any(|p| p.penetration > 0.01);
311                if !still_penetrating {
312                    continue;
313                }
314
315                let (nx, ny) = manifold.normal;
316
317                if let Some(a) = &mut self.bodies[a_idx] {
318                    if a.body_type == BodyType::Dynamic {
319                        let vn = a.vx * nx + a.vy * ny;
320                        if vn > 0.0 {
321                            a.vx -= vn * nx;
322                            a.vy -= vn * ny;
323                        }
324                    }
325                }
326                if let Some(b) = &mut self.bodies[b_idx] {
327                    if b.body_type == BodyType::Dynamic {
328                        let vn = b.vx * nx + b.vy * ny;
329                        if vn < 0.0 {
330                            b.vx -= vn * nx;
331                            b.vy -= vn * ny;
332                        }
333                    }
334                }
335            }
336        }
337
338        // Sleep update (once per frame)
339        update_sleep(&mut self.bodies, &self.contacts, fixed_dt);
340    }
341
342    /// Analytically update contact penetration from body-local anchors.
343    /// This avoids re-running narrowphase geometry tests during sub-steps.
344    /// O(contacts) vs O(contacts × shape_complexity)
345    ///
346    /// Note: Currently unused - analytical updating during position correction
347    /// didn't converge well in testing. Kept for future solver experiments.
348    #[allow(dead_code)]
349    fn update_contacts_analytically(&mut self) {
350        for manifold in &mut self.manifolds {
351            let a = match &self.bodies[manifold.body_a as usize] {
352                Some(b) => b,
353                None => continue,
354            };
355            let b = match &self.bodies[manifold.body_b as usize] {
356                Some(b) => b,
357                None => continue,
358            };
359
360            for point in &mut manifold.points {
361                // Transform local anchors to world space
362                let cos_a = a.angle.cos();
363                let sin_a = a.angle.sin();
364                let wa_x = point.local_a.0 * cos_a - point.local_a.1 * sin_a + a.x;
365                let wa_y = point.local_a.0 * sin_a + point.local_a.1 * cos_a + a.y;
366
367                let cos_b = b.angle.cos();
368                let sin_b = b.angle.sin();
369                let wb_x = point.local_b.0 * cos_b - point.local_b.1 * sin_b + b.x;
370                let wb_y = point.local_b.0 * sin_b + point.local_b.1 * cos_b + b.y;
371
372                // Compute separation along stored normal
373                // delta = wb - wa, penetration = -dot(delta, normal)
374                let dx = wb_x - wa_x;
375                let dy = wb_y - wa_y;
376                point.penetration = -(dx * manifold.normal.0 + dy * manifold.normal.1);
377            }
378        }
379    }
380
381    /// Legacy sub-step using single-contact solver
382    fn sub_step_legacy(&mut self, dt: f32) {
383        // 1. Integrate
384        for body in self.bodies.iter_mut().flatten() {
385            integrate(body, self.gravity.0, self.gravity.1, dt);
386        }
387
388        // 2. Broadphase
389        self.broadphase.clear();
390        for body in self.bodies.iter().flatten() {
391            let (min_x, min_y, max_x, max_y) = get_shape_aabb(body);
392            self.broadphase.insert(body.id, min_x, min_y, max_x, max_y);
393        }
394        let pairs = self.broadphase.get_pairs();
395
396        // 3. Narrowphase
397        self.contacts.clear();
398        for (id_a, id_b) in pairs {
399            let a_idx = id_a as usize;
400            let b_idx = id_b as usize;
401
402            // Layer/mask filtering
403            let (layer_a, mask_a, sleeping_a) = match &self.bodies[a_idx] {
404                Some(b) => (b.layer, b.mask, b.sleeping),
405                None => continue,
406            };
407            let (layer_b, mask_b, sleeping_b) = match &self.bodies[b_idx] {
408                Some(b) => (b.layer, b.mask, b.sleeping),
409                None => continue,
410            };
411
412            // Both layers must pass each other's masks
413            if (layer_a & mask_b) == 0 || (layer_b & mask_a) == 0 {
414                continue;
415            }
416
417            // Skip if both sleeping
418            if sleeping_a && sleeping_b {
419                continue;
420            }
421
422            // We need to borrow both bodies immutably. Use index-based access.
423            let body_a = self.bodies[a_idx].as_ref().unwrap();
424            let body_b = self.bodies[b_idx].as_ref().unwrap();
425
426            if let Some(contact) = test_collision(body_a, body_b) {
427                self.contacts.push(contact);
428            }
429        }
430
431        // 3b. Sort contacts bottom-up for stack stability.
432        self.contacts.sort_by(|a, b| {
433            let ay = self.bodies[a.body_a as usize]
434                .as_ref()
435                .map_or(0.0f32, |body| body.y)
436                .max(
437                    self.bodies[a.body_b as usize]
438                        .as_ref()
439                        .map_or(0.0f32, |body| body.y),
440                );
441            let by = self.bodies[b.body_a as usize]
442                .as_ref()
443                .map_or(0.0f32, |body| body.y)
444                .max(
445                    self.bodies[b.body_b as usize]
446                        .as_ref()
447                        .map_or(0.0f32, |body| body.y),
448                );
449            by.partial_cmp(&ay).unwrap_or(std::cmp::Ordering::Equal)
450        });
451
452        // 3c. Pre-compute velocity bias (restitution) and tangent direction for each contact.
453        // Must happen BEFORE warm start so bias reflects post-integration velocities.
454        let gravity_mag = (self.gravity.0 * self.gravity.0 + self.gravity.1 * self.gravity.1).sqrt();
455        let restitution_threshold = gravity_mag * dt * 1.5;
456        initialize_contacts(&self.bodies, &mut self.contacts, restitution_threshold);
457
458        // 3d. Warm start: initialize accumulated impulses from previous frame's cache
459        for contact in &mut self.contacts {
460            let key = (contact.body_a.min(contact.body_b), contact.body_a.max(contact.body_b));
461            if let Some(&(jn, jt)) = self.warm_cache.get(&key) {
462                // Scale slightly to avoid overshoot when contacts shift between frames.
463                // Island-based sleeping prevents the cascade that made 0.95 problematic
464                // with per-body sleeping (no body sleeps until the whole stack settles).
465                contact.accumulated_jn = jn * 0.95;
466                contact.accumulated_jt = jt * 0.95;
467            }
468        }
469        warm_start_contacts(&mut self.bodies, &self.contacts);
470
471        // 3c. Reset soft constraint accumulated impulses for this sub-step
472        // (Soft constraints apply force each sub-step based on current state,
473        // not iteratively converge like rigid constraints)
474        for constraint in &mut self.constraints {
475            match constraint {
476                Constraint::Distance { soft: Some(_), accumulated_impulse, .. } => {
477                    *accumulated_impulse = 0.0;
478                }
479                Constraint::Revolute { soft: Some(_), accumulated_impulse, .. } => {
480                    *accumulated_impulse = (0.0, 0.0);
481                }
482                _ => {}
483            }
484        }
485
486        // 4. Velocity solve (contacts + constraints)
487        for i in 0..self.solver_iterations {
488            let reverse = i % 2 == 1;
489            resolve_contacts_velocity_iteration(&mut self.bodies, &mut self.contacts, reverse);
490            solve_constraints(&mut self.bodies, &mut self.constraints, dt);
491        }
492
493        // 4b. Save accumulated impulses to warm cache for next frame
494        self.warm_cache.clear();
495        for contact in &self.contacts {
496            let key = (contact.body_a.min(contact.body_b), contact.body_a.max(contact.body_b));
497            self.warm_cache.insert(key, (contact.accumulated_jn, contact.accumulated_jt));
498        }
499
500        // 5. Position correction (3 iterations per sub-step, matching Box2D v2).
501        // With 4 sub-steps per frame, this gives 12 effective position corrections per
502        // frame. Combined with a Baumgarte factor of 0.2 (Box2D standard) and max
503        // correction clamping, this converges stably for large body stacks.
504        for i in 0..3 {
505            for contact in &mut self.contacts {
506                let a = &self.bodies[contact.body_a as usize];
507                let b = &self.bodies[contact.body_b as usize];
508                if let (Some(a), Some(b)) = (a, b) {
509                    if let Some(fresh) = test_collision(a, b) {
510                        contact.penetration = fresh.penetration;
511                        contact.normal = fresh.normal;
512                        contact.contact_point = fresh.contact_point;
513                    } else {
514                        contact.penetration = 0.0;
515                    }
516                }
517            }
518            resolve_contacts_position(&mut self.bodies, &self.contacts, i % 2 == 1);
519            solve_constraints_position(&mut self.bodies, &self.constraints);
520        }
521
522        // 6. Post-correction velocity clamping: after position correction, if bodies
523        // are still penetrating, zero out any velocity component driving them deeper.
524        // This prevents re-introduction of penetration on the next integration step.
525        //
526        // Contact normal points from body_a toward body_b.
527        // Body A "into contact" = toward B = +normal direction.
528        // Body B "into contact" = toward A = -normal direction.
529        for contact in &self.contacts {
530            let a_idx = contact.body_a as usize;
531            let b_idx = contact.body_b as usize;
532
533            // Re-test to get fresh penetration
534            let still_penetrating = match (&self.bodies[a_idx], &self.bodies[b_idx]) {
535                (Some(a), Some(b)) => test_collision(a, b).map_or(false, |c| c.penetration > 0.01),
536                _ => false,
537            };
538
539            if !still_penetrating {
540                continue;
541            }
542
543            let (nx, ny) = contact.normal;
544
545            // Clamp velocity of body A: remove component toward B (along +normal)
546            if let Some(a) = &mut self.bodies[a_idx] {
547                if a.body_type == BodyType::Dynamic {
548                    let vn = a.vx * nx + a.vy * ny;
549                    if vn > 0.0 {
550                        a.vx -= vn * nx;
551                        a.vy -= vn * ny;
552                    }
553                }
554            }
555            // Clamp velocity of body B: remove component toward A (along -normal)
556            if let Some(b) = &mut self.bodies[b_idx] {
557                if b.body_type == BodyType::Dynamic {
558                    let vn = b.vx * nx + b.vy * ny;
559                    if vn < 0.0 {
560                        b.vx -= vn * nx;
561                        b.vy -= vn * ny;
562                    }
563                }
564            }
565        }
566
567        // 7. Update sleep
568        update_sleep(&mut self.bodies, &self.contacts, dt);
569    }
570
571    pub fn add_body(
572        &mut self,
573        body_type: BodyType,
574        shape: Shape,
575        x: f32,
576        y: f32,
577        mass: f32,
578        material: Material,
579        layer: u16,
580        mask: u16,
581    ) -> BodyId {
582        let id = if let Some(recycled) = self.free_ids.pop() {
583            recycled
584        } else {
585            let id = self.next_id;
586            self.next_id += 1;
587            id
588        };
589
590        let (inv_mass, inertia, inv_inertia) = compute_mass_and_inertia(&shape, mass, body_type);
591
592        let body = RigidBody {
593            id,
594            body_type,
595            shape,
596            material,
597            x,
598            y,
599            angle: 0.0,
600            vx: 0.0,
601            vy: 0.0,
602            angular_velocity: 0.0,
603            fx: 0.0,
604            fy: 0.0,
605            torque: 0.0,
606            mass,
607            inv_mass,
608            inertia,
609            inv_inertia,
610            layer,
611            mask,
612            sleeping: false,
613            sleep_timer: 0.0,
614        };
615
616        let idx = id as usize;
617        if idx >= self.bodies.len() {
618            self.bodies.resize_with(idx + 1, || None);
619        }
620        self.bodies[idx] = Some(body);
621        id
622    }
623
624    pub fn remove_body(&mut self, id: BodyId) {
625        let idx = id as usize;
626        if idx < self.bodies.len() {
627            self.bodies[idx] = None;
628            self.free_ids.push(id);
629        }
630    }
631
632    pub fn get_body(&self, id: BodyId) -> Option<&RigidBody> {
633        self.bodies.get(id as usize)?.as_ref()
634    }
635
636    pub fn get_body_mut(&mut self, id: BodyId) -> Option<&mut RigidBody> {
637        self.bodies.get_mut(id as usize)?.as_mut()
638    }
639
640    pub fn set_velocity(&mut self, id: BodyId, vx: f32, vy: f32) {
641        if let Some(body) = self.get_body_mut(id) {
642            body.vx = vx;
643            body.vy = vy;
644            body.sleeping = false;
645            body.sleep_timer = 0.0;
646        }
647    }
648
649    pub fn set_angular_velocity(&mut self, id: BodyId, av: f32) {
650        if let Some(body) = self.get_body_mut(id) {
651            body.angular_velocity = av;
652            body.sleeping = false;
653            body.sleep_timer = 0.0;
654        }
655    }
656
657    pub fn apply_force(&mut self, id: BodyId, fx: f32, fy: f32) {
658        if let Some(body) = self.get_body_mut(id) {
659            body.fx += fx;
660            body.fy += fy;
661            body.sleeping = false;
662            body.sleep_timer = 0.0;
663        }
664    }
665
666    pub fn apply_impulse(&mut self, id: BodyId, ix: f32, iy: f32) {
667        if let Some(body) = self.get_body_mut(id) {
668            body.vx += ix * body.inv_mass;
669            body.vy += iy * body.inv_mass;
670            body.sleeping = false;
671            body.sleep_timer = 0.0;
672        }
673    }
674
675    pub fn set_position(&mut self, id: BodyId, x: f32, y: f32) {
676        if let Some(body) = self.get_body_mut(id) {
677            body.x = x;
678            body.y = y;
679            body.sleeping = false;
680            body.sleep_timer = 0.0;
681        }
682    }
683
684    pub fn set_collision_layers(&mut self, id: BodyId, layer: u16, mask: u16) {
685        if let Some(body) = self.get_body_mut(id) {
686            body.layer = layer;
687            body.mask = mask;
688        }
689    }
690
691    pub fn add_constraint(&mut self, constraint: Constraint) -> ConstraintId {
692        let id = self.next_constraint_id;
693        self.next_constraint_id += 1;
694
695        let constraint = match constraint {
696            Constraint::Distance {
697                body_a,
698                body_b,
699                distance,
700                anchor_a,
701                anchor_b,
702                soft,
703                ..
704            } => Constraint::Distance {
705                id,
706                body_a,
707                body_b,
708                distance,
709                anchor_a,
710                anchor_b,
711                soft,
712                accumulated_impulse: 0.0,
713            },
714            Constraint::Revolute {
715                body_a,
716                body_b,
717                anchor_a,
718                anchor_b,
719                soft,
720                ..
721            } => Constraint::Revolute {
722                id,
723                body_a,
724                body_b,
725                anchor_a,
726                anchor_b,
727                soft,
728                accumulated_impulse: (0.0, 0.0),
729            },
730        };
731        self.constraints.push(constraint);
732        id
733    }
734
735    pub fn remove_constraint(&mut self, id: ConstraintId) {
736        self.constraints.retain(|c| c.id() != id);
737    }
738
739    pub fn query_aabb(&self, min_x: f32, min_y: f32, max_x: f32, max_y: f32) -> Vec<BodyId> {
740        let mut result = Vec::new();
741        for body in self.bodies.iter().flatten() {
742            let (bmin_x, bmin_y, bmax_x, bmax_y) = get_shape_aabb(body);
743            if bmax_x >= min_x && bmin_x <= max_x && bmax_y >= min_y && bmin_y <= max_y {
744                result.push(body.id);
745            }
746        }
747        result
748    }
749
750    pub fn raycast(
751        &self,
752        ox: f32,
753        oy: f32,
754        dx: f32,
755        dy: f32,
756        max_dist: f32,
757    ) -> Option<(BodyId, f32, f32, f32)> {
758        let dir_len = (dx * dx + dy * dy).sqrt();
759        if dir_len < 1e-8 {
760            return None;
761        }
762        let ndx = dx / dir_len;
763        let ndy = dy / dir_len;
764
765        let mut closest: Option<(BodyId, f32, f32, f32)> = None;
766
767        for body in self.bodies.iter().flatten() {
768            let t = match &body.shape {
769                Shape::Circle { radius } => {
770                    ray_vs_circle(ox, oy, ndx, ndy, body.x, body.y, *radius)
771                }
772                Shape::AABB { half_w, half_h } => {
773                    ray_vs_aabb(ox, oy, ndx, ndy, body.x, body.y, *half_w, *half_h)
774                }
775                Shape::Polygon { vertices } => {
776                    ray_vs_polygon(ox, oy, ndx, ndy, body, vertices)
777                }
778            };
779
780            if let Some(t) = t {
781                if t >= 0.0 && t <= max_dist {
782                    let hit_x = ox + ndx * t;
783                    let hit_y = oy + ndy * t;
784                    if closest.is_none() || t < closest.unwrap().3 {
785                        closest = Some((body.id, hit_x, hit_y, t));
786                    }
787                }
788            }
789        }
790        closest
791    }
792
793    /// Return all contacts that occurred during the last step() call.
794    /// Accumulated across all sub-steps, de-duplicated by body pair.
795    /// This ensures game code sees every collision, even if the bodies
796    /// separated during a later sub-step.
797    pub fn get_contacts(&self) -> &[Contact] {
798        &self.frame_contacts
799    }
800
801    /// Return all contact manifolds from the last sub-step.
802    /// Each manifold contains up to 2 contact points with feature-based IDs.
803    /// Useful for debugging and visualization.
804    pub fn get_manifolds(&self) -> &[ContactManifold] {
805        &self.manifolds
806    }
807
808    /// Return all active (non-None) bodies.
809    pub fn all_bodies(&self) -> Vec<&RigidBody> {
810        self.bodies.iter().filter_map(|b| b.as_ref()).collect()
811    }
812
813    /// Return the world gravity.
814    pub fn gravity(&self) -> (f32, f32) {
815        self.gravity
816    }
817
818    /// Return the number of active bodies.
819    pub fn body_count(&self) -> usize {
820        self.bodies.iter().filter(|b| b.is_some()).count()
821    }
822}
823
824fn ray_vs_circle(
825    ox: f32, oy: f32,
826    dx: f32, dy: f32,
827    cx: f32, cy: f32,
828    radius: f32,
829) -> Option<f32> {
830    let fx = ox - cx;
831    let fy = oy - cy;
832    let a = dx * dx + dy * dy;
833    let b = 2.0 * (fx * dx + fy * dy);
834    let c = fx * fx + fy * fy - radius * radius;
835    let discriminant = b * b - 4.0 * a * c;
836    if discriminant < 0.0 {
837        return None;
838    }
839    let sqrt_d = discriminant.sqrt();
840    let t1 = (-b - sqrt_d) / (2.0 * a);
841    let t2 = (-b + sqrt_d) / (2.0 * a);
842    if t1 >= 0.0 {
843        Some(t1)
844    } else if t2 >= 0.0 {
845        Some(t2)
846    } else {
847        None
848    }
849}
850
851fn ray_vs_aabb(
852    ox: f32, oy: f32,
853    dx: f32, dy: f32,
854    cx: f32, cy: f32,
855    hw: f32, hh: f32,
856) -> Option<f32> {
857    let min_x = cx - hw;
858    let max_x = cx + hw;
859    let min_y = cy - hh;
860    let max_y = cy + hh;
861
862    let (mut tmin, mut tmax) = if dx.abs() < 1e-8 {
863        if ox < min_x || ox > max_x {
864            return None;
865        }
866        (f32::MIN, f32::MAX)
867    } else {
868        let inv_dx = 1.0 / dx;
869        let t1 = (min_x - ox) * inv_dx;
870        let t2 = (max_x - ox) * inv_dx;
871        (t1.min(t2), t1.max(t2))
872    };
873
874    let (tymin, tymax) = if dy.abs() < 1e-8 {
875        if oy < min_y || oy > max_y {
876            return None;
877        }
878        (f32::MIN, f32::MAX)
879    } else {
880        let inv_dy = 1.0 / dy;
881        let t1 = (min_y - oy) * inv_dy;
882        let t2 = (max_y - oy) * inv_dy;
883        (t1.min(t2), t1.max(t2))
884    };
885
886    tmin = tmin.max(tymin);
887    tmax = tmax.min(tymax);
888
889    if tmin > tmax || tmax < 0.0 {
890        return None;
891    }
892
893    Some(if tmin >= 0.0 { tmin } else { tmax })
894}
895
896fn ray_vs_polygon(
897    ox: f32, oy: f32,
898    dx: f32, dy: f32,
899    body: &RigidBody,
900    vertices: &[(f32, f32)],
901) -> Option<f32> {
902    let cos = body.angle.cos();
903    let sin = body.angle.sin();
904    let n = vertices.len();
905    if n < 3 {
906        return None;
907    }
908
909    let mut closest_t: Option<f32> = None;
910
911    for i in 0..n {
912        let (vx0, vy0) = vertices[i];
913        let (vx1, vy1) = vertices[(i + 1) % n];
914
915        // Transform to world space
916        let ax = vx0 * cos - vy0 * sin + body.x;
917        let ay = vx0 * sin + vy0 * cos + body.y;
918        let bx = vx1 * cos - vy1 * sin + body.x;
919        let by = vx1 * sin + vy1 * cos + body.y;
920
921        if let Some(t) = ray_vs_segment(ox, oy, dx, dy, ax, ay, bx, by) {
922            if closest_t.is_none() || t < closest_t.unwrap() {
923                closest_t = Some(t);
924            }
925        }
926    }
927    closest_t
928}
929
930fn ray_vs_segment(
931    ox: f32, oy: f32,
932    dx: f32, dy: f32,
933    ax: f32, ay: f32,
934    bx: f32, by: f32,
935) -> Option<f32> {
936    let ex = bx - ax;
937    let ey = by - ay;
938    let denom = dx * ey - dy * ex;
939    if denom.abs() < 1e-8 {
940        return None;
941    }
942    let t = ((ax - ox) * ey - (ay - oy) * ex) / denom;
943    let u = ((ax - ox) * dy - (ay - oy) * dx) / denom;
944    if t >= 0.0 && u >= 0.0 && u <= 1.0 {
945        Some(t)
946    } else {
947        None
948    }
949}