1use std::collections::{HashMap, VecDeque};
2
3use rapier3d::prelude::*;
4
5#[cfg(not(target_arch = "wasm32"))]
6use std::time::Instant;
7
8#[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
43pub 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#[derive(Clone, Debug, Default)]
62pub struct StepResult {
63 pub fractures: usize,
65 pub new_bodies: usize,
67 pub split_events: usize,
69 pub converged: bool,
71 pub split_edits: SplitEditStats,
73 pub split_sanitize_ms: f32,
75 pub split_estimate_ms: f32,
77 pub split_cohorts: Vec<SplitCohort>,
79}
80
81pub 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 forces_applied: bool,
106 frames_since_fracture: u32,
108}
109
110impl DestructibleSet {
111 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 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 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 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 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 self.solver.add_gravity(self.gravity);
305 self.forces_applied = false;
306
307 self.solver.update();
309 result.converged = self.solver.converged();
310
311 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 let dynamic_count = self.tracker.dynamic_body_count(bodies);
320 if self.policy.should_suppress(dynamic_count) {
321 return result;
322 }
323
324 let mut commands = self.solver.generate_fracture_commands();
326 if commands.is_empty() {
327 return result;
328 }
329
330 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 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 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 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 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 pub fn node_body(&self, node_index: u32) -> Option<RigidBodyHandle> {
391 self.tracker.node_body(node_index)
392 }
393
394 pub fn node_collider(&self, node_index: u32) -> Option<ColliderHandle> {
396 self.tracker.node_collider(node_index)
397 }
398
399 pub fn collider_node(&self, collider: ColliderHandle) -> Option<u32> {
401 self.tracker.collider_node(collider)
402 }
403
404 pub fn node_local_offset(&self, node_index: u32) -> Option<Vec3> {
406 self.tracker.node_local_offset(node_index)
407 }
408
409 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 pub fn is_support(&self, node_index: u32) -> bool {
428 self.tracker.is_support(node_index)
429 }
430
431 pub fn actor_count(&self) -> u32 {
433 self.solver.actor_count()
434 }
435
436 pub fn active_bond_count(&self) -> usize {
438 self.removed_bonds
439 .iter()
440 .filter(|removed| !**removed)
441 .count()
442 }
443
444 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 pub fn solver(&self) -> &ExtStressSolver {
475 &self.solver
476 }
477
478 pub fn solver_mut(&mut self) -> &mut ExtStressSolver {
480 &mut self.solver
481 }
482
483 pub fn policy(&self) -> &FracturePolicy {
485 &self.policy
486 }
487
488 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 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}