use glam::Vec3;
#[must_use]
pub fn angles_to_c_axis(tilt_deg: f32, azimuth_deg: f32) -> Vec3 {
let theta = tilt_deg.to_radians();
let phi = azimuth_deg.to_radians();
let (sin_theta, cos_theta) = theta.sin_cos();
let (sin_phi, cos_phi) = phi.sin_cos();
Vec3::new(sin_theta * cos_phi, cos_theta, sin_theta * sin_phi)
}
#[must_use]
pub fn c_axis_to_angles(axis: Vec3) -> (f32, f32) {
let axis = if axis.length_squared() > 1e-12 {
axis.normalize()
} else {
Vec3::Y
};
let tilt_deg = axis.y.clamp(-1.0, 1.0).acos().to_degrees();
let azimuth_deg = axis.z.atan2(axis.x).to_degrees();
let azimuth_deg = if azimuth_deg < 0.0 {
azimuth_deg + 360.0
} else {
azimuth_deg
};
(tilt_deg, azimuth_deg)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tilt_zero_reproduces_default_y_axis_exactly_for_any_azimuth() {
for azimuth_deg in [0.0, 45.0, 90.0, 123.4, 270.0, 359.9] {
assert_eq!(
angles_to_c_axis(0.0, azimuth_deg),
Vec3::Y,
"azimuth_deg={azimuth_deg}"
);
}
}
#[test]
fn tilt_ninety_azimuth_zero_matches_tourmaline_x_axis() {
let axis = angles_to_c_axis(90.0, 0.0);
assert!(
(axis - Vec3::X).length() < 1e-6,
"expected ~=Vec3::X, got {axis:?}"
);
}
#[test]
fn c_axis_to_angles_recovers_y_axis_as_zero_tilt() {
let (tilt_deg, _azimuth_deg) = c_axis_to_angles(Vec3::Y);
assert!(tilt_deg.abs() < 1e-4, "tilt_deg={tilt_deg}");
}
#[test]
fn c_axis_to_angles_recovers_x_axis_as_ninety_tilt_zero_azimuth() {
let (tilt_deg, azimuth_deg) = c_axis_to_angles(Vec3::X);
assert!((tilt_deg - 90.0).abs() < 1e-3, "tilt_deg={tilt_deg}");
assert!(azimuth_deg.abs() < 1e-3, "azimuth_deg={azimuth_deg}");
}
#[test]
fn c_axis_to_angles_handles_zero_length_input_without_panicking() {
let (tilt_deg, _azimuth_deg) = c_axis_to_angles(Vec3::ZERO);
assert!(tilt_deg.abs() < 1e-4, "tilt_deg={tilt_deg}");
}
#[test]
fn round_trips_angles_through_vector_and_back() {
for tilt_deg in [1.0, 15.0, 30.0, 45.0, 60.0, 75.0, 89.0, 90.0] {
for azimuth_deg in [0.0, 30.0, 90.0, 145.0, 180.0, 250.0, 300.0, 359.0] {
let axis = angles_to_c_axis(tilt_deg, azimuth_deg);
let (got_tilt, got_azimuth) = c_axis_to_angles(axis);
assert!(
(got_tilt - tilt_deg).abs() < 1e-2,
"tilt_deg={tilt_deg} azimuth_deg={azimuth_deg} got_tilt={got_tilt}"
);
assert!(
(got_azimuth - azimuth_deg).abs() < 1e-2,
"tilt_deg={tilt_deg} azimuth_deg={azimuth_deg} got_azimuth={got_azimuth}"
);
}
}
}
#[test]
fn round_trips_vectors_through_angles_and_back() {
let directions = [
Vec3::Y,
Vec3::X,
Vec3::new(0.5, 0.5, 0.72).normalize(),
Vec3::new(-0.3, 0.8, 0.5).normalize(),
Vec3::new(0.6, -0.2, 0.776_25).normalize(),
];
for axis in directions {
let (tilt_deg, azimuth_deg) = c_axis_to_angles(axis);
let round_tripped = angles_to_c_axis(tilt_deg, azimuth_deg);
assert!(
(round_tripped - axis).length() < 1e-4,
"axis={axis:?} round_tripped={round_tripped:?}"
);
}
}
}