Skip to main content

blast_stress_solver/rapier/
destructible.rs

1use std::collections::{HashMap, VecDeque};
2
3use rapier3d::prelude::*;
4
5#[cfg(not(target_arch = "wasm32"))]
6use std::time::Instant;
7
8// `std::time::Instant::now()` panics on `wasm32-unknown-unknown` because the
9// target has no monotonic clock. These timings are diagnostic-only, so wasm
10// uses a zero-duration shim instead of crashing.
11#[cfg(target_arch = "wasm32")]
12#[derive(Clone, Copy)]
13struct Instant;
14
15#[cfg(target_arch = "wasm32")]
16impl Instant {
17    fn now() -> Self {
18        Self
19    }
20
21    fn elapsed(&self) -> std::time::Duration {
22        std::time::Duration::ZERO
23    }
24}
25
26use crate::ext_stress_solver::ExtStressSolver;
27use crate::types::*;
28
29use super::body_tracker::{BodyTracker, SplitEditStats};
30use super::collision_groups::DebrisCollisionMode;
31use super::fracture_policy::FracturePolicy;
32use super::optimization::SleepThresholdOptions;
33use super::optimization::{DebrisCleanupOptions, OptimizationResult, SmallBodyDampingOptions};
34use super::resimulation::{BodySnapshots, ResimulationOptions};
35
36#[derive(Clone, Debug, Default)]
37pub struct SplitCohort {
38    pub source_bodies: Vec<RigidBodyHandle>,
39    pub target_bodies: Vec<RigidBodyHandle>,
40    pub target_sources: Vec<(RigidBodyHandle, RigidBodyHandle)>,
41}
42
43/// Configuration for creating a `DestructibleSet`.
44pub struct DestructibleConfig {
45    pub nodes: Vec<NodeDesc>,
46    pub bonds: Vec<BondDesc>,
47    pub node_sizes: Vec<Vec3>,
48    pub node_colliders: Vec<Option<ScenarioCollider>>,
49    pub solver_settings: SolverSettings,
50    pub gravity: Vec3,
51    pub fracture_policy: FracturePolicy,
52    pub resimulation: ResimulationOptions,
53    pub skip_single_bodies: bool,
54    pub sleep_thresholds: SleepThresholdOptions,
55    pub small_body_damping: SmallBodyDampingOptions,
56    pub debris_cleanup: DebrisCleanupOptions,
57    pub dynamic_body_ccd_enabled: bool,
58}
59
60/// Result of a single step.
61#[derive(Clone, Debug, Default)]
62pub struct StepResult {
63    /// Number of bonds that broke this frame.
64    pub fractures: usize,
65    /// Number of new rigid bodies created this frame.
66    pub new_bodies: usize,
67    /// Number of split events that occurred.
68    pub split_events: usize,
69    /// Whether the solver converged.
70    pub converged: bool,
71    /// Detailed split-edit instrumentation for this step.
72    pub split_edits: SplitEditStats,
73    /// Time spent sanitizing split events.
74    pub split_sanitize_ms: f32,
75    /// Time spent estimating split admission cost.
76    pub split_estimate_ms: f32,
77    /// Cohorts of body handles that were produced by the same split this step.
78    pub split_cohorts: Vec<SplitCohort>,
79}
80
81/// Main orchestrator for destructible structures with Rapier integration.
82///
83/// Manages the stress solver and the mapping between solver actors and Rapier bodies.
84/// Call `step()` each frame to advance the simulation.
85pub struct DestructibleSet {
86    solver: ExtStressSolver,
87    tracker: BodyTracker,
88    policy: FracturePolicy,
89    gravity: Vec3,
90    initialized: bool,
91    resimulation: ResimulationOptions,
92    skip_single_bodies: bool,
93    sleep_thresholds: SleepThresholdOptions,
94    small_body_damping: SmallBodyDampingOptions,
95    debris_cleanup: DebrisCleanupOptions,
96    dynamic_body_ccd_enabled: bool,
97    debris_collision_mode: DebrisCollisionMode,
98    ground_body_handle: Option<RigidBodyHandle>,
99    bond_table: Vec<BondDesc>,
100    node_bonds: Vec<Vec<u32>>,
101    removed_bonds: Vec<bool>,
102    destroyed_nodes: Vec<bool>,
103    pending_split_events: VecDeque<SplitEvent>,
104    /// Track whether forces were applied this frame (for idle skip).
105    forces_applied: bool,
106    /// Number of frames since the last fracture (for idle skip).
107    frames_since_fracture: u32,
108}
109
110impl DestructibleSet {
111    /// Create a new destructible set.
112    ///
113    /// You must call `initialize()` after creating the Rapier world to set up the initial bodies.
114    pub fn new(config: DestructibleConfig) -> Option<Self> {
115        let solver = ExtStressSolver::new(&config.nodes, &config.bonds, &config.solver_settings)?;
116        let node_count = config.nodes.len();
117        let mut node_bonds = vec![Vec::new(); node_count];
118        for (bond_index, bond) in config.bonds.iter().enumerate() {
119            if let Some(list) = node_bonds.get_mut(bond.node0 as usize) {
120                list.push(bond_index as u32);
121            }
122            if let Some(list) = node_bonds.get_mut(bond.node1 as usize) {
123                list.push(bond_index as u32);
124            }
125        }
126
127        let scenario_nodes: Vec<ScenarioNode> = config
128            .nodes
129            .iter()
130            .map(|n| ScenarioNode {
131                centroid: n.centroid,
132                mass: n.mass,
133                volume: n.volume,
134            })
135            .collect();
136
137        let tracker = BodyTracker::new(&scenario_nodes, config.node_sizes, config.node_colliders);
138        let mut tracker = tracker;
139        tracker.set_dynamic_body_ccd_enabled(config.dynamic_body_ccd_enabled);
140
141        Some(Self {
142            solver,
143            tracker,
144            policy: config.fracture_policy,
145            gravity: config.gravity,
146            initialized: false,
147            resimulation: config.resimulation,
148            skip_single_bodies: config.skip_single_bodies,
149            sleep_thresholds: config.sleep_thresholds,
150            small_body_damping: config.small_body_damping,
151            debris_cleanup: config.debris_cleanup,
152            dynamic_body_ccd_enabled: config.dynamic_body_ccd_enabled,
153            debris_collision_mode: DebrisCollisionMode::All,
154            ground_body_handle: None,
155            bond_table: config.bonds,
156            node_bonds,
157            removed_bonds: vec![false; node_count],
158            destroyed_nodes: vec![false; node_count],
159            pending_split_events: VecDeque::new(),
160            forces_applied: false,
161            frames_since_fracture: u32::MAX,
162        })
163    }
164
165    /// Create a destructible set from a scenario description.
166    ///
167    /// Node sizes are estimated from volume (cube root).
168    pub fn from_scenario(
169        scenario: &ScenarioDesc,
170        settings: SolverSettings,
171        gravity: Vec3,
172        policy: FracturePolicy,
173    ) -> Option<Self> {
174        let (nodes, bonds) = scenario.to_solver_descs();
175
176        let node_sizes: Vec<Vec3> = scenario
177            .node_sizes
178            .iter()
179            .copied()
180            .chain(
181                scenario
182                    .nodes
183                    .iter()
184                    .skip(scenario.node_sizes.len())
185                    .map(|n| {
186                        let side = n.volume.cbrt().max(0.01);
187                        Vec3::new(side, side, side)
188                    }),
189            )
190            .collect();
191        let node_colliders: Vec<Option<ScenarioCollider>> = scenario
192            .collider_shapes
193            .iter()
194            .cloned()
195            .chain(
196                scenario
197                    .nodes
198                    .iter()
199                    .skip(scenario.collider_shapes.len())
200                    .map(|_| None),
201            )
202            .collect();
203
204        Self::new(DestructibleConfig {
205            nodes,
206            bonds,
207            node_sizes,
208            node_colliders,
209            solver_settings: settings,
210            gravity,
211            fracture_policy: policy,
212            resimulation: ResimulationOptions::default(),
213            skip_single_bodies: false,
214            sleep_thresholds: SleepThresholdOptions::default(),
215            small_body_damping: SmallBodyDampingOptions::default(),
216            debris_cleanup: DebrisCleanupOptions::default(),
217            dynamic_body_ccd_enabled: false,
218        })
219    }
220
221    /// Set up initial Rapier bodies from the solver's actor table.
222    ///
223    /// Must be called once before the first `step()`.
224    pub fn initialize(
225        &mut self,
226        bodies: &mut RigidBodySet,
227        colliders: &mut ColliderSet,
228    ) -> Vec<RigidBodyHandle> {
229        let actors = self.solver.actors();
230        let handles = self.tracker.create_initial_bodies(
231            &actors,
232            bodies,
233            colliders,
234            0.0,
235            self.small_body_damping,
236            self.sleep_thresholds,
237            self.debris_cleanup.max_colliders_for_debris,
238        );
239        self.initialized = true;
240        handles
241    }
242
243    /// Run one simulation step:
244    /// 1. Apply gravity
245    /// 2. Update stress solver
246    /// 3. Generate and apply fractures (rate-limited by policy)
247    /// 4. Create/destroy Rapier bodies for split events
248    /// 5. Apply excess forces to newly separated actors
249    pub fn step(
250        &mut self,
251        bodies: &mut RigidBodySet,
252        colliders: &mut ColliderSet,
253        island_manager: &mut IslandManager,
254        impulse_joints: &mut ImpulseJointSet,
255        multibody_joints: &mut MultibodyJointSet,
256    ) -> StepResult {
257        self.step_with_time(
258            0.0,
259            bodies,
260            colliders,
261            island_manager,
262            impulse_joints,
263            multibody_joints,
264        )
265    }
266
267    pub fn step_with_time(
268        &mut self,
269        now_secs: f32,
270        bodies: &mut RigidBodySet,
271        colliders: &mut ColliderSet,
272        island_manager: &mut IslandManager,
273        impulse_joints: &mut ImpulseJointSet,
274        multibody_joints: &mut MultibodyJointSet,
275    ) -> StepResult {
276        assert!(self.initialized, "call initialize() before step()");
277
278        let mut result = StepResult::default();
279        let mut remaining_new_bodies = self.policy.clamp_new_bodies(usize::MAX);
280        let mut remaining_collider_migrations = self.policy.clamp_collider_migrations(usize::MAX);
281
282        self.process_pending_split_events(
283            now_secs,
284            bodies,
285            colliders,
286            island_manager,
287            impulse_joints,
288            multibody_joints,
289            &mut remaining_new_bodies,
290            &mut remaining_collider_migrations,
291            &mut result,
292        );
293
294        // Idle skip optimization
295        if self.policy.idle_skip
296            && !self.forces_applied
297            && self.frames_since_fracture > 2
298            && self.pending_split_events.is_empty()
299        {
300            return result;
301        }
302
303        // 1. Apply gravity
304        self.solver.add_gravity(self.gravity);
305        self.forces_applied = false;
306
307        // 2. Update solver
308        self.solver.update();
309        result.converged = self.solver.converged();
310
311        // 3. Check for overstressed bonds
312        let overstressed = self.solver.overstressed_bond_count();
313        if overstressed == 0 {
314            self.frames_since_fracture = self.frames_since_fracture.saturating_add(1);
315            return result;
316        }
317
318        // Check policy: should we suppress fractures?
319        let dynamic_count = self.tracker.dynamic_body_count(bodies);
320        if self.policy.should_suppress(dynamic_count) {
321            return result;
322        }
323
324        // 4. Generate fracture commands
325        let mut commands = self.solver.generate_fracture_commands();
326        if commands.is_empty() {
327            return result;
328        }
329
330        // Rate-limit fractures
331        let max_fractures = self.policy.max_fractures_per_frame;
332        if max_fractures > 0 {
333            let mut total = 0usize;
334            for cmd in &mut commands {
335                let remaining = (max_fractures as usize).saturating_sub(total);
336                if remaining == 0 {
337                    cmd.bond_fractures.clear();
338                } else if cmd.bond_fractures.len() > remaining {
339                    cmd.bond_fractures.truncate(remaining);
340                }
341                total += cmd.bond_fractures.len();
342            }
343            commands.retain(|c| !c.bond_fractures.is_empty());
344        }
345
346        result.fractures = commands.iter().map(|c| c.bond_fractures.len()).sum();
347
348        self.mark_bonds_removed(&commands);
349
350        // 5. Apply fracture commands
351        let events = self.solver.apply_fracture_commands(&commands);
352        result.split_events += events.len();
353        self.pending_split_events.extend(events);
354        self.process_pending_split_events(
355            now_secs,
356            bodies,
357            colliders,
358            island_manager,
359            impulse_joints,
360            multibody_joints,
361            &mut remaining_new_bodies,
362            &mut remaining_collider_migrations,
363            &mut result,
364        );
365
366        // Optional: kick separated actors with solver-reported excess forces.
367        if self.policy.apply_excess_forces {
368            self.apply_excess_forces(bodies);
369        }
370
371        self.frames_since_fracture = 0;
372        result
373    }
374
375    /// Apply an external force to a node (e.g., from a projectile impact).
376    pub fn add_force(&mut self, node_index: u32, position: Vec3, force: Vec3) {
377        self.solver
378            .add_force(node_index, position, force, ForceMode::Force);
379        self.forces_applied = true;
380    }
381
382    /// Apply an external acceleration to a node.
383    pub fn add_acceleration(&mut self, node_index: u32, position: Vec3, acceleration: Vec3) {
384        self.solver
385            .add_force(node_index, position, acceleration, ForceMode::Acceleration);
386        self.forces_applied = true;
387    }
388
389    /// Get the Rapier body handle for a node.
390    pub fn node_body(&self, node_index: u32) -> Option<RigidBodyHandle> {
391        self.tracker.node_body(node_index)
392    }
393
394    /// Get the Rapier collider handle for a node.
395    pub fn node_collider(&self, node_index: u32) -> Option<ColliderHandle> {
396        self.tracker.node_collider(node_index)
397    }
398
399    /// Get the node index owning a collider.
400    pub fn collider_node(&self, collider: ColliderHandle) -> Option<u32> {
401        self.tracker.collider_node(collider)
402    }
403
404    /// Get the node's local offset relative to its owning Rapier body.
405    pub fn node_local_offset(&self, node_index: u32) -> Option<Vec3> {
406        self.tracker.node_local_offset(node_index)
407    }
408
409    /// Get the node indices attached to a Rapier body.
410    pub fn body_nodes(&self, body_handle: RigidBodyHandle) -> Vec<u32> {
411        self.tracker.body_nodes(body_handle)
412    }
413
414    pub fn body_nodes_slice(&self, body_handle: RigidBodyHandle) -> &[u32] {
415        self.tracker.body_nodes_slice(body_handle)
416    }
417
418    pub fn body_node_count(&self, body_handle: RigidBodyHandle) -> usize {
419        self.tracker.body_node_count(body_handle)
420    }
421
422    pub fn body_has_support(&self, body_handle: RigidBodyHandle) -> bool {
423        self.tracker.body_has_support(body_handle)
424    }
425
426    /// Whether a node is a fixed support.
427    pub fn is_support(&self, node_index: u32) -> bool {
428        self.tracker.is_support(node_index)
429    }
430
431    /// Current actor count in the stress graph.
432    pub fn actor_count(&self) -> u32 {
433        self.solver.actor_count()
434    }
435
436    /// Number of bonds still considered active by the Rust-side fracture tracking.
437    pub fn active_bond_count(&self) -> usize {
438        self.removed_bonds
439            .iter()
440            .filter(|removed| !**removed)
441            .count()
442    }
443
444    /// Number of tracked Rapier bodies.
445    pub fn body_count(&self) -> usize {
446        self.tracker.body_count()
447    }
448
449    pub fn collider_count(&self) -> usize {
450        self.tracker.collider_count()
451    }
452
453    pub fn dynamic_body_count(&self, bodies: &RigidBodySet) -> usize {
454        self.tracker.dynamic_body_count(bodies)
455    }
456
457    pub fn awake_dynamic_body_count(&self, bodies: &RigidBodySet) -> usize {
458        self.tracker.awake_dynamic_body_count(bodies)
459    }
460
461    pub fn sleeping_dynamic_body_count(&self, bodies: &RigidBodySet) -> usize {
462        self.tracker.sleeping_dynamic_body_count(bodies)
463    }
464
465    pub fn support_body_count(&self) -> usize {
466        self.tracker.support_body_count()
467    }
468
469    pub fn ccd_enabled_body_count(&self, bodies: &RigidBodySet) -> usize {
470        self.tracker.ccd_enabled_body_count(bodies)
471    }
472
473    /// Access the underlying solver for advanced use.
474    pub fn solver(&self) -> &ExtStressSolver {
475        &self.solver
476    }
477
478    /// Mutable access to the underlying solver.
479    pub fn solver_mut(&mut self) -> &mut ExtStressSolver {
480        &mut self.solver
481    }
482
483    /// Access the fracture policy.
484    pub fn policy(&self) -> &FracturePolicy {
485        &self.policy
486    }
487
488    /// Mutable access to the fracture policy.
489    pub fn set_policy(&mut self, policy: FracturePolicy) {
490        self.policy = policy;
491    }
492
493    pub fn resimulation_options(&self) -> ResimulationOptions {
494        self.resimulation
495    }
496
497    pub fn set_resimulation_options(&mut self, options: ResimulationOptions) {
498        self.resimulation = options;
499    }
500
501    pub fn skip_single_bodies(&self) -> bool {
502        self.skip_single_bodies
503    }
504
505    pub fn set_skip_single_bodies(&mut self, enabled: bool) {
506        self.skip_single_bodies = enabled;
507    }
508
509    pub fn sleep_thresholds(&self) -> SleepThresholdOptions {
510        self.sleep_thresholds
511    }
512
513    pub fn set_sleep_thresholds(&mut self, options: SleepThresholdOptions) {
514        self.sleep_thresholds = options;
515    }
516
517    pub fn small_body_damping(&self) -> SmallBodyDampingOptions {
518        self.small_body_damping
519    }
520
521    pub fn set_small_body_damping(&mut self, options: SmallBodyDampingOptions) {
522        self.small_body_damping = options;
523    }
524
525    pub fn debris_cleanup(&self) -> DebrisCleanupOptions {
526        self.debris_cleanup
527    }
528
529    pub fn set_debris_cleanup(&mut self, options: DebrisCleanupOptions) {
530        self.debris_cleanup = options;
531    }
532
533    pub fn debris_collision_mode(&self) -> DebrisCollisionMode {
534        self.debris_collision_mode
535    }
536
537    pub fn set_debris_collision_mode(&mut self, mode: DebrisCollisionMode) {
538        self.debris_collision_mode = mode;
539        self.tracker.set_debris_collision_mode(mode);
540    }
541
542    pub fn ground_body_handle(&self) -> Option<RigidBodyHandle> {
543        self.ground_body_handle
544    }
545
546    pub fn set_ground_body_handle(&mut self, handle: Option<RigidBodyHandle>) {
547        self.ground_body_handle = handle;
548        self.tracker.set_ground_body_handle(handle);
549    }
550
551    pub fn refresh_collision_groups(&self, bodies: &RigidBodySet, colliders: &mut ColliderSet) {
552        self.tracker.refresh_collision_groups(
553            bodies,
554            colliders,
555            self.debris_cleanup.max_colliders_for_debris,
556        );
557    }
558
559    pub fn dynamic_body_ccd_enabled(&self) -> bool {
560        self.dynamic_body_ccd_enabled
561    }
562
563    pub fn set_dynamic_body_ccd_enabled(&mut self, enabled: bool) {
564        self.dynamic_body_ccd_enabled = enabled;
565        self.tracker.set_dynamic_body_ccd_enabled(enabled);
566    }
567
568    pub fn set_split_child_recentering_enabled(&mut self, enabled: bool) {
569        self.tracker.set_split_child_recentering_enabled(enabled);
570    }
571
572    pub fn set_split_child_velocity_fit_enabled(&mut self, enabled: bool) {
573        self.tracker.set_split_child_velocity_fit_enabled(enabled);
574    }
575
576    pub fn pending_split_event_count(&self) -> usize {
577        self.pending_split_events.len()
578    }
579
580    pub fn pending_new_body_count(&self, bodies: &RigidBodySet) -> usize {
581        self.pending_split_events
582            .iter()
583            .map(|event| {
584                self.tracker
585                    .estimate_split_cost(event, bodies)
586                    .create_bodies
587            })
588            .sum()
589    }
590
591    pub fn pending_collider_migration_count(&self, bodies: &RigidBodySet) -> usize {
592        self.pending_split_events
593            .iter()
594            .map(|event| {
595                self.tracker
596                    .estimate_split_cost(event, bodies)
597                    .collider_migrations
598            })
599            .sum()
600    }
601
602    pub fn needs_resimulation_snapshot(&self) -> bool {
603        if !self.resimulation.enabled {
604            return false;
605        }
606
607        !(self.policy.idle_skip
608            && !self.forces_applied
609            && self.frames_since_fracture > 2
610            && self.pending_split_events.is_empty())
611    }
612
613    pub fn capture_resimulation_snapshot(&self, bodies: &RigidBodySet) -> BodySnapshots {
614        BodySnapshots::capture(bodies)
615    }
616
617    pub fn mark_body_support_contact(
618        &mut self,
619        body_handle: RigidBodyHandle,
620        now_secs: f32,
621        bodies: &mut RigidBodySet,
622        colliders: &mut ColliderSet,
623    ) -> bool {
624        self.tracker.mark_support_contact(
625            body_handle,
626            now_secs,
627            bodies,
628            colliders,
629            self.small_body_damping,
630            self.sleep_thresholds,
631            self.debris_cleanup.max_colliders_for_debris,
632        )
633    }
634
635    pub fn process_optimizations(
636        &mut self,
637        now_secs: f32,
638        bodies: &mut RigidBodySet,
639        colliders: &mut ColliderSet,
640        island_manager: &mut IslandManager,
641        impulse_joints: &mut ImpulseJointSet,
642        multibody_joints: &mut MultibodyJointSet,
643    ) -> OptimizationResult {
644        let result = self.tracker.cleanup_expired_bodies(
645            now_secs,
646            self.debris_cleanup,
647            bodies,
648            colliders,
649            island_manager,
650            impulse_joints,
651            multibody_joints,
652        );
653        if !result.removed_nodes.is_empty() {
654            self.destroy_nodes_in_solver(&result.removed_nodes);
655        }
656        result
657    }
658
659    fn apply_excess_forces(&self, bodies: &mut RigidBodySet) {
660        let actors = self.solver.actors();
661        for actor in &actors {
662            if actor.nodes.is_empty() {
663                continue;
664            }
665            // Find the body for this actor
666            let body_handle = match self.tracker.node_body(actor.nodes[0]) {
667                Some(h) => h,
668                None => continue,
669            };
670            let body = match bodies.get(body_handle) {
671                Some(b) if b.is_dynamic() => b,
672                _ => continue,
673            };
674            let pos = body.translation();
675            let com = Vec3::new(pos.x, pos.y, pos.z);
676
677            if let Some((force, torque)) = self.solver.get_excess_forces(actor.actor_index, com) {
678                let force_mag = force.magnitude_squared();
679                let torque_mag = torque.magnitude_squared();
680                if force_mag > 1.0e-6 || torque_mag > 1.0e-6 {
681                    if let Some(body_mut) = bodies.get_mut(body_handle) {
682                        body_mut.add_force(vector![force.x, force.y, force.z], true);
683                        body_mut.add_torque(vector![torque.x, torque.y, torque.z], true);
684                    }
685                }
686            }
687        }
688    }
689
690    fn process_pending_split_events(
691        &mut self,
692        now_secs: f32,
693        bodies: &mut RigidBodySet,
694        colliders: &mut ColliderSet,
695        island_manager: &mut IslandManager,
696        impulse_joints: &mut ImpulseJointSet,
697        multibody_joints: &mut MultibodyJointSet,
698        remaining_new_bodies: &mut usize,
699        remaining_collider_migrations: &mut usize,
700        result: &mut StepResult,
701    ) {
702        let pending_len = self.pending_split_events.len();
703
704        for _ in 0..pending_len {
705            let Some(event) = self.pending_split_events.pop_front() else {
706                break;
707            };
708            let sanitize_started_at = Instant::now();
709            let Some(filtered_event) = self.sanitize_split_event(
710                &event,
711                bodies,
712                colliders,
713                island_manager,
714                impulse_joints,
715                multibody_joints,
716            ) else {
717                result.split_sanitize_ms +=
718                    sanitize_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
719                continue;
720            };
721            result.split_sanitize_ms +=
722                sanitize_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
723
724            if !self.policy.split_budgets_unlimited() {
725                let estimate_started_at = Instant::now();
726                let cost = self.tracker.estimate_split_cost(&filtered_event, bodies);
727                result.split_estimate_ms +=
728                    estimate_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
729                if cost.create_bodies > *remaining_new_bodies
730                    || cost.collider_migrations > *remaining_collider_migrations
731                {
732                    self.pending_split_events.push_front(filtered_event);
733                    break;
734                }
735
736                *remaining_new_bodies = (*remaining_new_bodies).saturating_sub(cost.create_bodies);
737                *remaining_collider_migrations =
738                    (*remaining_collider_migrations).saturating_sub(cost.collider_migrations);
739            }
740
741            let split_result = self.tracker.handle_split(
742                &filtered_event,
743                bodies,
744                colliders,
745                island_manager,
746                impulse_joints,
747                multibody_joints,
748                now_secs,
749                self.small_body_damping,
750                self.sleep_thresholds,
751                self.debris_cleanup.max_colliders_for_debris,
752            );
753
754            result.new_bodies += split_result.new_handles.len();
755            result.split_edits.plan_ms += split_result.stats.plan_ms;
756            result.split_edits.apply_ms += split_result.stats.apply_ms;
757            result.split_edits.child_pose_ms += split_result.stats.child_pose_ms;
758            result.split_edits.velocity_fit_ms += split_result.stats.velocity_fit_ms;
759            result.split_edits.sleep_init_ms += split_result.stats.sleep_init_ms;
760            result.split_edits.body_create_ms += split_result.stats.body_create_ms;
761            result.split_edits.collider_move_ms += split_result.stats.collider_move_ms;
762            result.split_edits.collider_insert_ms += split_result.stats.collider_insert_ms;
763            result.split_edits.body_retire_ms += split_result.stats.body_retire_ms;
764            result.split_edits.reused_bodies += split_result.stats.reused_bodies;
765            result.split_edits.recycled_bodies += split_result.stats.recycled_bodies;
766            result.split_edits.created_bodies += split_result.stats.created_bodies;
767            result.split_edits.retired_bodies += split_result.stats.retired_bodies;
768            result.split_edits.body_type_flips += split_result.stats.body_type_flips;
769            result.split_edits.moved_colliders += split_result.stats.moved_colliders;
770            result.split_edits.inserted_colliders += split_result.stats.inserted_colliders;
771            result.split_edits.removed_colliders += split_result.stats.removed_colliders;
772            if split_result.cohort_handles.len() > 1 {
773                result.split_cohorts.push(SplitCohort {
774                    source_bodies: split_result.source_handles,
775                    target_bodies: split_result.cohort_handles,
776                    target_sources: split_result.restore_sources,
777                });
778            }
779        }
780    }
781
782    fn sanitize_split_event(
783        &mut self,
784        event: &SplitEvent,
785        bodies: &mut RigidBodySet,
786        colliders: &mut ColliderSet,
787        island_manager: &mut IslandManager,
788        impulse_joints: &mut ImpulseJointSet,
789        multibody_joints: &mut MultibodyJointSet,
790    ) -> Option<SplitEvent> {
791        let mut destroyed_nodes = Vec::new();
792        let mut children = Vec::new();
793
794        for child in &event.children {
795            let filtered_nodes: Vec<u32> = child
796                .nodes
797                .iter()
798                .copied()
799                .filter(|node| !self.destroyed_nodes[*node as usize])
800                .collect();
801            if filtered_nodes.is_empty() {
802                continue;
803            }
804
805            let is_child_support = filtered_nodes
806                .iter()
807                .any(|node| self.tracker.is_support(*node));
808            let should_destroy =
809                (!is_child_support && self.skip_single_bodies && filtered_nodes.len() <= 1)
810                    || (!is_child_support
811                        && self.policy.min_child_node_count > 1
812                        && filtered_nodes.len() < self.policy.min_child_node_count as usize);
813
814            if should_destroy {
815                destroyed_nodes.extend(filtered_nodes);
816            } else {
817                children.push(SplitChild {
818                    actor_index: child.actor_index,
819                    nodes: filtered_nodes,
820                });
821            }
822        }
823
824        if !destroyed_nodes.is_empty() {
825            let removed_nodes = self.tracker.destroy_nodes(
826                &destroyed_nodes,
827                bodies,
828                colliders,
829                island_manager,
830                impulse_joints,
831                multibody_joints,
832            );
833            self.destroy_nodes_in_solver(&removed_nodes);
834        }
835
836        if children.is_empty() {
837            None
838        } else {
839            Some(SplitEvent {
840                parent_actor_index: event.parent_actor_index,
841                children,
842            })
843        }
844    }
845
846    fn destroy_nodes_in_solver(&mut self, nodes: &[u32]) {
847        let mut targets = Vec::new();
848        for &node in nodes {
849            let idx = node as usize;
850            if idx >= self.destroyed_nodes.len() || self.destroyed_nodes[idx] {
851                continue;
852            }
853            self.destroyed_nodes[idx] = true;
854            targets.push(node);
855        }
856        if targets.is_empty() {
857            return;
858        }
859
860        let actors = self.solver.actors();
861        let mut node_to_actor = HashMap::new();
862        for actor in &actors {
863            for &node in &actor.nodes {
864                node_to_actor.insert(node, actor.actor_index);
865            }
866        }
867
868        let mut commands_by_actor: HashMap<u32, Vec<BondFracture>> = HashMap::new();
869        for &node in &targets {
870            for &bond_index in &self.node_bonds[node as usize] {
871                let bond_index = bond_index as usize;
872                if self.removed_bonds.get(bond_index).copied().unwrap_or(true) {
873                    continue;
874                }
875                self.removed_bonds[bond_index] = true;
876                let bond = self.bond_table[bond_index];
877                let actor_index = node_to_actor
878                    .get(&bond.node0)
879                    .copied()
880                    .or_else(|| node_to_actor.get(&bond.node1).copied());
881                let Some(actor_index) = actor_index else {
882                    continue;
883                };
884                commands_by_actor
885                    .entry(actor_index)
886                    .or_default()
887                    .push(BondFracture {
888                        userdata: bond_index as u32,
889                        node_index0: bond.node0,
890                        node_index1: bond.node1,
891                        health: 0.0,
892                    });
893            }
894        }
895
896        if commands_by_actor.is_empty() {
897            return;
898        }
899
900        let commands: Vec<FractureCommand> = commands_by_actor
901            .into_iter()
902            .map(|(actor_index, bond_fractures)| FractureCommand {
903                actor_index,
904                bond_fractures,
905            })
906            .collect();
907        let events = self.solver.apply_fracture_commands(&commands);
908        self.pending_split_events.extend(events);
909    }
910
911    fn mark_bonds_removed(&mut self, commands: &[FractureCommand]) {
912        for command in commands {
913            for fracture in &command.bond_fractures {
914                let bond_index = self.resolve_bond_index(
915                    fracture.userdata,
916                    fracture.node_index0,
917                    fracture.node_index1,
918                );
919                if let Some(idx) = bond_index {
920                    if let Some(removed) = self.removed_bonds.get_mut(idx) {
921                        *removed = true;
922                    }
923                }
924            }
925        }
926    }
927
928    fn resolve_bond_index(&self, userdata: u32, node0: u32, node1: u32) -> Option<usize> {
929        let idx = userdata as usize;
930        if self
931            .bond_table
932            .get(idx)
933            .is_some_and(|bond| Self::bond_matches(bond, node0, node1))
934        {
935            return Some(idx);
936        }
937
938        self.node_bonds
939            .get(node0 as usize)
940            .into_iter()
941            .flatten()
942            .find_map(|bond_index| {
943                let idx = *bond_index as usize;
944                self.bond_table
945                    .get(idx)
946                    .filter(|bond| Self::bond_matches(bond, node0, node1))
947                    .map(|_| idx)
948            })
949    }
950
951    fn bond_matches(bond: &BondDesc, node0: u32, node1: u32) -> bool {
952        (bond.node0 == node0 && bond.node1 == node1) || (bond.node0 == node1 && bond.node1 == node0)
953    }
954}