use crate::sim::math::{Quat, Vec3};
use crate::{JointMotor, JointSpec};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum JointKind {
Fixed,
Revolute,
Spherical,
Prismatic,
}
impl JointKind {
pub(crate) fn is_linear_axis(self) -> bool {
self == JointKind::Prismatic
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct JointFrame {
pub(crate) kind: JointKind,
pub(crate) axis_a: Vec3,
pub(crate) axis_b: Vec3,
pub(crate) rest: Quat,
pub(crate) limits: Option<[f32; 2]>,
pub(crate) motor: Option<JointMotor>,
}
impl JointFrame {
pub(crate) fn new(spec: JointSpec, rotation_a: Quat, rotation_b: Quat) -> Self {
let (kind, axis, limits, motor) = match spec {
JointSpec::Fixed => (JointKind::Fixed, [0.0, 1.0, 0.0], None, None),
JointSpec::Spherical => (JointKind::Spherical, [0.0, 1.0, 0.0], None, None),
JointSpec::Revolute {
axis,
limits,
motor,
} => (JointKind::Revolute, axis, limits, motor),
JointSpec::Prismatic {
axis,
limits,
motor,
} => (JointKind::Prismatic, axis, limits, motor),
};
let axis = normalize_axis(axis);
JointFrame {
kind,
axis_a: axis,
axis_b: axis,
rest: rotation_a.conjugate().mul(rotation_b).normalize(),
limits: order_limits(limits),
motor: usable_motor(motor),
}
}
pub(crate) fn is_driven(&self) -> bool {
self.motor
.is_some_and(|m| m.target_velocity != 0.0 && m.max_force > 0.0)
}
pub(crate) fn hinge_angle(&self, rotation_a: Quat, rotation_b: Quat) -> f32 {
let delta = self
.rest
.conjugate()
.mul(rotation_a.conjugate().mul(rotation_b));
let (sin_part, cos_part) = if delta.w < 0.0 {
(
-Vec3::from_array([delta.x, delta.y, delta.z]).dot(self.axis_b),
-delta.w,
)
} else {
(
Vec3::from_array([delta.x, delta.y, delta.z]).dot(self.axis_b),
delta.w,
)
};
2.0 * libm::atan2f(sin_part, cos_part)
}
}
pub(crate) fn normalize_axis(axis: [f32; 3]) -> Vec3 {
let v = Vec3::from_array(axis);
if !v.is_finite() {
return Vec3::Y;
}
let length = v.length();
if length > 1.0e-6 {
v * (1.0 / length)
} else {
Vec3::Y
}
}
fn order_limits(limits: Option<[f32; 2]>) -> Option<[f32; 2]> {
let [low, high] = limits?;
if !low.is_finite() || !high.is_finite() {
return None;
}
Some(if low <= high {
[low, high]
} else {
[high, low]
})
}
fn usable_motor(motor: Option<JointMotor>) -> Option<JointMotor> {
let m = motor?;
if !m.target_velocity.is_finite() || !m.max_force.is_finite() || m.max_force <= 0.0 {
return None;
}
Some(m)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sim::math::vec3;
const HINGE: [f32; 3] = [0.0, 0.0, 1.0];
fn revolute(limits: Option<[f32; 2]>, motor: Option<JointMotor>) -> JointSpec {
JointSpec::Revolute {
axis: HINGE,
limits,
motor,
}
}
#[test]
fn each_spec_selects_the_rows_its_kind_leaves_open() {
let at_rest = |spec| JointFrame::new(spec, Quat::IDENTITY, Quat::IDENTITY).kind;
assert_eq!(at_rest(JointSpec::Fixed), JointKind::Fixed);
assert_eq!(at_rest(JointSpec::Spherical), JointKind::Spherical);
assert_eq!(at_rest(revolute(None, None)), JointKind::Revolute);
assert_eq!(
at_rest(JointSpec::Prismatic {
axis: HINGE,
limits: None,
motor: None,
}),
JointKind::Prismatic
);
assert!(JointKind::Prismatic.is_linear_axis());
assert!(!JointKind::Revolute.is_linear_axis());
}
#[test]
fn a_zero_length_axis_falls_back_to_up() {
assert_eq!(normalize_axis([0.0, 0.0, 0.0]), Vec3::Y);
assert_eq!(normalize_axis([f32::NAN, 1.0, 0.0]), Vec3::Y);
assert_eq!(normalize_axis([1.0e-9, 0.0, 0.0]), Vec3::Y);
let n = normalize_axis([3.0, 0.0, 4.0]);
assert!((n.x - 0.6).abs() < 1.0e-6 && (n.z - 0.8).abs() < 1.0e-6);
assert!((normalize_axis([0.0, -2.0, 0.0]) - vec3(0.0, -1.0, 0.0)).length() < 1.0e-6);
}
#[test]
fn limits_the_wrong_way_round_are_swapped_and_unusable_ones_dropped() {
assert_eq!(order_limits(Some([0.5, -0.5])), Some([-0.5, 0.5]));
assert_eq!(order_limits(Some([-0.5, 0.5])), Some([-0.5, 0.5]));
assert_eq!(order_limits(Some([0.25, 0.25])), Some([0.25, 0.25]));
assert_eq!(order_limits(Some([f32::NAN, 1.0])), None);
assert_eq!(order_limits(None), None);
}
#[test]
fn a_motor_with_no_force_ceiling_drives_nothing() {
let driving = JointMotor {
target_velocity: 2.0,
max_force: 10.0,
};
assert_eq!(usable_motor(Some(driving)), Some(driving));
assert_eq!(
usable_motor(Some(JointMotor {
target_velocity: 2.0,
max_force: 0.0,
})),
None
);
assert_eq!(
usable_motor(Some(JointMotor {
target_velocity: f32::NAN,
max_force: 1.0,
})),
None
);
}
#[test]
fn only_a_motor_with_somewhere_to_go_counts_as_driving() {
let frame = |motor| JointFrame::new(revolute(None, motor), Quat::IDENTITY, Quat::IDENTITY);
assert!(!frame(None).is_driven());
assert!(
!frame(Some(JointMotor {
target_velocity: 0.0,
max_force: 5.0,
}))
.is_driven()
);
assert!(
frame(Some(JointMotor {
target_velocity: 3.0,
max_force: 5.0,
}))
.is_driven()
);
}
#[test]
fn a_hinge_reads_zero_at_the_pose_it_was_made_at() {
let a = Quat::from_euler_deg([10.0, 25.0, -40.0]);
let b = Quat::from_euler_deg([0.0, 0.0, 30.0]);
let frame = JointFrame::new(revolute(None, None), a, b);
assert!(frame.hinge_angle(a, b).abs() < 1.0e-5);
}
#[test]
fn a_hinge_measures_the_turn_it_has_taken_about_its_axis() {
let frame = JointFrame::new(revolute(None, None), Quat::IDENTITY, Quat::IDENTITY);
for degrees in [-170.0f32, -90.0, -1.0, 0.0, 45.0, 90.0, 179.0] {
let radians = degrees.to_radians();
let turned = Quat::from_axis_angle(Vec3::Z, radians);
let measured = frame.hinge_angle(Quat::IDENTITY, turned);
assert!(
(measured - radians).abs() < 1.0e-4,
"{degrees} degrees read as {measured}"
);
}
}
#[test]
fn a_hinge_angle_stays_inside_half_a_turn_either_way() {
let frame = JointFrame::new(revolute(None, None), Quat::IDENTITY, Quat::IDENTITY);
for degrees in [181.0f32, 270.0, 359.0] {
let turned = Quat::from_axis_angle(Vec3::Z, degrees.to_radians());
let measured = frame.hinge_angle(Quat::IDENTITY, turned);
assert!(
measured.abs() <= core::f32::consts::PI + 1.0e-4,
"{degrees} degrees read as {measured}"
);
assert!(measured < 0.0, "{degrees} degrees must read as negative");
}
}
}