use super::vector::Displacement;
use super::xyz::XYZ;
use crate::centers::ReferenceCenter;
use crate::frames::ReferenceFrame;
use qtty::length::LengthUnit;
use qtty::Quantity;
use std::marker::PhantomData;
use std::ops::Mul;
#[repr(transparent)]
#[derive(Debug, Clone, Copy)]
pub struct Direction<F: ReferenceFrame> {
pub(in crate::cartesian) xyz: XYZ<f64>,
_frame: PhantomData<F>,
}
impl<F: ReferenceFrame> Direction<F> {
#[inline]
#[must_use]
pub fn new(x: f64, y: f64, z: f64) -> Self {
Self::try_new(x, y, z).expect("Cannot create Direction from zero vector")
}
#[inline]
#[must_use]
pub fn try_new(x: f64, y: f64, z: f64) -> Option<Self> {
XYZ::new(x, y, z)
.try_normalize()
.map(Self::from_xyz_unchecked)
}
#[inline]
pub fn normalize(x: f64, y: f64, z: f64) -> Self {
Self::new(x, y, z)
}
#[inline]
pub fn from_array(v: [f64; 3]) -> Self {
Self::new(v[0], v[1], v[2])
}
}
impl<F: ReferenceFrame> Direction<F> {
#[inline]
pub(crate) fn from_xyz_unchecked(xyz: XYZ<f64>) -> Self {
Self {
xyz,
_frame: PhantomData,
}
}
#[inline]
pub fn new_unchecked(x: f64, y: f64, z: f64) -> Self {
Self::from_xyz_unchecked(XYZ::new(x, y, z))
}
}
impl<F: ReferenceFrame> Direction<F> {
#[inline]
pub fn renormalize(&mut self) {
let mag = self.xyz.magnitude();
if mag.is_finite() && mag > f64::EPSILON {
self.xyz = self.xyz.scale(1.0 / mag);
}
}
#[inline]
#[must_use]
pub fn renormalized(mut self) -> Self {
self.renormalize();
self
}
}
impl<F: ReferenceFrame> Direction<F> {
#[inline]
pub fn x(&self) -> f64 {
self.xyz.x()
}
#[inline]
pub fn y(&self) -> f64 {
self.xyz.y()
}
#[inline]
pub fn z(&self) -> f64 {
self.xyz.z()
}
#[inline]
pub fn as_array(&self) -> [f64; 3] {
*self.xyz.as_array()
}
#[inline]
pub fn reinterpret_frame<F2: ReferenceFrame>(self) -> Direction<F2> {
Direction::new_unchecked(self.x(), self.y(), self.z())
}
}
impl<F: ReferenceFrame> Direction<F> {
#[inline]
pub fn scale<U: LengthUnit>(&self, magnitude: Quantity<U>) -> Displacement<F, U> {
Displacement::new(
magnitude * self.x(),
magnitude * self.y(),
magnitude * self.z(),
)
}
#[inline]
pub fn position<C, U>(&self, magnitude: Quantity<U>) -> super::Position<C, F, U>
where
C: ReferenceCenter<Params = ()>,
U: LengthUnit,
{
super::Position::new(
magnitude * self.x(),
magnitude * self.y(),
magnitude * self.z(),
)
}
#[inline]
pub fn position_with_params<C, U>(
&self,
center_params: C::Params,
magnitude: Quantity<U>,
) -> super::Position<C, F, U>
where
C: ReferenceCenter,
U: LengthUnit,
{
super::Position::new_with_params(
center_params,
magnitude * self.x(),
magnitude * self.y(),
magnitude * self.z(),
)
}
}
impl<F: ReferenceFrame, U: LengthUnit> Mul<Quantity<U>> for Direction<F> {
type Output = Displacement<F, U>;
#[inline]
fn mul(self, magnitude: Quantity<U>) -> Self::Output {
self.scale(magnitude)
}
}
forward_ref_binop! { impl[F: ReferenceFrame, U: LengthUnit] Mul, mul for Direction<F>, Quantity<U> }
impl<F: ReferenceFrame, U: LengthUnit> Mul<Direction<F>> for Quantity<U> {
type Output = Displacement<F, U>;
#[inline]
fn mul(self, dir: Direction<F>) -> Self::Output {
dir.scale(self)
}
}
forward_ref_binop! { impl[F: ReferenceFrame, U: LengthUnit] Mul, mul for Quantity<U>, Direction<F> }
impl<F: ReferenceFrame> Direction<F> {
#[inline]
pub fn dot(&self, other: &Self) -> f64 {
self.xyz.dot(&other.xyz)
}
#[inline]
pub fn cross(&self, other: &Self) -> Option<Self> {
self.xyz
.cross(&other.xyz)
.try_normalize()
.map(Self::from_xyz_unchecked)
}
#[inline]
pub fn negate(&self) -> Self {
Self::from_xyz_unchecked(self.xyz.neg())
}
#[inline]
pub fn angle_to(&self, other: &Self) -> f64 {
self.dot(other).clamp(-1.0, 1.0).acos()
}
}
impl<F: ReferenceFrame> Direction<F> {
pub fn to_spherical(&self) -> crate::spherical::Direction<F> {
let (polar, azimuth) = crate::spherical::xyz_to_polar_azimuth(self.x(), self.y(), self.z());
crate::spherical::Direction::<F>::new_unchecked(polar, azimuth)
}
}
impl<F: ReferenceFrame> Direction<F> {
pub fn display(&self) -> String {
format!("{self}")
}
}
impl_quantity_fmt_triplet! {
impl[F] for Direction<F>
where { F: ReferenceFrame, },
fmt_each: {},
|this, f, FmtOne| {
write!(f, "Frame: {}, X: ", F::frame_name())?;
FmtOne::fmt(&this.x(), f)?;
write!(f, ", Y: ")?;
FmtOne::fmt(&this.y(), f)?;
write!(f, ", Z: ")?;
FmtOne::fmt(&this.z(), f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DeriveReferenceCenter as ReferenceCenter;
use crate::DeriveReferenceFrame as ReferenceFrame;
#[allow(unused_imports)]
use qtty::angular::{Degrees, Radians};
#[allow(unused_imports)]
use qtty::length::{Kilometers, Meters};
use qtty::units::Meter;
use qtty::Quantity;
use qtty::M;
#[derive(Debug, Copy, Clone, ReferenceFrame)]
struct TestFrame;
#[derive(Debug, Copy, Clone, ReferenceCenter)]
struct TestCenter;
#[derive(Clone, Debug, Default, PartialEq)]
struct TestParams {
tag: i32,
}
#[derive(Debug, Copy, Clone, ReferenceCenter)]
#[center(params = TestParams)]
struct ParamCenter;
#[test]
fn test_direction_normalization() {
let dir = Direction::<TestFrame>::new(3.0, 4.0, 0.0);
let norm = (dir.x() * dir.x() + dir.y() * dir.y() + dir.z() * dir.z()).sqrt();
assert!((norm - 1.0).abs() < 1e-12);
assert!((dir.x() - 0.6).abs() < 1e-12);
assert!((dir.y() - 0.8).abs() < 1e-12);
}
#[test]
fn test_direction_scale() {
let dir = Direction::<TestFrame>::new(1.0, 0.0, 0.0);
let vec = dir.scale(Quantity::<Meter>::new(5.0));
assert!((vec.x().value() - 5.0).abs() < 1e-12);
assert!(vec.y().value().abs() < 1e-12);
assert!(vec.z().value().abs() < 1e-12);
}
#[test]
fn test_direction_dot_product() {
let a = Direction::<TestFrame>::new(1.0, 0.0, 0.0);
let b = Direction::<TestFrame>::new(0.0, 1.0, 0.0);
assert!(a.dot(&b).abs() < 1e-12);
assert!((a.dot(&a) - 1.0).abs() < 1e-12);
assert!((a.dot(&a.negate()) + 1.0).abs() < 1e-12);
}
#[test]
fn test_direction_cross_product() {
let x = Direction::<TestFrame>::new(1.0, 0.0, 0.0);
let y = Direction::<TestFrame>::new(0.0, 1.0, 0.0);
let z = x.cross(&y).expect("perpendicular directions");
assert!(z.x().abs() < 1e-12);
assert!(z.y().abs() < 1e-12);
assert!((z.z() - 1.0).abs() < 1e-12);
}
#[test]
fn test_direction_try_new_zero() {
assert!(Direction::<TestFrame>::try_new(0.0, 0.0, 0.0).is_none());
}
#[test]
fn test_direction_angle() {
let a = Direction::<TestFrame>::new(1.0, 0.0, 0.0);
let b = Direction::<TestFrame>::new(0.0, 1.0, 0.0);
let angle = a.angle_to(&b);
assert!((angle - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
}
#[test]
fn test_direction_helpers_and_accessors() {
let dir = Direction::<TestFrame>::normalize(0.0, 3.0, 4.0);
let arr = dir.as_array();
assert!((arr[0] - 0.0).abs() < 1e-12);
assert!((arr[1] - 0.6).abs() < 1e-12);
assert!((arr[2] - 0.8).abs() < 1e-12);
let from_arr = Direction::<TestFrame>::from_array([0.0, 3.0, 4.0]);
assert!((from_arr.y() - 0.6).abs() < 1e-12);
assert!((from_arr.z() - 0.8).abs() < 1e-12);
let unchecked = Direction::<TestFrame>::new_unchecked(1.0, 0.0, 0.0);
assert!((unchecked.x() - 1.0).abs() < 1e-12);
assert!(unchecked.y().abs() < 1e-12);
let spherical = unchecked.to_spherical();
assert!((spherical.polar.value()).abs() < 1e-12);
assert!((spherical.azimuth.value()).abs() < 1e-12);
}
#[test]
fn test_direction_position_helpers() {
let dir = Direction::<TestFrame>::new(1.0, 0.0, 0.0);
let pos = dir.position::<TestCenter, Meter>(2.0 * M);
assert!((pos.x().value() - 2.0).abs() < 1e-12);
assert!(pos.y().value().abs() < 1e-12);
let params = TestParams { tag: 7 };
let pos_params = dir.position_with_params::<ParamCenter, Meter>(params.clone(), 3.0 * M);
assert_eq!(pos_params.center_params(), ¶ms);
assert!((pos_params.x().value() - 3.0).abs() < 1e-12);
}
#[test]
fn test_direction_scaling_operator_left() {
let dir = Direction::<TestFrame>::new(0.0, 1.0, 0.0);
let disp: Displacement<TestFrame, Meter> = 4.0 * M * dir;
assert!((disp.y().value() - 4.0).abs() < 1e-12);
assert!(disp.x().value().abs() < 1e-12);
}
#[test]
fn test_direction_display() {
let dir = Direction::<TestFrame>::new(1.0, 0.0, 0.0);
let text = dir.display();
assert!(text.contains("Frame: TestFrame"));
assert!(text.contains("X: 1"));
let text_prec = format!("{:.3}", dir);
let expected_x = format!("{:.3}", dir.x());
assert!(text_prec.contains(&format!("X: {expected_x}")));
let text_exp = format!("{:.2e}", dir);
let expected_y = format!("{:.2e}", dir.y());
assert!(text_exp.contains(&format!("Y: {expected_y}")));
}
#[test]
fn direction_has_xyz_layout() {
assert_eq!(
core::mem::size_of::<Direction<TestFrame>>(),
core::mem::size_of::<XYZ<f64>>()
);
assert_eq!(
core::mem::align_of::<Direction<TestFrame>>(),
core::mem::align_of::<XYZ<f64>>()
);
}
#[test]
fn test_direction_renormalize_after_rotation_chain() {
use crate::Rotation3;
use qtty::angular::Radians;
struct Xs(u64);
impl Xs {
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x.wrapping_mul(0x2545F4914F6CDD1D)
}
fn next_unit(&mut self) -> f64 {
((self.next_u64() >> 11) as f64) * (1.0 / ((1u64 << 53) as f64))
}
fn next_signed(&mut self) -> f64 {
self.next_unit() * 2.0 - 1.0
}
}
let mag_of =
|d: &Direction<TestFrame>| (d.x() * d.x() + d.y() * d.y() + d.z() * d.z()).sqrt();
let mut rng = Xs(0x00C0_FFEE_DEAD_BEEF_u64);
let initial = Direction::<TestFrame>::new(1.0, 0.0, 0.0);
let mut drifting = initial;
let mut periodic = initial;
const N: usize = 10_000;
const PERIOD: usize = 100;
for i in 1..=N {
let mut ax = rng.next_signed();
let mut ay = rng.next_signed();
let mut az = rng.next_signed();
let an = (ax * ax + ay * ay + az * az).sqrt();
if an < 1e-12 {
ax = 1.0;
ay = 0.0;
az = 0.0;
} else {
ax /= an;
ay /= an;
az /= an;
}
let angle = Radians::new(rng.next_signed() * 1e-3);
let rot = Rotation3::from_axis_angle([ax, ay, az], angle);
drifting = rot * drifting;
periodic = rot * periodic;
if i % PERIOD == 0 {
let drift_mag = mag_of(&drifting);
assert!(
(drift_mag - 1.0).abs() < 1e-3,
"drift too large at step {i}: {drift_mag}"
);
let renormed = drifting.renormalized();
let renormed_mag = mag_of(&renormed);
assert!(
(renormed_mag - 1.0).abs() < 1e-15,
"renormalized magnitude not unit at step {i}: {renormed_mag}"
);
periodic.renormalize();
let periodic_mag = mag_of(&periodic);
assert!(
(periodic_mag - 1.0).abs() < 1e-15,
"periodic chain not unit immediately after renormalize: {periodic_mag}"
);
}
}
let final_mag = mag_of(&periodic);
assert!(
(final_mag - 1.0).abs() < 1e-12,
"periodic-renormalization chain drifted: {final_mag}"
);
}
#[test]
fn test_direction_renormalize_leaves_degenerate_unchanged() {
let mut bad = Direction::<TestFrame>::new_unchecked(f64::NAN, 0.0, 0.0);
bad.renormalize();
assert!(bad.x().is_nan());
}
}