use std::collections::{HashMap, VecDeque};
use rapier3d::prelude::*;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
#[cfg(target_arch = "wasm32")]
#[derive(Clone, Copy)]
struct Instant;
#[cfg(target_arch = "wasm32")]
impl Instant {
fn now() -> Self {
Self
}
fn elapsed(&self) -> std::time::Duration {
std::time::Duration::ZERO
}
}
use crate::ext_stress_solver::ExtStressSolver;
use crate::types::*;
use super::body_tracker::{BodyTracker, SplitEditStats};
use super::collision_groups::DebrisCollisionMode;
use super::fracture_policy::FracturePolicy;
use super::optimization::SleepThresholdOptions;
use super::optimization::{DebrisCleanupOptions, OptimizationResult, SmallBodyDampingOptions};
use super::resimulation::{BodySnapshots, ResimulationOptions};
#[derive(Clone, Debug, Default)]
pub struct SplitCohort {
pub source_bodies: Vec<RigidBodyHandle>,
pub target_bodies: Vec<RigidBodyHandle>,
}
pub struct DestructibleConfig {
pub nodes: Vec<NodeDesc>,
pub bonds: Vec<BondDesc>,
pub node_sizes: Vec<Vec3>,
pub node_colliders: Vec<Option<ScenarioCollider>>,
pub solver_settings: SolverSettings,
pub gravity: Vec3,
pub fracture_policy: FracturePolicy,
pub resimulation: ResimulationOptions,
pub skip_single_bodies: bool,
pub sleep_thresholds: SleepThresholdOptions,
pub small_body_damping: SmallBodyDampingOptions,
pub debris_cleanup: DebrisCleanupOptions,
pub dynamic_body_ccd_enabled: bool,
}
#[derive(Clone, Debug, Default)]
pub struct StepResult {
pub fractures: usize,
pub new_bodies: usize,
pub split_events: usize,
pub converged: bool,
pub split_edits: SplitEditStats,
pub split_sanitize_ms: f32,
pub split_estimate_ms: f32,
pub split_cohorts: Vec<SplitCohort>,
}
pub struct DestructibleSet {
solver: ExtStressSolver,
tracker: BodyTracker,
policy: FracturePolicy,
gravity: Vec3,
initialized: bool,
resimulation: ResimulationOptions,
skip_single_bodies: bool,
sleep_thresholds: SleepThresholdOptions,
small_body_damping: SmallBodyDampingOptions,
debris_cleanup: DebrisCleanupOptions,
dynamic_body_ccd_enabled: bool,
debris_collision_mode: DebrisCollisionMode,
ground_body_handle: Option<RigidBodyHandle>,
bond_table: Vec<BondDesc>,
node_bonds: Vec<Vec<u32>>,
removed_bonds: Vec<bool>,
destroyed_nodes: Vec<bool>,
pending_split_events: VecDeque<SplitEvent>,
forces_applied: bool,
frames_since_fracture: u32,
}
impl DestructibleSet {
pub fn new(config: DestructibleConfig) -> Option<Self> {
let solver = ExtStressSolver::new(&config.nodes, &config.bonds, &config.solver_settings)?;
let node_count = config.nodes.len();
let mut node_bonds = vec![Vec::new(); node_count];
for (bond_index, bond) in config.bonds.iter().enumerate() {
if let Some(list) = node_bonds.get_mut(bond.node0 as usize) {
list.push(bond_index as u32);
}
if let Some(list) = node_bonds.get_mut(bond.node1 as usize) {
list.push(bond_index as u32);
}
}
let scenario_nodes: Vec<ScenarioNode> = config
.nodes
.iter()
.map(|n| ScenarioNode {
centroid: n.centroid,
mass: n.mass,
volume: n.volume,
})
.collect();
let tracker = BodyTracker::new(&scenario_nodes, config.node_sizes, config.node_colliders);
let mut tracker = tracker;
tracker.set_dynamic_body_ccd_enabled(config.dynamic_body_ccd_enabled);
Some(Self {
solver,
tracker,
policy: config.fracture_policy,
gravity: config.gravity,
initialized: false,
resimulation: config.resimulation,
skip_single_bodies: config.skip_single_bodies,
sleep_thresholds: config.sleep_thresholds,
small_body_damping: config.small_body_damping,
debris_cleanup: config.debris_cleanup,
dynamic_body_ccd_enabled: config.dynamic_body_ccd_enabled,
debris_collision_mode: DebrisCollisionMode::All,
ground_body_handle: None,
bond_table: config.bonds,
node_bonds,
removed_bonds: vec![false; node_count],
destroyed_nodes: vec![false; node_count],
pending_split_events: VecDeque::new(),
forces_applied: false,
frames_since_fracture: u32::MAX,
})
}
pub fn from_scenario(
scenario: &ScenarioDesc,
settings: SolverSettings,
gravity: Vec3,
policy: FracturePolicy,
) -> Option<Self> {
let (nodes, bonds) = scenario.to_solver_descs();
let node_sizes: Vec<Vec3> = scenario
.node_sizes
.iter()
.copied()
.chain(
scenario
.nodes
.iter()
.skip(scenario.node_sizes.len())
.map(|n| {
let side = n.volume.cbrt().max(0.01);
Vec3::new(side, side, side)
}),
)
.collect();
let node_colliders: Vec<Option<ScenarioCollider>> = scenario
.collider_shapes
.iter()
.cloned()
.chain(
scenario
.nodes
.iter()
.skip(scenario.collider_shapes.len())
.map(|_| None),
)
.collect();
Self::new(DestructibleConfig {
nodes,
bonds,
node_sizes,
node_colliders,
solver_settings: settings,
gravity,
fracture_policy: policy,
resimulation: ResimulationOptions::default(),
skip_single_bodies: false,
sleep_thresholds: SleepThresholdOptions::default(),
small_body_damping: SmallBodyDampingOptions::default(),
debris_cleanup: DebrisCleanupOptions::default(),
dynamic_body_ccd_enabled: false,
})
}
pub fn initialize(
&mut self,
bodies: &mut RigidBodySet,
colliders: &mut ColliderSet,
) -> Vec<RigidBodyHandle> {
let actors = self.solver.actors();
let handles = self.tracker.create_initial_bodies(
&actors,
bodies,
colliders,
0.0,
self.small_body_damping,
self.sleep_thresholds,
self.debris_cleanup.max_colliders_for_debris,
);
self.initialized = true;
handles
}
pub fn step(
&mut self,
bodies: &mut RigidBodySet,
colliders: &mut ColliderSet,
island_manager: &mut IslandManager,
impulse_joints: &mut ImpulseJointSet,
multibody_joints: &mut MultibodyJointSet,
) -> StepResult {
self.step_with_time(
0.0,
bodies,
colliders,
island_manager,
impulse_joints,
multibody_joints,
)
}
pub fn step_with_time(
&mut self,
now_secs: f32,
bodies: &mut RigidBodySet,
colliders: &mut ColliderSet,
island_manager: &mut IslandManager,
impulse_joints: &mut ImpulseJointSet,
multibody_joints: &mut MultibodyJointSet,
) -> StepResult {
assert!(self.initialized, "call initialize() before step()");
let mut result = StepResult::default();
let mut remaining_new_bodies = self.policy.clamp_new_bodies(usize::MAX);
let mut remaining_collider_migrations = self.policy.clamp_collider_migrations(usize::MAX);
self.process_pending_split_events(
now_secs,
bodies,
colliders,
island_manager,
impulse_joints,
multibody_joints,
&mut remaining_new_bodies,
&mut remaining_collider_migrations,
&mut result,
);
if self.policy.idle_skip
&& !self.forces_applied
&& self.frames_since_fracture > 2
&& self.pending_split_events.is_empty()
{
return result;
}
self.solver.add_gravity(self.gravity);
self.forces_applied = false;
self.solver.update();
result.converged = self.solver.converged();
let overstressed = self.solver.overstressed_bond_count();
if overstressed == 0 {
self.frames_since_fracture = self.frames_since_fracture.saturating_add(1);
return result;
}
let dynamic_count = self.tracker.dynamic_body_count(bodies);
if self.policy.should_suppress(dynamic_count) {
return result;
}
let mut commands = self.solver.generate_fracture_commands();
if commands.is_empty() {
return result;
}
let max_fractures = self.policy.max_fractures_per_frame;
if max_fractures > 0 {
let mut total = 0usize;
for cmd in &mut commands {
let remaining = (max_fractures as usize).saturating_sub(total);
if remaining == 0 {
cmd.bond_fractures.clear();
} else if cmd.bond_fractures.len() > remaining {
cmd.bond_fractures.truncate(remaining);
}
total += cmd.bond_fractures.len();
}
commands.retain(|c| !c.bond_fractures.is_empty());
}
result.fractures = commands.iter().map(|c| c.bond_fractures.len()).sum();
self.mark_bonds_removed(&commands);
let events = self.solver.apply_fracture_commands(&commands);
result.split_events += events.len();
self.pending_split_events.extend(events);
self.process_pending_split_events(
now_secs,
bodies,
colliders,
island_manager,
impulse_joints,
multibody_joints,
&mut remaining_new_bodies,
&mut remaining_collider_migrations,
&mut result,
);
if self.policy.apply_excess_forces {
self.apply_excess_forces(bodies);
}
self.frames_since_fracture = 0;
result
}
pub fn add_force(&mut self, node_index: u32, position: Vec3, force: Vec3) {
self.solver
.add_force(node_index, position, force, ForceMode::Force);
self.forces_applied = true;
}
pub fn add_acceleration(&mut self, node_index: u32, position: Vec3, acceleration: Vec3) {
self.solver
.add_force(node_index, position, acceleration, ForceMode::Acceleration);
self.forces_applied = true;
}
pub fn node_body(&self, node_index: u32) -> Option<RigidBodyHandle> {
self.tracker.node_body(node_index)
}
pub fn node_collider(&self, node_index: u32) -> Option<ColliderHandle> {
self.tracker.node_collider(node_index)
}
pub fn collider_node(&self, collider: ColliderHandle) -> Option<u32> {
self.tracker.collider_node(collider)
}
pub fn node_local_offset(&self, node_index: u32) -> Option<Vec3> {
self.tracker.node_local_offset(node_index)
}
pub fn body_nodes(&self, body_handle: RigidBodyHandle) -> Vec<u32> {
self.tracker.body_nodes(body_handle)
}
pub fn body_nodes_slice(&self, body_handle: RigidBodyHandle) -> &[u32] {
self.tracker.body_nodes_slice(body_handle)
}
pub fn body_node_count(&self, body_handle: RigidBodyHandle) -> usize {
self.tracker.body_node_count(body_handle)
}
pub fn body_has_support(&self, body_handle: RigidBodyHandle) -> bool {
self.tracker.body_has_support(body_handle)
}
pub fn is_support(&self, node_index: u32) -> bool {
self.tracker.is_support(node_index)
}
pub fn actor_count(&self) -> u32 {
self.solver.actor_count()
}
pub fn active_bond_count(&self) -> usize {
self.removed_bonds
.iter()
.filter(|removed| !**removed)
.count()
}
pub fn body_count(&self) -> usize {
self.tracker.body_count()
}
pub fn collider_count(&self) -> usize {
self.tracker.collider_count()
}
pub fn dynamic_body_count(&self, bodies: &RigidBodySet) -> usize {
self.tracker.dynamic_body_count(bodies)
}
pub fn awake_dynamic_body_count(&self, bodies: &RigidBodySet) -> usize {
self.tracker.awake_dynamic_body_count(bodies)
}
pub fn sleeping_dynamic_body_count(&self, bodies: &RigidBodySet) -> usize {
self.tracker.sleeping_dynamic_body_count(bodies)
}
pub fn support_body_count(&self) -> usize {
self.tracker.support_body_count()
}
pub fn ccd_enabled_body_count(&self, bodies: &RigidBodySet) -> usize {
self.tracker.ccd_enabled_body_count(bodies)
}
pub fn solver(&self) -> &ExtStressSolver {
&self.solver
}
pub fn solver_mut(&mut self) -> &mut ExtStressSolver {
&mut self.solver
}
pub fn policy(&self) -> &FracturePolicy {
&self.policy
}
pub fn set_policy(&mut self, policy: FracturePolicy) {
self.policy = policy;
}
pub fn resimulation_options(&self) -> ResimulationOptions {
self.resimulation
}
pub fn set_resimulation_options(&mut self, options: ResimulationOptions) {
self.resimulation = options;
}
pub fn skip_single_bodies(&self) -> bool {
self.skip_single_bodies
}
pub fn set_skip_single_bodies(&mut self, enabled: bool) {
self.skip_single_bodies = enabled;
}
pub fn sleep_thresholds(&self) -> SleepThresholdOptions {
self.sleep_thresholds
}
pub fn set_sleep_thresholds(&mut self, options: SleepThresholdOptions) {
self.sleep_thresholds = options;
}
pub fn small_body_damping(&self) -> SmallBodyDampingOptions {
self.small_body_damping
}
pub fn set_small_body_damping(&mut self, options: SmallBodyDampingOptions) {
self.small_body_damping = options;
}
pub fn debris_cleanup(&self) -> DebrisCleanupOptions {
self.debris_cleanup
}
pub fn set_debris_cleanup(&mut self, options: DebrisCleanupOptions) {
self.debris_cleanup = options;
}
pub fn debris_collision_mode(&self) -> DebrisCollisionMode {
self.debris_collision_mode
}
pub fn set_debris_collision_mode(&mut self, mode: DebrisCollisionMode) {
self.debris_collision_mode = mode;
self.tracker.set_debris_collision_mode(mode);
}
pub fn ground_body_handle(&self) -> Option<RigidBodyHandle> {
self.ground_body_handle
}
pub fn set_ground_body_handle(&mut self, handle: Option<RigidBodyHandle>) {
self.ground_body_handle = handle;
self.tracker.set_ground_body_handle(handle);
}
pub fn refresh_collision_groups(&self, bodies: &RigidBodySet, colliders: &mut ColliderSet) {
self.tracker.refresh_collision_groups(
bodies,
colliders,
self.debris_cleanup.max_colliders_for_debris,
);
}
pub fn dynamic_body_ccd_enabled(&self) -> bool {
self.dynamic_body_ccd_enabled
}
pub fn set_dynamic_body_ccd_enabled(&mut self, enabled: bool) {
self.dynamic_body_ccd_enabled = enabled;
self.tracker.set_dynamic_body_ccd_enabled(enabled);
}
pub fn set_split_child_recentering_enabled(&mut self, enabled: bool) {
self.tracker.set_split_child_recentering_enabled(enabled);
}
pub fn set_split_child_velocity_fit_enabled(&mut self, enabled: bool) {
self.tracker.set_split_child_velocity_fit_enabled(enabled);
}
pub fn pending_split_event_count(&self) -> usize {
self.pending_split_events.len()
}
pub fn pending_new_body_count(&self, bodies: &RigidBodySet) -> usize {
self.pending_split_events
.iter()
.map(|event| {
self.tracker
.estimate_split_cost(event, bodies)
.create_bodies
})
.sum()
}
pub fn pending_collider_migration_count(&self, bodies: &RigidBodySet) -> usize {
self.pending_split_events
.iter()
.map(|event| {
self.tracker
.estimate_split_cost(event, bodies)
.collider_migrations
})
.sum()
}
pub fn needs_resimulation_snapshot(&self) -> bool {
if !self.resimulation.enabled {
return false;
}
!(self.policy.idle_skip
&& !self.forces_applied
&& self.frames_since_fracture > 2
&& self.pending_split_events.is_empty())
}
pub fn capture_resimulation_snapshot(&self, bodies: &RigidBodySet) -> BodySnapshots {
BodySnapshots::capture(bodies)
}
pub fn mark_body_support_contact(
&mut self,
body_handle: RigidBodyHandle,
now_secs: f32,
bodies: &mut RigidBodySet,
colliders: &mut ColliderSet,
) -> bool {
self.tracker.mark_support_contact(
body_handle,
now_secs,
bodies,
colliders,
self.small_body_damping,
self.sleep_thresholds,
self.debris_cleanup.max_colliders_for_debris,
)
}
pub fn process_optimizations(
&mut self,
now_secs: f32,
bodies: &mut RigidBodySet,
colliders: &mut ColliderSet,
island_manager: &mut IslandManager,
impulse_joints: &mut ImpulseJointSet,
multibody_joints: &mut MultibodyJointSet,
) -> OptimizationResult {
let result = self.tracker.cleanup_expired_bodies(
now_secs,
self.debris_cleanup,
bodies,
colliders,
island_manager,
impulse_joints,
multibody_joints,
);
if !result.removed_nodes.is_empty() {
self.destroy_nodes_in_solver(&result.removed_nodes);
}
result
}
fn apply_excess_forces(&self, bodies: &mut RigidBodySet) {
let actors = self.solver.actors();
for actor in &actors {
if actor.nodes.is_empty() {
continue;
}
let body_handle = match self.tracker.node_body(actor.nodes[0]) {
Some(h) => h,
None => continue,
};
let body = match bodies.get(body_handle) {
Some(b) if b.is_dynamic() => b,
_ => continue,
};
let pos = body.translation();
let com = Vec3::new(pos.x, pos.y, pos.z);
if let Some((force, torque)) = self.solver.get_excess_forces(actor.actor_index, com) {
let force_mag = force.magnitude_squared();
let torque_mag = torque.magnitude_squared();
if force_mag > 1.0e-6 || torque_mag > 1.0e-6 {
if let Some(body_mut) = bodies.get_mut(body_handle) {
body_mut.add_force(vector![force.x, force.y, force.z], true);
body_mut.add_torque(vector![torque.x, torque.y, torque.z], true);
}
}
}
}
}
fn process_pending_split_events(
&mut self,
now_secs: f32,
bodies: &mut RigidBodySet,
colliders: &mut ColliderSet,
island_manager: &mut IslandManager,
impulse_joints: &mut ImpulseJointSet,
multibody_joints: &mut MultibodyJointSet,
remaining_new_bodies: &mut usize,
remaining_collider_migrations: &mut usize,
result: &mut StepResult,
) {
let pending_len = self.pending_split_events.len();
for _ in 0..pending_len {
let Some(event) = self.pending_split_events.pop_front() else {
break;
};
let sanitize_started_at = Instant::now();
let Some(filtered_event) = self.sanitize_split_event(
&event,
bodies,
colliders,
island_manager,
impulse_joints,
multibody_joints,
) else {
result.split_sanitize_ms +=
sanitize_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
continue;
};
result.split_sanitize_ms +=
sanitize_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
if !self.policy.split_budgets_unlimited() {
let estimate_started_at = Instant::now();
let cost = self.tracker.estimate_split_cost(&filtered_event, bodies);
result.split_estimate_ms +=
estimate_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
if cost.create_bodies > *remaining_new_bodies
|| cost.collider_migrations > *remaining_collider_migrations
{
self.pending_split_events.push_front(filtered_event);
break;
}
*remaining_new_bodies = (*remaining_new_bodies).saturating_sub(cost.create_bodies);
*remaining_collider_migrations =
(*remaining_collider_migrations).saturating_sub(cost.collider_migrations);
}
let split_result = self.tracker.handle_split(
&filtered_event,
bodies,
colliders,
island_manager,
impulse_joints,
multibody_joints,
now_secs,
self.small_body_damping,
self.sleep_thresholds,
self.debris_cleanup.max_colliders_for_debris,
);
result.new_bodies += split_result.new_handles.len();
result.split_edits.plan_ms += split_result.stats.plan_ms;
result.split_edits.apply_ms += split_result.stats.apply_ms;
result.split_edits.child_pose_ms += split_result.stats.child_pose_ms;
result.split_edits.velocity_fit_ms += split_result.stats.velocity_fit_ms;
result.split_edits.sleep_init_ms += split_result.stats.sleep_init_ms;
result.split_edits.body_create_ms += split_result.stats.body_create_ms;
result.split_edits.collider_move_ms += split_result.stats.collider_move_ms;
result.split_edits.collider_insert_ms += split_result.stats.collider_insert_ms;
result.split_edits.body_retire_ms += split_result.stats.body_retire_ms;
result.split_edits.reused_bodies += split_result.stats.reused_bodies;
result.split_edits.recycled_bodies += split_result.stats.recycled_bodies;
result.split_edits.created_bodies += split_result.stats.created_bodies;
result.split_edits.retired_bodies += split_result.stats.retired_bodies;
result.split_edits.body_type_flips += split_result.stats.body_type_flips;
result.split_edits.moved_colliders += split_result.stats.moved_colliders;
result.split_edits.inserted_colliders += split_result.stats.inserted_colliders;
result.split_edits.removed_colliders += split_result.stats.removed_colliders;
if split_result.cohort_handles.len() > 1 {
result.split_cohorts.push(SplitCohort {
source_bodies: split_result.source_handles,
target_bodies: split_result.cohort_handles,
});
}
}
}
fn sanitize_split_event(
&mut self,
event: &SplitEvent,
bodies: &mut RigidBodySet,
colliders: &mut ColliderSet,
island_manager: &mut IslandManager,
impulse_joints: &mut ImpulseJointSet,
multibody_joints: &mut MultibodyJointSet,
) -> Option<SplitEvent> {
let mut destroyed_nodes = Vec::new();
let mut children = Vec::new();
for child in &event.children {
let filtered_nodes: Vec<u32> = child
.nodes
.iter()
.copied()
.filter(|node| !self.destroyed_nodes[*node as usize])
.collect();
if filtered_nodes.is_empty() {
continue;
}
let is_child_support = filtered_nodes
.iter()
.any(|node| self.tracker.is_support(*node));
let should_destroy =
(!is_child_support && self.skip_single_bodies && filtered_nodes.len() <= 1)
|| (!is_child_support
&& self.policy.min_child_node_count > 1
&& filtered_nodes.len() < self.policy.min_child_node_count as usize);
if should_destroy {
destroyed_nodes.extend(filtered_nodes);
} else {
children.push(SplitChild {
actor_index: child.actor_index,
nodes: filtered_nodes,
});
}
}
if !destroyed_nodes.is_empty() {
let removed_nodes = self.tracker.destroy_nodes(
&destroyed_nodes,
bodies,
colliders,
island_manager,
impulse_joints,
multibody_joints,
);
self.destroy_nodes_in_solver(&removed_nodes);
}
if children.is_empty() {
None
} else {
Some(SplitEvent {
parent_actor_index: event.parent_actor_index,
children,
})
}
}
fn destroy_nodes_in_solver(&mut self, nodes: &[u32]) {
let mut targets = Vec::new();
for &node in nodes {
let idx = node as usize;
if idx >= self.destroyed_nodes.len() || self.destroyed_nodes[idx] {
continue;
}
self.destroyed_nodes[idx] = true;
targets.push(node);
}
if targets.is_empty() {
return;
}
let actors = self.solver.actors();
let mut node_to_actor = HashMap::new();
for actor in &actors {
for &node in &actor.nodes {
node_to_actor.insert(node, actor.actor_index);
}
}
let mut commands_by_actor: HashMap<u32, Vec<BondFracture>> = HashMap::new();
for &node in &targets {
for &bond_index in &self.node_bonds[node as usize] {
let bond_index = bond_index as usize;
if self.removed_bonds.get(bond_index).copied().unwrap_or(true) {
continue;
}
self.removed_bonds[bond_index] = true;
let bond = self.bond_table[bond_index];
let actor_index = node_to_actor
.get(&bond.node0)
.copied()
.or_else(|| node_to_actor.get(&bond.node1).copied());
let Some(actor_index) = actor_index else {
continue;
};
commands_by_actor
.entry(actor_index)
.or_default()
.push(BondFracture {
userdata: bond_index as u32,
node_index0: bond.node0,
node_index1: bond.node1,
health: 0.0,
});
}
}
if commands_by_actor.is_empty() {
return;
}
let commands: Vec<FractureCommand> = commands_by_actor
.into_iter()
.map(|(actor_index, bond_fractures)| FractureCommand {
actor_index,
bond_fractures,
})
.collect();
let events = self.solver.apply_fracture_commands(&commands);
self.pending_split_events.extend(events);
}
fn mark_bonds_removed(&mut self, commands: &[FractureCommand]) {
for command in commands {
for fracture in &command.bond_fractures {
let bond_index = self.resolve_bond_index(
fracture.userdata,
fracture.node_index0,
fracture.node_index1,
);
if let Some(idx) = bond_index {
if let Some(removed) = self.removed_bonds.get_mut(idx) {
*removed = true;
}
}
}
}
}
fn resolve_bond_index(&self, userdata: u32, node0: u32, node1: u32) -> Option<usize> {
let idx = userdata as usize;
if self
.bond_table
.get(idx)
.is_some_and(|bond| Self::bond_matches(bond, node0, node1))
{
return Some(idx);
}
self.node_bonds
.get(node0 as usize)
.into_iter()
.flatten()
.find_map(|bond_index| {
let idx = *bond_index as usize;
self.bond_table
.get(idx)
.filter(|bond| Self::bond_matches(bond, node0, node1))
.map(|_| idx)
})
}
fn bond_matches(bond: &BondDesc, node0: u32, node1: u32) -> bool {
(bond.node0 == node0 && bond.node1 == node1) || (bond.node0 == node1 && bond.node1 == node0)
}
}