use nalgebra::SVector;
use crate::body::{BodyHandle, RigidBody};
use crate::constraint::Constraint;
pub struct HingeJoint<const D: usize> {
pub body_a: BodyHandle,
pub body_b: BodyHandle,
pub anchor_a: SVector<f64, D>,
pub anchor_b: SVector<f64, D>,
pub plane_axis_a: usize,
pub plane_axis_b: usize,
pub stiffness: f64,
pub angle_limits: Option<(f64, f64)>,
pub motor: Option<super::MotorDrive>,
}
impl<const D: usize> HingeJoint<D> {
pub fn new(
body_a: BodyHandle,
body_b: BodyHandle,
plane_axis_a: usize,
plane_axis_b: usize,
) -> Self {
debug_assert!(plane_axis_a < D && plane_axis_b < D);
debug_assert!(plane_axis_a != plane_axis_b);
Self {
body_a,
body_b,
anchor_a: SVector::zeros(),
anchor_b: SVector::zeros(),
plane_axis_a,
plane_axis_b,
stiffness: 1.0,
angle_limits: None,
motor: None,
}
}
pub fn with_motor(mut self, motor: super::MotorDrive) -> Self {
self.motor = Some(motor);
self
}
pub fn with_anchors(
body_a: BodyHandle,
body_b: BodyHandle,
anchor_a: SVector<f64, D>,
anchor_b: SVector<f64, D>,
plane_axis_a: usize,
plane_axis_b: usize,
) -> Self {
Self {
body_a,
body_b,
anchor_a,
anchor_b,
plane_axis_a,
plane_axis_b,
stiffness: 1.0,
angle_limits: None,
motor: None,
}
}
pub fn with_limits(mut self, min: f64, max: f64) -> Self {
self.angle_limits = Some((min, max));
self
}
}
impl<const D: usize> Constraint<D> for HingeJoint<D> {
fn bodies(&self) -> (BodyHandle, BodyHandle) {
(self.body_a, self.body_b)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn solve(&self, body_a: &mut RigidBody<D>, body_b: &mut RigidBody<D>, _dt: f64) {
let world_a = body_a.transform.translation.0
+ body_a.transform.rotation.rotate_vector(&self.anchor_a);
let world_b = body_b.transform.translation.0
+ body_b.transform.rotation.rotate_vector(&self.anchor_b);
let error = world_b - world_a;
if error.norm() < 1e-15 {
return;
}
let correction = error * self.stiffness;
let total_inv_mass = body_a.inv_mass + body_b.inv_mass;
if total_inv_mass < 1e-15 {
return;
}
let ratio_a = body_a.inv_mass / total_inv_mass;
let ratio_b = body_b.inv_mass / total_inv_mass;
if body_a.is_dynamic() {
body_a.transform.translation.0 += correction * ratio_a;
}
if body_b.is_dynamic() {
body_b.transform.translation.0 -= correction * ratio_b;
}
}
fn solve_velocity(
&self,
body_a: &mut RigidBody<D>,
body_b: &mut RigidBody<D>,
_dt: f64,
mut callback: Option<&mut dyn crate::world::PhysicsCallback<D>>,
) {
let world_a = body_a.transform.translation.0
+ body_a.transform.rotation.rotate_vector(&self.anchor_a);
let world_b = body_b.transform.translation.0
+ body_b.transform.rotation.rotate_vector(&self.anchor_b);
let joint_dir = world_b - world_a;
let dist = joint_dir.norm();
if dist > 1e-15 {
let normal = joint_dir / dist;
let rel_vel = body_b.linear_velocity - body_a.linear_velocity;
let vel_along = rel_vel.dot(&normal);
let total_inv = body_a.inv_mass + body_b.inv_mass;
if total_inv > 1e-15 {
let impulse_mag = -vel_along * self.stiffness / total_inv;
let impulse = normal * impulse_mag;
if body_a.is_dynamic() {
body_a.linear_velocity -= impulse * body_a.inv_mass;
}
if body_b.is_dynamic() {
body_b.linear_velocity += impulse * body_b.inv_mass;
}
}
}
let num_bivector_components = D * (D - 1) / 2;
if num_bivector_components <= 1 && self.motor.is_none() {
return; }
let (pa, pb) = if self.plane_axis_a < self.plane_axis_b {
(self.plane_axis_a, self.plane_axis_b)
} else {
(self.plane_axis_b, self.plane_axis_a)
};
for i in 0..D {
for j in (i + 1)..D {
if i == pa && j == pb {
continue; }
let rel_component =
body_b.angular_velocity.get(i, j) - body_a.angular_velocity.get(i, j);
if rel_component.abs() < 1e-10 {
continue;
}
let correction = rel_component * self.stiffness * 0.5;
if body_a.is_dynamic() {
body_a.angular_velocity.set(
i,
j,
body_a.angular_velocity.get(i, j) + correction,
);
}
if body_b.is_dynamic() {
body_b.angular_velocity.set(
i,
j,
body_b.angular_velocity.get(i, j) - correction,
);
}
}
}
if let Some(ref motor) = self.motor {
let rel_angular_vel =
body_b.angular_velocity.get(pa, pb) - body_a.angular_velocity.get(pa, pb);
let torque = (motor.kp * (motor.target_pos - rel_angular_vel)
- rel_angular_vel * motor.kd)
.clamp(-motor.max_force, motor.max_force);
let power = torque * rel_angular_vel;
let work_joules = power * _dt;
if let Some(ref mut cb) = callback {
cb.record_work(self.body_b, work_joules);
}
let impulse = torque * _dt;
if body_a.is_dynamic() {
body_a.angular_velocity.set(
pa,
pb,
body_a.angular_velocity.get(pa, pb) - impulse * 0.5,
);
}
if body_b.is_dynamic() {
body_b.angular_velocity.set(
pa,
pb,
body_b.angular_velocity.get(pa, pb) + impulse * 0.5,
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use symtropy_math::Point;
#[test]
fn hinge_maintains_position() {
let mut a = RigidBody::<3>::static_body(
BodyHandle(0),
Point::origin(),
Box::new(symtropy_math::Sphere::new(Point::origin(), 0.1)),
);
let mut b =
RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([3.0, 0.0, 0.0]), 0.5, 1.0);
let joint = HingeJoint::new(BodyHandle(0), BodyHandle(1), 0, 1);
for _ in 0..20 {
joint.solve(&mut a, &mut b, 0.016);
}
let dist = a.transform.translation.distance(&b.transform.translation);
assert!(dist < 0.1, "hinge should connect bodies, dist = {dist}");
}
#[test]
fn hinge_allows_rotation_in_plane() {
let mut a = RigidBody::<3>::static_body(
BodyHandle(0),
Point::origin(),
Box::new(symtropy_math::Sphere::new(Point::origin(), 0.1)),
);
let mut b = RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::origin(), 0.5, 1.0);
b.angular_velocity.set(0, 1, 5.0);
b.angular_velocity.set(0, 2, 3.0);
let joint = HingeJoint::new(BodyHandle(0), BodyHandle(1), 0, 1);
joint.solve_velocity(&mut a, &mut b, 0.016);
let xy_after = b.angular_velocity.get(0, 1);
assert!(
xy_after.abs() > 1.0,
"XY rotation should be mostly preserved, got {xy_after}"
);
let xz_after = b.angular_velocity.get(0, 2);
assert!(
xz_after.abs() < 3.0,
"XZ rotation should be reduced from 3.0, got {xz_after}"
);
}
#[test]
fn hinge_4d() {
let mut a = RigidBody::<4>::dynamic_sphere(BodyHandle(0), Point::origin(), 0.5, 1.0);
let mut b = RigidBody::<4>::dynamic_sphere(
BodyHandle(1),
Point::new([2.0, 0.0, 0.0, 0.0]),
0.5,
1.0,
);
let joint = HingeJoint::new(BodyHandle(0), BodyHandle(1), 0, 3);
for _ in 0..20 {
joint.solve(&mut a, &mut b, 0.016);
}
let dist = a.transform.translation.distance(&b.transform.translation);
assert!(dist < 0.1, "4D hinge, dist = {dist}");
}
#[test]
fn hinge_2d_trivial() {
let mut a = RigidBody::<2>::dynamic_sphere(BodyHandle(0), Point::origin(), 0.5, 1.0);
let mut b = RigidBody::<2>::dynamic_sphere(BodyHandle(1), Point::new([2.0, 0.0]), 0.5, 1.0);
b.angular_velocity.set(0, 1, 3.0);
let joint = HingeJoint::new(BodyHandle(0), BodyHandle(1), 0, 1);
joint.solve_velocity(&mut a, &mut b, 0.016);
assert!(
b.angular_velocity.get(0, 1).abs() > 0.0,
"2D hinge should allow rotation"
);
}
}