Skip to main content

rapier2d/geometry/narrow_phase/
pair_management.rs

1//! Contact/intersection pair lifecycle: collider removal, user-change handling,
2//! pair insertion/removal in the interaction graphs (with persistent
3//! solver-structure mirroring), and broad-phase event registration.
4
5use super::{
6    ColliderGraphIndices, NarrowPhase, PairRemovalMode, assign_pair_solver_color,
7    clear_pair_solver_color,
8};
9use crate::alloc_prelude::*;
10use crate::dynamics::solver::solver_contact_graph::GraphPos;
11use crate::dynamics::{IslandManager, RigidBodySet};
12use crate::geometry::{
13    BroadPhasePairEvent, ColliderChanges, ColliderGraphIndex, ColliderHandle, ColliderPair,
14    ColliderSet, CollisionEvent, ContactManifoldData, ContactPair, InteractionGraph,
15    IntersectionPair, PairEventStatus,
16};
17use crate::pipeline::{ActiveEvents, EventHandler};
18use crate::prelude::CollisionEventFlags;
19use parry::utils::hashmap::HashMap;
20
21impl NarrowPhase {
22    /// Maintain the narrow-phase internal state by taking collider removal into account.
23    #[profiling::function]
24    pub fn handle_user_changes(
25        &mut self,
26        mut islands: Option<&mut IslandManager>,
27        modified_colliders: &[ColliderHandle],
28        removed_colliders: &[ColliderHandle],
29        colliders: &mut ColliderSet,
30        bodies: &mut RigidBodySet,
31        events: &dyn EventHandler,
32    ) {
33        // TODO: avoid these hash-maps.
34        // They are necessary to handle the swap-remove done internally
35        // by the contact/intersection graphs when a node is removed.
36        let mut prox_id_remap = HashMap::default();
37        let mut contact_id_remap = HashMap::default();
38
39        for collider in removed_colliders {
40            // NOTE: if the collider does not have any graph indices currently, there is nothing
41            // to remove in the narrow-phase for this collider.
42            if let Some(graph_idx) = self
43                .graph_indices
44                .remove(collider.0, ColliderGraphIndices::invalid())
45            {
46                let intersection_graph_id = prox_id_remap
47                    .get(collider)
48                    .copied()
49                    .unwrap_or(graph_idx.intersection_graph_index);
50                let contact_graph_id = contact_id_remap
51                    .get(collider)
52                    .copied()
53                    .unwrap_or(graph_idx.contact_graph_index);
54
55                self.remove_collider(
56                    intersection_graph_id,
57                    contact_graph_id,
58                    islands.as_deref_mut(),
59                    colliders,
60                    bodies,
61                    &mut prox_id_remap,
62                    &mut contact_id_remap,
63                    events,
64                );
65            }
66        }
67
68        self.handle_user_changes_on_colliders(
69            islands,
70            modified_colliders,
71            colliders,
72            bodies,
73            events,
74        );
75    }
76
77    #[profiling::function]
78    pub(crate) fn remove_collider(
79        &mut self,
80        intersection_graph_id: ColliderGraphIndex,
81        contact_graph_id: ColliderGraphIndex,
82        mut islands: Option<&mut IslandManager>,
83        colliders: &mut ColliderSet,
84        bodies: &mut RigidBodySet,
85        prox_id_remap: &mut HashMap<ColliderHandle, ColliderGraphIndex>,
86        contact_id_remap: &mut HashMap<ColliderHandle, ColliderGraphIndex>,
87        events: &dyn EventHandler,
88    ) {
89        // Wake up every body in contact with the deleted collider and generate Stopped collision events.
90        if let Some(islands) = islands.as_deref_mut() {
91            for (a, b, pair) in self.contact_graph.interactions_with(contact_graph_id) {
92                if let Some(parent) = colliders.get(a).and_then(|c| c.parent.as_ref()) {
93                    islands.wake_up(bodies, parent.handle, true)
94                }
95
96                if let Some(parent) = colliders.get(b).and_then(|c| c.parent.as_ref()) {
97                    islands.wake_up(bodies, parent.handle, true)
98                }
99
100                if pair
101                    .event_status
102                    .contains(PairEventStatus::START_EVENT_EMITTED)
103                {
104                    events.handle_collision_event(
105                        bodies,
106                        colliders,
107                        CollisionEvent::Stopped(a, b, CollisionEventFlags::REMOVED),
108                        Some(pair),
109                    );
110                }
111            }
112        } else {
113            // If there is no island, don’t wake-up bodies, but do send the Stopped collision event.
114            for (a, b, pair) in self.contact_graph.interactions_with(contact_graph_id) {
115                if pair
116                    .event_status
117                    .contains(PairEventStatus::START_EVENT_EMITTED)
118                {
119                    events.handle_collision_event(
120                        bodies,
121                        colliders,
122                        CollisionEvent::Stopped(a, b, CollisionEventFlags::REMOVED),
123                        Some(pair),
124                    );
125                }
126            }
127        }
128
129        // Generate Stopped collision events for intersections.
130        for (a, b, pair) in self
131            .intersection_graph
132            .interactions_with(intersection_graph_id)
133        {
134            if pair.start_event_emitted {
135                events.handle_collision_event(
136                    bodies,
137                    colliders,
138                    CollisionEvent::Stopped(
139                        a,
140                        b,
141                        CollisionEventFlags::REMOVED | CollisionEventFlags::SENSOR,
142                    ),
143                    None,
144                );
145            }
146        }
147
148        // We have to manage the fact that one other collider will
149        // have its graph index changed because of the node's swap-remove.
150        if let Some(replacement) = self.intersection_graph.remove_node(intersection_graph_id) {
151            if let Some(replacement) = self.graph_indices.get_mut(replacement.0) {
152                replacement.intersection_graph_index = intersection_graph_id;
153            } else {
154                prox_id_remap.insert(replacement, intersection_graph_id);
155                // I feel like this should never happen now that the narrow-phase is the one owning
156                // the graph_indices. Let's put an unreachable in there and see if anybody still manages
157                // to reach it. If nobody does, we will remove this.
158                unreachable!();
159            }
160        }
161
162        // Node removal swap-removes contact-graph edges, shifting the edge indices the
163        // solver graph's `ContactRef`s and the force-event list are keyed by (neither is
164        // mirrored through node removal), so both must be rebuilt from scratch next step.
165        self.solver_graph_valid = false;
166        self.force_list_valid = false;
167        let pair_solver_hints = &mut self.pair_solver_hints;
168        // Persistent islands can't be rebuilt lazily like the tables above:
169        // unlink each removed edge and mirror each swap-remove exactly.
170        let mut edges_len = self.contact_graph.graph.edges.len();
171        let mut pi = islands.map(|i| &mut i.persistent);
172        if let Some(replacement) = self
173            .contact_graph
174            .remove_node_with(contact_graph_id, &mut |e| {
175                // Mirror the edges vec's swap_remove on the solver hints (on length
176                // mismatch — deserialized state — drop them; they rebuild lazily).
177                if e.index() < pair_solver_hints.len() {
178                    pair_solver_hints.swap_remove(e.index());
179                } else {
180                    pair_solver_hints.clear();
181                }
182
183                edges_len -= 1;
184                if let Some(pi) = pi.as_deref_mut() {
185                    pi.unlink_contact(e.index() as u32);
186                    pi.contact_edge_removed(e.index() as u32, edges_len as u32);
187                }
188            })
189        {
190            if let Some(replacement) = self.graph_indices.get_mut(replacement.0) {
191                replacement.contact_graph_index = contact_graph_id;
192            } else {
193                contact_id_remap.insert(replacement, contact_graph_id);
194                // I feel like this should never happen now that the narrow-phase is the one owning
195                // the graph_indices. Let's put an unreachable in there and see if anybody still manages
196                // to reach it. If nobody does, we will remove this.
197                unreachable!();
198            }
199        }
200    }
201
202    #[profiling::function]
203    pub(crate) fn handle_user_changes_on_colliders(
204        &mut self,
205        mut islands: Option<&mut IslandManager>,
206        modified_colliders: &[ColliderHandle],
207        colliders: &ColliderSet,
208        bodies: &mut RigidBodySet,
209        events: &dyn EventHandler,
210    ) {
211        let mut pairs_to_remove = vec![];
212
213        for handle in modified_colliders {
214            // NOTE: we use `get` because the collider may no longer
215            //       exist if it has been removed.
216            if let Some(co) = colliders.get(*handle) {
217                // Any user modification can flip the contact-force-event config
218                // (`ActiveEvents`/threshold have no change flag), so flag its pairs for
219                // force-event membership reconciliation at the next graph maintenance.
220                if let Some(gid) = self.graph_indices.get(handle.0) {
221                    if InteractionGraph::<ColliderHandle, ContactPair>::is_graph_index_valid(
222                        gid.contact_graph_index,
223                    ) {
224                        for edge in self.contact_graph.graph.edges(gid.contact_graph_index) {
225                            self.force_event_flagged.push(edge.id().index() as u32);
226                        }
227                    }
228                }
229
230                if !co.changes.needs_narrow_phase_update() {
231                    // No flag relevant to the narrow-phase is enabled for this collider.
232                    continue;
233                }
234
235                if let Some(gid) = self.graph_indices.get(handle.0) {
236                    // For each modified colliders, we need to wake-up the bodies it is in contact with
237                    // so that the narrow-phase properly takes into account the change in, e.g.,
238                    // collision groups. Waking up the modified collider's parent isn't enough because
239                    // it could be a fixed or kinematic body which don't propagate the wake-up state.
240                    if let Some(islands) = islands.as_deref_mut() {
241                        if let Some(co_parent) = &co.parent {
242                            islands.wake_up(bodies, co_parent.handle, true);
243                        }
244
245                        for inter in self
246                            .contact_graph
247                            .interactions_with(gid.contact_graph_index)
248                        {
249                            let other_handle = if *handle == inter.0 { inter.1 } else { inter.0 };
250                            let other_parent = colliders
251                                .get(other_handle)
252                                .and_then(|co| co.parent.as_ref());
253
254                            if let Some(other_parent) = other_parent {
255                                islands.wake_up(bodies, other_parent.handle, true);
256                            }
257                        }
258                    }
259
260                    // A parent or effective-dominance change (re-parenting, body type
261                    // change) invalidates the solver graph colors of this collider's
262                    // pairs: release and re-assign them with the current bodies.
263                    if co.changes.intersects(
264                        ColliderChanges::PARENT | ColliderChanges::PARENT_EFFECTIVE_DOMINANCE,
265                    ) {
266                        let mut edges_to_recolor = alloc::vec::Vec::new();
267                        for edge in self.contact_graph.graph.edges(gid.contact_graph_index) {
268                            edges_to_recolor.push(edge.id().index());
269                        }
270
271                        for edge_id in edges_to_recolor {
272                            let pair = &mut self.contact_graph.graph.edges[edge_id].weight;
273                            clear_pair_solver_color(&mut self.body_solver_color_masks, pair);
274
275                            let touching = pair.has_any_active_contact();
276                            if touching {
277                                let body_info = |co: ColliderHandle| {
278                                    colliders
279                                        .get(co)
280                                        .and_then(|co| co.parent.as_ref())
281                                        .map(|p| {
282                                            let rb = &bodies[p.handle];
283                                            (p.handle.into_raw_parts().0, rb.is_fixed())
284                                        })
285                                };
286                                let info1 = body_info(pair.collider1);
287                                let info2 = body_info(pair.collider2);
288                                assign_pair_solver_color(
289                                    &mut self.body_solver_color_masks,
290                                    pair,
291                                    info1,
292                                    info2,
293                                );
294                            }
295
296                            // Persistent islands: after re-parenting or a body type change,
297                            // a link recorded with the old endpoints may no longer describe
298                            // connectivity (a link to a fixed body doesn't connect). Refresh it.
299                            if let Some(islands) = islands.as_deref_mut() {
300                                let parent = |co: ColliderHandle| {
301                                    colliders.get(co).and_then(|c| c.parent.map(|p| p.handle))
302                                };
303                                let pair = &self.contact_graph.graph.edges[edge_id].weight;
304                                let (co1, co2) = (pair.collider1, pair.collider2);
305                                islands.persistent.unlink_contact(edge_id as u32);
306                                if touching {
307                                    islands.persistent.link_contact(
308                                        bodies,
309                                        edge_id as u32,
310                                        parent(co1),
311                                        parent(co2),
312                                    );
313                                }
314                            }
315                        }
316                    }
317
318                    // For each collider which had their sensor status modified, we need
319                    // to transfer their contact/intersection graph edges to the intersection/contact graph.
320                    // To achieve this we will remove the relevant contact/intersection pairs form the
321                    // contact/intersection graphs, and then add them into the other graph.
322                    if co.changes.intersects(ColliderChanges::TYPE) {
323                        if co.is_sensor() {
324                            // Find the contact pairs for this collider and
325                            // push them to `pairs_to_remove`.
326                            for inter in self
327                                .contact_graph
328                                .interactions_with(gid.contact_graph_index)
329                            {
330                                pairs_to_remove.push((
331                                    ColliderPair::new(inter.0, inter.1),
332                                    PairRemovalMode::FromContactGraph,
333                                ));
334                            }
335                        } else {
336                            // Find the contact pairs for this collider and
337                            // push them to `pairs_to_remove` if both involved
338                            // colliders are not sensors.
339                            for inter in self
340                                .intersection_graph
341                                .interactions_with(gid.intersection_graph_index)
342                                .filter(|(h1, h2, _)| {
343                                    !colliders[*h1].is_sensor() && !colliders[*h2].is_sensor()
344                                })
345                            {
346                                pairs_to_remove.push((
347                                    ColliderPair::new(inter.0, inter.1),
348                                    PairRemovalMode::FromIntersectionGraph,
349                                ));
350                            }
351                        }
352                    }
353
354                    // NOTE: if a collider only changed parent, we don’t need to remove it from any
355                    //       of the graphs as re-parenting doesn’t change the sensor status of a
356                    //       collider. If needed, their collision/intersection data will be
357                    //       updated/removed automatically in the contact or intersection update
358                    //       functions.
359                }
360            }
361        }
362
363        // Remove the pair from the relevant graph.
364        for pair in &pairs_to_remove {
365            self.remove_pair(
366                islands.as_deref_mut(),
367                colliders,
368                bodies,
369                &pair.0,
370                events,
371                pair.1,
372            );
373        }
374
375        // Add the removed pair to the relevant graph.
376        for pair in pairs_to_remove {
377            self.add_pair(colliders, &pair.0);
378        }
379    }
380
381    #[profiling::function]
382    fn remove_pair(
383        &mut self,
384        mut islands: Option<&mut IslandManager>,
385        colliders: &ColliderSet,
386        bodies: &mut RigidBodySet,
387        pair: &ColliderPair,
388        events: &dyn EventHandler,
389        mode: PairRemovalMode,
390    ) {
391        if let (Some(co1), Some(co2)) =
392            (colliders.get(pair.collider1), colliders.get(pair.collider2))
393        {
394            // TODO: could we just unwrap here?
395            // Don't we have the guarantee that we will get a `AddPair` before a `DeletePair`?
396            if let (Some(gid1), Some(gid2)) = (
397                self.graph_indices.get(pair.collider1.0),
398                self.graph_indices.get(pair.collider2.0),
399            ) {
400                if mode == PairRemovalMode::FromIntersectionGraph
401                    || (mode == PairRemovalMode::Auto && (co1.is_sensor() || co2.is_sensor()))
402                {
403                    let intersection = self
404                        .intersection_graph
405                        .remove_edge(gid1.intersection_graph_index, gid2.intersection_graph_index);
406
407                    // Emit an intersection lost event if we had an intersection before removing the edge.
408                    if let Some(mut intersection) = intersection {
409                        if intersection.intersecting
410                            && (co1.flags.active_events | co2.flags.active_events)
411                                .contains(ActiveEvents::COLLISION_EVENTS)
412                        {
413                            intersection.emit_stop_event(
414                                bodies,
415                                colliders,
416                                pair.collider1,
417                                pair.collider2,
418                                events,
419                            )
420                        }
421                    }
422                } else {
423                    // O(1) maintenance of the persistent solver structures through the edges vec's
424                    // swap-remove (with stored-position fixup) — a global rebuild
425                    // is O(all pairs). If a rebuild is pending, `ContactRef`/`graph_pos` are garbage: skip it.
426                    let solver_graph_valid = self.solver_graph_valid;
427                    let graph = &mut self.solver_contact_graph;
428                    let force_list = &mut self.force_event_pairs;
429                    let force_pos = &mut self.force_event_pos;
430                    let pair_solver_hints = &mut self.pair_solver_hints;
431                    let num_edges = self.contact_graph.graph.edges.len();
432                    let edges_ptr = self.contact_graph.graph.edges.as_mut_ptr();
433                    let mut removed_index = usize::MAX;
434                    let mut consistent = true;
435                    let contact_pair = self.contact_graph.remove_edge_with(
436                        gid1.contact_graph_index,
437                        gid2.contact_graph_index,
438                        &mut |e| {
439                            removed_index = e.index();
440                            // Mirror the edges vec's swap_remove on the solver hints
441                            // (on length mismatch — deserialized state — drop them
442                            // and fall back to a full solver-graph rebuild).
443                            if e.index() < pair_solver_hints.len() {
444                                pair_solver_hints.swap_remove(e.index());
445                            } else {
446                                pair_solver_hints.clear();
447                                consistent = false;
448                            }
449
450                            if e.index() >= num_edges {
451                                consistent = false;
452                                return;
453                            }
454
455                            // Pull the removed pair's manifolds out of the buckets. Raw ordinal
456                            // access: the fixup may rewrite another manifold of this same pair via
457                            // `edges_ptr`. SAFETY: runs before the swap_remove (indices live); single-threaded.
458                            if solver_graph_valid {
459                                unsafe {
460                                    let pair: *mut ContactPair =
461                                        &mut (*edges_ptr.add(e.index())).weight;
462                                    let sm = (*pair).solver_manifolds_mut();
463                                    let (sm_ptr, num) = (sm.as_mut_ptr(), sm.len());
464                                    for ordinal in 0..num {
465                                        let mdata: *mut ContactManifoldData =
466                                            &mut (*sm_ptr.add(ordinal)).data;
467                                        let pos = (*mdata).graph_pos;
468                                        if pos.is_some() {
469                                            Self::remove_and_fixup(graph, edges_ptr, pos);
470                                            (*mdata).graph_pos = GraphPos::NONE;
471                                        }
472                                    }
473                                }
474                            }
475
476                            // Force-event list: drop the removed pair's membership, then
477                            // mirror the swap-remove on the back-ref array (grown at pair
478                            // add so it matches the edges vec; anything else = deserialized).
479                            if force_pos.len() == num_edges {
480                                let cur = force_pos[e.index()];
481                                if cur != u32::MAX {
482                                    force_list.swap_remove(cur as usize);
483                                    if (cur as usize) < force_list.len() {
484                                        force_pos[force_list[cur as usize] as usize] = cur;
485                                    }
486                                    force_pos[e.index()] = u32::MAX;
487                                }
488                                force_pos.swap_remove(e.index());
489                            } else {
490                                force_pos.clear();
491                                force_list.clear();
492                                consistent = false;
493                            }
494                        },
495                    );
496
497                    if !consistent {
498                        // Deserialized/degenerate bookkeeping: rebuild from scratch.
499                        self.solver_graph_valid = false;
500                        self.force_list_valid = false;
501                    } else if removed_index < self.contact_graph.graph.edges.len() {
502                        // A pair was swap-moved into the removed slot: rewrite the
503                        // edge index its persistent entries are keyed by.
504                        let new_edge = removed_index as u32;
505                        let moved = &mut self.contact_graph.graph.edges[removed_index].weight;
506                        let graph = &mut self.solver_contact_graph;
507                        // Same rebuild-pending gate as the removal loop above:
508                        // stale `graph_pos` back-refs must not drive bucket writes.
509                        if solver_graph_valid {
510                            for m in moved.solver_manifolds_mut() {
511                                if m.data.graph_pos.is_some() {
512                                    graph.rewrite_edge(m.data.graph_pos, new_edge);
513                                }
514                            }
515                        }
516                        let fpos = self
517                            .force_event_pos
518                            .get(removed_index)
519                            .copied()
520                            .unwrap_or(u32::MAX);
521                        if fpos != u32::MAX {
522                            self.force_event_pairs[fpos as usize] = new_edge;
523                        }
524                    }
525
526                    // Persistent islands: unlink the removed pair (if it was
527                    // touching) and mirror the edges-vec swap-remove on the
528                    // link-location table.
529                    if removed_index != usize::MAX && num_edges > 0 {
530                        if let Some(islands) = islands.as_deref_mut() {
531                            islands.persistent.unlink_contact(removed_index as u32);
532                            islands
533                                .persistent
534                                .contact_edge_removed(removed_index as u32, num_edges as u32 - 1);
535                        }
536                    }
537
538                    // Emit a contact stopped event if we had a contact before removing the edge.
539                    // Also wake up the dynamic bodies that were in contact.
540                    if let Some(mut ctct) = contact_pair {
541                        clear_pair_solver_color(&mut self.body_solver_color_masks, &mut ctct);
542
543                        if ctct.has_any_active_contact() {
544                            if let Some(islands) = islands {
545                                if let Some(co_parent1) = &co1.parent {
546                                    islands.wake_up(bodies, co_parent1.handle, true);
547                                }
548
549                                if let Some(co_parent2) = co2.parent {
550                                    islands.wake_up(bodies, co_parent2.handle, true);
551                                }
552                            }
553
554                            if (co1.flags.active_events | co2.flags.active_events)
555                                .contains(ActiveEvents::COLLISION_EVENTS)
556                            {
557                                ctct.emit_stop_event(bodies, colliders, events);
558                            }
559                        }
560
561                        // Retire the pair for reuse by `add_pair` (bounded pool).
562                        if self.retired_pairs.len() < 2048 {
563                            self.retired_pairs.push(ctct);
564                        }
565                    }
566                }
567            }
568        }
569    }
570
571    #[profiling::function]
572    fn add_pair(&mut self, colliders: &ColliderSet, pair: &ColliderPair) {
573        if let (Some(co1), Some(co2)) =
574            (colliders.get(pair.collider1), colliders.get(pair.collider2))
575        {
576            // These colliders have no parents - continue.
577
578            let (gid1, gid2) = self.graph_indices.ensure_pair_exists(
579                pair.collider1.0,
580                pair.collider2.0,
581                ColliderGraphIndices::invalid(),
582            );
583
584            if co1.is_sensor() || co2.is_sensor() {
585                // NOTE: the collider won't have a graph index as long
586                // as it does not interact with anything.
587                if !InteractionGraph::<(), ()>::is_graph_index_valid(gid1.intersection_graph_index)
588                {
589                    gid1.intersection_graph_index =
590                        self.intersection_graph.graph.add_node(pair.collider1);
591                }
592
593                if !InteractionGraph::<(), ()>::is_graph_index_valid(gid2.intersection_graph_index)
594                {
595                    gid2.intersection_graph_index =
596                        self.intersection_graph.graph.add_node(pair.collider2);
597                }
598
599                if self
600                    .intersection_graph
601                    .graph
602                    .find_edge(gid1.intersection_graph_index, gid2.intersection_graph_index)
603                    .is_none()
604                {
605                    let _ = self.intersection_graph.add_edge(
606                        gid1.intersection_graph_index,
607                        gid2.intersection_graph_index,
608                        IntersectionPair::new(),
609                    );
610                }
611            } else {
612                // NOTE: same code as above, but for the contact graph.
613                // TODO: refactor both pieces of code somehow?
614
615                // NOTE: the collider won't have a graph index as long
616                // as it does not interact with anything.
617                if !InteractionGraph::<(), ()>::is_graph_index_valid(gid1.contact_graph_index) {
618                    gid1.contact_graph_index = self.contact_graph.graph.add_node(pair.collider1);
619                }
620
621                if !InteractionGraph::<(), ()>::is_graph_index_valid(gid2.contact_graph_index) {
622                    gid2.contact_graph_index = self.contact_graph.graph.add_node(pair.collider2);
623                }
624
625                if self
626                    .contact_graph
627                    .graph
628                    .find_edge(gid1.contact_graph_index, gid2.contact_graph_index)
629                    .is_none()
630                {
631                    let interaction = if let Some(mut retired) = self.retired_pairs.pop() {
632                        retired.reset_for_reuse(pair.collider1, pair.collider2);
633                        retired
634                    } else {
635                        ContactPair::new(pair.collider1, pair.collider2)
636                    };
637                    // Keep the solver hints aligned with the edges vec at all times: add/remove
638                    // events interleave within a step, so deferred growth would desync the swap_remove
639                    // mirroring. On mismatch (deserialized), drop them — they rebuild lazily.
640                    if self.pair_solver_hints.len() == self.contact_graph.graph.edges.len() {
641                        self.pair_solver_hints.push(0);
642                    } else {
643                        self.pair_solver_hints.clear();
644                    }
645                    // Same for the force-event back-ref array: the O(1) removal maintenance
646                    // mirrors the edges vec's swap_remove on it, so it must track the edge
647                    // count exactly (not lazily at the next maintenance).
648                    if self.force_event_pos.len() == self.contact_graph.graph.edges.len() {
649                        self.force_event_pos.push(u32::MAX);
650                    } else {
651                        self.force_event_pos.clear();
652                        self.force_event_pairs.clear();
653                        self.force_list_valid = false;
654                    }
655                    let _ = self.contact_graph.add_edge(
656                        gid1.contact_graph_index,
657                        gid2.contact_graph_index,
658                        interaction,
659                    );
660                }
661            }
662        }
663    }
664
665    pub(crate) fn register_pairs(
666        &mut self,
667        mut islands: Option<&mut IslandManager>,
668        colliders: &ColliderSet,
669        bodies: &mut RigidBodySet,
670        broad_phase_events: &[BroadPhasePairEvent],
671        events: &dyn EventHandler,
672    ) {
673        for event in broad_phase_events {
674            match event {
675                BroadPhasePairEvent::AddPair(pair) => {
676                    self.add_pair(colliders, pair);
677                }
678                BroadPhasePairEvent::DeletePair(pair) => {
679                    self.remove_pair(
680                        islands.as_deref_mut(),
681                        colliders,
682                        bodies,
683                        pair,
684                        events,
685                        PairRemovalMode::Auto,
686                    );
687                }
688            }
689        }
690    }
691}