ballistics_engine/
wind.rs1use nalgebra::Vector3;
2use std::f64::consts::PI;
3
4const KMH_TO_MPS: f64 = 1000.0 / 3600.0;
6
7pub type WindSegment = (f64, f64, f64);
10
11#[derive(Debug, Clone)]
13pub struct WindSock {
14 winds: Vec<WindSegment>,
16 current: usize,
18 next_range: f64,
20 current_vec: Vector3<f64>,
22}
23
24impl WindSock {
25 pub fn new(mut segments: Vec<WindSegment>) -> Self {
30 segments.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap());
32
33 let (current, next_range, current_vec) = if segments.is_empty() {
34 (0, f64::INFINITY, Vector3::zeros())
35 } else {
36 let vec = Self::calc_vec(&segments[0]);
37 (0, segments[0].2, vec)
38 };
39
40 WindSock {
41 winds: segments,
42 current,
43 next_range,
44 current_vec,
45 }
46 }
47
48 fn calc_vec(seg: &WindSegment) -> Vector3<f64> {
50 let (speed_kmh, angle_deg, _) = *seg;
51
52 let speed_mps = speed_kmh * KMH_TO_MPS;
54 let angle_rad = angle_deg * PI / 180.0;
55
56 Vector3::new(
64 -speed_mps * angle_rad.sin(), 0.0, -speed_mps * angle_rad.cos(), )
68 }
69
70 pub fn vector_for_range(&mut self, range_m: f64) -> Vector3<f64> {
75 if range_m.is_nan() {
77 return Vector3::zeros();
78 }
79
80 if range_m >= self.next_range {
82 self.current += 1;
83 if self.current >= self.winds.len() {
84 self.current_vec = Vector3::zeros();
85 self.next_range = f64::INFINITY;
86 } else {
87 let seg = &self.winds[self.current];
88 self.current_vec = Self::calc_vec(seg);
89 self.next_range = seg.2;
90 }
91 }
92
93 self.current_vec
94 }
95
96 pub fn vector_for_range_stateless(&self, range_m: f64) -> Vector3<f64> {
101 if range_m.is_nan() {
103 return Vector3::zeros();
104 }
105
106 for segment in &self.winds {
108 if range_m < segment.2 {
109 return Self::calc_vec(segment);
110 }
111 }
112
113 Vector3::zeros()
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn test_wind_sock_empty() {
124 let sock = WindSock::new(vec![]);
125 assert_eq!(sock.vector_for_range_stateless(50.0), Vector3::zeros());
126 }
127
128 #[test]
129 fn test_wind_sock_single_segment() {
130 let sock = WindSock::new(vec![(16.0934, 90.0, 100.0)]);
132
133 let vec_50 = sock.vector_for_range_stateless(50.0);
135 println!("vec_50 = [{}, {}, {}]", vec_50[0], vec_50[1], vec_50[2]);
136 assert!(vec_50.norm() > 0.0);
137 assert!(vec_50[0] < 0.0, "X component should be negative for 90° wind, got {}", vec_50[0]);
139 assert_eq!(vec_50[1], 0.0); assert!(vec_50[2].abs() < 0.01, "Z component should be nearly zero for 90° wind, got {}", vec_50[2]);
141
142 let vec_150 = sock.vector_for_range_stateless(150.0);
144 assert_eq!(vec_150, Vector3::zeros());
145 }
146
147 #[test]
148 fn test_wind_sock_multiple_segments() {
149 let sock = WindSock::new(vec![
151 (16.0934, 90.0, 50.0), (24.1401, 45.0, 100.0), (8.0467, 180.0, 200.0), ]);
155
156 let vec_25 = sock.vector_for_range_stateless(25.0);
158 println!("vec_25 = [{}, {}, {}]", vec_25[0], vec_25[1], vec_25[2]);
159 assert!(vec_25.norm() > 0.0);
160 assert!(vec_25[0] < 0.0, "90° wind should have negative X"); let vec_75 = sock.vector_for_range_stateless(75.0);
163 println!("vec_75 = [{}, {}, {}]", vec_75[0], vec_75[1], vec_75[2]);
164 assert!(vec_75.norm() > vec_25.norm()); assert!(vec_75[0] < 0.0); assert!(vec_75[2] < 0.0); let vec_150 = sock.vector_for_range_stateless(150.0);
169 println!("vec_150 = [{}, {}, {}]", vec_150[0], vec_150[1], vec_150[2]);
170 assert!(vec_150.norm() < vec_75.norm()); assert!(vec_150[0].abs() < 0.01, "180° wind should have near-zero X, got {}", vec_150[0]); assert!(vec_150[2] > 0.0, "180° wind should have positive Z (tailwind), got {}", vec_150[2]); let vec_250 = sock.vector_for_range_stateless(250.0);
175 assert_eq!(vec_250, Vector3::zeros()); }
177
178 #[test]
179 fn test_wind_conversion() {
180 let sock = WindSock::new(vec![(16.0934, 0.0, 100.0)]);
182 let vec = sock.vector_for_range_stateless(50.0);
183
184 let expected_speed = 16.0934 * KMH_TO_MPS;
185 assert!((vec.norm() - expected_speed).abs() < 0.01);
186 }
187}