#[inline]
pub fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
pub fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
pub fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
[a[0] * s, a[1] * s, a[2] * s]
}
#[inline]
pub fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
pub fn norm3(a: [f64; 3]) -> f64 {
dot3(a, a).sqrt()
}
#[inline]
pub fn normalize3(a: [f64; 3]) -> [f64; 3] {
let n = norm3(a);
if n < 1e-14 {
[0.0; 3]
} else {
scale3(a, 1.0 / n)
}
}
#[inline]
pub fn clampf(v: f64, lo: f64, hi: f64) -> f64 {
v.max(lo).min(hi)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JointType {
Revolute,
Prismatic,
BallSocket,
}
#[derive(Debug, Clone)]
pub struct JointLimits {
pub lower: f64,
pub upper: f64,
pub enabled: bool,
}
impl JointLimits {
pub fn new(lower: f64, upper: f64) -> Self {
Self {
lower,
upper,
enabled: true,
}
}
#[inline]
pub fn is_violated(&self, value: f64) -> bool {
self.enabled && (value < self.lower || value > self.upper)
}
#[inline]
pub fn penetration(&self, value: f64) -> f64 {
if value < self.lower {
self.lower - value
} else if value > self.upper {
value - self.upper
} else {
0.0
}
}
#[inline]
pub fn clamp(&self, value: f64) -> f64 {
clampf(value, self.lower, self.upper)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StopType {
Hard,
Soft {
stiffness: f64,
damping: f64,
},
}
impl StopType {
pub fn constraint_force(&self, penetration: f64, velocity: f64) -> f64 {
match *self {
StopType::Hard => {
if penetration > 0.0 {
1e9 * penetration
} else {
0.0
}
}
StopType::Soft { stiffness, damping } => {
if penetration > 0.0 {
stiffness * penetration - damping * velocity.min(0.0)
} else {
0.0
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct JointLimitConstraint {
pub body_a: usize,
pub body_b: usize,
pub joint_type: JointType,
pub limits: JointLimits,
pub stop_type: StopType,
pub position: f64,
pub velocity: f64,
pub warm_impulse: f64,
pub axis: [f64; 3],
}
impl JointLimitConstraint {
pub fn new(
body_a: usize,
body_b: usize,
joint_type: JointType,
limits: JointLimits,
stop_type: StopType,
axis: [f64; 3],
) -> Self {
Self {
body_a,
body_b,
joint_type,
limits,
stop_type,
position: 0.0,
velocity: 0.0,
warm_impulse: 0.0,
axis: normalize3(axis),
}
}
pub fn is_active(&self) -> bool {
self.limits.is_violated(self.position)
}
pub fn penetration_depth(&self) -> f64 {
self.limits.penetration(self.position)
}
pub fn compute_impulse(&self, inv_mass_a: f64, inv_mass_b: f64, dt: f64) -> f64 {
let pen = self.penetration_depth();
if pen <= 0.0 {
return 0.0;
}
let force = self.stop_type.constraint_force(pen, self.velocity);
let eff_mass = inv_mass_a + inv_mass_b;
if eff_mass < 1e-14 {
return 0.0;
}
force * dt / eff_mass
}
pub fn apply_warm_start(&self) -> f64 {
self.warm_impulse.max(0.0)
}
pub fn store_impulse(&mut self, impulse: f64) {
self.warm_impulse = impulse.max(0.0);
}
}
#[derive(Debug, Clone)]
pub struct BallSocketLimitConstraint {
pub body_a: usize,
pub body_b: usize,
pub cone_axis: [f64; 3],
pub max_angle: f64,
pub stop_type: StopType,
pub current_angle: f64,
pub warm_impulse: f64,
}
impl BallSocketLimitConstraint {
pub fn new(
body_a: usize,
body_b: usize,
cone_axis: [f64; 3],
max_angle: f64,
stop_type: StopType,
) -> Self {
Self {
body_a,
body_b,
cone_axis: normalize3(cone_axis),
max_angle,
stop_type,
current_angle: 0.0,
warm_impulse: 0.0,
}
}
pub fn update_angle(&mut self, child_axis_world: [f64; 3]) {
let d = dot3(self.cone_axis, normalize3(child_axis_world));
let d_clamped = clampf(d, -1.0, 1.0);
self.current_angle = d_clamped.acos();
}
pub fn penetration(&self) -> f64 {
(self.current_angle - self.max_angle).max(0.0)
}
pub fn is_violated(&self) -> bool {
self.current_angle > self.max_angle
}
pub fn compute_impulse(&self, inv_inertia_a: f64, inv_inertia_b: f64, dt: f64) -> f64 {
let pen = self.penetration();
if pen <= 0.0 {
return 0.0;
}
let force = self.stop_type.constraint_force(pen, 0.0);
let eff = inv_inertia_a + inv_inertia_b;
if eff < 1e-14 { 0.0 } else { force * dt / eff }
}
}
#[derive(Debug, Clone)]
pub struct GearRatioCollision {
pub joint_a: usize,
pub joint_b: usize,
pub ratio: f64,
pub efficiency: f64,
}
impl GearRatioCollision {
pub fn new(joint_a: usize, joint_b: usize, ratio: f64, efficiency: f64) -> Self {
Self {
joint_a,
joint_b,
ratio,
efficiency: efficiency.clamp(0.0, 1.0),
}
}
pub fn propagate_impulse(&self, impulse_a: f64) -> f64 {
impulse_a * self.ratio * self.efficiency
}
pub fn reflected_inertia(&self, inertia_b: f64) -> f64 {
inertia_b * self.ratio * self.ratio
}
}
#[derive(Debug, Clone)]
pub struct CableChainLimit {
pub body_a: usize,
pub body_b: usize,
pub min_length: f64,
pub max_length: f64,
pub current_length: f64,
pub relative_velocity: f64,
pub impulse: f64,
pub stiffness: f64,
pub damping: f64,
}
impl CableChainLimit {
pub fn new(
body_a: usize,
body_b: usize,
min_length: f64,
max_length: f64,
stiffness: f64,
damping: f64,
) -> Self {
Self {
body_a,
body_b,
min_length,
max_length,
current_length: 0.0,
relative_velocity: 0.0,
impulse: 0.0,
stiffness,
damping,
}
}
pub fn extension_violation(&self) -> f64 {
(self.current_length - self.max_length).max(0.0)
}
pub fn compression_violation(&self) -> f64 {
(self.min_length - self.current_length).max(0.0)
}
pub fn net_penetration(&self) -> f64 {
self.extension_violation() + self.compression_violation()
}
pub fn compute_impulse(&self, inv_mass_a: f64, inv_mass_b: f64, dt: f64) -> f64 {
let pen = self.net_penetration();
if pen <= 0.0 {
return 0.0;
}
let force = self.stiffness * pen - self.damping * self.relative_velocity.min(0.0);
let eff = inv_mass_a + inv_mass_b;
if eff < 1e-14 { 0.0 } else { force * dt / eff }
}
}
#[derive(Debug, Clone)]
pub struct ArticulatedBodyJointStop {
pub joint_id: usize,
pub effective_inertia: f64,
pub position: f64,
pub velocity: f64,
pub limits: JointLimits,
pub stop_type: StopType,
pub warm_impulse: f64,
}
impl ArticulatedBodyJointStop {
pub fn new(
joint_id: usize,
effective_inertia: f64,
limits: JointLimits,
stop_type: StopType,
) -> Self {
Self {
joint_id,
effective_inertia,
position: 0.0,
velocity: 0.0,
limits,
stop_type,
warm_impulse: 0.0,
}
}
pub fn compute_impulse(&self, dt: f64) -> f64 {
let pen = self.limits.penetration(self.position);
if pen <= 0.0 {
return 0.0;
}
let force = self.stop_type.constraint_force(pen, self.velocity);
if self.effective_inertia < 1e-14 {
0.0
} else {
force * dt / self.effective_inertia
}
}
pub fn velocity_impulse(&self, restitution: f64) -> f64 {
if !self.limits.is_violated(self.position) {
return 0.0;
}
let vel_out = -(1.0 + restitution) * self.velocity;
if self.effective_inertia < 1e-14 {
0.0
} else {
vel_out / self.effective_inertia
}
}
}
#[derive(Debug, Clone)]
pub struct MotorHardLimit {
pub max_torque: f64,
pub max_velocity: f64,
pub position_limits: JointLimits,
pub angle: f64,
pub angular_velocity: f64,
pub inertia: f64,
pub stalled: bool,
}
impl MotorHardLimit {
pub fn new(max_torque: f64, max_velocity: f64, lower: f64, upper: f64, inertia: f64) -> Self {
Self {
max_torque,
max_velocity,
position_limits: JointLimits::new(lower, upper),
angle: 0.0,
angular_velocity: 0.0,
inertia,
stalled: false,
}
}
pub fn clamp_torque(&self, torque: f64) -> f64 {
clampf(torque, -self.max_torque, self.max_torque)
}
pub fn clamp_velocity(&self, vel: f64) -> f64 {
clampf(vel, -self.max_velocity, self.max_velocity)
}
pub fn check_stall(&mut self) {
self.stalled = self.position_limits.is_violated(self.angle);
}
pub fn limit_impulse(&self, dt: f64) -> f64 {
if !self.stalled {
return 0.0;
}
let pen = self.position_limits.penetration(self.angle);
if self.inertia < 1e-14 {
0.0
} else {
1e6 * pen * dt / self.inertia
}
}
}
#[derive(Debug, Clone)]
pub struct JointFrictionConstraint {
pub body_a: usize,
pub body_b: usize,
pub axis: [f64; 3],
pub friction_coefficient: f64,
pub normal_force: f64,
pub relative_velocity: f64,
pub warm_impulse: f64,
}
impl JointFrictionConstraint {
pub fn new(body_a: usize, body_b: usize, axis: [f64; 3], friction_coefficient: f64) -> Self {
Self {
body_a,
body_b,
axis: normalize3(axis),
friction_coefficient,
normal_force: 0.0,
relative_velocity: 0.0,
warm_impulse: 0.0,
}
}
pub fn max_friction_impulse(&self, inv_mass_eff: f64, dt: f64) -> f64 {
let _ = dt;
let max_f = self.friction_coefficient * self.normal_force.abs();
let _ = inv_mass_eff;
max_f
}
pub fn compute_impulse(&self, inv_mass_eff: f64, dt: f64) -> f64 {
if inv_mass_eff < 1e-14 {
return 0.0;
}
let delta_v = -self.relative_velocity;
let raw_impulse = delta_v / inv_mass_eff;
let max_imp = self.max_friction_impulse(inv_mass_eff, dt);
clampf(raw_impulse, -max_imp, max_imp)
}
pub fn warm_start_impulse(&self, inv_mass_eff: f64, dt: f64) -> f64 {
let max_imp = self.max_friction_impulse(inv_mass_eff, dt);
clampf(self.warm_impulse, -max_imp, max_imp)
}
}
#[derive(Debug, Clone, Default)]
pub struct JointComplianceParams {
pub erp: f64,
pub cfm: f64,
}
impl JointComplianceParams {
pub fn new(erp: f64, cfm: f64) -> Self {
Self {
erp: clampf(erp, 0.0, 1.0),
cfm: cfm.max(0.0),
}
}
pub fn rigid() -> Self {
Self::new(0.2, 0.0)
}
pub fn soft(stiffness: f64, damping: f64, dt: f64) -> Self {
let denom = dt * stiffness + damping;
if denom < 1e-14 {
Self::rigid()
} else {
Self::new(dt * stiffness / denom, 1.0 / denom)
}
}
pub fn positional_bias(&self, error: f64, dt: f64) -> f64 {
if dt < 1e-14 {
0.0
} else {
self.erp * error / dt
}
}
pub fn softened_keff(&self, k_eff: f64) -> f64 {
k_eff + self.cfm
}
}
#[derive(Debug, Clone, Default)]
pub struct JointLimitWarmStart {
entries: Vec<WarmEntry>,
}
#[derive(Debug, Clone)]
struct WarmEntry {
body_a: usize,
body_b: usize,
axis_idx: usize,
impulse: f64,
}
impl JointLimitWarmStart {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn store(&mut self, body_a: usize, body_b: usize, axis_idx: usize, impulse: f64) {
for entry in &mut self.entries {
if entry.body_a == body_a && entry.body_b == body_b && entry.axis_idx == axis_idx {
entry.impulse = impulse;
return;
}
}
self.entries.push(WarmEntry {
body_a,
body_b,
axis_idx,
impulse,
});
}
pub fn retrieve(&self, body_a: usize, body_b: usize, axis_idx: usize) -> f64 {
for entry in &self.entries {
if entry.body_a == body_a && entry.body_b == body_b && entry.axis_idx == axis_idx {
return entry.impulse;
}
}
0.0
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn scale(&mut self, factor: f64) {
for entry in &mut self.entries {
entry.impulse *= factor;
}
}
}
#[derive(Debug, Clone)]
pub struct JointLimitSolver {
pub constraints: Vec<JointLimitConstraint>,
pub warm_start: JointLimitWarmStart,
pub compliance: JointComplianceParams,
pub iterations: usize,
}
impl JointLimitSolver {
pub fn new(iterations: usize) -> Self {
Self {
constraints: Vec::new(),
warm_start: JointLimitWarmStart::new(),
compliance: JointComplianceParams::rigid(),
iterations,
}
}
pub fn add_constraint(&mut self, c: JointLimitConstraint) {
self.constraints.push(c);
}
pub fn apply_warm_starts(&mut self) {
for c in &mut self.constraints {
let wi = self.warm_start.retrieve(c.body_a, c.body_b, 0);
c.warm_impulse = wi;
}
}
pub fn solve(&mut self, inv_masses: &[f64], dt: f64) -> f64 {
self.apply_warm_starts();
let mut total_impulse = 0.0f64;
for _iter in 0..self.iterations {
for c in &mut self.constraints {
let im_a = if c.body_a < inv_masses.len() {
inv_masses[c.body_a]
} else {
0.0
};
let im_b = if c.body_b < inv_masses.len() {
inv_masses[c.body_b]
} else {
0.0
};
let impulse = c.compute_impulse(im_a, im_b, dt);
let new_acc = (c.warm_impulse + impulse).max(0.0);
let delta = new_acc - c.warm_impulse;
c.warm_impulse = new_acc;
total_impulse += delta.abs();
}
}
for c in &self.constraints {
self.warm_start.store(c.body_a, c.body_b, 0, c.warm_impulse);
}
total_impulse
}
pub fn clear_constraints(&mut self) {
self.constraints.clear();
}
}
#[derive(Debug, Clone)]
pub struct JointStopContact {
pub joint_id: usize,
pub normal: [f64; 3],
pub depth: f64,
pub relative_velocity: f64,
pub restitution: f64,
}
impl JointStopContact {
pub fn new(
joint_id: usize,
normal: [f64; 3],
depth: f64,
relative_velocity: f64,
restitution: f64,
) -> Self {
Self {
joint_id,
normal: normalize3(normal),
depth,
relative_velocity,
restitution,
}
}
pub fn compute_impulse(&self, eff_mass: f64, beta: f64, dt: f64) -> f64 {
if eff_mass < 1e-14 {
return 0.0;
}
let baumgarte_bias = beta * self.depth / dt;
let restitution_bias = self.restitution * self.relative_velocity.min(0.0).abs();
let target_vel = baumgarte_bias + restitution_bias;
let delta_v = target_vel - self.relative_velocity.min(0.0);
(delta_v / eff_mass).max(0.0)
}
}
#[derive(Debug, Clone)]
pub struct PrismaticJointLimit {
pub body_a: usize,
pub body_b: usize,
pub axis: [f64; 3],
pub limits: JointLimits,
pub stop_type: StopType,
pub translation: f64,
pub velocity: f64,
pub warm_impulse: f64,
pub compliance: JointComplianceParams,
}
impl PrismaticJointLimit {
pub fn new(
body_a: usize,
body_b: usize,
axis: [f64; 3],
lower: f64,
upper: f64,
stop_type: StopType,
compliance: JointComplianceParams,
) -> Self {
Self {
body_a,
body_b,
axis: normalize3(axis),
limits: JointLimits::new(lower, upper),
stop_type,
translation: 0.0,
velocity: 0.0,
warm_impulse: 0.0,
compliance,
}
}
pub fn penetration(&self) -> f64 {
self.limits.penetration(self.translation)
}
pub fn bias_velocity(&self, dt: f64) -> f64 {
self.compliance.positional_bias(self.penetration(), dt)
}
pub fn constraint_row(&self) -> f64 {
if self.translation < self.limits.lower {
1.0
} else if self.translation > self.limits.upper {
-1.0
} else {
0.0
}
}
pub fn solve_impulse(&self, inv_mass_a: f64, inv_mass_b: f64, dt: f64) -> f64 {
let pen = self.penetration();
if pen <= 0.0 {
return 0.0;
}
let bias = self.bias_velocity(dt);
let k_eff_raw = inv_mass_a + inv_mass_b;
let k_eff = self.compliance.softened_keff(k_eff_raw);
if k_eff < 1e-14 {
return 0.0;
}
let lambda = (bias - self.velocity) / k_eff;
lambda.max(0.0)
}
}
#[derive(Debug, Clone)]
pub struct RevoluteJointLimit {
pub body_a: usize,
pub body_b: usize,
pub axis: [f64; 3],
pub limits: JointLimits,
pub stop_type: StopType,
pub angle: f64,
pub ang_velocity: f64,
pub warm_impulse: f64,
pub compliance: JointComplianceParams,
pub restitution: f64,
}
impl RevoluteJointLimit {
pub fn new(
body_a: usize,
body_b: usize,
axis: [f64; 3],
lower: f64,
upper: f64,
stop_type: StopType,
compliance: JointComplianceParams,
restitution: f64,
) -> Self {
Self {
body_a,
body_b,
axis: normalize3(axis),
limits: JointLimits::new(lower, upper),
stop_type,
angle: 0.0,
ang_velocity: 0.0,
warm_impulse: 0.0,
compliance,
restitution,
}
}
pub fn penetration(&self) -> f64 {
self.limits.penetration(self.angle)
}
pub fn is_active(&self) -> bool {
self.limits.is_violated(self.angle)
}
pub fn solve_impulse(&self, inv_inertia_a: f64, inv_inertia_b: f64, dt: f64) -> f64 {
let pen = self.penetration();
if pen <= 0.0 {
return 0.0;
}
let bias = self.compliance.positional_bias(pen, dt);
let restitution_term = if self.ang_velocity.abs() > 1e-6 {
self.restitution * self.ang_velocity.abs()
} else {
0.0
};
let k_eff_raw = inv_inertia_a + inv_inertia_b;
let k_eff = self.compliance.softened_keff(k_eff_raw);
if k_eff < 1e-14 {
return 0.0;
}
let target = bias + restitution_term;
let lambda = (target - self.ang_velocity) / k_eff;
(self.warm_impulse + lambda).max(0.0) - self.warm_impulse
}
}
#[derive(Debug, Clone, Default)]
pub struct JointLimitPipeline {
pub revolute: Vec<RevoluteJointLimit>,
pub prismatic: Vec<PrismaticJointLimit>,
pub ball_socket: Vec<BallSocketLimitConstraint>,
pub cables: Vec<CableChainLimit>,
pub ab_stops: Vec<ArticulatedBodyJointStop>,
pub motors: Vec<MotorHardLimit>,
pub warm_start: JointLimitWarmStart,
pub compliance: JointComplianceParams,
pub iterations: usize,
}
impl JointLimitPipeline {
pub fn new(iterations: usize) -> Self {
Self {
revolute: Vec::new(),
prismatic: Vec::new(),
ball_socket: Vec::new(),
cables: Vec::new(),
ab_stops: Vec::new(),
motors: Vec::new(),
warm_start: JointLimitWarmStart::new(),
compliance: JointComplianceParams::rigid(),
iterations,
}
}
pub fn step(&mut self, inv_masses: &[f64], inv_inertias: &[f64], dt: f64) -> f64 {
let mut total = 0.0f64;
for c in &mut self.revolute {
let ia = if c.body_a < inv_inertias.len() {
inv_inertias[c.body_a]
} else {
0.0
};
let ib = if c.body_b < inv_inertias.len() {
inv_inertias[c.body_b]
} else {
0.0
};
for _ in 0..self.iterations {
let imp = c.solve_impulse(ia, ib, dt);
c.warm_impulse = (c.warm_impulse + imp).max(0.0);
total += imp.abs();
}
}
for c in &mut self.prismatic {
let ma = if c.body_a < inv_masses.len() {
inv_masses[c.body_a]
} else {
0.0
};
let mb = if c.body_b < inv_masses.len() {
inv_masses[c.body_b]
} else {
0.0
};
for _ in 0..self.iterations {
let imp = c.solve_impulse(ma, mb, dt);
c.warm_impulse = (c.warm_impulse + imp).max(0.0);
total += imp.abs();
}
}
for c in &mut self.ab_stops {
for _ in 0..self.iterations {
let imp = c.compute_impulse(dt);
c.warm_impulse = (c.warm_impulse + imp).max(0.0);
total += imp.abs();
}
}
for c in &self.cables {
let ma = if c.body_a < inv_masses.len() {
inv_masses[c.body_a]
} else {
0.0
};
let mb = if c.body_b < inv_masses.len() {
inv_masses[c.body_b]
} else {
0.0
};
let imp = c.compute_impulse(ma, mb, dt);
total += imp.abs();
}
total
}
pub fn active_count(&self) -> usize {
self.revolute.iter().filter(|c| c.is_active()).count()
+ self
.prismatic
.iter()
.filter(|c| c.limits.is_violated(c.translation))
.count()
+ self.ball_socket.iter().filter(|c| c.is_violated()).count()
}
}
pub fn wrap_angle(angle: f64) -> f64 {
use std::f64::consts::PI;
let mut a = angle;
while a > PI {
a -= 2.0 * PI;
}
while a <= -PI {
a += 2.0 * PI;
}
a
}
pub fn angle_diff(a: f64, b: f64) -> f64 {
wrap_angle(b - a)
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_joint_limits_violation() {
let lim = JointLimits::new(-1.0, 1.0);
assert!(lim.is_violated(-1.5));
assert!(lim.is_violated(1.5));
assert!(!lim.is_violated(0.0));
assert!(!lim.is_violated(-1.0));
assert!(!lim.is_violated(1.0));
}
#[test]
fn test_joint_limits_penetration() {
let lim = JointLimits::new(-PI / 2.0, PI / 2.0);
let pen = lim.penetration(-PI);
assert!((pen - (PI / 2.0)).abs() < 1e-10);
assert_eq!(lim.penetration(0.0), 0.0);
}
#[test]
fn test_joint_limits_clamp() {
let lim = JointLimits::new(-1.0, 2.0);
assert_eq!(lim.clamp(-5.0), -1.0);
assert_eq!(lim.clamp(5.0), 2.0);
assert_eq!(lim.clamp(0.5), 0.5);
}
#[test]
fn test_stop_type_hard_force() {
let s = StopType::Hard;
assert!(s.constraint_force(0.1, 0.0) > 0.0);
assert_eq!(s.constraint_force(0.0, 0.0), 0.0);
}
#[test]
fn test_stop_type_soft_force() {
let s = StopType::Soft {
stiffness: 1000.0,
damping: 10.0,
};
let f = s.constraint_force(0.01, 0.0);
assert!((f - 10.0).abs() < 1e-10);
}
#[test]
fn test_stop_type_soft_no_violation() {
let s = StopType::Soft {
stiffness: 1000.0,
damping: 10.0,
};
assert_eq!(s.constraint_force(-0.1, 0.0), 0.0);
}
#[test]
fn test_joint_limit_constraint_impulse() {
let lim = JointLimits::new(-1.0, 1.0);
let mut c = JointLimitConstraint::new(
0,
1,
JointType::Revolute,
lim,
StopType::Soft {
stiffness: 1000.0,
damping: 0.0,
},
[0.0, 0.0, 1.0],
);
c.position = 1.5; let imp = c.compute_impulse(1.0, 1.0, 0.01);
assert!(imp > 0.0, "impulse should be positive");
}
#[test]
fn test_warm_start_positive_clamped() {
let lim = JointLimits::new(-1.0, 1.0);
let mut c = JointLimitConstraint::new(
0,
1,
JointType::Revolute,
lim,
StopType::Hard,
[1.0, 0.0, 0.0],
);
c.store_impulse(-5.0);
assert_eq!(c.apply_warm_start(), 0.0);
c.store_impulse(3.0);
assert_eq!(c.apply_warm_start(), 3.0);
}
#[test]
fn test_ball_socket_angle_update() {
let mut bs =
BallSocketLimitConstraint::new(0, 1, [0.0, 0.0, 1.0], PI / 4.0, StopType::Hard);
bs.update_angle([0.0, 0.0, 1.0]);
assert!(bs.current_angle.abs() < 1e-10);
assert!(!bs.is_violated());
bs.update_angle([1.0, 0.0, 0.0]);
assert!((bs.current_angle - PI / 2.0).abs() < 1e-10);
assert!(bs.is_violated());
}
#[test]
fn test_ball_socket_penetration() {
let mut bs =
BallSocketLimitConstraint::new(0, 1, [0.0, 0.0, 1.0], PI / 6.0, StopType::Hard);
bs.current_angle = PI / 3.0;
let pen = bs.penetration();
assert!((pen - PI / 6.0).abs() < 1e-10);
}
#[test]
fn test_gear_propagate_impulse() {
let gear = GearRatioCollision::new(0, 1, 3.0, 1.0);
assert!((gear.propagate_impulse(10.0) - 30.0).abs() < 1e-10);
}
#[test]
fn test_gear_reflected_inertia() {
let gear = GearRatioCollision::new(0, 1, 2.0, 0.9);
assert!((gear.reflected_inertia(5.0) - 20.0).abs() < 1e-10);
}
#[test]
fn test_cable_extension_violation() {
let mut cable = CableChainLimit::new(0, 1, 0.0, 2.0, 1000.0, 10.0);
cable.current_length = 2.5;
assert!((cable.extension_violation() - 0.5).abs() < 1e-10);
assert_eq!(cable.compression_violation(), 0.0);
}
#[test]
fn test_chain_compression_violation() {
let mut chain = CableChainLimit::new(0, 1, 1.0, 3.0, 500.0, 5.0);
chain.current_length = 0.5;
assert!((chain.compression_violation() - 0.5).abs() < 1e-10);
}
#[test]
fn test_cable_impulse_positive_on_violation() {
let mut cable = CableChainLimit::new(0, 1, 0.0, 1.0, 1000.0, 0.0);
cable.current_length = 1.2;
let imp = cable.compute_impulse(1.0, 1.0, 0.01);
assert!(imp > 0.0);
}
#[test]
fn test_ab_stop_impulse() {
let lim = JointLimits::new(-1.0, 1.0);
let mut stop = ArticulatedBodyJointStop::new(
0,
2.0,
lim,
StopType::Soft {
stiffness: 500.0,
damping: 0.0,
},
);
stop.position = 1.5;
let imp = stop.compute_impulse(0.01);
assert!(imp > 0.0);
}
#[test]
fn test_ab_stop_velocity_impulse_restitution() {
let lim = JointLimits::new(-1.0, 1.0);
let mut stop = ArticulatedBodyJointStop::new(0, 1.0, lim, StopType::Hard);
stop.position = 1.5;
stop.velocity = -2.0;
let vi = stop.velocity_impulse(0.5);
assert!((vi - 3.0).abs() < 1e-10);
}
#[test]
fn test_motor_clamp_torque() {
let m = MotorHardLimit::new(10.0, 5.0, -PI, PI, 0.1);
assert_eq!(m.clamp_torque(20.0), 10.0);
assert_eq!(m.clamp_torque(-20.0), -10.0);
assert_eq!(m.clamp_torque(5.0), 5.0);
}
#[test]
fn test_motor_stall_detection() {
let mut m = MotorHardLimit::new(10.0, 5.0, -PI, PI, 0.1);
m.angle = PI + 0.1; m.check_stall();
assert!(m.stalled);
m.angle = 0.0;
m.check_stall();
assert!(!m.stalled);
}
#[test]
fn test_motor_limit_impulse_not_stalled() {
let m = MotorHardLimit::new(10.0, 5.0, -PI, PI, 0.1);
assert_eq!(m.limit_impulse(0.01), 0.0);
}
#[test]
fn test_friction_impulse_computed() {
let mut fric = JointFrictionConstraint::new(0, 1, [0.0, 0.0, 1.0], 0.3);
fric.normal_force = 100.0;
fric.relative_velocity = 2.0;
let imp = fric.compute_impulse(2.0, 0.01);
assert!((imp - (-1.0)).abs() < 1e-10);
}
#[test]
fn test_friction_warm_start_clamped() {
let mut fric = JointFrictionConstraint::new(0, 1, [1.0, 0.0, 0.0], 0.1);
fric.normal_force = 50.0;
fric.warm_impulse = 1000.0; let ws = fric.warm_start_impulse(1.0, 0.01);
assert!(ws <= 5.0 + 1e-10);
}
#[test]
fn test_compliance_rigid() {
let c = JointComplianceParams::rigid();
assert!((c.erp - 0.2).abs() < 1e-10);
assert_eq!(c.cfm, 0.0);
}
#[test]
fn test_compliance_soft() {
let c = JointComplianceParams::soft(1000.0, 10.0, 0.01);
assert!((c.erp - 0.5).abs() < 1e-10);
assert!((c.cfm - 0.05).abs() < 1e-10);
}
#[test]
fn test_compliance_positional_bias() {
let c = JointComplianceParams::new(0.5, 0.0);
let bias = c.positional_bias(0.1, 0.01);
assert!((bias - 5.0).abs() < 1e-10);
}
#[test]
fn test_warm_start_store_retrieve() {
let mut ws = JointLimitWarmStart::new();
ws.store(0, 1, 2, 5.5);
assert!((ws.retrieve(0, 1, 2) - 5.5).abs() < 1e-10);
assert_eq!(ws.retrieve(0, 1, 3), 0.0); }
#[test]
fn test_warm_start_scale() {
let mut ws = JointLimitWarmStart::new();
ws.store(0, 1, 0, 10.0);
ws.scale(0.5);
assert!((ws.retrieve(0, 1, 0) - 5.0).abs() < 1e-10);
}
#[test]
fn test_warm_start_clear() {
let mut ws = JointLimitWarmStart::new();
ws.store(0, 1, 0, 1.0);
ws.clear();
assert!(ws.is_empty());
}
#[test]
fn test_solver_no_violation() {
let mut solver = JointLimitSolver::new(4);
let lim = JointLimits::new(-1.0, 1.0);
let c = JointLimitConstraint::new(
0,
1,
JointType::Revolute,
lim,
StopType::Soft {
stiffness: 100.0,
damping: 0.0,
},
[0.0, 0.0, 1.0],
);
solver.add_constraint(c);
let inv_masses = vec![1.0, 1.0];
let total = solver.solve(&inv_masses, 0.01);
assert_eq!(total, 0.0);
}
#[test]
fn test_solver_violation_produces_impulse() {
let mut solver = JointLimitSolver::new(4);
let lim = JointLimits::new(-1.0, 1.0);
let mut c = JointLimitConstraint::new(
0,
1,
JointType::Revolute,
lim,
StopType::Soft {
stiffness: 100.0,
damping: 0.0,
},
[0.0, 0.0, 1.0],
);
c.position = 2.0; solver.add_constraint(c);
let inv_masses = vec![1.0, 1.0];
let total = solver.solve(&inv_masses, 0.01);
assert!(total > 0.0, "expected positive impulse, got {}", total);
}
#[test]
fn test_prismatic_penetration() {
let mut pj = PrismaticJointLimit::new(
0,
1,
[1.0, 0.0, 0.0],
-0.5,
0.5,
StopType::Hard,
JointComplianceParams::rigid(),
);
pj.translation = 0.8;
assert!((pj.penetration() - 0.3).abs() < 1e-10);
}
#[test]
fn test_prismatic_constraint_row() {
let mut pj = PrismaticJointLimit::new(
0,
1,
[0.0, 1.0, 0.0],
-1.0,
1.0,
StopType::Hard,
JointComplianceParams::rigid(),
);
pj.translation = 1.5;
assert_eq!(pj.constraint_row(), -1.0);
pj.translation = -1.5;
assert_eq!(pj.constraint_row(), 1.0);
pj.translation = 0.0;
assert_eq!(pj.constraint_row(), 0.0);
}
#[test]
fn test_revolute_is_active() {
let mut rev = RevoluteJointLimit::new(
0,
1,
[0.0, 1.0, 0.0],
-PI / 2.0,
PI / 2.0,
StopType::Hard,
JointComplianceParams::rigid(),
0.0,
);
rev.angle = PI;
assert!(rev.is_active());
rev.angle = 0.0;
assert!(!rev.is_active());
}
#[test]
fn test_revolute_solve_impulse_positive() {
let mut rev = RevoluteJointLimit::new(
0,
1,
[0.0, 0.0, 1.0],
-PI / 4.0,
PI / 4.0,
StopType::Hard,
JointComplianceParams::new(0.8, 0.0),
0.0,
);
rev.angle = PI / 2.0; rev.ang_velocity = 1.0;
let imp = rev.solve_impulse(1.0, 1.0, 0.01);
assert!(imp >= 0.0);
}
#[test]
fn test_joint_stop_contact_impulse() {
let contact = JointStopContact::new(
0,
[0.0, 1.0, 0.0],
0.05,
-1.0, 0.2,
);
let imp = contact.compute_impulse(2.0, 0.2, 0.01);
assert!(imp >= 0.0);
}
#[test]
fn test_pipeline_active_count() {
let mut pipe = JointLimitPipeline::new(4);
let mut rev = RevoluteJointLimit::new(
0,
1,
[0.0, 0.0, 1.0],
-1.0,
1.0,
StopType::Hard,
JointComplianceParams::rigid(),
0.0,
);
rev.angle = 2.0; pipe.revolute.push(rev);
assert_eq!(pipe.active_count(), 1);
}
#[test]
fn test_wrap_angle() {
assert!((wrap_angle(3.0 * PI) - PI).abs() < 1e-10);
assert!(
(wrap_angle(-3.0 * PI) - (-PI)).abs() < 1e-10
|| (wrap_angle(-3.0 * PI) - PI).abs() < 1e-10
);
assert!(wrap_angle(0.5).abs() - 0.5 < 1e-10);
}
#[test]
fn test_angle_diff_wrap() {
let d = angle_diff(PI * 0.9, -PI * 0.9);
assert!(d.abs() < PI + 1e-10);
}
#[test]
fn test_normalize3_zero() {
let v = normalize3([0.0, 0.0, 0.0]);
assert_eq!(v, [0.0, 0.0, 0.0]);
}
#[test]
fn test_normalize3_unit() {
let v = normalize3([3.0, 4.0, 0.0]);
let n = norm3(v);
assert!((n - 1.0).abs() < 1e-10);
}
#[test]
fn test_clampf() {
assert_eq!(clampf(5.0, 0.0, 3.0), 3.0);
assert_eq!(clampf(-1.0, 0.0, 3.0), 0.0);
assert_eq!(clampf(1.5, 0.0, 3.0), 1.5);
}
#[test]
fn test_solver_clear_constraints() {
let mut solver = JointLimitSolver::new(2);
let lim = JointLimits::new(-1.0, 1.0);
let c = JointLimitConstraint::new(
0,
1,
JointType::Prismatic,
lim,
StopType::Hard,
[1.0, 0.0, 0.0],
);
solver.add_constraint(c);
assert_eq!(solver.constraints.len(), 1);
solver.clear_constraints();
assert!(solver.constraints.is_empty());
}
}