rustial_engine/visualization/
scalar_field.rs1#[derive(Debug, Clone)]
14pub struct ScalarField2D {
15 pub rows: usize,
17 pub cols: usize,
19 pub data: Vec<f32>,
21 pub min: f32,
23 pub max: f32,
25 pub nan_value: Option<f32>,
27 pub generation: u64,
29 pub value_generation: u64,
32}
33
34impl ScalarField2D {
35 pub fn from_data(rows: usize, cols: usize, data: Vec<f32>) -> Self {
39 assert_eq!(
40 data.len(),
41 rows * cols,
42 "data length must equal rows * cols"
43 );
44 let (min, max) = min_max(&data, None);
45 Self {
46 rows,
47 cols,
48 data,
49 min,
50 max,
51 nan_value: None,
52 generation: 0,
53 value_generation: 0,
54 }
55 }
56
57 pub fn from_data_with_range(
59 rows: usize,
60 cols: usize,
61 data: Vec<f32>,
62 min: f32,
63 max: f32,
64 ) -> Self {
65 assert_eq!(
66 data.len(),
67 rows * cols,
68 "data length must equal rows * cols"
69 );
70 Self {
71 rows,
72 cols,
73 data,
74 min,
75 max,
76 nan_value: None,
77 generation: 0,
78 value_generation: 0,
79 }
80 }
81
82 pub fn sample(&self, row: usize, col: usize) -> Option<f32> {
86 if row >= self.rows || col >= self.cols {
87 return None;
88 }
89 let v = self.data[row * self.cols + col];
90 if let Some(nan) = self.nan_value {
91 if (v - nan).abs() < f32::EPSILON {
92 return None;
93 }
94 }
95 if v.is_nan() {
96 return None;
97 }
98 Some(v)
99 }
100
101 pub fn normalized(&self, row: usize, col: usize) -> Option<f32> {
106 let v = self.sample(row, col)?;
107 let range = self.max - self.min;
108 if range.abs() < f32::EPSILON {
109 return Some(0.5);
110 }
111 Some(((v - self.min) / range).clamp(0.0, 1.0))
112 }
113
114 pub fn update_values(&mut self, new_data: Vec<f32>) {
119 assert_eq!(
120 new_data.len(),
121 self.rows * self.cols,
122 "new data length must match existing topology"
123 );
124 let (min, max) = min_max(&new_data, self.nan_value);
125 self.data = new_data;
126 self.min = min;
127 self.max = max;
128 self.value_generation = self.value_generation.wrapping_add(1);
129 }
130}
131
132fn min_max(data: &[f32], nan_value: Option<f32>) -> (f32, f32) {
133 let mut lo = f32::INFINITY;
134 let mut hi = f32::NEG_INFINITY;
135 for &v in data {
136 if v.is_nan() {
137 continue;
138 }
139 if let Some(nan) = nan_value {
140 if (v - nan).abs() < f32::EPSILON {
141 continue;
142 }
143 }
144 lo = lo.min(v);
145 hi = hi.max(v);
146 }
147 if lo > hi {
148 (0.0, 0.0)
149 } else {
150 (lo, hi)
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn from_data_computes_min_max() {
160 let field = ScalarField2D::from_data(2, 3, vec![1.0, 5.0, 3.0, 2.0, 4.0, 0.0]);
161 assert!((field.min - 0.0).abs() < 1e-6);
162 assert!((field.max - 5.0).abs() < 1e-6);
163 }
164
165 #[test]
166 fn sample_basic() {
167 let field = ScalarField2D::from_data(2, 2, vec![10.0, 20.0, 30.0, 40.0]);
168 assert_eq!(field.sample(0, 0), Some(10.0));
169 assert_eq!(field.sample(1, 1), Some(40.0));
170 assert_eq!(field.sample(2, 0), None);
171 }
172
173 #[test]
174 fn sample_nan_value() {
175 let mut field = ScalarField2D::from_data(1, 3, vec![1.0, -9999.0, 3.0]);
176 field.nan_value = Some(-9999.0);
177 assert_eq!(field.sample(0, 0), Some(1.0));
178 assert_eq!(field.sample(0, 1), None);
179 assert_eq!(field.sample(0, 2), Some(3.0));
180 }
181
182 #[test]
183 fn normalized_range() {
184 let field = ScalarField2D::from_data(1, 3, vec![0.0, 50.0, 100.0]);
185 assert!((field.normalized(0, 0).unwrap() - 0.0).abs() < 1e-6);
186 assert!((field.normalized(0, 1).unwrap() - 0.5).abs() < 1e-6);
187 assert!((field.normalized(0, 2).unwrap() - 1.0).abs() < 1e-6);
188 }
189
190 #[test]
191 fn normalized_constant_field() {
192 let field = ScalarField2D::from_data(1, 2, vec![5.0, 5.0]);
193 assert!((field.normalized(0, 0).unwrap() - 0.5).abs() < 1e-6);
194 }
195
196 #[test]
197 fn update_values_bumps_generation() {
198 let mut field = ScalarField2D::from_data(1, 2, vec![1.0, 2.0]);
199 assert_eq!(field.value_generation, 0);
200 field.update_values(vec![3.0, 4.0]);
201 assert_eq!(field.value_generation, 1);
202 assert!((field.min - 3.0).abs() < 1e-6);
203 assert!((field.max - 4.0).abs() < 1e-6);
204 }
205
206 #[test]
207 fn normalized_with_nan() {
208 let field = ScalarField2D::from_data(1, 3, vec![0.0, f32::NAN, 100.0]);
209 assert!(field.normalized(0, 1).is_none());
210 }
211}