Skip to main content

alice_physics/
event.rs

1//! Physics Event System
2//!
3//! Provides collision event reporting (begin/persist/end), trigger events and
4//! joint break events.
5//! Events are collected during `step()` and can be consumed after each frame.
6
7use crate::math::{Fix128, Vec3Fix};
8
9#[cfg(not(feature = "std"))]
10use alloc::collections::{BTreeMap, BTreeSet};
11#[cfg(not(feature = "std"))]
12use alloc::vec::Vec;
13#[cfg(feature = "std")]
14use std::collections::{BTreeMap, BTreeSet};
15
16/// Type of contact event
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum ContactEventType {
19    /// First frame of contact
20    Begin,
21    /// Contact persists from previous frame
22    Persist,
23    /// Contact ended (bodies separated)
24    End,
25}
26
27/// A contact event between two bodies
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub struct ContactEvent {
30    /// First body index
31    pub body_a: usize,
32    /// Second body index
33    pub body_b: usize,
34    /// Event type
35    pub event_type: ContactEventType,
36    /// Contact normal, unit length, pointing from `body_b` toward `body_a`
37    /// (the crate-wide [`Contact::normal`](crate::collider::Contact::normal)
38    /// contract): moving `body_a` along `+normal` separates the pair.
39    /// Zero for [`ContactEventType::End`].
40    pub normal: Vec3Fix,
41    /// Contact point (world space)
42    pub point: Vec3Fix,
43    /// Penetration depth
44    pub depth: Fix128,
45    /// Relative velocity along the normal, `(v_a − v_b) · normal`: negative
46    /// while the bodies approach each other
47    pub relative_velocity: Fix128,
48}
49
50/// A trigger event (overlap without physics response)
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub struct TriggerEvent {
53    /// Trigger body index
54    pub trigger_body: usize,
55    /// Other body index
56    pub other_body: usize,
57    /// Whether this is an enter or exit event
58    pub entered: bool,
59}
60
61/// A joint removed by [`crate::solver::PhysicsWorld::step`] because its
62/// reaction force exceeded its `break_force`
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub struct JointBreakEvent {
65    /// Index the joint had in `PhysicsWorld::joints` when it broke. Removal is
66    /// a swap-remove (as [`crate::solver::PhysicsWorld::remove_joint`]), so the
67    /// joint that was last now has this index.
68    pub index: usize,
69    /// The removed joint, as it was configured
70    pub joint: crate::joint::Joint,
71    /// The reaction force its last solve transmitted
72    /// ([`crate::joint::solve_joints_with_reaction_forces`], N),
73    /// strictly greater than its `break_force`
74    pub force: Fix128,
75}
76
77/// Manages physics events for one simulation step
78pub struct EventCollector {
79    /// Contact events this frame
80    pub(crate) contact_events: Vec<ContactEvent>,
81    /// Trigger events this frame
82    pub(crate) trigger_events: Vec<TriggerEvent>,
83    /// Joints broken this frame
84    pub(crate) joint_break_events: Vec<JointBreakEvent>,
85    /// Active contact pairs from previous frame (for begin/persist/end tracking)
86    pub(crate) prev_pairs: Vec<(usize, usize)>,
87    /// Active contact pairs this frame (ordered set: membership tests are
88    /// O(log n); the linear `Vec::contains` made `report_contact` O(pairs²) —
89    /// 3 ms per detection at 2700 contacts, run 8× per frame since 1.2.0)
90    /// Active contact pairs this frame
91    pub(crate) curr_pairs: BTreeSet<(usize, usize)>,
92    /// Active trigger overlaps from previous frame: (normalized pair, (trigger, other) roles of the enter frame)
93    pub(crate) prev_triggers: Vec<((usize, usize), (usize, usize))>,
94    /// Active trigger overlaps this frame (normalized pair -> roles as reported)
95    pub(crate) curr_triggers: BTreeMap<(usize, usize), (usize, usize)>,
96}
97
98impl EventCollector {
99    /// Create a new event collector
100    #[must_use]
101    pub const fn new() -> Self {
102        Self {
103            contact_events: Vec::new(),
104            trigger_events: Vec::new(),
105            joint_break_events: Vec::new(),
106            prev_pairs: Vec::new(),
107            curr_pairs: BTreeSet::new(),
108            prev_triggers: Vec::new(),
109            curr_triggers: BTreeMap::new(),
110        }
111    }
112
113    /// Begin a new frame: swap previous/current pair tracking
114    pub fn begin_frame(&mut self) {
115        self.contact_events.clear();
116        self.trigger_events.clear();
117        self.joint_break_events.clear();
118        // BTreeSet iterates in sorted order, so `prev_*` are sorted for
119        // `binary_search` without an explicit sort.
120        self.prev_pairs.clear();
121        self.prev_pairs.extend(self.curr_pairs.iter().copied());
122        self.curr_pairs.clear();
123        self.prev_triggers.clear();
124        self.prev_triggers
125            .extend(self.curr_triggers.iter().map(|(k, v)| (*k, *v)));
126        self.curr_triggers.clear();
127    }
128
129    /// Report a contact between two bodies
130    ///
131    /// `normal` points from `body_b` toward `body_a` as given here. The event
132    /// stores the pair as `(min, max)`; when that swaps the bodies the normal
133    /// is negated so it still points from the event's `body_b` toward its
134    /// `body_a`. `relative_velocity` (`(v_a − v_b) · normal`) and `depth` are
135    /// unchanged by the swap.
136    pub fn report_contact(
137        &mut self,
138        body_a: usize,
139        body_b: usize,
140        normal: Vec3Fix,
141        point: Vec3Fix,
142        depth: Fix128,
143        relative_velocity: Fix128,
144    ) {
145        let pair = normalize_pair(body_a, body_b);
146        // B→A of the stored pair: negate when the pair was swapped.
147        let normal = if pair.0 == body_a { normal } else { -normal };
148        let was_active = self.prev_pairs.binary_search(&pair).is_ok();
149        // Collision detection runs once per substep since 1.2.0; a pair is
150        // reported once per frame (first substep that sees it).
151        if !self.curr_pairs.insert(pair) {
152            return;
153        }
154
155        let event_type = if was_active {
156            ContactEventType::Persist
157        } else {
158            ContactEventType::Begin
159        };
160
161        self.contact_events.push(ContactEvent {
162            body_a: pair.0,
163            body_b: pair.1,
164            event_type,
165            normal,
166            point,
167            depth,
168            relative_velocity,
169        });
170    }
171
172    /// Report a trigger overlap
173    pub fn report_trigger(&mut self, trigger_body: usize, other_body: usize) {
174        let pair = normalize_pair(trigger_body, other_body);
175        // One report per pair per frame (detection runs once per substep since
176        // 1.2.0); same rule as `report_contact`.
177        if self.curr_triggers.contains_key(&pair) {
178            return;
179        }
180
181        let prev = self
182            .prev_triggers
183            .binary_search_by_key(&pair, |&(k, _)| k)
184            .ok()
185            .map(|i| self.prev_triggers[i].1);
186        // Roles stay those of the enter frame while the overlap lasts, so the
187        // exit event names the same trigger body as the enter event even if a
188        // later frame reports the pair in the other order.
189        self.curr_triggers
190            .insert(pair, prev.unwrap_or((trigger_body, other_body)));
191
192        if prev.is_none() {
193            self.trigger_events.push(TriggerEvent {
194                trigger_body,
195                other_body,
196                entered: true,
197            });
198        }
199    }
200
201    /// Finalize frame: generate End events for contacts/triggers that stopped
202    pub fn end_frame(&mut self) {
203        // Contact end events
204        for &pair in &self.prev_pairs {
205            if !self.curr_pairs.contains(&pair) {
206                self.contact_events.push(ContactEvent {
207                    body_a: pair.0,
208                    body_b: pair.1,
209                    event_type: ContactEventType::End,
210                    normal: Vec3Fix::ZERO,
211                    point: Vec3Fix::ZERO,
212                    depth: Fix128::ZERO,
213                    relative_velocity: Fix128::ZERO,
214                });
215            }
216        }
217
218        // Trigger exit events
219        for &(pair, (trigger_body, other_body)) in &self.prev_triggers {
220            if !self.curr_triggers.contains_key(&pair) {
221                // Roles as given in the frame that reported the overlap, so
222                // the exit event names the same trigger body as the enter event.
223                self.trigger_events.push(TriggerEvent {
224                    trigger_body,
225                    other_body,
226                    entered: false,
227                });
228            }
229        }
230    }
231
232    /// Get all contact events for this frame
233    #[inline]
234    #[must_use]
235    pub fn contact_events(&self) -> &[ContactEvent] {
236        &self.contact_events
237    }
238
239    /// Get all trigger events for this frame
240    #[inline]
241    #[must_use]
242    pub fn trigger_events(&self) -> &[TriggerEvent] {
243        &self.trigger_events
244    }
245
246    /// Get the joints broken this frame, in the order they were removed
247    /// (substep by substep; within one substep from the highest index down)
248    #[inline]
249    #[must_use]
250    pub fn joint_break_events(&self) -> &[JointBreakEvent] {
251        &self.joint_break_events
252    }
253
254    /// Record a joint broken this frame
255    pub(crate) fn report_joint_break(&mut self, event: JointBreakEvent) {
256        self.joint_break_events.push(event);
257    }
258
259    /// Drain joint break events (consumes them)
260    #[inline]
261    pub fn drain_joint_break_events(&mut self) -> Vec<JointBreakEvent> {
262        core::mem::take(&mut self.joint_break_events)
263    }
264
265    /// Drain contact events (consumes them)
266    #[inline]
267    pub fn drain_contact_events(&mut self) -> Vec<ContactEvent> {
268        core::mem::take(&mut self.contact_events)
269    }
270
271    /// Drain trigger events (consumes them)
272    #[inline]
273    pub fn drain_trigger_events(&mut self) -> Vec<TriggerEvent> {
274        core::mem::take(&mut self.trigger_events)
275    }
276
277    /// Check if there are any events this frame
278    #[inline]
279    #[must_use]
280    pub fn has_events(&self) -> bool {
281        !self.contact_events.is_empty()
282            || !self.trigger_events.is_empty()
283            || !self.joint_break_events.is_empty()
284    }
285}
286
287impl Default for EventCollector {
288    fn default() -> Self {
289        Self::new()
290    }
291}
292
293/// Normalize a body pair so that the smaller index is first (deterministic ordering)
294#[inline]
295const fn normalize_pair(a: usize, b: usize) -> (usize, usize) {
296    if a <= b {
297        (a, b)
298    } else {
299        (b, a)
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    /// A 2 kg body hung from a static anchor under g = 8 transmits exactly
308    /// m·g = 16 N on the first solve (substeps 1, damping 1), so a joint rated
309    /// 15 N breaks through `PhysicsWorld::step` and the world's collector
310    /// reports it once, with that force
311    #[test]
312    fn step_reports_a_broken_joint_with_its_reaction_force() {
313        use crate::joint::{BallJoint, Joint};
314        use crate::solver::{PhysicsConfig, PhysicsWorld, RigidBody};
315        let config = PhysicsConfig {
316            substeps: 1,
317            gravity: Vec3Fix::new(Fix128::ZERO, Fix128::from_int(-8), Fix128::ZERO),
318            damping: Fix128::ONE,
319            ..PhysicsConfig::default()
320        };
321        let mut w = PhysicsWorld::new(config);
322        w.add_body(RigidBody::new_static(Vec3Fix::ZERO));
323        w.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::from_int(2)));
324        let joint = Joint::Ball(
325            BallJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO)
326                .with_break_force(Fix128::from_int(15)),
327        );
328        w.add_joint(joint);
329        w.step(Fix128::from_ratio(1, 64));
330        assert!(w.joints.is_empty());
331        assert!(w.events.has_events());
332        assert_eq!(
333            w.events.joint_break_events(),
334            &[JointBreakEvent {
335                index: 0,
336                joint,
337                force: Fix128::from_int(16)
338            }]
339        );
340        // the next frame starts empty: nothing is left to break
341        w.step(Fix128::from_ratio(1, 64));
342        assert!(w.events.joint_break_events().is_empty());
343    }
344
345    /// The collector keeps joint breaks for one frame: reported events are
346    /// readable and drainable, `begin_frame` clears them, and they alone make
347    /// `has_events` true
348    #[test]
349    fn joint_break_events_live_for_one_frame() {
350        use crate::joint::{BallJoint, Joint};
351        let event = JointBreakEvent {
352            index: 3,
353            joint: Joint::Ball(BallJoint::new(1, 2, Vec3Fix::ZERO, Vec3Fix::UNIT_Y)),
354            force: Fix128::from_int(7),
355        };
356        let mut events = EventCollector::default();
357        events.begin_frame();
358        assert!(!events.has_events());
359        events.report_joint_break(event);
360        events.report_joint_break(JointBreakEvent { index: 1, ..event });
361        assert!(events.has_events(), "a joint break alone is an event");
362        assert_eq!(events.joint_break_events().len(), 2);
363        assert_eq!(events.joint_break_events()[1].index, 1);
364        assert_eq!(
365            events.drain_joint_break_events(),
366            vec![event, JointBreakEvent { index: 1, ..event }]
367        );
368        assert!(events.joint_break_events().is_empty() && !events.has_events());
369        events.report_joint_break(event);
370        events.begin_frame();
371        assert!(
372            events.joint_break_events().is_empty(),
373            "begin_frame clears them"
374        );
375    }
376
377    #[test]
378    fn test_contact_begin() {
379        let mut events = EventCollector::new();
380        events.begin_frame();
381        events.report_contact(
382            0,
383            1,
384            Vec3Fix::UNIT_Y,
385            Vec3Fix::ZERO,
386            Fix128::ONE,
387            Fix128::ZERO,
388        );
389        events.end_frame();
390
391        assert_eq!(events.contact_events().len(), 1);
392        assert_eq!(
393            events.contact_events()[0].event_type,
394            ContactEventType::Begin
395        );
396    }
397
398    #[test]
399    fn test_contact_persist() {
400        let mut events = EventCollector::new();
401
402        // Frame 1: begin
403        events.begin_frame();
404        events.report_contact(
405            0,
406            1,
407            Vec3Fix::UNIT_Y,
408            Vec3Fix::ZERO,
409            Fix128::ONE,
410            Fix128::ZERO,
411        );
412        events.end_frame();
413
414        // Frame 2: persist
415        events.begin_frame();
416        events.report_contact(
417            0,
418            1,
419            Vec3Fix::UNIT_Y,
420            Vec3Fix::ZERO,
421            Fix128::ONE,
422            Fix128::ZERO,
423        );
424        events.end_frame();
425
426        assert_eq!(
427            events.contact_events()[0].event_type,
428            ContactEventType::Persist
429        );
430    }
431
432    #[test]
433    fn test_contact_end() {
434        let mut events = EventCollector::new();
435
436        // Frame 1: begin
437        events.begin_frame();
438        events.report_contact(
439            0,
440            1,
441            Vec3Fix::UNIT_Y,
442            Vec3Fix::ZERO,
443            Fix128::ONE,
444            Fix128::ZERO,
445        );
446        events.end_frame();
447
448        // Frame 2: no contact => end event
449        events.begin_frame();
450        events.end_frame();
451
452        let end_events: Vec<_> = events
453            .contact_events()
454            .iter()
455            .filter(|e| e.event_type == ContactEventType::End)
456            .collect();
457        assert_eq!(end_events.len(), 1);
458        assert_eq!(end_events[0].body_a, 0);
459        assert_eq!(end_events[0].body_b, 1);
460    }
461
462    #[test]
463    fn test_trigger_enter_exit() {
464        let mut events = EventCollector::new();
465
466        // Frame 1: trigger enter
467        events.begin_frame();
468        events.report_trigger(0, 1);
469        events.end_frame();
470
471        assert_eq!(events.trigger_events().len(), 1);
472        assert!(events.trigger_events()[0].entered);
473
474        // Frame 2: still overlapping - no new event
475        events.begin_frame();
476        events.report_trigger(0, 1);
477        events.end_frame();
478
479        // Only trigger exit events would be new; no enter since it persists
480        let enters = events.trigger_events().iter().filter(|e| e.entered).count();
481        assert_eq!(enters, 0);
482
483        // Frame 3: trigger exit
484        events.begin_frame();
485        events.end_frame();
486
487        let exits = events
488            .trigger_events()
489            .iter()
490            .filter(|e| !e.entered)
491            .count();
492        assert_eq!(exits, 1);
493    }
494
495    #[test]
496    fn test_pair_normalization() {
497        let mut events = EventCollector::new();
498        events.begin_frame();
499        // Report in both orders
500        events.report_contact(
501            3,
502            1,
503            Vec3Fix::UNIT_Y,
504            Vec3Fix::ZERO,
505            Fix128::ONE,
506            Fix128::ZERO,
507        );
508        events.end_frame();
509
510        // Should be normalized to (1, 3)
511        assert_eq!(events.contact_events()[0].body_a, 1);
512        assert_eq!(events.contact_events()[0].body_b, 3);
513    }
514
515    #[test]
516    fn has_events_tracks_contact_and_trigger_queues_across_frame_lifecycle() {
517        let mut events = EventCollector::new();
518        assert!(!events.has_events());
519        events.begin_frame();
520        assert!(!events.has_events());
521
522        // contact だけ → true、drain で空になれば false
523        events.report_contact(
524            0,
525            1,
526            Vec3Fix::UNIT_Y,
527            Vec3Fix::ZERO,
528            Fix128::ONE,
529            Fix128::ZERO,
530        );
531        assert!(events.has_events());
532        assert_eq!(events.drain_contact_events().len(), 1);
533        assert!(!events.has_events());
534
535        // trigger だけ → true (contact 側が空でも)
536        events.report_trigger(5, 6);
537        assert!(events.has_events());
538        assert!(events.contact_events().is_empty());
539        assert_eq!(events.drain_trigger_events().len(), 1);
540        assert!(!events.has_events());
541
542        // 同じ pair の再報告は event を生まない (curr_pairs 重複) → false のまま
543        events.report_contact(
544            1,
545            0,
546            Vec3Fix::UNIT_Y,
547            Vec3Fix::ZERO,
548            Fix128::ONE,
549            Fix128::ZERO,
550        );
551        assert!(!events.has_events());
552        events.end_frame();
553        assert!(!events.has_events(), "全 pair 継続中なので End/exit なし");
554
555        // 次 frame: 何も報告せず end_frame → End 1 + exit 1 が生成され true
556        events.begin_frame();
557        assert!(!events.has_events(), "begin_frame は queue を空にする");
558        events.end_frame();
559        assert!(events.has_events());
560        assert_eq!(events.contact_events().len(), 1);
561        assert_eq!(events.contact_events()[0].event_type, ContactEventType::End);
562        assert_eq!(events.trigger_events().len(), 1);
563        assert!(!events.trigger_events()[0].entered);
564
565        // 更に次 frame: 完全に静か → false
566        events.begin_frame();
567        events.end_frame();
568        assert!(!events.has_events());
569    }
570}