use core::marker::PhantomData;
use astrodyn_quantities::aliases::{Force, Torque};
use astrodyn_quantities::frame::Frame;
use glam::{DMat3, DVec3};
#[inline]
pub fn shift_wrench_to_parent(
f_child: DVec3,
tau_child: DVec3,
r_parent_to_child: DVec3,
t_parent_child: DMat3,
) -> (DVec3, DVec3) {
let t_t = t_parent_child.transpose();
let f_parent = t_t * f_child;
let tau_parent = t_t * tau_child + r_parent_to_child.cross(f_parent);
(f_parent, tau_parent)
}
#[inline]
pub fn shift_wrench_to_parent_typed<F: Frame>(
f_child: Force<F>,
tau_child: Torque<F>,
r_parent_to_child: DVec3,
t_parent_child: DMat3,
) -> (Force<F>, Torque<F>) {
let (f, t) = shift_wrench_to_parent(
f_child.raw_si(),
tau_child.raw_si(),
r_parent_to_child,
t_parent_child,
);
(Force::<F>::from_raw_si(f), Torque::<F>::from_raw_si(t))
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Wrench {
pub force: DVec3,
pub torque: DVec3,
_frame: PhantomData<()>,
}
impl Wrench {
#[inline]
pub const fn new(force: DVec3, torque: DVec3) -> Self {
Self {
force,
torque,
_frame: PhantomData,
}
}
#[inline]
pub const fn zero() -> Self {
Self::new(DVec3::ZERO, DVec3::ZERO)
}
#[inline]
pub fn shift_to_parent(self, r_parent_to_child: DVec3, t_parent_child: DMat3) -> Self {
let (f, t) =
shift_wrench_to_parent(self.force, self.torque, r_parent_to_child, t_parent_child);
Self::new(f, t)
}
#[inline]
pub fn accumulate(&mut self, other: Wrench) {
self.force += other.force;
self.torque += other.torque;
}
}
impl core::ops::Add for Wrench {
type Output = Wrench;
#[inline]
fn add(self, rhs: Wrench) -> Wrench {
Self::new(self.force + rhs.force, self.torque + rhs.torque)
}
}
impl core::ops::AddAssign for Wrench {
#[inline]
fn add_assign(&mut self, rhs: Wrench) {
self.accumulate(rhs);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_close(a: DVec3, b: DVec3, tol: f64, label: &str) {
let d = (a - b).length();
assert!(d < tol, "{label}: |Δ|={d:.3e}, a={a:?}, b={b:?}");
}
#[test]
fn identity_at_zero_offset_zero_rotation() {
let f = DVec3::new(1.0, 2.0, 3.0);
let t = DVec3::new(-0.5, 0.25, 7.0);
let (f_out, t_out) = shift_wrench_to_parent(f, t, DVec3::ZERO, DMat3::IDENTITY);
assert_close(f_out, f, 1e-15, "force");
assert_close(t_out, t, 1e-15, "torque");
}
#[test]
fn pure_offset_creates_cross_term() {
let f = DVec3::new(10.0, 0.0, 0.0);
let r = DVec3::new(0.0, 1.0, 0.0);
let (f_out, t_out) = shift_wrench_to_parent(f, DVec3::ZERO, r, DMat3::IDENTITY);
assert_eq!(f_out, f);
assert_eq!(t_out, DVec3::new(0.0, 0.0, -10.0));
}
#[test]
fn pure_rotation_no_offset_matches_jeod_convention() {
let t_pc = DMat3::from_cols(
DVec3::new(0.0, 1.0, 0.0),
DVec3::new(-1.0, 0.0, 0.0),
DVec3::new(0.0, 0.0, 1.0),
);
let f_child = DVec3::new(5.0, 0.0, 0.0);
let (f_parent, _) = shift_wrench_to_parent(f_child, DVec3::ZERO, DVec3::ZERO, t_pc);
assert_close(
f_parent,
DVec3::new(0.0, -5.0, 0.0),
1e-15,
"rot child→parent",
);
}
#[test]
fn additivity_over_siblings() {
let f1 = DVec3::new(1.0, 2.0, 3.0);
let t1 = DVec3::new(0.5, 0.0, -1.0);
let f2 = DVec3::new(-2.0, 0.5, 4.0);
let t2 = DVec3::new(0.0, 1.0, 0.5);
let r = DVec3::new(0.1, -0.2, 0.3);
let t_pc = DMat3::IDENTITY;
let (a_f1, a_t1) = shift_wrench_to_parent(f1, t1, r, t_pc);
let (a_f2, a_t2) = shift_wrench_to_parent(f2, t2, r, t_pc);
let a_f = a_f1 + a_f2;
let a_t = a_t1 + a_t2;
let (b_f, b_t) = shift_wrench_to_parent(f1 + f2, t1 + t2, r, t_pc);
assert_close(a_f, b_f, 1e-15, "sum-then-shift force");
assert_close(a_t, b_t, 1e-15, "sum-then-shift torque");
}
#[test]
fn sign_pure_torque_no_force() {
let tau = DVec3::new(1.0, -2.0, 3.0);
let r = DVec3::new(5.0, 5.0, 5.0);
let (f_p, tau_p) = shift_wrench_to_parent(DVec3::ZERO, tau, r, DMat3::IDENTITY);
assert_eq!(f_p, DVec3::ZERO);
assert_eq!(tau_p, tau);
}
#[test]
fn typed_round_trip() {
use astrodyn_quantities::frame::RootInertial;
let f = Force::<RootInertial>::from_raw_si(DVec3::new(2.0, -1.0, 4.0));
let tau = Torque::<RootInertial>::from_raw_si(DVec3::new(0.0, 0.5, -0.25));
let r = DVec3::new(1.0, 0.0, 0.0);
let (f_p, tau_p) = shift_wrench_to_parent_typed(f, tau, r, DMat3::IDENTITY);
assert_eq!(f_p.raw_si(), f.raw_si());
let expected_tau = tau.raw_si() + DVec3::new(0.0, -4.0, -1.0);
assert_eq!(tau_p.raw_si(), expected_tau);
}
#[test]
fn wrench_helpers_compose() {
let w = Wrench::new(DVec3::new(1.0, 0.0, 0.0), DVec3::ZERO);
let shifted = w.shift_to_parent(DVec3::new(0.0, 1.0, 0.0), DMat3::IDENTITY);
assert_eq!(shifted.force, DVec3::new(1.0, 0.0, 0.0));
assert_eq!(shifted.torque, DVec3::new(0.0, 0.0, -1.0));
let mut acc = Wrench::zero();
acc.accumulate(shifted);
acc += Wrench::new(DVec3::new(0.0, 1.0, 0.0), DVec3::new(0.0, 0.0, 0.5));
assert_eq!(acc.force, DVec3::new(1.0, 1.0, 0.0));
assert_eq!(acc.torque, DVec3::new(0.0, 0.0, -0.5));
}
}