#![allow(clippy::float_cmp, clippy::suboptimal_flops)]
use crate::{Degrees, Radians, Validate, two_sum};
use core::{cmp::Ordering, ops::Neg};
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: Radians = Radians(9.67e7 * f64::EPSILON);
pub const MAX_COS_ANGLE_IS_ONE: Radians = Radians(3.35e7 * f64::EPSILON);
#[must_use]
fn to_radians(angle: Degrees) -> Radians {
if angle.0.abs() == 30.0 {
Radians(core::f64::consts::FRAC_PI_6.copysign(angle.0))
} else {
Radians(angle.0.to_radians())
}
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
#[repr(transparent)]
pub struct UnitNegRange(pub f64);
impl Default for UnitNegRange {
fn default() -> Self {
Self(0.0)
}
}
impl UnitNegRange {
#[must_use]
pub const fn clamp(value: f64) -> Self {
Self(value.clamp(-1.0, 1.0))
}
#[must_use]
pub const fn abs(self) -> Self {
Self(self.0.abs())
}
}
impl Validate for UnitNegRange {
fn is_valid(&self) -> bool {
(-1.0..=1.0).contains(&self.0)
}
}
impl Neg for UnitNegRange {
type Output = Self;
fn neg(self) -> Self {
Self(0.0 - self.0)
}
}
#[must_use]
pub fn sq_a_minus_sq_b(a: UnitNegRange, b: UnitNegRange) -> UnitNegRange {
UnitNegRange((a.0 - b.0) * (a.0 + b.0))
}
#[must_use]
pub fn one_minus_sq_value(a: UnitNegRange) -> UnitNegRange {
sq_a_minus_sq_b(UnitNegRange(1.0), a)
}
#[must_use]
pub fn swap_sin_cos(a: UnitNegRange) -> UnitNegRange {
UnitNegRange(libm::sqrt(one_minus_sq_value(a).0))
}
#[must_use]
pub fn cosine_from_sine(a: UnitNegRange, sign: f64) -> UnitNegRange {
if a.0.abs() > MAX_COS_ANGLE_IS_ONE.0 {
let b = swap_sin_cos(a);
if b.0 > 0.0 {
UnitNegRange(b.0.copysign(sign))
} else {
b
}
} else {
UnitNegRange(1.0_f64.copysign(sign))
}
}
#[must_use]
pub fn sine(angle: Radians) -> UnitNegRange {
let angle_abs = angle.0.abs();
if angle_abs == core::f64::consts::FRAC_PI_4 {
UnitNegRange(core::f64::consts::FRAC_1_SQRT_2.copysign(angle.0))
} else if angle_abs > MAX_LINEAR_SIN_ANGLE.0 {
UnitNegRange(libm::sin(angle.0))
} else {
UnitNegRange(angle.0)
}
}
#[must_use]
pub fn cosine(angle: Radians, sin: UnitNegRange) -> UnitNegRange {
let angle_abs = angle.0.abs();
if angle_abs == core::f64::consts::FRAC_PI_4 {
UnitNegRange(
core::f64::consts::FRAC_1_SQRT_2.copysign(core::f64::consts::FRAC_PI_2 - angle_abs),
)
} else {
cosine_from_sine(sin, core::f64::consts::FRAC_PI_2 - angle_abs)
}
}
#[must_use]
fn assign_sin_cos_to_quadrant(
sin: UnitNegRange,
cos: UnitNegRange,
q: i32,
) -> (UnitNegRange, UnitNegRange) {
match q & 3 {
1 => (cos, -sin), 2 => (-sin, -cos), 3 => (-cos, sin), _ => (sin, cos),
}
}
#[must_use]
pub fn sincos(radians: Radians) -> (UnitNegRange, UnitNegRange) {
let rq: (f64, i32) = libm::remquo(radians.0, core::f64::consts::FRAC_PI_2);
let radians_q = Radians(rq.0);
let sin = sine(radians_q);
assign_sin_cos_to_quadrant(sin, cosine(radians_q, sin), rq.1)
}
#[must_use]
pub fn sincos_diff(a: Radians, b: Radians) -> (UnitNegRange, UnitNegRange) {
let delta = two_sum(a.0, -b.0);
let rq: (f64, i32) = libm::remquo(delta.0, core::f64::consts::FRAC_PI_2);
let radians_q = Radians(rq.0 + delta.1);
let sin = sine(radians_q);
assign_sin_cos_to_quadrant(sin, cosine(radians_q, sin), rq.1)
}
#[must_use]
pub fn arctan2(sin: UnitNegRange, cos: UnitNegRange) -> Radians {
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 => core::f64::consts::FRAC_PI_4,
Ordering::Less => libm::atan2(sin_abs, cos_abs),
Ordering::Greater => core::f64::consts::FRAC_PI_2 - libm::atan2(cos_abs, sin_abs),
};
let radians_pi = if cos.0 < 0.0 {
core::f64::consts::PI - radians_pi_2
} else {
radians_pi_2
};
Radians(radians_pi.copysign(sin.0))
}
#[must_use]
pub fn sincosd(degrees: Degrees) -> (UnitNegRange, UnitNegRange) {
let rq: (f64, i32) = libm::remquo(degrees.0, 90.0);
let radians_q = to_radians(Degrees(rq.0));
let sin = sine(radians_q);
assign_sin_cos_to_quadrant(sin, cosine(radians_q, sin), rq.1)
}
#[must_use]
pub fn sincosd_diff(a: Degrees, b: Degrees) -> (UnitNegRange, UnitNegRange) {
let delta = two_sum(a.0, -b.0);
let rq: (f64, i32) = libm::remquo(delta.0, 90.0);
let radians_q = to_radians(Degrees(rq.0 + delta.1));
let sin = sine(radians_q);
assign_sin_cos_to_quadrant(sin, cosine(radians_q, sin), rq.1)
}
#[must_use]
fn arctan2_degrees(sin_abs: f64, cos_abs: f64) -> f64 {
if sin_abs == 0.5 {
30.0
} else {
libm::atan2(sin_abs, cos_abs).to_degrees()
}
}
#[must_use]
pub fn arctan2d(sin: UnitNegRange, cos: UnitNegRange) -> Degrees {
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 => 45.0,
Ordering::Less => arctan2_degrees(sin_abs, cos_abs),
Ordering::Greater => 90.0 - arctan2_degrees(cos_abs, sin_abs),
};
let degrees_180 = if cos.0 < 0.0 {
180.0 - degrees_90
} else {
degrees_90
};
Degrees(degrees_180.copysign(sin.0))
}
#[must_use]
pub fn csc(sin: UnitNegRange) -> Option<f64> {
if sin.0.abs() >= SQ_EPSILON {
Some(1.0 / sin.0)
} else {
None
}
}
#[must_use]
pub fn sec(cos: UnitNegRange) -> Option<f64> {
if cos.0.abs() >= SQ_EPSILON {
Some(1.0 / cos.0)
} else {
None
}
}
#[must_use]
pub fn tan(sin: UnitNegRange, cos: UnitNegRange) -> Option<f64> {
sec(cos).map(|secant| sin.0 * secant)
}
#[must_use]
pub fn cot(sin: UnitNegRange, cos: UnitNegRange) -> Option<f64> {
csc(sin).map(|cosecant| cos.0 * cosecant)
}
#[must_use]
pub fn sine_diff(
sin_a: UnitNegRange,
cos_a: UnitNegRange,
sin_b: UnitNegRange,
cos_b: UnitNegRange,
) -> UnitNegRange {
UnitNegRange::clamp(sin_a.0 * cos_b.0 - sin_b.0 * cos_a.0)
}
#[must_use]
pub fn sine_sum(
sin_a: UnitNegRange,
cos_a: UnitNegRange,
sin_b: UnitNegRange,
cos_b: UnitNegRange,
) -> UnitNegRange {
sine_diff(sin_a, cos_a, -sin_b, cos_b)
}
#[must_use]
pub fn cosine_diff(
sin_a: UnitNegRange,
cos_a: UnitNegRange,
sin_b: UnitNegRange,
cos_b: UnitNegRange,
) -> UnitNegRange {
UnitNegRange::clamp(cos_a.0 * cos_b.0 + sin_a.0 * sin_b.0)
}
#[must_use]
pub fn cosine_sum(
sin_a: UnitNegRange,
cos_a: UnitNegRange,
sin_b: UnitNegRange,
cos_b: UnitNegRange,
) -> UnitNegRange {
cosine_diff(sin_a, cos_a, -sin_b, cos_b)
}
#[must_use]
pub fn sq_sine_half(cos: UnitNegRange) -> f64 {
(1.0 - cos.0) * 0.5
}
#[must_use]
pub fn sq_cosine_half(cos: UnitNegRange) -> f64 {
(1.0 + cos.0) * 0.5
}
#[must_use]
pub fn calculate_adjacent_length(length: f64, hypotenuse: f64) -> f64 {
if length <= 0.0 {
hypotenuse
} else if length >= hypotenuse {
0.0
} else {
libm::sqrt((hypotenuse - length) * (hypotenuse + length))
}
}
#[must_use]
pub fn spherical_adjacent_length(a: Radians, c: Radians) -> Radians {
if a <= Radians(0.0) {
c
} else if a >= c {
Radians(0.0)
} else {
Radians(libm::acos(libm::cos(c.0) / libm::cos(a.0)))
}
}
#[must_use]
pub fn spherical_hypotenuse_length(a: Radians, b: Radians) -> Radians {
if a <= Radians(0.0) {
b
} else if b <= Radians(0.0) {
a
} else {
Radians(libm::acos(libm::cos(a.0) * libm::cos(b.0)))
}
}
#[must_use]
pub fn spherical_cosine_rule(cos_angle: UnitNegRange, length: Radians) -> Radians {
Radians(libm::atan(cos_angle.0 * libm::tan(length.0)))
}
#[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: UnitNegRange = -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.0, sine(MAX_LINEAR_SIN_ANGLE).0);
let s = sine(MAX_COS_ANGLE_IS_ONE);
assert_eq!(
MAX_COS_ANGLE_IS_ONE.0.cos(),
cosine(MAX_COS_ANGLE_IS_ONE, s).0
);
assert_eq!(1.0, MAX_COS_ANGLE_IS_ONE.0.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);
}
}