pub use glam::{Vec2, Vec3, vec2, vec3};
pub fn normalize_angle(degrees: f32) -> f32 {
degrees.rem_euclid(360.0)
}
pub fn angle_difference(from: f32, to: f32) -> f32 {
let d = normalize_angle(to - from);
if d > 180.0 { d - 360.0 } else { d }
}
pub fn forward(yaw: f32) -> Vec3 {
let r = yaw.to_radians();
Vec3::new(r.sin(), r.cos(), 0.0)
}
pub fn right(yaw: f32) -> Vec3 {
forward(yaw + 90.0)
}
pub fn forward_3d(yaw: f32, pitch: f32) -> Vec3 {
let p = pitch.clamp(-90.0, 90.0).to_radians();
forward(yaw) * p.cos() + Vec3::Z * p.sin()
}
pub fn up_3d(yaw: f32, pitch: f32) -> Vec3 {
let p = pitch.clamp(-90.0, 90.0).to_radians();
Vec3::Z * p.cos() - forward(yaw) * p.sin()
}
pub fn elevation(from: Vec3, to: Vec3) -> f32 {
let d = to - from;
let horizontal = d.x.hypot(d.y);
if horizontal == 0.0 && d.z == 0.0 {
return 0.0;
}
d.z.atan2(horizontal).to_degrees()
}
pub fn look_at(from: Vec3, to: Vec3) -> (f32, f32) {
(bearing(from, to), elevation(from, to))
}
pub fn bearing(from: Vec3, to: Vec3) -> f32 {
let d = to - from;
if d.x == 0.0 && d.y == 0.0 {
return 0.0;
}
normalize_angle(d.x.atan2(d.y).to_degrees())
}
pub fn move_towards(position: Vec3, yaw: f32, distance: f32) -> Vec3 {
position + forward(yaw) * distance
}
pub fn direction_name(degrees: f32) -> &'static str {
const NAMES: [&str; 8] =
["north", "north east", "east", "south east", "south", "south west", "west", "north west"];
let i = ((normalize_angle(degrees) + 22.5) / 45.0) as usize % 8;
NAMES[i]
}
pub fn clock_position(relative_degrees: f32) -> u32 {
let hour = ((normalize_angle(relative_degrees) + 15.0) / 30.0) as u32 % 12;
if hour == 0 { 12 } else { hour }
}
pub fn describe_position(from: Vec3, yaw: f32, target: Vec3) -> String {
crate::lang::Lang::new().describe_position(from, yaw, target)
}
pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t
}
pub fn db_to_linear(db: f32) -> f32 {
10f32.powf(db / 20.0)
}
pub fn linear_to_db(linear: f32) -> f32 {
20.0 * linear.max(1e-6).log10()
}
pub mod random {
pub fn range(min: f32, max: f32) -> f32 {
min + fastrand::f32() * (max - min)
}
pub fn int(min: i64, max: i64) -> i64 {
fastrand::i64(min..=max)
}
pub fn chance(probability: f32) -> bool {
fastrand::f32() < probability
}
pub fn pick<T>(items: &[T]) -> Option<&T> {
if items.is_empty() { None } else { Some(&items[fastrand::usize(..items.len())]) }
}
pub fn seed(seed: u64) {
fastrand::seed(seed);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compass() {
assert!((forward(90.0) - Vec3::X).length() < 1e-5);
assert!((bearing(Vec3::ZERO, Vec3::new(0.0, -5.0, 0.0)) - 180.0).abs() < 1e-3);
assert_eq!(direction_name(44.0), "north east");
assert_eq!(direction_name(359.0), "north");
assert_eq!(angle_difference(350.0, 10.0), 20.0);
assert_eq!(clock_position(90.0), 3);
assert_eq!(clock_position(0.0), 12);
assert_eq!(describe_position(Vec3::ZERO, 0.0, Vec3::new(5.0, 0.0, 0.0)), "3 o'clock, 5 meters");
}
#[test]
fn three_d() {
assert!((forward_3d(0.0, 90.0) - Vec3::Z).length() < 1e-5);
assert!((forward_3d(90.0, 0.0) - Vec3::X).length() < 1e-5);
assert!((up_3d(0.0, 0.0) - Vec3::Z).length() < 1e-5);
assert!((up_3d(0.0, 90.0) + Vec3::Y).length() < 1e-5, "looking up, the head points south");
assert!((elevation(Vec3::ZERO, Vec3::new(3.0, 0.0, 3.0)) - 45.0).abs() < 1e-3);
let (yaw, pitch) = look_at(Vec3::ZERO, Vec3::new(0.0, -4.0, -4.0));
assert!((yaw - 180.0).abs() < 1e-3 && (pitch + 45.0).abs() < 1e-3);
assert_eq!(describe_position(Vec3::ZERO, 0.0, Vec3::new(0.0, 10.0, 4.0)), "12 o'clock, 11 meters, 4 meters above");
assert_eq!(describe_position(Vec3::ZERO, 0.0, Vec3::new(0.0, 0.0, -5.0)), "directly below, 5 meters");
assert_eq!(describe_position(Vec3::ZERO, 0.0, Vec3::new(0.0, 1.0, 0.0)), "12 o'clock, 1 meter");
}
}