use super::vector::Displacement;
use super::xyz::XYZ;
use crate::centers::ReferenceCenter;
use crate::frames::ReferenceFrame;
use qtty::{LengthUnit, 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_vec3(vec: nalgebra::Vector3<f64>) -> Self {
Self::new(vec.x, vec.y, vec.z)
}
}
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 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_vec3(&self) -> nalgebra::Vector3<f64> {
*self.xyz.as_vec3()
}
#[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)
}
}
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)
}
}
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_raw(polar, azimuth)
}
}
impl<F: ReferenceFrame> Direction<F> {
pub fn display(&self) -> String {
format!("{self}")
}
}
impl<F: ReferenceFrame> std::fmt::Display for Direction<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Frame: {}, X: ", F::frame_name())?;
std::fmt::Display::fmt(&self.x(), f)?;
write!(f, ", Y: ")?;
std::fmt::Display::fmt(&self.y(), f)?;
write!(f, ", Z: ")?;
std::fmt::Display::fmt(&self.z(), f)
}
}
impl<F: ReferenceFrame> std::fmt::LowerExp for Direction<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Frame: {}, X: ", F::frame_name())?;
std::fmt::LowerExp::fmt(&self.x(), f)?;
write!(f, ", Y: ")?;
std::fmt::LowerExp::fmt(&self.y(), f)?;
write!(f, ", Z: ")?;
std::fmt::LowerExp::fmt(&self.z(), f)
}
}
impl<F: ReferenceFrame> std::fmt::UpperExp for Direction<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Frame: {}, X: ", F::frame_name())?;
std::fmt::UpperExp::fmt(&self.x(), f)?;
write!(f, ", Y: ")?;
std::fmt::UpperExp::fmt(&self.y(), f)?;
write!(f, ", Z: ")?;
std::fmt::UpperExp::fmt(&self.z(), f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DeriveReferenceCenter as ReferenceCenter;
use crate::DeriveReferenceFrame as ReferenceFrame;
use qtty::*;
#[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 vec3 = dir.as_vec3();
assert!((vec3.x - 0.0).abs() < 1e-12);
assert!((vec3.y - 0.6).abs() < 1e-12);
assert!((vec3.z - 0.8).abs() < 1e-12);
let from_vec3 = Direction::<TestFrame>::from_vec3(nalgebra::Vector3::new(0.0, 3.0, 4.0));
assert!((from_vec3.y() - 0.6).abs() < 1e-12);
assert!((from_vec3.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>>()
);
}
}