use crate::geometry::Vector3;
use approx::{AbsDiffEq, RelativeEq};
use core::ops::Mul;
pub use error::QuaternionError;
mod error;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Quaternion {
pub w: f64,
pub x: f64,
pub y: f64,
pub z: f64,
}
impl Quaternion {
#[must_use]
pub const fn from_wxyz(
w: f64,
x: f64,
y: f64,
z: f64,
) -> Self {
Self { w, x, y, z }
}
#[must_use]
pub const fn identity() -> Self {
Self {
w: 1.0,
x: 0.0,
y: 0.0,
z: 0.0,
}
}
#[must_use = "this returns the result of the operation, without modifying the original"]
#[inline]
pub fn conjugate(self) -> Quaternion {
Quaternion {
w: self.w,
x: -self.x,
y: -self.y,
z: -self.z,
}
}
#[inline]
pub fn normalize(self) -> Result<Quaternion, QuaternionError> {
let norm = self.norm();
if !norm.is_finite() {
return Err(QuaternionError::NonFinite);
}
if norm < f64::EPSILON {
return Err(QuaternionError::ZeroLengthNormalization);
}
Ok(self.scale(1.0 / norm))
}
#[must_use = "this returns the result of the operation, without modifying the original"]
#[inline]
pub fn norm(self) -> f64 {
libm::sqrt(self.w * self.w + self.x * self.x + self.y * self.y + self.z * self.z)
}
#[must_use = "this returns the result of the operation, without modifying the original"]
#[inline]
fn scale(
self,
factor: f64,
) -> Quaternion {
Quaternion {
w: self.w * factor,
x: self.x * factor,
y: self.y * factor,
z: self.z * factor,
}
}
#[must_use = "this returns the result of the operation, without modifying the original"]
#[inline]
fn weighted_sum(
a: Quaternion,
wa: f64,
b: Quaternion,
wb: f64,
) -> Quaternion {
Quaternion {
w: a.w * wa + b.w * wb,
x: a.x * wa + b.x * wb,
y: a.y * wa + b.y * wb,
z: a.z * wa + b.z * wb,
}
}
#[must_use = "this returns the result of the operation, without modifying the original"]
#[inline]
pub fn rotate_vector(
self,
v: Vector3,
) -> Vector3 {
let q_vec = Quaternion {
w: 0.0,
x: v.x,
y: v.y,
z: v.z,
};
let q_res = self.mul(q_vec).mul(self.conjugate());
Vector3 {
x: q_res.x,
y: q_res.y,
z: q_res.z,
}
}
#[must_use = "this returns the result of the operation, without modifying the original"]
#[inline]
pub fn slerp(
self,
other: Quaternion,
t: f64,
) -> Quaternion {
let t = t.clamp(0.0, 1.0);
let mut other = other;
let mut dot = self.w * other.w + self.x * other.x + self.y * other.y + self.z * other.z;
if dot < 0.0 {
other = other.scale(-1.0);
dot = -dot;
}
let dot = dot.clamp(-1.0, 1.0);
if dot > 1.0 - f64::EPSILON {
let blended = Self::weighted_sum(self, 1.0 - t, other, t);
let norm = blended.norm();
return if norm < f64::EPSILON {
blended
} else {
blended.scale(1.0 / norm)
};
}
let theta = libm::acos(dot);
let sin_theta = libm::sin(theta);
let scale_self = libm::sin((1.0 - t) * theta) / sin_theta;
let scale_other = libm::sin(t * theta) / sin_theta;
Self::weighted_sum(self, scale_self, other, scale_other)
}
}
impl Mul for Quaternion {
type Output = Quaternion;
#[inline]
fn mul(
self,
other: Quaternion,
) -> Quaternion {
Quaternion {
w: self.w * other.w - self.x * other.x - self.y * other.y - self.z * other.z,
x: self.w * other.x + self.x * other.w + self.y * other.z - self.z * other.y,
y: self.w * other.y - self.x * other.z + self.y * other.w + self.z * other.x,
z: self.w * other.z + self.x * other.y - self.y * other.x + self.z * other.w,
}
}
}
impl AbsDiffEq for Quaternion {
type Epsilon = f64;
fn default_epsilon() -> Self::Epsilon {
f64::EPSILON
}
fn abs_diff_eq(
&self,
other: &Self,
epsilon: Self::Epsilon,
) -> bool {
f64::abs_diff_eq(&self.w, &other.w, epsilon)
&& f64::abs_diff_eq(&self.x, &other.x, epsilon)
&& f64::abs_diff_eq(&self.y, &other.y, epsilon)
&& f64::abs_diff_eq(&self.z, &other.z, epsilon)
}
}
impl RelativeEq for Quaternion {
fn default_max_relative() -> Self::Epsilon {
f64::EPSILON
}
fn relative_eq(
&self,
other: &Self,
epsilon: Self::Epsilon,
max_relative: Self::Epsilon,
) -> bool {
f64::relative_eq(&self.w, &other.w, epsilon, max_relative)
&& f64::relative_eq(&self.x, &other.x, epsilon, max_relative)
&& f64::relative_eq(&self.y, &other.y, epsilon, max_relative)
&& f64::relative_eq(&self.z, &other.z, epsilon, max_relative)
}
}
#[cfg(test)]
mod tests;