Skip to main content

blast_stress_solver/rapier/
body_tracker.rs

1use std::collections::{HashMap, HashSet};
2
3#[cfg(not(target_arch = "wasm32"))]
4use std::time::Instant;
5
6// `std::time::Instant::now()` panics on `wasm32-unknown-unknown` because the
7// target has no monotonic clock. We only use it for split-edit telemetry, so a
8// zero-duration shim keeps those stats at 0.0 ms on wasm without trapping.
9#[cfg(target_arch = "wasm32")]
10#[derive(Clone, Copy)]
11struct Instant;
12
13#[cfg(target_arch = "wasm32")]
14impl Instant {
15    fn now() -> Self {
16        Self
17    }
18
19    fn elapsed(&self) -> std::time::Duration {
20        std::time::Duration::ZERO
21    }
22}
23
24use rapier3d::na::UnitQuaternion;
25use rapier3d::prelude::*;
26
27use super::collision_groups::{apply_collision_groups_for_body, DebrisCollisionMode};
28use super::optimization::{
29    DebrisCleanupOptions, OptimizationMode, OptimizationResult, SleepThresholdOptions,
30    SmallBodyDampingOptions,
31};
32use crate::types::*;
33
34#[derive(Clone, Copy)]
35struct BodyState {
36    position: Isometry<Real>,
37    linvel: Vector<Real>,
38    angvel: Vector<Real>,
39    was_sleeping: bool,
40}
41
42#[derive(Clone, Copy, Debug)]
43struct BodyMetadata {
44    created_at: f32,
45    first_support_contact_at: Option<f32>,
46    damping_promoted: bool,
47    sleep_threshold_configured: bool,
48    has_support: bool,
49}
50
51#[derive(Clone, Copy)]
52struct NodeSourceState {
53    body_handle: RigidBodyHandle,
54    collider_handle: Option<ColliderHandle>,
55    world_centroid: Vec3,
56    world_velocity: Vec3,
57}
58
59struct SplitPlanningData {
60    existing: Vec<super::split_migrator::ExistingBodyState>,
61    parent_bodies: Vec<RigidBodyHandle>,
62    parent_states: HashMap<RigidBodyHandle, BodyState>,
63    node_sources: HashMap<u32, NodeSourceState>,
64}
65
66#[derive(Clone, Copy)]
67enum PlannedBodyTarget {
68    Existing(RigidBodyHandle),
69    Recycled(RigidBodyHandle),
70    Created(RigidBodyHandle),
71}
72
73impl PlannedBodyTarget {
74    fn handle(self) -> RigidBodyHandle {
75        match self {
76            Self::Existing(handle) | Self::Recycled(handle) | Self::Created(handle) => handle,
77        }
78    }
79}
80
81#[derive(Clone, Copy)]
82struct ChildTargetState {
83    pose: Isometry<Real>,
84    linvel: Vector<Real>,
85    angvel: Vector<Real>,
86    should_sleep: bool,
87}
88
89#[derive(Clone, Copy, Debug, Default)]
90pub struct SplitCost {
91    pub create_bodies: usize,
92    pub collider_migrations: usize,
93}
94
95#[derive(Clone, Copy, Debug, Default)]
96pub struct SplitEditStats {
97    pub plan_ms: f32,
98    pub apply_ms: f32,
99    pub child_pose_ms: f32,
100    pub velocity_fit_ms: f32,
101    pub sleep_init_ms: f32,
102    pub body_create_ms: f32,
103    pub collider_move_ms: f32,
104    pub collider_insert_ms: f32,
105    pub body_retire_ms: f32,
106    pub reused_bodies: usize,
107    pub recycled_bodies: usize,
108    pub created_bodies: usize,
109    pub retired_bodies: usize,
110    pub body_type_flips: usize,
111    pub moved_colliders: usize,
112    pub inserted_colliders: usize,
113    pub removed_colliders: usize,
114}
115
116#[derive(Debug, Default)]
117pub struct SplitApplyResult {
118    pub new_handles: Vec<RigidBodyHandle>,
119    pub cohort_handles: Vec<RigidBodyHandle>,
120    pub source_handles: Vec<RigidBodyHandle>,
121    pub restore_sources: Vec<(RigidBodyHandle, RigidBodyHandle)>,
122    pub stats: SplitEditStats,
123}
124
125/// Tracks the mapping between stress graph nodes and Rapier rigid bodies.
126pub struct BodyTracker {
127    /// For each node index, which body it belongs to (if any).
128    node_to_body: Vec<Option<RigidBodyHandle>>,
129    /// For each node index, which collider it belongs to (if any).
130    node_to_collider: Vec<Option<ColliderHandle>>,
131    /// For each collider handle, which node index it belongs to.
132    collider_to_node: HashMap<ColliderHandle, u32>,
133    /// For each body handle, which node indices it contains.
134    body_to_nodes: HashMap<RigidBodyHandle, Vec<u32>>,
135    /// Set of support (fixed, mass=0) node indices.
136    support_nodes: HashSet<u32>,
137    /// Per-node size used for creating colliders.
138    node_sizes: Vec<Vec3>,
139    /// Optional exact per-node collider shapes.
140    node_colliders: Vec<Option<ScenarioCollider>>,
141    /// Per-node centroid.
142    node_centroids: Vec<Vec3>,
143    /// Per-node mass.
144    node_masses: Vec<f32>,
145    /// Per-node local offset relative to its owning body.
146    node_local_offsets: Vec<Vec3>,
147    /// Per-body lifecycle metadata used for replay stabilization.
148    body_metadata: HashMap<RigidBodyHandle, BodyMetadata>,
149    /// Whether newly created dynamic bodies should enable CCD.
150    dynamic_body_ccd_enabled: bool,
151    /// Collision filtering mode for small debris bodies.
152    debris_collision_mode: DebrisCollisionMode,
153    /// Optional ground body used by debris-ground-only filtering.
154    ground_body_handle: Option<RigidBodyHandle>,
155    /// Whether split children should be recentered to their own pose on handoff.
156    split_child_recentering_enabled: bool,
157    /// Whether split children should get fitted kinematics on handoff.
158    split_child_velocity_fit_enabled: bool,
159}
160
161impl BodyTracker {
162    /// Create a new tracker from a scenario description.
163    pub fn new(
164        nodes: &[ScenarioNode],
165        node_sizes: Vec<Vec3>,
166        node_colliders: Vec<Option<ScenarioCollider>>,
167    ) -> Self {
168        let support_nodes: HashSet<u32> = nodes
169            .iter()
170            .enumerate()
171            .filter(|(_, n)| n.mass == 0.0)
172            .map(|(i, _)| i as u32)
173            .collect();
174
175        let node_centroids = nodes.iter().map(|n| n.centroid).collect();
176        let node_masses = nodes.iter().map(|n| n.mass).collect();
177
178        Self {
179            node_to_body: vec![None; nodes.len()],
180            node_to_collider: vec![None; nodes.len()],
181            collider_to_node: HashMap::new(),
182            body_to_nodes: HashMap::new(),
183            support_nodes,
184            node_sizes,
185            node_colliders,
186            node_centroids,
187            node_masses,
188            node_local_offsets: vec![Vec3::ZERO; nodes.len()],
189            body_metadata: HashMap::new(),
190            dynamic_body_ccd_enabled: false,
191            debris_collision_mode: DebrisCollisionMode::All,
192            ground_body_handle: None,
193            split_child_recentering_enabled: true,
194            split_child_velocity_fit_enabled: true,
195        }
196    }
197
198    pub fn set_dynamic_body_ccd_enabled(&mut self, enabled: bool) {
199        self.dynamic_body_ccd_enabled = enabled;
200    }
201
202    pub fn debris_collision_mode(&self) -> DebrisCollisionMode {
203        self.debris_collision_mode
204    }
205
206    pub fn set_debris_collision_mode(&mut self, mode: DebrisCollisionMode) {
207        self.debris_collision_mode = mode;
208    }
209
210    pub fn ground_body_handle(&self) -> Option<RigidBodyHandle> {
211        self.ground_body_handle
212    }
213
214    pub fn set_ground_body_handle(&mut self, handle: Option<RigidBodyHandle>) {
215        self.ground_body_handle = handle;
216    }
217
218    pub fn set_split_child_recentering_enabled(&mut self, enabled: bool) {
219        self.split_child_recentering_enabled = enabled;
220    }
221
222    pub fn set_split_child_velocity_fit_enabled(&mut self, enabled: bool) {
223        self.split_child_velocity_fit_enabled = enabled;
224    }
225
226    pub fn refresh_collision_groups(
227        &self,
228        bodies: &RigidBodySet,
229        colliders: &mut ColliderSet,
230        max_colliders_for_debris: usize,
231    ) {
232        if let Some(ground_body_handle) = self.ground_body_handle {
233            apply_collision_groups_for_body(
234                ground_body_handle,
235                bodies,
236                colliders,
237                self.ground_body_handle,
238                self.debris_collision_mode,
239                true,
240                max_colliders_for_debris,
241            );
242        }
243
244        for &body_handle in self.body_to_nodes.keys() {
245            apply_collision_groups_for_body(
246                body_handle,
247                bodies,
248                colliders,
249                self.ground_body_handle,
250                self.debris_collision_mode,
251                self.debris_collision_active(body_handle),
252                max_colliders_for_debris,
253            );
254        }
255    }
256
257    /// Create initial Rapier bodies from the actor table.
258    ///
259    /// Each actor becomes one rigid body. Support-only actors become fixed bodies.
260    /// Returns the list of created body handles.
261    pub fn create_initial_bodies(
262        &mut self,
263        actors: &[Actor],
264        bodies: &mut RigidBodySet,
265        colliders: &mut ColliderSet,
266        created_at: f32,
267        small_body_damping: SmallBodyDampingOptions,
268        sleep_thresholds: SleepThresholdOptions,
269        max_colliders_for_debris: usize,
270    ) -> Vec<RigidBodyHandle> {
271        let mut handles = Vec::new();
272
273        for actor in actors {
274            if actor.nodes.is_empty() {
275                continue;
276            }
277
278            let has_support = actor.nodes.iter().any(|n| self.support_nodes.contains(n));
279
280            // Compute center of mass for the actor
281            let com = self.compute_actor_com(&actor.nodes);
282
283            let body = if has_support {
284                bodies.insert(RigidBodyBuilder::fixed().translation(vector![com.x, com.y, com.z]))
285            } else {
286                bodies.insert(
287                    RigidBodyBuilder::dynamic()
288                        .translation(vector![com.x, com.y, com.z])
289                        .ccd_enabled(self.dynamic_body_ccd_enabled),
290                )
291            };
292
293            // Create colliders for each node
294            for &node_idx in &actor.nodes {
295                let ni = node_idx as usize;
296                let centroid = self.node_centroids[ni];
297
298                // Collider position is relative to the body
299                let local_x = centroid.x - com.x;
300                let local_y = centroid.y - com.y;
301                let local_z = centroid.z - com.z;
302
303                let collider =
304                    self.build_node_collider(node_idx, Vec3::new(local_x, local_y, local_z));
305                let collider_handle = colliders.insert_with_parent(collider, body, bodies);
306                self.attach_node(
307                    node_idx,
308                    body,
309                    collider_handle,
310                    Vec3::new(local_x, local_y, local_z),
311                );
312            }
313
314            self.body_to_nodes.insert(body, actor.nodes.clone());
315            self.update_body_metadata(body, &actor.nodes, created_at);
316            self.promote_small_body_damping(body, bodies, &small_body_damping);
317            self.configure_sleep_thresholds(body, bodies, &sleep_thresholds);
318            self.apply_collision_groups(body, bodies, colliders, max_colliders_for_debris);
319            handles.push(body);
320        }
321
322        handles
323    }
324
325    /// Handle a split event: create new bodies for child actors.
326    ///
327    /// Returns newly created body handles.
328    pub fn handle_split(
329        &mut self,
330        event: &SplitEvent,
331        bodies: &mut RigidBodySet,
332        colliders: &mut ColliderSet,
333        island_manager: &mut IslandManager,
334        impulse_joints: &mut ImpulseJointSet,
335        multibody_joints: &mut MultibodyJointSet,
336        created_at: f32,
337        small_body_damping: SmallBodyDampingOptions,
338        sleep_thresholds: SleepThresholdOptions,
339        max_colliders_for_debris: usize,
340    ) -> SplitApplyResult {
341        let mut result = SplitApplyResult::default();
342        let planning = self.collect_split_planning_data(event, bodies);
343        result.source_handles = planning.parent_bodies.clone();
344        let plan_started_at = Instant::now();
345        let child_support: Vec<super::split_migrator::PlannerChildSupport> = event
346            .children
347            .iter()
348            .map(|child| super::split_migrator::PlannerChildSupport {
349                is_support: child.nodes.iter().any(|n| self.support_nodes.contains(n)),
350            })
351            .collect();
352        let plan = super::split_migrator::plan_split_migration_with_support(
353            &planning.existing,
354            &event.children,
355            &child_support,
356        );
357        result.stats.plan_ms = plan_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
358        result.stats.reused_bodies = plan.reuse.len();
359        let reused_by_body: HashMap<RigidBodyHandle, usize> = plan
360            .reuse
361            .iter()
362            .map(|entry| (entry.body_handle, entry.child_index))
363            .collect();
364        let mut recycled_fixed = Vec::new();
365        let mut recycled_dynamic = Vec::new();
366        for &body_handle in &planning.parent_bodies {
367            if reused_by_body.contains_key(&body_handle) {
368                continue;
369            }
370            if bodies
371                .get(body_handle)
372                .map(|body| body.is_dynamic())
373                .unwrap_or(false)
374            {
375                recycled_dynamic.push(body_handle);
376            } else {
377                recycled_fixed.push(body_handle);
378            }
379        }
380
381        let mut child_targets: HashMap<usize, PlannedBodyTarget> = HashMap::new();
382        let mut child_target_states: HashMap<usize, ChildTargetState> = HashMap::new();
383        let apply_started_at = Instant::now();
384
385        for entry in &plan.reuse {
386            let child = &event.children[entry.child_index];
387            let child_has_support = child.nodes.iter().any(|n| self.support_nodes.contains(n));
388            let (target_state, child_pose_ms, velocity_fit_ms) =
389                self.compute_child_target_state(child, &planning, child_has_support);
390            result.stats.child_pose_ms += child_pose_ms;
391            result.stats.velocity_fit_ms += velocity_fit_ms;
392            let flipped =
393                self.reconcile_reused_body_type(entry.body_handle, child_has_support, bodies);
394            result.stats.body_type_flips += usize::from(flipped);
395            if let Some(body) = bodies.get_mut(entry.body_handle) {
396                body.set_position(target_state.pose, false);
397            }
398            child_targets.insert(
399                entry.child_index,
400                PlannedBodyTarget::Existing(entry.body_handle),
401            );
402            child_target_states.insert(entry.child_index, target_state);
403        }
404
405        for entry in &plan.create {
406            let child = &event.children[entry.child_index];
407            let child_has_support = child.nodes.iter().any(|n| self.support_nodes.contains(n));
408            let (target_state, child_pose_ms, velocity_fit_ms) =
409                self.compute_child_target_state(child, &planning, child_has_support);
410            result.stats.child_pose_ms += child_pose_ms;
411            result.stats.velocity_fit_ms += velocity_fit_ms;
412
413            let recycled_handle = if child_has_support {
414                recycled_fixed.pop().or_else(|| recycled_dynamic.pop())
415            } else {
416                recycled_dynamic.pop().or_else(|| recycled_fixed.pop())
417            };
418
419            if let Some(body_handle) = recycled_handle {
420                let flipped = self.reinitialize_recycled_body(
421                    body_handle,
422                    child_has_support,
423                    target_state,
424                    bodies,
425                );
426                result.stats.body_type_flips += usize::from(flipped);
427                result.stats.recycled_bodies += 1;
428                child_targets.insert(entry.child_index, PlannedBodyTarget::Recycled(body_handle));
429                child_target_states.insert(entry.child_index, target_state);
430                continue;
431            }
432
433            let body_create_started_at = Instant::now();
434            let body_handle = self.create_body_for_child(child, target_state, bodies);
435            result.stats.body_create_ms +=
436                body_create_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
437            result.stats.created_bodies += 1;
438            result.new_handles.push(body_handle);
439            child_targets.insert(entry.child_index, PlannedBodyTarget::Created(body_handle));
440            child_target_states.insert(entry.child_index, target_state);
441        }
442
443        for (child_index, target) in child_targets.iter().map(|(k, v)| (*k, *v)) {
444            let child = &event.children[child_index];
445            let target_body = target.handle();
446            if let Some(source_body) = self.dominant_child_body_handle(child, &planning) {
447                result.restore_sources.push((target_body, source_body));
448            }
449            let target_state = *child_target_states
450                .get(&child_index)
451                .unwrap_or_else(|| panic!("missing target state for child {}", child_index));
452            let target_pose = target_state.pose;
453
454            for &n in &child.nodes {
455                let Some(source) = planning.node_sources.get(&n) else {
456                    continue;
457                };
458                let local_offset = self.local_offset_for_pose(source, &target_pose);
459                let already_on_target = self.node_body(n) == Some(target_body);
460                if already_on_target
461                    && self.node_local_offset(n).is_some_and(|current| {
462                        (current - local_offset).magnitude_squared() <= 1.0e-8
463                    })
464                {
465                    continue;
466                }
467                let move_started_at = Instant::now();
468                let collider_handle = if already_on_target {
469                    self.update_node_collider_local_pose(
470                        source.collider_handle,
471                        local_offset,
472                        colliders,
473                    )
474                } else {
475                    self.move_node_collider_to_body(
476                        n,
477                        target_body,
478                        source.collider_handle,
479                        local_offset,
480                        bodies,
481                        colliders,
482                    )
483                };
484                let move_ms = move_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
485                if source.collider_handle.is_some() {
486                    result.stats.moved_colliders += 1;
487                    result.stats.collider_move_ms += move_ms;
488                } else {
489                    result.stats.inserted_colliders += 1;
490                    result.stats.collider_insert_ms += move_ms;
491                }
492                self.attach_node(n, target_body, collider_handle, local_offset);
493            }
494
495            self.body_to_nodes.insert(target_body, child.nodes.clone());
496            let reset_metadata = !matches!(target, PlannedBodyTarget::Existing(_));
497            self.refresh_body_metadata(target_body, &child.nodes, created_at, reset_metadata);
498            self.promote_small_body_damping(target_body, bodies, &small_body_damping);
499            self.configure_sleep_thresholds(target_body, bodies, &sleep_thresholds);
500            self.apply_collision_groups(target_body, bodies, colliders, max_colliders_for_debris);
501            let sleep_started_at = Instant::now();
502            self.apply_child_kinematics(
503                target_body,
504                child.nodes.iter().any(|n| self.support_nodes.contains(n)),
505                target_state,
506                bodies,
507            );
508            result.stats.sleep_init_ms += sleep_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
509        }
510
511        result.cohort_handles = child_targets
512            .values()
513            .map(|target| target.handle())
514            .collect::<HashSet<_>>()
515            .into_iter()
516            .collect();
517
518        let reused_handles: HashSet<RigidBodyHandle> = reused_by_body.keys().copied().collect();
519        let recycled_handles: HashSet<RigidBodyHandle> = child_targets
520            .values()
521            .filter_map(|target| match target {
522                PlannedBodyTarget::Recycled(handle) => Some(*handle),
523                _ => None,
524            })
525            .collect();
526        for &body_handle in &planning.parent_bodies {
527            if reused_handles.contains(&body_handle) || recycled_handles.contains(&body_handle) {
528                continue;
529            }
530            let retire_started_at = Instant::now();
531            self.body_to_nodes.remove(&body_handle);
532            self.body_metadata.remove(&body_handle);
533            if bodies.get(body_handle).is_some() {
534                bodies.remove(
535                    body_handle,
536                    island_manager,
537                    colliders,
538                    impulse_joints,
539                    multibody_joints,
540                    true,
541                );
542                result.stats.retired_bodies += 1;
543            }
544            result.stats.body_retire_ms +=
545                retire_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
546        }
547
548        result.stats.apply_ms = apply_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
549        result
550    }
551
552    /// Get the body handle for a node.
553    pub fn node_body(&self, node_index: u32) -> Option<RigidBodyHandle> {
554        self.node_to_body
555            .get(node_index as usize)
556            .copied()
557            .flatten()
558    }
559
560    /// Get the collider handle for a node.
561    pub fn node_collider(&self, node_index: u32) -> Option<ColliderHandle> {
562        self.node_to_collider
563            .get(node_index as usize)
564            .copied()
565            .flatten()
566    }
567
568    /// Get the node owning a collider.
569    pub fn collider_node(&self, collider: ColliderHandle) -> Option<u32> {
570        self.collider_to_node.get(&collider).copied()
571    }
572
573    /// Get the node local offset relative to its owning body.
574    pub fn node_local_offset(&self, node_index: u32) -> Option<Vec3> {
575        let idx = node_index as usize;
576        if self.node_to_body.get(idx).copied().flatten().is_some() {
577            self.node_local_offsets.get(idx).copied()
578        } else {
579            None
580        }
581    }
582
583    /// Get the node indices attached to a body.
584    pub fn body_nodes(&self, body_handle: RigidBodyHandle) -> Vec<u32> {
585        self.body_to_nodes
586            .get(&body_handle)
587            .cloned()
588            .unwrap_or_default()
589    }
590
591    pub fn body_nodes_slice(&self, body_handle: RigidBodyHandle) -> &[u32] {
592        self.body_to_nodes
593            .get(&body_handle)
594            .map(Vec::as_slice)
595            .unwrap_or(&[])
596    }
597
598    pub fn body_node_count(&self, body_handle: RigidBodyHandle) -> usize {
599        self.body_to_nodes
600            .get(&body_handle)
601            .map(Vec::len)
602            .unwrap_or(0)
603    }
604
605    pub fn body_has_support(&self, body_handle: RigidBodyHandle) -> bool {
606        self.body_metadata
607            .get(&body_handle)
608            .map(|metadata| metadata.has_support)
609            .unwrap_or(false)
610    }
611
612    /// Whether a node is a support (fixed) node.
613    pub fn is_support(&self, node_index: u32) -> bool {
614        self.support_nodes.contains(&node_index)
615    }
616
617    /// Number of tracked bodies.
618    pub fn body_count(&self) -> usize {
619        self.body_to_nodes.len()
620    }
621
622    /// Number of tracked colliders owned by destructible nodes.
623    pub fn collider_count(&self) -> usize {
624        self.node_to_collider
625            .iter()
626            .filter(|handle| handle.is_some())
627            .count()
628    }
629
630    /// Number of dynamic bodies.
631    pub fn dynamic_body_count(&self, bodies: &RigidBodySet) -> usize {
632        self.body_to_nodes
633            .keys()
634            .filter(|h| bodies.get(**h).map(|b| b.is_dynamic()).unwrap_or(false))
635            .count()
636    }
637
638    pub fn awake_dynamic_body_count(&self, bodies: &RigidBodySet) -> usize {
639        self.body_to_nodes
640            .keys()
641            .filter(|h| {
642                bodies
643                    .get(**h)
644                    .map(|b| b.is_dynamic() && !b.is_sleeping())
645                    .unwrap_or(false)
646            })
647            .count()
648    }
649
650    pub fn sleeping_dynamic_body_count(&self, bodies: &RigidBodySet) -> usize {
651        self.body_to_nodes
652            .keys()
653            .filter(|h| {
654                bodies
655                    .get(**h)
656                    .map(|b| b.is_dynamic() && b.is_sleeping())
657                    .unwrap_or(false)
658            })
659            .count()
660    }
661
662    pub fn support_body_count(&self) -> usize {
663        self.body_metadata
664            .values()
665            .filter(|metadata| metadata.has_support)
666            .count()
667    }
668
669    pub fn ccd_enabled_body_count(&self, bodies: &RigidBodySet) -> usize {
670        self.body_to_nodes
671            .keys()
672            .filter(|h| bodies.get(**h).map(|b| b.is_ccd_enabled()).unwrap_or(false))
673            .count()
674    }
675
676    pub fn estimate_split_cost(&self, event: &SplitEvent, bodies: &RigidBodySet) -> SplitCost {
677        let existing = self.collect_existing_body_states(event, bodies);
678        let child_support: Vec<super::split_migrator::PlannerChildSupport> = event
679            .children
680            .iter()
681            .map(|child| super::split_migrator::PlannerChildSupport {
682                is_support: child.nodes.iter().any(|n| self.support_nodes.contains(n)),
683            })
684            .collect();
685        let plan = super::split_migrator::plan_split_migration_with_support(
686            &existing,
687            &event.children,
688            &child_support,
689        );
690        let recyclable_bodies = existing.len().saturating_sub(plan.reuse.len());
691        SplitCost {
692            create_bodies: plan.create.len().saturating_sub(recyclable_bodies),
693            collider_migrations: event.children.iter().map(|child| child.nodes.len()).sum(),
694        }
695    }
696
697    pub fn mark_support_contact(
698        &mut self,
699        body_handle: RigidBodyHandle,
700        now_secs: f32,
701        bodies: &mut RigidBodySet,
702        colliders: &mut ColliderSet,
703        damping: SmallBodyDampingOptions,
704        sleep_thresholds: SleepThresholdOptions,
705        max_colliders_for_debris: usize,
706    ) -> bool {
707        let collider_count = self.body_node_count(body_handle);
708        let Some(metadata) = self.body_metadata.get_mut(&body_handle) else {
709            return false;
710        };
711        if metadata.has_support {
712            return false;
713        }
714        metadata.first_support_contact_at.get_or_insert(now_secs);
715        let mut changed = false;
716        if damping.mode == OptimizationMode::AfterGroundCollision
717            && !metadata.damping_promoted
718            && collider_count <= damping.collider_count_threshold
719        {
720            if Self::apply_small_body_damping(bodies, body_handle, &damping) {
721                metadata.damping_promoted = true;
722                changed = true;
723            }
724        }
725        if sleep_thresholds.mode == OptimizationMode::AfterGroundCollision
726            && !metadata.sleep_threshold_configured
727        {
728            if Self::apply_sleep_thresholds(bodies, body_handle, &sleep_thresholds) {
729                metadata.sleep_threshold_configured = true;
730                changed = true;
731            }
732        }
733        if metadata.first_support_contact_at == Some(now_secs)
734            && self.debris_collision_mode != DebrisCollisionMode::All
735        {
736            self.apply_collision_groups(body_handle, bodies, colliders, max_colliders_for_debris);
737            changed = true;
738        }
739        changed
740    }
741
742    pub fn cleanup_expired_bodies(
743        &mut self,
744        now_secs: f32,
745        cleanup: DebrisCleanupOptions,
746        bodies: &mut RigidBodySet,
747        colliders: &mut ColliderSet,
748        island_manager: &mut IslandManager,
749        impulse_joints: &mut ImpulseJointSet,
750        multibody_joints: &mut MultibodyJointSet,
751    ) -> OptimizationResult {
752        let mut result = OptimizationResult::default();
753        if cleanup.mode == OptimizationMode::Off {
754            return result;
755        }
756
757        let mut expired = Vec::new();
758        for (&body_handle, metadata) in &self.body_metadata {
759            if metadata.has_support {
760                continue;
761            }
762            let Some(body) = bodies.get(body_handle) else {
763                continue;
764            };
765            if !body.is_dynamic() {
766                continue;
767            }
768            if self.body_node_count(body_handle) > cleanup.max_colliders_for_debris {
769                continue;
770            }
771
772            let anchor = match cleanup.mode {
773                OptimizationMode::Off => None,
774                OptimizationMode::Always => Some(metadata.created_at),
775                OptimizationMode::AfterGroundCollision => metadata.first_support_contact_at,
776            };
777            let Some(anchor) = anchor else {
778                continue;
779            };
780            if cleanup.mode == OptimizationMode::AfterGroundCollision && !body.is_sleeping() {
781                continue;
782            }
783            if now_secs - anchor >= cleanup.debris_ttl_secs {
784                expired.push(body_handle);
785            }
786        }
787
788        for body_handle in expired {
789            let removed_nodes = self.remove_body(
790                body_handle,
791                bodies,
792                colliders,
793                island_manager,
794                impulse_joints,
795                multibody_joints,
796            );
797            if !removed_nodes.is_empty() {
798                result.removed_bodies.push(body_handle);
799                result.removed_nodes.extend(removed_nodes);
800            }
801        }
802
803        result
804    }
805
806    pub fn destroy_nodes(
807        &mut self,
808        nodes: &[u32],
809        bodies: &mut RigidBodySet,
810        colliders: &mut ColliderSet,
811        island_manager: &mut IslandManager,
812        impulse_joints: &mut ImpulseJointSet,
813        multibody_joints: &mut MultibodyJointSet,
814    ) -> Vec<u32> {
815        let mut removed = Vec::new();
816        let mut affected_bodies = HashSet::new();
817
818        for &node_index in nodes {
819            let idx = node_index as usize;
820            let Some(body_handle) = self.node_to_body[idx] else {
821                continue;
822            };
823            if let Some(collider_handle) = self.node_to_collider[idx] {
824                colliders.remove(collider_handle, island_manager, bodies, true);
825            }
826            self.detach_node(node_index);
827            if let Some(body_nodes) = self.body_to_nodes.get_mut(&body_handle) {
828                body_nodes.retain(|node| *node != node_index);
829            }
830            affected_bodies.insert(body_handle);
831            removed.push(node_index);
832        }
833
834        for body_handle in affected_bodies {
835            let should_remove = self
836                .body_to_nodes
837                .get(&body_handle)
838                .map(|nodes| nodes.is_empty())
839                .unwrap_or(true);
840            if should_remove {
841                self.body_to_nodes.remove(&body_handle);
842                self.body_metadata.remove(&body_handle);
843                if bodies.get(body_handle).is_some() {
844                    bodies.remove(
845                        body_handle,
846                        island_manager,
847                        colliders,
848                        impulse_joints,
849                        multibody_joints,
850                        true,
851                    );
852                }
853            } else if let Some(metadata) = self.body_metadata.get_mut(&body_handle) {
854                metadata.has_support = self
855                    .body_to_nodes
856                    .get(&body_handle)
857                    .map(|nodes| nodes.iter().any(|node| self.support_nodes.contains(node)))
858                    .unwrap_or(false);
859            }
860        }
861
862        removed
863    }
864
865    fn compute_actor_com(&self, nodes: &[u32]) -> Vec3 {
866        if nodes.is_empty() {
867            return Vec3::ZERO;
868        }
869        let mut sum = Vec3::ZERO;
870        for &n in nodes {
871            sum += self.node_centroids[n as usize];
872        }
873        sum / nodes.len() as f32
874    }
875
876    fn compute_child_target_state(
877        &self,
878        child: &SplitChild,
879        planning: &SplitPlanningData,
880        has_support: bool,
881    ) -> (ChildTargetState, f32, f32) {
882        let pose_started_at = Instant::now();
883        let pose = if self.split_child_recentering_enabled {
884            self.compute_child_target_pose(child, planning, has_support)
885        } else {
886            self.inherited_child_target_pose(child, planning)
887        };
888        let child_pose_ms = pose_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
889        let velocity_fit_started_at = Instant::now();
890        let (linvel, angvel, should_sleep) = if self.split_child_velocity_fit_enabled {
891            self.fit_child_motion(child, planning, has_support, &pose)
892        } else {
893            self.inherited_child_motion(child, planning, has_support)
894        };
895        let velocity_fit_ms = velocity_fit_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
896        (
897            ChildTargetState {
898                pose,
899                linvel,
900                angvel,
901                should_sleep,
902            },
903            child_pose_ms,
904            velocity_fit_ms,
905        )
906    }
907
908    fn inherited_child_target_pose(
909        &self,
910        child: &SplitChild,
911        planning: &SplitPlanningData,
912    ) -> Isometry<Real> {
913        let Some(body_handle) = self.dominant_child_body_handle(child, planning) else {
914            return Isometry::identity();
915        };
916        planning
917            .parent_states
918            .get(&body_handle)
919            .map(|state| state.position)
920            .unwrap_or_else(Isometry::identity)
921    }
922
923    fn compute_child_target_pose(
924        &self,
925        child: &SplitChild,
926        planning: &SplitPlanningData,
927        has_support: bool,
928    ) -> Isometry<Real> {
929        let translation = if has_support {
930            self.child_world_centroid(child, planning)
931        } else {
932            self.child_world_center_of_mass(child, planning)
933        };
934        let rotation = self.dominant_child_rotation(child, planning);
935        Isometry::from_parts(
936            Translation::new(translation.x, translation.y, translation.z),
937            rotation,
938        )
939    }
940
941    fn child_world_centroid(&self, child: &SplitChild, planning: &SplitPlanningData) -> Vec3 {
942        let mut sum = Vec3::ZERO;
943        let mut count = 0usize;
944        for &node in &child.nodes {
945            if let Some(source) = planning.node_sources.get(&node) {
946                sum += source.world_centroid;
947                count += 1;
948            }
949        }
950        if count == 0 {
951            self.compute_actor_com(&child.nodes)
952        } else {
953            sum / count as f32
954        }
955    }
956
957    fn child_world_center_of_mass(&self, child: &SplitChild, planning: &SplitPlanningData) -> Vec3 {
958        let mut weighted_sum = Vec3::ZERO;
959        let mut total_mass = 0.0f32;
960        for &node in &child.nodes {
961            if let Some(source) = planning.node_sources.get(&node) {
962                let mass = self.node_masses[node as usize].max(0.0);
963                if mass > 0.0 {
964                    weighted_sum += source.world_centroid * mass;
965                    total_mass += mass;
966                }
967            }
968        }
969        if total_mass <= f32::EPSILON {
970            self.child_world_centroid(child, planning)
971        } else {
972            weighted_sum / total_mass
973        }
974    }
975
976    fn dominant_child_rotation(
977        &self,
978        child: &SplitChild,
979        planning: &SplitPlanningData,
980    ) -> UnitQuaternion<Real> {
981        let Some(body_handle) = self.dominant_child_body_handle(child, planning) else {
982            return UnitQuaternion::identity();
983        };
984        planning
985            .parent_states
986            .get(&body_handle)
987            .map(|state| state.position.rotation)
988            .unwrap_or_else(UnitQuaternion::identity)
989    }
990
991    fn dominant_child_body_handle(
992        &self,
993        child: &SplitChild,
994        planning: &SplitPlanningData,
995    ) -> Option<RigidBodyHandle> {
996        let mut body_weights: HashMap<RigidBodyHandle, f32> = HashMap::new();
997        for &node in &child.nodes {
998            let Some(source) = planning.node_sources.get(&node) else {
999                continue;
1000            };
1001            *body_weights.entry(source.body_handle).or_insert(0.0) +=
1002                self.node_masses[node as usize].max(1.0);
1003        }
1004        let Some((&body_handle, _)) = body_weights
1005            .iter()
1006            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
1007        else {
1008            return None;
1009        };
1010        Some(body_handle)
1011    }
1012
1013    fn fit_child_motion(
1014        &self,
1015        child: &SplitChild,
1016        planning: &SplitPlanningData,
1017        has_support: bool,
1018        target_pose: &Isometry<Real>,
1019    ) -> (Vector<Real>, Vector<Real>, bool) {
1020        if has_support {
1021            return (Vector::zeros(), Vector::zeros(), false);
1022        }
1023
1024        let child_com = Point::from(target_pose.translation.vector);
1025        let mut linvel_sum = Vector::zeros();
1026        let mut total_mass = 0.0f32;
1027        let mut all_sleeping = true;
1028        let mut samples = Vec::new();
1029        let mut source_weights: HashMap<RigidBodyHandle, f32> = HashMap::new();
1030
1031        for &node in &child.nodes {
1032            let Some(source) = planning.node_sources.get(&node) else {
1033                continue;
1034            };
1035            let Some(parent_state) = planning.parent_states.get(&source.body_handle) else {
1036                continue;
1037            };
1038            let mass = self.node_masses[node as usize].max(1.0e-4);
1039            let velocity = vector![
1040                source.world_velocity.x,
1041                source.world_velocity.y,
1042                source.world_velocity.z
1043            ];
1044            linvel_sum += velocity * mass;
1045            total_mass += mass;
1046            all_sleeping &= parent_state.was_sleeping;
1047            *source_weights.entry(source.body_handle).or_insert(0.0) += mass;
1048            let point = point![
1049                source.world_centroid.x,
1050                source.world_centroid.y,
1051                source.world_centroid.z
1052            ];
1053            samples.push((point, velocity, mass));
1054        }
1055
1056        if samples.is_empty() || total_mass <= f32::EPSILON {
1057            return (Vector::zeros(), Vector::zeros(), false);
1058        }
1059
1060        let linvel = linvel_sum / total_mass;
1061        let mut normal = [[0.0f32; 3]; 3];
1062        let mut rhs = [0.0f32; 3];
1063        for (point, velocity, mass) in &samples {
1064            let r = point - child_com;
1065            let r_vec = Vec3::new(r.x, r.y, r.z);
1066            let v_rel = Vec3::new(
1067                velocity.x - linvel.x,
1068                velocity.y - linvel.y,
1069                velocity.z - linvel.z,
1070            );
1071            let r2 = r_vec.magnitude_squared();
1072            let rr = [
1073                [r_vec.x * r_vec.x, r_vec.x * r_vec.y, r_vec.x * r_vec.z],
1074                [r_vec.y * r_vec.x, r_vec.y * r_vec.y, r_vec.y * r_vec.z],
1075                [r_vec.z * r_vec.x, r_vec.z * r_vec.y, r_vec.z * r_vec.z],
1076            ];
1077            for row in 0..3 {
1078                for col in 0..3 {
1079                    normal[row][col] += mass * ((if row == col { r2 } else { 0.0 }) - rr[row][col]);
1080                }
1081            }
1082            let cross = r_vec.cross(v_rel);
1083            rhs[0] += mass * cross.x;
1084            rhs[1] += mass * cross.y;
1085            rhs[2] += mass * cross.z;
1086        }
1087
1088        let dominant_source = source_weights
1089            .iter()
1090            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
1091            .and_then(|(handle, _)| planning.parent_states.get(handle));
1092
1093        let angvel = self
1094            .solve_symmetric_3x3(normal, rhs)
1095            .map(|omega| vector![omega.x, omega.y, omega.z])
1096            .or_else(|| dominant_source.map(|state| state.angvel))
1097            .unwrap_or_else(Vector::zeros);
1098
1099        let should_sleep =
1100            all_sleeping && linvel.norm_squared() <= 1.0e-4 && angvel.norm_squared() <= 1.0e-4;
1101
1102        (linvel, angvel, should_sleep)
1103    }
1104
1105    fn inherited_child_motion(
1106        &self,
1107        child: &SplitChild,
1108        planning: &SplitPlanningData,
1109        has_support: bool,
1110    ) -> (Vector<Real>, Vector<Real>, bool) {
1111        if has_support {
1112            return (Vector::zeros(), Vector::zeros(), false);
1113        }
1114        let Some(body_handle) = self.dominant_child_body_handle(child, planning) else {
1115            return (Vector::zeros(), Vector::zeros(), false);
1116        };
1117        let Some(state) = planning.parent_states.get(&body_handle) else {
1118            return (Vector::zeros(), Vector::zeros(), false);
1119        };
1120        let should_sleep = state.was_sleeping
1121            && state.linvel.norm_squared() <= 1.0e-4
1122            && state.angvel.norm_squared() <= 1.0e-4;
1123        (state.linvel, state.angvel, should_sleep)
1124    }
1125
1126    fn desired_body_type(has_support: bool) -> RigidBodyType {
1127        if has_support {
1128            RigidBodyType::Fixed
1129        } else {
1130            RigidBodyType::Dynamic
1131        }
1132    }
1133
1134    fn reconcile_reused_body_type(
1135        &self,
1136        body_handle: RigidBodyHandle,
1137        has_support: bool,
1138        bodies: &mut RigidBodySet,
1139    ) -> bool {
1140        let desired = Self::desired_body_type(has_support);
1141        let Some(body) = bodies.get_mut(body_handle) else {
1142            return false;
1143        };
1144        if body.body_type() == desired {
1145            return false;
1146        }
1147        body.set_body_type(desired, true);
1148        if desired == RigidBodyType::Dynamic {
1149            body.wake_up(true);
1150        }
1151        true
1152    }
1153
1154    fn reinitialize_recycled_body(
1155        &self,
1156        body_handle: RigidBodyHandle,
1157        has_support: bool,
1158        target: ChildTargetState,
1159        bodies: &mut RigidBodySet,
1160    ) -> bool {
1161        let desired = Self::desired_body_type(has_support);
1162        let Some(body) = bodies.get_mut(body_handle) else {
1163            return false;
1164        };
1165        let flipped = body.body_type() != desired;
1166        if flipped {
1167            body.set_body_type(desired, true);
1168        }
1169        body.set_position(target.pose, false);
1170        if desired == RigidBodyType::Dynamic {
1171            body.set_linvel(target.linvel, false);
1172            body.set_angvel(target.angvel, false);
1173            if target.should_sleep {
1174                body.sleep();
1175            } else {
1176                body.wake_up(true);
1177            }
1178        }
1179        flipped
1180    }
1181
1182    fn create_body_for_child(
1183        &self,
1184        child: &SplitChild,
1185        target: ChildTargetState,
1186        bodies: &mut RigidBodySet,
1187    ) -> RigidBodyHandle {
1188        let has_support = child.nodes.iter().any(|n| self.support_nodes.contains(n));
1189        if has_support {
1190            bodies.insert(RigidBodyBuilder::fixed().pose(target.pose))
1191        } else {
1192            let handle = bodies.insert(
1193                RigidBodyBuilder::dynamic()
1194                    .pose(target.pose)
1195                    .linvel(target.linvel)
1196                    .angvel(target.angvel)
1197                    .ccd_enabled(self.dynamic_body_ccd_enabled),
1198            );
1199            if target.should_sleep {
1200                if let Some(body) = bodies.get_mut(handle) {
1201                    body.sleep();
1202                }
1203            }
1204            handle
1205        }
1206    }
1207
1208    fn apply_child_kinematics(
1209        &self,
1210        body_handle: RigidBodyHandle,
1211        has_support: bool,
1212        target: ChildTargetState,
1213        bodies: &mut RigidBodySet,
1214    ) {
1215        let Some(body) = bodies.get_mut(body_handle) else {
1216            return;
1217        };
1218        body.set_position(target.pose, false);
1219        if has_support || !body.is_dynamic() {
1220            return;
1221        }
1222        let current_linvel = *body.linvel();
1223        let current_angvel = *body.angvel();
1224        let velocity_changed = (current_linvel - target.linvel).norm_squared() > 1.0e-6
1225            || (current_angvel - target.angvel).norm_squared() > 1.0e-6;
1226        body.set_linvel(target.linvel, false);
1227        body.set_angvel(target.angvel, false);
1228        if target.should_sleep {
1229            body.sleep();
1230        } else if velocity_changed || body.is_sleeping() {
1231            body.wake_up(true);
1232        }
1233    }
1234
1235    fn solve_symmetric_3x3(&self, m: [[f32; 3]; 3], rhs: [f32; 3]) -> Option<Vec3> {
1236        let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
1237            - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
1238            + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
1239        if det.abs() <= 1.0e-6 {
1240            return None;
1241        }
1242        let inv_det = 1.0 / det;
1243        let inv = [
1244            [
1245                (m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_det,
1246                (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * inv_det,
1247                (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_det,
1248            ],
1249            [
1250                (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * inv_det,
1251                (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_det,
1252                (m[0][2] * m[1][0] - m[0][0] * m[1][2]) * inv_det,
1253            ],
1254            [
1255                (m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_det,
1256                (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * inv_det,
1257                (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_det,
1258            ],
1259        ];
1260        Some(Vec3::new(
1261            inv[0][0] * rhs[0] + inv[0][1] * rhs[1] + inv[0][2] * rhs[2],
1262            inv[1][0] * rhs[0] + inv[1][1] * rhs[1] + inv[1][2] * rhs[2],
1263            inv[2][0] * rhs[0] + inv[2][1] * rhs[1] + inv[2][2] * rhs[2],
1264        ))
1265    }
1266
1267    fn attach_node(
1268        &mut self,
1269        node_index: u32,
1270        body: RigidBodyHandle,
1271        collider: ColliderHandle,
1272        local_offset: Vec3,
1273    ) {
1274        let idx = node_index as usize;
1275        self.node_to_body[idx] = Some(body);
1276        self.node_to_collider[idx] = Some(collider);
1277        self.collider_to_node.insert(collider, node_index);
1278        self.node_local_offsets[idx] = local_offset;
1279    }
1280
1281    fn detach_node(&mut self, node_index: u32) {
1282        let idx = node_index as usize;
1283        if let Some(collider) = self.node_to_collider[idx].take() {
1284            self.collider_to_node.remove(&collider);
1285        }
1286        self.node_to_body[idx] = None;
1287        self.node_local_offsets[idx] = Vec3::ZERO;
1288    }
1289
1290    fn update_body_metadata(
1291        &mut self,
1292        body_handle: RigidBodyHandle,
1293        nodes: &[u32],
1294        created_at: f32,
1295    ) {
1296        let has_support = nodes.iter().any(|node| self.support_nodes.contains(node));
1297        self.body_metadata.insert(
1298            body_handle,
1299            BodyMetadata {
1300                created_at,
1301                first_support_contact_at: None,
1302                damping_promoted: false,
1303                sleep_threshold_configured: false,
1304                has_support,
1305            },
1306        );
1307    }
1308
1309    fn refresh_body_metadata(
1310        &mut self,
1311        body_handle: RigidBodyHandle,
1312        nodes: &[u32],
1313        created_at: f32,
1314        reset: bool,
1315    ) {
1316        if reset || !self.body_metadata.contains_key(&body_handle) {
1317            self.update_body_metadata(body_handle, nodes, created_at);
1318            return;
1319        }
1320
1321        let has_support = nodes.iter().any(|node| self.support_nodes.contains(node));
1322        if let Some(metadata) = self.body_metadata.get_mut(&body_handle) {
1323            let support_changed = metadata.has_support != has_support;
1324            metadata.has_support = has_support;
1325            if support_changed && !has_support {
1326                metadata.first_support_contact_at = None;
1327                metadata.damping_promoted = false;
1328                metadata.sleep_threshold_configured = false;
1329            }
1330        }
1331    }
1332
1333    fn promote_small_body_damping(
1334        &mut self,
1335        body_handle: RigidBodyHandle,
1336        bodies: &mut RigidBodySet,
1337        damping: &SmallBodyDampingOptions,
1338    ) {
1339        if damping.mode != OptimizationMode::Always {
1340            return;
1341        }
1342        if self.body_node_count(body_handle) > damping.collider_count_threshold {
1343            return;
1344        }
1345        if Self::apply_small_body_damping(bodies, body_handle, damping) {
1346            if let Some(metadata) = self.body_metadata.get_mut(&body_handle) {
1347                metadata.damping_promoted = true;
1348            }
1349        }
1350    }
1351
1352    fn configure_sleep_thresholds(
1353        &mut self,
1354        body_handle: RigidBodyHandle,
1355        bodies: &mut RigidBodySet,
1356        sleep_thresholds: &SleepThresholdOptions,
1357    ) {
1358        if sleep_thresholds.mode != OptimizationMode::Always {
1359            return;
1360        }
1361        if Self::apply_sleep_thresholds(bodies, body_handle, sleep_thresholds) {
1362            if let Some(metadata) = self.body_metadata.get_mut(&body_handle) {
1363                metadata.sleep_threshold_configured = true;
1364            }
1365        }
1366    }
1367
1368    fn apply_small_body_damping(
1369        bodies: &mut RigidBodySet,
1370        body_handle: RigidBodyHandle,
1371        damping: &SmallBodyDampingOptions,
1372    ) -> bool {
1373        let Some(body) = bodies.get_mut(body_handle) else {
1374            return false;
1375        };
1376        if !body.is_dynamic() {
1377            return false;
1378        }
1379        let mut changed = false;
1380        if body.linear_damping() < damping.min_linear_damping {
1381            body.set_linear_damping(damping.min_linear_damping);
1382            changed = true;
1383        }
1384        if body.angular_damping() < damping.min_angular_damping {
1385            body.set_angular_damping(damping.min_angular_damping);
1386            changed = true;
1387        }
1388        if changed {
1389            body.wake_up(true);
1390        }
1391        changed
1392    }
1393
1394    fn apply_sleep_thresholds(
1395        bodies: &mut RigidBodySet,
1396        body_handle: RigidBodyHandle,
1397        sleep_thresholds: &SleepThresholdOptions,
1398    ) -> bool {
1399        let Some(body) = bodies.get_mut(body_handle) else {
1400            return false;
1401        };
1402        if !body.is_dynamic() {
1403            return false;
1404        }
1405        let activation = body.activation_mut();
1406        let desired_linear = sleep_thresholds.linear_threshold.max(0.0);
1407        let desired_angular = sleep_thresholds.angular_threshold.max(0.0);
1408        let changed = (activation.normalized_linear_threshold - desired_linear).abs()
1409            > Real::EPSILON
1410            || (activation.angular_threshold - desired_angular).abs() > Real::EPSILON;
1411        activation.normalized_linear_threshold = desired_linear;
1412        activation.angular_threshold = desired_angular;
1413        if changed {
1414            body.wake_up(true);
1415        }
1416        changed
1417    }
1418
1419    fn remove_body(
1420        &mut self,
1421        body_handle: RigidBodyHandle,
1422        bodies: &mut RigidBodySet,
1423        colliders: &mut ColliderSet,
1424        island_manager: &mut IslandManager,
1425        impulse_joints: &mut ImpulseJointSet,
1426        multibody_joints: &mut MultibodyJointSet,
1427    ) -> Vec<u32> {
1428        let nodes = self.body_to_nodes.remove(&body_handle).unwrap_or_default();
1429        for node in &nodes {
1430            self.detach_node(*node);
1431        }
1432        self.body_metadata.remove(&body_handle);
1433        if bodies.get(body_handle).is_some() {
1434            bodies.remove(
1435                body_handle,
1436                island_manager,
1437                colliders,
1438                impulse_joints,
1439                multibody_joints,
1440                true,
1441            );
1442        }
1443        nodes
1444    }
1445
1446    fn collect_existing_body_states(
1447        &self,
1448        event: &SplitEvent,
1449        bodies: &RigidBodySet,
1450    ) -> Vec<super::split_migrator::ExistingBodyState> {
1451        let mut parent_bodies = HashSet::new();
1452        for child in &event.children {
1453            for &node in &child.nodes {
1454                if let Some(handle) = self.node_body(node) {
1455                    parent_bodies.insert(handle);
1456                }
1457            }
1458        }
1459
1460        parent_bodies
1461            .into_iter()
1462            .map(|handle| super::split_migrator::ExistingBodyState {
1463                handle,
1464                node_indices: self
1465                    .body_to_nodes
1466                    .get(&handle)
1467                    .map(|nodes| nodes.iter().copied().collect())
1468                    .unwrap_or_default(),
1469                is_fixed: bodies.get(handle).map(|b| !b.is_dynamic()).unwrap_or(false),
1470            })
1471            .collect()
1472    }
1473
1474    fn collect_split_planning_data(
1475        &self,
1476        event: &SplitEvent,
1477        bodies: &RigidBodySet,
1478    ) -> SplitPlanningData {
1479        let mut parent_bodies = HashSet::new();
1480        let mut node_sources = HashMap::new();
1481
1482        for child in &event.children {
1483            for &node in &child.nodes {
1484                let idx = node as usize;
1485                let Some(body_handle) = self.node_to_body.get(idx).copied().flatten() else {
1486                    continue;
1487                };
1488                let Some(body) = bodies.get(body_handle) else {
1489                    continue;
1490                };
1491                let local_offset = self.node_local_offsets[idx];
1492                let world = body.position().transform_point(&point![
1493                    local_offset.x,
1494                    local_offset.y,
1495                    local_offset.z
1496                ]);
1497                let world_velocity = body.velocity_at_point(&world);
1498                parent_bodies.insert(body_handle);
1499                node_sources.entry(node).or_insert(NodeSourceState {
1500                    body_handle,
1501                    collider_handle: self.node_to_collider[idx],
1502                    world_centroid: Vec3::new(world.x, world.y, world.z),
1503                    world_velocity: Vec3::new(world_velocity.x, world_velocity.y, world_velocity.z),
1504                });
1505            }
1506        }
1507
1508        let parent_bodies: Vec<RigidBodyHandle> = parent_bodies.into_iter().collect();
1509        let parent_states = parent_bodies
1510            .iter()
1511            .filter_map(|&handle| {
1512                let body = bodies.get(handle)?;
1513                Some((
1514                    handle,
1515                    BodyState {
1516                        position: *body.position(),
1517                        linvel: *body.linvel(),
1518                        angvel: *body.angvel(),
1519                        was_sleeping: body.is_sleeping(),
1520                    },
1521                ))
1522            })
1523            .collect();
1524
1525        SplitPlanningData {
1526            existing: self.collect_existing_body_states(event, bodies),
1527            parent_bodies,
1528            parent_states,
1529            node_sources,
1530        }
1531    }
1532
1533    fn local_offset_for_pose(
1534        &self,
1535        source: &NodeSourceState,
1536        target_pose: &Isometry<Real>,
1537    ) -> Vec3 {
1538        let local = target_pose.inverse_transform_point(&point![
1539            source.world_centroid.x,
1540            source.world_centroid.y,
1541            source.world_centroid.z
1542        ]);
1543        Vec3::new(local.x, local.y, local.z)
1544    }
1545
1546    fn insert_node_collider(
1547        &self,
1548        node_index: u32,
1549        body_handle: RigidBodyHandle,
1550        local_offset: Vec3,
1551        bodies: &mut RigidBodySet,
1552        colliders: &mut ColliderSet,
1553    ) -> ColliderHandle {
1554        let collider = self.build_node_collider(node_index, local_offset);
1555        colliders.insert_with_parent(collider, body_handle, bodies)
1556    }
1557
1558    fn build_node_collider(&self, node_index: u32, local_offset: Vec3) -> ColliderBuilder {
1559        let ni = node_index as usize;
1560        let fallback_size = self.node_sizes[ni];
1561        let fallback_half = vector![
1562            fallback_size.x * 0.5,
1563            fallback_size.y * 0.5,
1564            fallback_size.z * 0.5
1565        ];
1566        let base = match self.node_colliders.get(ni).and_then(Option::as_ref) {
1567            Some(ScenarioCollider::Cuboid { half_extents }) => {
1568                ColliderBuilder::cuboid(half_extents.x, half_extents.y, half_extents.z)
1569            }
1570            Some(ScenarioCollider::ConvexHull { points }) => {
1571                let hull_points: Vec<Point<Real>> = points
1572                    .iter()
1573                    .map(|point| point![point.x, point.y, point.z])
1574                    .collect();
1575                ColliderBuilder::convex_hull(&hull_points).unwrap_or_else(|| {
1576                    ColliderBuilder::cuboid(fallback_half.x, fallback_half.y, fallback_half.z)
1577                })
1578            }
1579            None => ColliderBuilder::cuboid(fallback_half.x, fallback_half.y, fallback_half.z),
1580        };
1581
1582        let collider = base
1583            .translation(vector![local_offset.x, local_offset.y, local_offset.z])
1584            .active_events(ActiveEvents::CONTACT_FORCE_EVENTS | ActiveEvents::COLLISION_EVENTS)
1585            .active_hooks(ActiveHooks::FILTER_CONTACT_PAIRS | ActiveHooks::FILTER_INTERSECTION_PAIR)
1586            .contact_force_event_threshold(0.0)
1587            .friction(0.25)
1588            .restitution(0.0);
1589
1590        if self.support_nodes.contains(&node_index) {
1591            collider.mass(0.0)
1592        } else {
1593            collider.mass(self.node_masses[ni].max(0.0))
1594        }
1595    }
1596
1597    fn move_node_collider_to_body(
1598        &self,
1599        node_index: u32,
1600        body_handle: RigidBodyHandle,
1601        collider_handle: Option<ColliderHandle>,
1602        local_offset: Vec3,
1603        bodies: &mut RigidBodySet,
1604        colliders: &mut ColliderSet,
1605    ) -> ColliderHandle {
1606        if let Some(collider_handle) = collider_handle {
1607            colliders.set_parent(collider_handle, Some(body_handle), bodies);
1608            if let Some(collider) = colliders.get_mut(collider_handle) {
1609                collider.set_position_wrt_parent(Isometry::translation(
1610                    local_offset.x,
1611                    local_offset.y,
1612                    local_offset.z,
1613                ));
1614            }
1615            collider_handle
1616        } else {
1617            self.insert_node_collider(node_index, body_handle, local_offset, bodies, colliders)
1618        }
1619    }
1620
1621    fn update_node_collider_local_pose(
1622        &self,
1623        collider_handle: Option<ColliderHandle>,
1624        local_offset: Vec3,
1625        colliders: &mut ColliderSet,
1626    ) -> ColliderHandle {
1627        let collider_handle = collider_handle.expect("existing node collider should exist");
1628        if let Some(collider) = colliders.get_mut(collider_handle) {
1629            collider.set_position_wrt_parent(Isometry::translation(
1630                local_offset.x,
1631                local_offset.y,
1632                local_offset.z,
1633            ));
1634        }
1635        collider_handle
1636    }
1637
1638    fn apply_collision_groups(
1639        &self,
1640        body_handle: RigidBodyHandle,
1641        bodies: &RigidBodySet,
1642        colliders: &mut ColliderSet,
1643        max_colliders_for_debris: usize,
1644    ) {
1645        apply_collision_groups_for_body(
1646            body_handle,
1647            bodies,
1648            colliders,
1649            self.ground_body_handle,
1650            self.debris_collision_mode,
1651            self.debris_collision_active(body_handle),
1652            max_colliders_for_debris,
1653        );
1654    }
1655
1656    fn debris_collision_active(&self, body_handle: RigidBodyHandle) -> bool {
1657        self.body_metadata
1658            .get(&body_handle)
1659            .and_then(|metadata| metadata.first_support_contact_at)
1660            .is_some()
1661    }
1662}