1pub use glam::{Vec2, Vec3, vec2, vec3};
44
45pub fn normalize_angle(degrees: f32) -> f32 {
47 degrees.rem_euclid(360.0)
48}
49
50pub fn angle_difference(from: f32, to: f32) -> f32 {
53 let d = normalize_angle(to - from);
54 if d > 180.0 { d - 360.0 } else { d }
55}
56
57pub fn forward(yaw: f32) -> Vec3 {
59 let r = yaw.to_radians();
60 Vec3::new(r.sin(), r.cos(), 0.0)
61}
62
63pub fn right(yaw: f32) -> Vec3 {
65 forward(yaw + 90.0)
66}
67
68pub fn forward_3d(yaw: f32, pitch: f32) -> Vec3 {
71 let p = pitch.clamp(-90.0, 90.0).to_radians();
72 forward(yaw) * p.cos() + Vec3::Z * p.sin()
73}
74
75pub fn up_3d(yaw: f32, pitch: f32) -> Vec3 {
78 let p = pitch.clamp(-90.0, 90.0).to_radians();
79 Vec3::Z * p.cos() - forward(yaw) * p.sin()
80}
81
82pub fn elevation(from: Vec3, to: Vec3) -> f32 {
85 let d = to - from;
86 let horizontal = d.x.hypot(d.y);
87 if horizontal == 0.0 && d.z == 0.0 {
88 return 0.0;
89 }
90 d.z.atan2(horizontal).to_degrees()
91}
92
93pub fn look_at(from: Vec3, to: Vec3) -> (f32, f32) {
96 (bearing(from, to), elevation(from, to))
97}
98
99pub fn bearing(from: Vec3, to: Vec3) -> f32 {
101 let d = to - from;
102 if d.x == 0.0 && d.y == 0.0 {
103 return 0.0;
104 }
105 normalize_angle(d.x.atan2(d.y).to_degrees())
106}
107
108pub fn move_towards(position: Vec3, yaw: f32, distance: f32) -> Vec3 {
110 position + forward(yaw) * distance
111}
112
113pub fn direction_name(degrees: f32) -> &'static str {
115 const NAMES: [&str; 8] =
116 ["north", "north east", "east", "south east", "south", "south west", "west", "north west"];
117 let i = ((normalize_angle(degrees) + 22.5) / 45.0) as usize % 8;
118 NAMES[i]
119}
120
121pub fn clock_position(relative_degrees: f32) -> u32 {
124 let hour = ((normalize_angle(relative_degrees) + 15.0) / 30.0) as u32 % 12;
125 if hour == 0 { 12 } else { hour }
126}
127
128pub fn describe_position(from: Vec3, yaw: f32, target: Vec3) -> String {
136 let d = target - from;
137 let meters = |n: i64| if n == 1 { "1 meter".to_string() } else { format!("{n} meters") };
138 let distance = meters(d.length().round() as i64);
139 if d.x.hypot(d.y) < 0.5 && d.z.abs() >= 0.5 {
140 let side = if d.z > 0.0 { "above" } else { "below" };
141 return format!("directly {side}, {distance}");
142 }
143 let relative = angle_difference(yaw, bearing(from, target));
144 let mut text = format!("{} o'clock, {distance}", clock_position(relative));
145 let height = d.z.round() as i64;
146 if height != 0 {
147 let side = if height > 0 { "above" } else { "below" };
148 text.push_str(&format!(", {} {side}", meters(height.abs())));
149 }
150 text
151}
152
153pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
155 a + (b - a) * t
156}
157
158pub fn db_to_linear(db: f32) -> f32 {
160 10f32.powf(db / 20.0)
161}
162
163pub fn linear_to_db(linear: f32) -> f32 {
165 20.0 * linear.max(1e-6).log10()
166}
167
168pub mod random {
170 pub fn range(min: f32, max: f32) -> f32 {
172 min + fastrand::f32() * (max - min)
173 }
174
175 pub fn int(min: i64, max: i64) -> i64 {
177 fastrand::i64(min..=max)
178 }
179
180 pub fn chance(probability: f32) -> bool {
182 fastrand::f32() < probability
183 }
184
185 pub fn pick<T>(items: &[T]) -> Option<&T> {
187 if items.is_empty() { None } else { Some(&items[fastrand::usize(..items.len())]) }
188 }
189
190 pub fn seed(seed: u64) {
192 fastrand::seed(seed);
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn compass() {
202 assert!((forward(90.0) - Vec3::X).length() < 1e-5);
203 assert!((bearing(Vec3::ZERO, Vec3::new(0.0, -5.0, 0.0)) - 180.0).abs() < 1e-3);
204 assert_eq!(direction_name(44.0), "north east");
205 assert_eq!(direction_name(359.0), "north");
206 assert_eq!(angle_difference(350.0, 10.0), 20.0);
207 assert_eq!(clock_position(90.0), 3);
208 assert_eq!(clock_position(0.0), 12);
209 assert_eq!(describe_position(Vec3::ZERO, 0.0, Vec3::new(5.0, 0.0, 0.0)), "3 o'clock, 5 meters");
210 }
211
212 #[test]
213 fn three_d() {
214 assert!((forward_3d(0.0, 90.0) - Vec3::Z).length() < 1e-5);
215 assert!((forward_3d(90.0, 0.0) - Vec3::X).length() < 1e-5);
216 assert!((up_3d(0.0, 0.0) - Vec3::Z).length() < 1e-5);
217 assert!((up_3d(0.0, 90.0) + Vec3::Y).length() < 1e-5, "looking up, the head points south");
218 assert!((elevation(Vec3::ZERO, Vec3::new(3.0, 0.0, 3.0)) - 45.0).abs() < 1e-3);
219 let (yaw, pitch) = look_at(Vec3::ZERO, Vec3::new(0.0, -4.0, -4.0));
220 assert!((yaw - 180.0).abs() < 1e-3 && (pitch + 45.0).abs() < 1e-3);
221 assert_eq!(describe_position(Vec3::ZERO, 0.0, Vec3::new(0.0, 10.0, 4.0)), "12 o'clock, 11 meters, 4 meters above");
222 assert_eq!(describe_position(Vec3::ZERO, 0.0, Vec3::new(0.0, 0.0, -5.0)), "directly below, 5 meters");
223 assert_eq!(describe_position(Vec3::ZERO, 0.0, Vec3::new(0.0, 1.0, 0.0)), "12 o'clock, 1 meter");
224 }
225}