use crate::math::{Fix128, Vec3Fix};
use crate::solver::RigidBody;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[derive(Clone, Debug)]
pub struct RopeAttachment {
pub rope_particle: usize,
pub body_index: usize,
pub local_anchor: Vec3Fix,
pub compliance: Fix128,
pub break_force: Option<Fix128>,
}
impl RopeAttachment {
#[must_use]
pub const fn new(rope_particle: usize, body_index: usize, local_anchor: Vec3Fix) -> Self {
Self {
rope_particle,
body_index,
local_anchor,
compliance: Fix128::ZERO,
break_force: None,
}
}
#[must_use]
pub const fn with_break_force(
rope_particle: usize,
body_index: usize,
local_anchor: Vec3Fix,
break_force: Fix128,
) -> Self {
Self {
rope_particle,
body_index,
local_anchor,
compliance: Fix128::ZERO,
break_force: Some(break_force),
}
}
#[must_use]
pub const fn compliance(mut self, c: Fix128) -> Self {
self.compliance = c;
self
}
}
pub fn solve_rope_attachments(
attachments: &[RopeAttachment],
rope_positions: &mut [Vec3Fix],
rope_velocities: &mut [Vec3Fix],
bodies: &[RigidBody],
dt: Fix128,
) -> Vec<Option<usize>> {
if dt.is_zero() {
return Vec::new();
}
let dt_sq = dt * dt;
let mut broken = Vec::new();
for (ai, att) in attachments.iter().enumerate() {
if att.rope_particle >= rope_positions.len() || att.body_index >= bodies.len() {
continue;
}
let body = &bodies[att.body_index];
let world_anchor = body.position + body.rotation.rotate_vec(att.local_anchor);
let rope_pos = rope_positions[att.rope_particle];
let delta = rope_pos - world_anchor;
let dist_sq = delta.dot(delta);
if dist_sq.is_zero() {
continue;
}
let dist = dist_sq.sqrt();
let alpha = if dt_sq.is_zero() {
Fix128::ZERO
} else {
att.compliance / dt_sq
};
let w_rope = Fix128::ONE; let w_body = body.inv_mass;
let w_sum = w_rope + w_body + alpha;
if w_sum.is_zero() {
continue;
}
let lambda = dist / w_sum;
let force_estimate = lambda / dt_sq;
if let Some(max_force) = att.break_force {
if force_estimate > max_force {
broken.push(Some(ai));
continue;
}
}
let direction = delta / dist;
let rope_correction = direction * (lambda * w_rope);
rope_positions[att.rope_particle] = rope_pos - rope_correction;
let vel_change = rope_correction / dt;
rope_velocities[att.rope_particle] = rope_velocities[att.rope_particle] - vel_change;
}
broken
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::math::QuatFix;
use crate::solver::BodyType;
const fn make_static_body(pos: Vec3Fix) -> RigidBody {
RigidBody {
position: pos,
velocity: Vec3Fix::ZERO,
inv_mass: Fix128::ZERO,
inv_inertia: Vec3Fix::ZERO,
prev_position: pos,
rotation: QuatFix::IDENTITY,
angular_velocity: Vec3Fix::ZERO,
prev_rotation: QuatFix::IDENTITY,
restitution: Fix128::ZERO,
friction: Fix128::ZERO,
gravity_scale: Fix128::ONE,
linear_damping: Fix128::ONE,
angular_damping: Fix128::ONE,
is_sensor: false,
body_type: BodyType::Static,
kinematic_target: None,
}
}
#[test]
fn test_new_attachment() {
let att = RopeAttachment::new(0, 1, Vec3Fix::UNIT_X);
assert_eq!(att.rope_particle, 0);
assert_eq!(att.body_index, 1);
assert!(att.compliance.is_zero());
assert!(att.break_force.is_none());
}
#[test]
fn test_with_break_force() {
let att = RopeAttachment::with_break_force(2, 3, Vec3Fix::ZERO, Fix128::from_int(100));
assert_eq!(att.rope_particle, 2);
assert!(att.break_force.is_some());
}
#[test]
fn test_compliance_builder() {
let att = RopeAttachment::new(0, 0, Vec3Fix::ZERO).compliance(Fix128::from_ratio(1, 10));
assert!(!att.compliance.is_zero());
}
#[test]
fn test_solve_pulls_rope_to_anchor() {
let body = make_static_body(Vec3Fix::from_int(10, 0, 0));
let bodies = vec![body];
let att = RopeAttachment::new(0, 0, Vec3Fix::ZERO);
let mut rope_pos = vec![Vec3Fix::from_int(0, 0, 0)];
let mut rope_vel = vec![Vec3Fix::ZERO];
let dt = Fix128::from_ratio(1, 60);
let _ = solve_rope_attachments(&[att], &mut rope_pos, &mut rope_vel, &bodies, dt);
assert!(rope_pos[0].x > Fix128::ZERO);
}
#[test]
fn test_solve_already_at_anchor() {
let body = make_static_body(Vec3Fix::from_int(5, 5, 5));
let bodies = vec![body];
let att = RopeAttachment::new(0, 0, Vec3Fix::ZERO);
let mut rope_pos = vec![Vec3Fix::from_int(5, 5, 5)];
let mut rope_vel = vec![Vec3Fix::ZERO];
let dt = Fix128::from_ratio(1, 60);
let broken = solve_rope_attachments(&[att], &mut rope_pos, &mut rope_vel, &bodies, dt);
assert!(broken.is_empty());
assert_eq!(rope_pos[0].x.hi, 5);
}
#[test]
fn test_break_force_exceeded() {
let body = make_static_body(Vec3Fix::from_int(1000, 0, 0));
let bodies = vec![body];
let att =
RopeAttachment::with_break_force(0, 0, Vec3Fix::ZERO, Fix128::from_ratio(1, 10000));
let mut rope_pos = vec![Vec3Fix::ZERO];
let mut rope_vel = vec![Vec3Fix::ZERO];
let dt = Fix128::from_ratio(1, 60);
let broken = solve_rope_attachments(&[att], &mut rope_pos, &mut rope_vel, &bodies, dt);
assert!(!broken.is_empty());
}
#[test]
fn test_break_force_not_exceeded() {
let body = make_static_body(Vec3Fix::from_int(0, 0, 0));
let bodies = vec![body];
let att = RopeAttachment::with_break_force(0, 0, Vec3Fix::ZERO, Fix128::from_int(1000000));
let mut rope_pos = vec![Vec3Fix::ZERO];
let mut rope_vel = vec![Vec3Fix::ZERO];
let dt = Fix128::from_ratio(1, 60);
let broken = solve_rope_attachments(&[att], &mut rope_pos, &mut rope_vel, &bodies, dt);
assert!(broken.is_empty());
}
#[test]
fn test_zero_dt_no_effect() {
let body = make_static_body(Vec3Fix::from_int(10, 0, 0));
let bodies = vec![body];
let att = RopeAttachment::new(0, 0, Vec3Fix::ZERO);
let mut rope_pos = vec![Vec3Fix::ZERO];
let mut rope_vel = vec![Vec3Fix::ZERO];
let broken =
solve_rope_attachments(&[att], &mut rope_pos, &mut rope_vel, &bodies, Fix128::ZERO);
assert!(broken.is_empty());
assert!(rope_pos[0].x.is_zero());
}
#[test]
fn test_invalid_indices_skipped() {
let body = make_static_body(Vec3Fix::ZERO);
let bodies = vec![body];
let att = RopeAttachment::new(99, 0, Vec3Fix::ZERO);
let mut rope_pos = vec![Vec3Fix::ZERO];
let mut rope_vel = vec![Vec3Fix::ZERO];
let dt = Fix128::from_ratio(1, 60);
let _ = solve_rope_attachments(&[att], &mut rope_pos, &mut rope_vel, &bodies, dt);
}
}