#![allow(clippy::float_cmp, clippy::suboptimal_flops)]
use crate::vector2d;
use crate::{Degrees, Radians, Validate, two_sum};
use core::{cmp::Ordering, ops::Neg};
use num_traits::{Float, float::FloatConst};
pub const SQ_EPSILON: f64 = f64::EPSILON * f64::EPSILON;
#[allow(clippy::excessive_precision, clippy::unreadable_literal)]
pub const SQRT_3: f64 = 1.732050807568877293527446341505872367_f64;
pub const COS_30_DEGREES: f64 = SQRT_3 / 2.0;
pub const MAX_LINEAR_SIN_ANGLE: f64 = 9.67e7 * f64::EPSILON;
pub const MAX_COS_ANGLE_IS_ONE: f64 = 3.35e7 * f64::EPSILON;
pub const THIRTY: f64 = 30.0;
pub const FORTY_FIVE: f64 = 45.0;
#[must_use]
fn to_radians<T: Float + FloatConst>(angle: Degrees<T>) -> Radians<T> {
let thirty = T::from(THIRTY).expect("Could not convert constant to Float");
if angle.0.abs() == thirty {
Radians(T::FRAC_PI_6().copysign(angle.0))
} else {
Radians(angle.0.to_radians())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd)]
#[repr(transparent)]
pub struct UnitNegRange<T: Float>(pub T);
impl<T: Float> Default for UnitNegRange<T> {
fn default() -> Self {
Self(T::zero())
}
}
impl<T: Float> UnitNegRange<T> {
#[must_use]
pub fn clamp(value: T) -> Self {
Self(value.clamp(-T::one(), T::one()))
}
#[must_use]
pub fn abs(self) -> Self {
Self(self.0.abs())
}
}
impl<T: Float> Validate for UnitNegRange<T> {
fn is_valid(&self) -> bool {
(-T::one()..=T::one()).contains(&self.0)
}
}
impl<T: Float> Neg for UnitNegRange<T> {
type Output = Self;
fn neg(self) -> Self {
Self(T::zero() - self.0)
}
}
#[must_use]
pub fn sq_a_minus_sq_b<T: Float>(a: UnitNegRange<T>, b: UnitNegRange<T>) -> UnitNegRange<T> {
UnitNegRange::<T>((a.0 - b.0) * (a.0 + b.0))
}
#[must_use]
pub fn one_minus_sq_value<T: Float>(a: UnitNegRange<T>) -> UnitNegRange<T> {
sq_a_minus_sq_b(UnitNegRange(T::one()), a)
}
#[must_use]
pub fn swap_sin_cos<T: Float>(a: UnitNegRange<T>) -> UnitNegRange<T> {
UnitNegRange(one_minus_sq_value(a).0.sqrt())
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn cosine_from_sine<T: Float>(a: UnitNegRange<T>, sign: T) -> UnitNegRange<T> {
let max_cos_angle_is_one =
T::from(MAX_COS_ANGLE_IS_ONE).expect("Could not convert constant to Float");
if a.0.abs() > max_cos_angle_is_one {
let b = swap_sin_cos(a);
if b.0 > T::zero() {
UnitNegRange(b.0.copysign(sign))
} else {
b
}
} else {
UnitNegRange(T::one().copysign(sign))
}
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn sine<T: Float + FloatConst>(angle: Radians<T>) -> UnitNegRange<T> {
let max_linear_sin_angle =
T::from(MAX_LINEAR_SIN_ANGLE).expect("Could not convert constant to Float");
let angle_abs = angle.0.abs();
if angle_abs == T::FRAC_PI_4() {
UnitNegRange(T::FRAC_1_SQRT_2().copysign(angle.0))
} else if angle_abs > max_linear_sin_angle {
UnitNegRange(angle.0.sin())
} else {
UnitNegRange(angle.0)
}
}
#[must_use]
pub fn cosine<T: Float + FloatConst>(angle: Radians<T>, sin: UnitNegRange<T>) -> UnitNegRange<T> {
let angle_abs = angle.0.abs();
if angle_abs == T::FRAC_PI_4() {
UnitNegRange(T::FRAC_1_SQRT_2().copysign(T::FRAC_PI_2() - angle_abs))
} else {
cosine_from_sine(sin, T::FRAC_PI_2() - angle_abs)
}
}
#[must_use]
fn assign_sin_cos_to_quadrant<T: Float>(
sin: UnitNegRange<T>,
cos: UnitNegRange<T>,
q: i32,
) -> (UnitNegRange<T>, UnitNegRange<T>) {
match q & 3 {
1 => (cos, -sin), 2 => (-sin, -cos), 3 => (-cos, sin), _ => (sin, cos),
}
}
#[must_use]
pub fn sincos<T>(radians: Radians<T>) -> (UnitNegRange<T>, UnitNegRange<T>)
where
T: Float + FloatConst,
f64: From<T>,
{
let radians = f64::from(radians.0);
let rq = libm::remquo(radians, core::f64::consts::FRAC_PI_2);
let radians_q = T::from(rq.0).expect("Could not convert value to Float");
let radians_q = Radians(radians_q);
let sin = sine(radians_q);
assign_sin_cos_to_quadrant(sin, cosine(radians_q, sin), rq.1)
}
#[must_use]
pub fn sincos_diff<T>(a: Radians<T>, b: Radians<T>) -> (UnitNegRange<T>, UnitNegRange<T>)
where
T: Float + FloatConst,
f64: From<T>,
{
let delta = two_sum(a.0, -b.0);
let radians = f64::from(delta.0);
let rq = libm::remquo(radians, core::f64::consts::FRAC_PI_2);
let radians_q = T::from(rq.0).expect("Could not convert value to Float");
let radians_q = Radians(radians_q + delta.1);
let sin = sine(radians_q);
assign_sin_cos_to_quadrant(sin, cosine(radians_q, sin), rq.1)
}
#[must_use]
pub fn arctan2<T: Float + FloatConst>(sin: UnitNegRange<T>, cos: UnitNegRange<T>) -> Radians<T> {
let sin_abs = sin.0.abs();
let cos_abs = cos.0.abs();
let radians_pi_2 = match sin_abs.partial_cmp(&cos_abs).expect("sin or cos is NaN") {
Ordering::Equal => T::FRAC_PI_4(),
Ordering::Less => sin_abs.atan2(cos_abs),
Ordering::Greater => T::FRAC_PI_2() - cos_abs.atan2(sin_abs),
};
let radians_pi = if cos.0 < T::zero() {
T::PI() - radians_pi_2
} else {
radians_pi_2
};
Radians(radians_pi.copysign(sin.0))
}
#[must_use]
pub fn sincosd<T>(degrees: Degrees<T>) -> (UnitNegRange<T>, UnitNegRange<T>)
where
T: Float + FloatConst,
f64: From<T>,
{
let rq: (f64, i32) = libm::remquo(f64::from(degrees.0), 90.0);
let radians_q = T::from(rq.0).expect("Could not convert value to Float");
let radians_q = to_radians(Degrees(radians_q));
let sin = sine(radians_q);
assign_sin_cos_to_quadrant(sin, cosine(radians_q, sin), rq.1)
}
#[must_use]
pub fn sincosd_diff<T>(a: Degrees<T>, b: Degrees<T>) -> (UnitNegRange<T>, UnitNegRange<T>)
where
T: Float + FloatConst,
f64: From<T>,
{
let delta = two_sum(a.0, -b.0);
let rq: (f64, i32) = libm::remquo(f64::from(delta.0), 90.0);
let radians_q = T::from(rq.0).expect("Could not convert value to Float");
let radians_q = to_radians(Degrees(radians_q + delta.1));
let sin = sine(radians_q);
assign_sin_cos_to_quadrant(sin, cosine(radians_q, sin), rq.1)
}
#[must_use]
fn arctan2_degrees<T: Float + FloatConst>(sin_abs: T, cos_abs: T) -> T {
let half = T::one() / (T::one() + T::one());
let thirty = T::from(THIRTY).expect("Could not convert constant to Float");
if sin_abs == half {
thirty
} else {
sin_abs.atan2(cos_abs).to_degrees()
}
}
#[must_use]
pub fn arctan2d<T>(sin: UnitNegRange<T>, cos: UnitNegRange<T>) -> Degrees<T>
where
T: Float + FloatConst,
f64: From<T>,
{
let forty_five = T::from(FORTY_FIVE).expect("Could not convert constant to Float");
let ninety = forty_five + forty_five;
let one_eighty = ninety + ninety;
let sin_abs = sin.0.abs();
let cos_abs = cos.0.abs();
let degrees_90 = match sin_abs.partial_cmp(&cos_abs).expect("sin or cos is NaN") {
Ordering::Equal => forty_five,
Ordering::Less => arctan2_degrees(sin_abs, cos_abs),
Ordering::Greater => ninety - arctan2_degrees(cos_abs, sin_abs),
};
let degrees_180 = if cos.0 < T::zero() {
one_eighty - degrees_90
} else {
degrees_90
};
Degrees(degrees_180.copysign(sin.0))
}
#[must_use]
pub fn csc<T: Float>(sin: UnitNegRange<T>) -> Option<T> {
let sq_epsilon = T::epsilon() * T::epsilon();
if sin.0.abs() >= sq_epsilon {
Some(T::one() / sin.0)
} else {
None
}
}
#[must_use]
pub fn sec<T: Float>(cos: UnitNegRange<T>) -> Option<T> {
let sq_epsilon = T::epsilon() * T::epsilon();
if cos.0.abs() >= sq_epsilon {
Some(T::one() / cos.0)
} else {
None
}
}
#[must_use]
pub fn tan<T: Float>(sin: UnitNegRange<T>, cos: UnitNegRange<T>) -> Option<T> {
sec(cos).map(|secant| sin.0 * secant)
}
#[must_use]
pub fn cot<T: Float>(sin: UnitNegRange<T>, cos: UnitNegRange<T>) -> Option<T> {
csc(sin).map(|cosecant| cos.0 * cosecant)
}
#[must_use]
pub fn sine_diff<T: Float>(
sin_a: UnitNegRange<T>,
cos_a: UnitNegRange<T>,
sin_b: UnitNegRange<T>,
cos_b: UnitNegRange<T>,
) -> UnitNegRange<T> {
UnitNegRange::clamp(vector2d::perp_product(sin_a.0, cos_a.0, sin_b.0, cos_b.0))
}
#[must_use]
pub fn sine_sum<T: Float>(
sin_a: UnitNegRange<T>,
cos_a: UnitNegRange<T>,
sin_b: UnitNegRange<T>,
cos_b: UnitNegRange<T>,
) -> UnitNegRange<T> {
sine_diff(sin_a, cos_a, -sin_b, cos_b)
}
#[must_use]
pub fn cosine_diff<T: Float>(
sin_a: UnitNegRange<T>,
cos_a: UnitNegRange<T>,
sin_b: UnitNegRange<T>,
cos_b: UnitNegRange<T>,
) -> UnitNegRange<T> {
UnitNegRange::clamp(vector2d::dot_product(sin_a.0, cos_a.0, sin_b.0, cos_b.0))
}
#[must_use]
pub fn cosine_sum<T: Float>(
sin_a: UnitNegRange<T>,
cos_a: UnitNegRange<T>,
sin_b: UnitNegRange<T>,
cos_b: UnitNegRange<T>,
) -> UnitNegRange<T> {
cosine_diff(sin_a, cos_a, -sin_b, cos_b)
}
#[must_use]
pub fn sq_sine_half<T: Float>(cos: UnitNegRange<T>) -> T {
let half = T::one() / (T::one() + T::one());
(T::one() - cos.0) * half
}
#[must_use]
pub fn sq_cosine_half<T: Float>(cos: UnitNegRange<T>) -> T {
let half = T::one() / (T::one() + T::one());
(T::one() + cos.0) * half
}
#[must_use]
pub fn calculate_adjacent_length<T: Float>(length: T, hypotenuse: T) -> T {
if length <= T::zero() {
hypotenuse
} else if length >= hypotenuse {
T::zero()
} else {
((hypotenuse - length) * (hypotenuse + length)).sqrt()
}
}
#[must_use]
pub fn spherical_adjacent_length<T: Float + FloatConst>(
a: Radians<T>,
c: Radians<T>,
) -> Radians<T> {
if a <= Radians(T::zero()) {
c
} else if a >= c {
Radians(T::zero())
} else {
Radians((c.0.cos() / a.0.cos()).acos())
}
}
#[must_use]
pub fn spherical_hypotenuse_length<T: Float + FloatConst>(
a: Radians<T>,
b: Radians<T>,
) -> Radians<T> {
if a <= Radians(T::zero()) {
b
} else if b <= Radians(T::zero()) {
a
} else {
Radians((a.0.cos() * b.0.cos()).acos())
}
}
#[must_use]
pub fn spherical_cosine_rule<T: Float + FloatConst>(
cos_angle: UnitNegRange<T>,
length: Radians<T>,
) -> Radians<T> {
Radians((cos_angle.0 * length.0.tan()).atan())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::is_within_tolerance;
#[test]
fn unit_neg_range_traits() {
let zero = UnitNegRange::default();
assert_eq!(UnitNegRange(0.0), zero);
let one = UnitNegRange(1.0);
let one_clone = one.clone();
assert_eq!(one_clone, one);
let minus_one = -one;
assert_eq!(minus_one, UnitNegRange(-1.0));
assert!(minus_one < one);
assert_eq!(one, minus_one.abs());
print!("UnitNegRange: {:?}", one);
}
#[test]
fn unit_neg_range_clamp() {
assert_eq!(-1.0, UnitNegRange::clamp(-1.0 - f64::EPSILON).0);
assert_eq!(-1.0, UnitNegRange::clamp(-1.0).0);
assert_eq!(1.0, UnitNegRange::clamp(1.0).0);
assert_eq!(1.0, UnitNegRange::clamp(1.0 + f64::EPSILON).0);
}
#[test]
fn unit_neg_range_is_valid() {
assert!(!UnitNegRange(-1.0 - f64::EPSILON).is_valid());
assert!(UnitNegRange(-1.0).is_valid());
assert!(UnitNegRange(1.0).is_valid());
assert!(!UnitNegRange(1.0 + f64::EPSILON).is_valid());
}
#[test]
fn test_trig_functions() {
let cos_60 = UnitNegRange(0.5);
let sin_60 = swap_sin_cos(cos_60);
assert_eq!(COS_30_DEGREES, sin_60.0);
let sin_120 = sin_60;
let cos_120 = cosine_from_sine(sin_120, -1.0);
let zero = cosine_from_sine(UnitNegRange(1.0), -1.0);
assert_eq!(0.0, zero.0);
assert!(zero.0.is_sign_positive());
let recip_sq_epsilon = 1.0 / SQ_EPSILON;
let sin_msq_epsilon = UnitNegRange(-SQ_EPSILON);
assert_eq!(-recip_sq_epsilon, csc(sin_msq_epsilon).unwrap());
assert_eq!(-recip_sq_epsilon, sec(sin_msq_epsilon).unwrap());
let cos_msq_epsilon = swap_sin_cos(sin_msq_epsilon);
assert_eq!(1.0, sec(cos_msq_epsilon).unwrap());
assert_eq!(1.0, csc(cos_msq_epsilon).unwrap());
assert_eq!(-SQ_EPSILON, tan(sin_msq_epsilon, cos_msq_epsilon).unwrap());
assert_eq!(
-recip_sq_epsilon,
cot(sin_msq_epsilon, cos_msq_epsilon).unwrap()
);
assert!(is_within_tolerance(
sin_120.0,
sine_sum(sin_60, cos_60, sin_60, cos_60).0,
f64::EPSILON
));
assert!(is_within_tolerance(
cos_120.0,
cosine_sum(sin_60, cos_60, sin_60, cos_60).0,
f64::EPSILON
));
let result = sq_sine_half(cos_120);
assert_eq!(sin_60.0, result.sqrt());
let result = sq_cosine_half(cos_120);
assert!(is_within_tolerance(cos_60.0, result.sqrt(), f64::EPSILON));
}
#[test]
fn test_small_angle_conversion() {
assert_eq!(MAX_LINEAR_SIN_ANGLE, sine(Radians(MAX_LINEAR_SIN_ANGLE)).0);
let s = sine(Radians(MAX_COS_ANGLE_IS_ONE));
assert_eq!(
MAX_COS_ANGLE_IS_ONE.cos(),
cosine(Radians(MAX_COS_ANGLE_IS_ONE), s).0
);
assert_eq!(1.0, MAX_COS_ANGLE_IS_ONE.cos());
let angle = Radians(4.74e7 * f64::EPSILON);
assert_eq!(1.0, angle.0.cos());
let s = sine(angle);
let result = cosine(angle, s);
assert_eq!(1.0 - f64::EPSILON / 2.0, result.0);
assert!(result.0 < angle.0.cos());
}
#[test]
fn test_radians_conversion() {
assert!(
core::f64::consts::FRAC_PI_2
!= core::f64::consts::FRAC_PI_3 + core::f64::consts::FRAC_PI_6
);
assert_eq!(
core::f64::consts::FRAC_PI_2 + f64::EPSILON,
core::f64::consts::FRAC_PI_3 + core::f64::consts::FRAC_PI_6
);
assert_eq!(
core::f64::consts::FRAC_PI_2,
2.0 * core::f64::consts::FRAC_PI_4
);
assert_eq!(core::f64::consts::PI, 2.0 * core::f64::consts::FRAC_PI_2);
assert_eq!(core::f64::consts::FRAC_PI_4, 45.0_f64.to_radians());
assert_eq!(
core::f64::consts::FRAC_1_SQRT_2 - 0.5 * f64::EPSILON,
core::f64::consts::FRAC_PI_4.sin()
);
let result = sincos(Radians(-core::f64::consts::FRAC_PI_6));
assert_eq!(-0.5, result.0.0);
assert_eq!(COS_30_DEGREES, result.1.0);
assert_eq!(-core::f64::consts::FRAC_PI_6, arctan2(result.0, result.1).0);
let result = sincos(Radians(core::f64::consts::FRAC_PI_3));
assert!(is_within_tolerance(
COS_30_DEGREES,
result.0.0,
f64::EPSILON
));
assert!(is_within_tolerance(0.5, result.1.0, f64::EPSILON));
assert_eq!(core::f64::consts::FRAC_PI_3, arctan2(result.0, result.1).0);
let result = sincos(Radians(-core::f64::consts::PI));
assert_eq!(0.0, result.0.0);
assert_eq!(-1.0, result.1.0);
assert_eq!(core::f64::consts::PI, arctan2(result.0, result.1).0);
let result = sincos_diff(
Radians(core::f64::consts::PI),
Radians(core::f64::consts::FRAC_PI_4),
);
assert_eq!(core::f64::consts::FRAC_1_SQRT_2, result.0.0);
assert_eq!(-core::f64::consts::FRAC_1_SQRT_2, result.1.0);
assert_eq!(
core::f64::consts::PI - core::f64::consts::FRAC_PI_4,
arctan2(result.0, result.1).0
);
let result = sincos_diff(
Radians(3.0 * core::f64::consts::TAU),
Radians(core::f64::consts::FRAC_PI_3),
);
assert!(is_within_tolerance(
-COS_30_DEGREES,
result.0.0,
f64::EPSILON
));
assert!(is_within_tolerance(0.5, result.1.0, f64::EPSILON));
assert_eq!(-core::f64::consts::FRAC_PI_3, arctan2(result.0, result.1).0);
}
#[test]
fn test_degrees_conversion() {
assert_eq!(90.0, 60.0 + 30.0);
assert_eq!(90.0, 2.0 * 45.0);
assert_eq!(180.0, 2.0 * 90.0);
let result = sincosd(Degrees(-30.0));
assert_eq!(-0.5, result.0.0);
assert_eq!(COS_30_DEGREES, result.1.0);
assert_eq!(-30.0, arctan2d(result.0, result.1).0);
let result = sincosd(Degrees(60.0));
assert_eq!(COS_30_DEGREES, result.0.0);
assert_eq!(0.5, result.1.0);
assert_eq!(60.0, arctan2d(result.0, result.1).0);
let result = sincosd(Degrees(-180.0));
assert_eq!(0.0, result.0.0);
assert_eq!(-1.0, result.1.0);
assert_eq!(180.0, arctan2d(result.0, result.1).0);
let result = sincosd_diff(Degrees(180.0), Degrees(45.0));
assert_eq!(core::f64::consts::FRAC_1_SQRT_2, result.0.0);
assert_eq!(-core::f64::consts::FRAC_1_SQRT_2, result.1.0);
assert_eq!(180.0 - 45.0, arctan2d(result.0, result.1).0);
let result = sincosd_diff(Degrees(1080.0), Degrees(60.0));
assert_eq!(-COS_30_DEGREES, result.0.0);
assert_eq!(0.5, result.1.0);
assert_eq!(-60.0, arctan2d(result.0, result.1).0);
}
#[test]
fn test_calculate_adjacent_length() {
assert_eq!(0.0, calculate_adjacent_length(5.0, 5.0));
assert_eq!(5.0, calculate_adjacent_length(0.0, 5.0));
assert_eq!(0.0, calculate_adjacent_length(6.0, 5.0));
assert_eq!(3.0, calculate_adjacent_length(4.0, 5.0));
}
#[test]
fn test_spherical_adjacent_length() {
assert_eq!(
Radians(0.0),
spherical_adjacent_length(Radians(5.0_f64.to_radians()), Radians(5.0_f64.to_radians()))
);
assert_eq!(
Radians(5.0_f64.to_radians()),
spherical_adjacent_length(Radians(0.0), Radians(5.0_f64.to_radians()))
);
assert_eq!(
Radians(0.0),
spherical_adjacent_length(Radians(6.0_f64.to_radians()), Radians(5.0_f64.to_radians()))
);
let result =
spherical_adjacent_length(Radians(4.0_f64.to_radians()), Radians(5.0_f64.to_radians()));
assert!(is_within_tolerance(3.0_f64.to_radians(), result.0, 1.0e-4));
}
#[test]
fn test_spherical_hypotenuse_length() {
let zero = Radians(0.0);
let three = Radians(3.0_f64.to_radians());
let four = Radians(4.0_f64.to_radians());
assert_eq!(three, spherical_hypotenuse_length(-four, three));
assert_eq!(four, spherical_hypotenuse_length(four, -three));
assert_eq!(three, spherical_hypotenuse_length(zero, three));
assert_eq!(four, spherical_hypotenuse_length(four, zero));
assert_eq!(zero, spherical_hypotenuse_length(zero, zero));
let result = Radians(0.087240926337265545);
assert_eq!(result, spherical_hypotenuse_length(four, three));
assert_eq!(result, spherical_hypotenuse_length(three, four));
}
#[test]
fn test_spherical_cosine_rule() {
let result = spherical_cosine_rule(UnitNegRange(0.0), Radians(1.0));
assert_eq!(0.0, result.0);
let result = spherical_cosine_rule(UnitNegRange(0.8660254037844386), Radians(0.5));
assert_eq!(0.44190663576327144, result.0);
let result = spherical_cosine_rule(UnitNegRange(0.5), Radians(1.0));
assert_eq!(0.66161993185017653, result.0);
let result = spherical_cosine_rule(UnitNegRange(1.0), Radians(1.0));
assert_eq!(1.0, result.0);
}
}