use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UpAxis {
#[default]
Y,
Z,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FrameConvention {
pub up_axis: UpAxis,
pub scale: f64,
}
impl Default for FrameConvention {
fn default() -> Self {
Self {
up_axis: UpAxis::Y,
scale: 1.0,
}
}
}
impl FrameConvention {
#[must_use]
pub fn position(&self, p: [f32; 3]) -> [f64; 3] {
let [x, y, z] = p.map(|v| f64::from(v) * self.scale);
match self.up_axis {
UpAxis::Y => [x, -z, y],
UpAxis::Z => [x, y, z],
}
}
#[must_use]
pub fn vector(&self, v: [f32; 3]) -> [f64; 3] {
let [x, y, z] = v.map(f64::from);
match self.up_axis {
UpAxis::Y => [x, -z, y],
UpAxis::Z => [x, y, z],
}
}
#[must_use]
#[allow(clippy::many_single_char_names)]
pub fn orientation(&self, q: [f32; 4]) -> Option<[f64; 4]> {
let [x, y, z, w] = q.map(f64::from);
let converted = match self.up_axis {
UpAxis::Y => [x, -z, y, w],
UpAxis::Z => [x, y, z, w],
};
quat::normalize(converted)
}
}
pub mod quat {
pub type Quat = [f64; 4];
pub const IDENTITY: Quat = [0.0, 0.0, 0.0, 1.0];
#[must_use]
pub fn normalize(q: Quat) -> Option<Quat> {
let n = q.iter().map(|v| v * v).sum::<f64>().sqrt();
if !n.is_finite() || n < 1e-9 {
return None;
}
Some(q.map(|v| v / n))
}
#[must_use]
pub fn mul(a: Quat, b: Quat) -> Quat {
let [ax, ay, az, aw] = a;
let [bx, by, bz, bw] = b;
[
aw * bx + ax * bw + ay * bz - az * by,
aw * by - ax * bz + ay * bw + az * bx,
aw * bz + ax * by - ay * bx + az * bw,
aw * bw - ax * bx - ay * by - az * bz,
]
}
#[must_use]
pub fn conj(q: Quat) -> Quat {
[-q[0], -q[1], -q[2], q[3]]
}
#[must_use]
pub fn rotate(q: Quat, v: [f64; 3]) -> [f64; 3] {
let r = mul(mul(q, [v[0], v[1], v[2], 0.0]), conj(q));
[r[0], r[1], r[2]]
}
#[must_use]
pub fn log(q: Quat) -> [f64; 3] {
let q = if q[3] < 0.0 { q.map(|v| -v) } else { q };
let s = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2]).sqrt();
if s < 1e-12 {
return [2.0 * q[0], 2.0 * q[1], 2.0 * q[2]];
}
let angle = 2.0 * s.atan2(q[3]);
[q[0] / s * angle, q[1] / s * angle, q[2] / s * angle]
}
#[must_use]
pub fn from_axis_angle(axis: [f64; 3], angle: f64) -> Quat {
let (s, c) = (angle / 2.0).sin_cos();
[axis[0] * s, axis[1] * s, axis[2] * s, c]
}
#[must_use]
pub fn from_rpy(roll: f64, pitch: f64, yaw: f64) -> Quat {
let qx = from_axis_angle([1.0, 0.0, 0.0], roll);
let qy = from_axis_angle([0.0, 1.0, 0.0], pitch);
let qz = from_axis_angle([0.0, 0.0, 1.0], yaw);
mul(qz, mul(qy, qx))
}
}
#[cfg(test)]
mod tests {
use super::quat::*;
use super::*;
fn close(a: &[f64], b: &[f64]) -> bool {
a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6)
}
#[test]
fn y_up_to_z_up() {
let c = FrameConvention::default();
assert!(close(&c.position([0.0, 1.0, 0.0]), &[0.0, 0.0, 1.0]));
assert!(close(&c.position([0.0, 0.0, 1.0]), &[0.0, -1.0, 0.0]));
assert!(close(&c.position([1.0, 0.0, 0.0]), &[1.0, 0.0, 0.0]));
let angle = 0.7_f64;
let motive = from_axis_angle([0.0, 1.0, 0.0], angle).map(|v| v as f32);
let ros = c.orientation(motive).unwrap();
assert!(close(&ros, &from_axis_angle([0.0, 0.0, 1.0], angle)));
let q = normalize([0.1, 0.5, -0.3, 0.8]).unwrap();
let v = [0.3_f32, -1.2, 2.0];
let rotated_native = rotate(q, v.map(f64::from)).map(|x| x as f32);
let lhs = c.position(rotated_native);
let rhs = rotate(c.orientation(q.map(|x| x as f32)).unwrap(), c.position(v));
assert!(lhs.iter().zip(rhs).all(|(a, b)| (a - b).abs() < 1e-5));
}
#[test]
fn z_up_passthrough_and_invalid() {
let c = FrameConvention {
up_axis: UpAxis::Z,
scale: 0.001,
};
assert!(close(
&c.position([1000.0, 2000.0, 3000.0]),
&[1.0, 2.0, 3.0]
));
assert!(c.orientation([0.0; 4]).is_none());
assert!(c.orientation([f32::NAN, 0.0, 0.0, 1.0]).is_none());
}
#[test]
fn quaternion_algebra() {
let q = from_axis_angle([0.0, 0.0, 1.0], std::f64::consts::FRAC_PI_2);
assert!(close(&rotate(q, [1.0, 0.0, 0.0]), &[0.0, 1.0, 0.0]));
assert!(close(&mul(q, conj(q)), &IDENTITY));
assert!(close(&log(q), &[0.0, 0.0, std::f64::consts::FRAC_PI_2]));
assert!(close(
&log(q.map(|v| -v)),
&[0.0, 0.0, std::f64::consts::FRAC_PI_2]
));
assert!(close(
&from_rpy(0.0, 0.0, 0.3),
&from_axis_angle([0.0, 0.0, 1.0], 0.3)
));
}
}