1use crate::math::{Fix128, Vec3Fix};
13
14#[cfg(not(feature = "std"))]
15use alloc::vec;
16#[cfg(not(feature = "std"))]
17use alloc::vec::Vec;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum SliceAxis {
26 X,
28 Y,
30 Z,
32}
33
34#[derive(Clone, Debug)]
36pub struct HeatmapConfig {
37 pub resolution_x: usize,
39 pub resolution_y: usize,
41 pub slice_axis: SliceAxis,
43 pub slice_offset: Fix128,
45 pub bounds_min: Vec3Fix,
47 pub bounds_max: Vec3Fix,
49}
50
51#[derive(Clone, Debug)]
53pub struct Heatmap {
54 pub data: Vec<Fix128>,
56 pub width: usize,
58 pub height: usize,
60 pub min_value: Fix128,
62 pub max_value: Fix128,
64}
65
66fn sample_point(config: &HeatmapConfig, u_frac: Fix128, v_frac: Fix128) -> Vec3Fix {
72 let bmin = config.bounds_min;
73 let bmax = config.bounds_max;
74
75 match config.slice_axis {
76 SliceAxis::X => Vec3Fix::new(
77 config.slice_offset,
78 bmin.y + (bmax.y - bmin.y) * u_frac,
79 bmin.z + (bmax.z - bmin.z) * v_frac,
80 ),
81 SliceAxis::Y => Vec3Fix::new(
82 bmin.x + (bmax.x - bmin.x) * u_frac,
83 config.slice_offset,
84 bmin.z + (bmax.z - bmin.z) * v_frac,
85 ),
86 SliceAxis::Z => Vec3Fix::new(
87 bmin.x + (bmax.x - bmin.x) * u_frac,
88 bmin.y + (bmax.y - bmin.y) * v_frac,
89 config.slice_offset,
90 ),
91 }
92}
93
94#[must_use]
105pub fn generate_stress_heatmap(
106 body_positions: &[Vec3Fix],
107 contacts: &[(Vec3Fix, Vec3Fix, Fix128)],
108 config: &HeatmapConfig,
109) -> Heatmap {
110 let w = config.resolution_x.max(1);
111 let h = config.resolution_y.max(1);
112 let mut data = vec![Fix128::ZERO; w * h];
113
114 let _ = body_positions; let influence_radius = Fix128::from_int(2);
117 let radius_sq = influence_radius * influence_radius;
118
119 for iy in 0..h {
120 for ix in 0..w {
121 let u = Fix128::from_ratio(ix as i64, w as i64);
122 let v = Fix128::from_ratio(iy as i64, h as i64);
123 let sample = sample_point(config, u, v);
124
125 let mut stress = Fix128::ZERO;
126
127 for &(ref contact_point, ref _normal, force) in contacts {
128 let diff = sample - *contact_point;
129 let dist_sq = diff.dot(diff);
130
131 if dist_sq < radius_sq {
132 let dist = dist_sq.sqrt();
134 let falloff = Fix128::ONE - dist / influence_radius;
135 stress = stress + force * falloff;
136 }
137 }
138
139 data[iy * w + ix] = stress;
140 }
141 }
142
143 compute_heatmap_minmax(data, w, h)
144}
145
146#[must_use]
156pub fn generate_temperature_heatmap<F>(temperature_fn: F, config: &HeatmapConfig) -> Heatmap
157where
158 F: Fn(Vec3Fix) -> Fix128,
159{
160 let w = config.resolution_x.max(1);
161 let h = config.resolution_y.max(1);
162 let mut data = vec![Fix128::ZERO; w * h];
163
164 for iy in 0..h {
165 for ix in 0..w {
166 let u = Fix128::from_ratio(ix as i64, w as i64);
167 let v = Fix128::from_ratio(iy as i64, h as i64);
168 let sample = sample_point(config, u, v);
169 data[iy * w + ix] = temperature_fn(sample);
170 }
171 }
172
173 compute_heatmap_minmax(data, w, h)
174}
175
176fn compute_heatmap_minmax(data: Vec<Fix128>, width: usize, height: usize) -> Heatmap {
178 let mut min_val = data.first().copied().unwrap_or(Fix128::ZERO);
179 let mut max_val = min_val;
180
181 for &v in &data {
182 if v < min_val {
183 min_val = v;
184 }
185 if v > max_val {
186 max_val = v;
187 }
188 }
189
190 Heatmap {
191 data,
192 width,
193 height,
194 min_value: min_val,
195 max_value: max_val,
196 }
197}
198
199#[must_use]
211pub fn heatmap_to_rgba(heatmap: &Heatmap) -> Vec<[u8; 4]> {
212 let range = heatmap.max_value - heatmap.min_value;
213 let has_range = !range.is_zero();
214
215 heatmap
216 .data
217 .iter()
218 .map(|&val| {
219 let t = if has_range {
220 ((val - heatmap.min_value) / range).to_f64().clamp(0.0, 1.0)
221 } else {
222 0.5
223 };
224 viridis_color(t)
225 })
226 .collect()
227}
228
229#[allow(clippy::suboptimal_flops)]
234fn viridis_color(t: f64) -> [u8; 4] {
235 const STOPS: [(f64, f64, f64); 5] = [
237 (0.267, 0.004, 0.329), (0.282, 0.140, 0.458), (0.127, 0.566, 0.551), (0.544, 0.774, 0.247), (0.993, 0.906, 0.144), ];
243
244 let t = t.clamp(0.0, 1.0);
245 let segment = crate::det_math::min64(t * 4.0, 3.999);
246 let idx = segment as usize;
247 let frac = segment - idx as f64;
248
249 let (r0, g0, b0) = STOPS[idx];
250 let (r1, g1, b1) = STOPS[idx + 1];
251
252 let r = (r1 - r0) * frac + r0;
253 let g = (g1 - g0) * frac + g0;
254 let b = (b1 - b0) * frac + b0;
255
256 [(r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8, 255]
257}
258
259impl crate::solver::PhysicsWorld {
264 #[must_use]
269 pub fn stress_heatmap(&self, dt: Fix128, config: &HeatmapConfig) -> Heatmap {
270 let positions: Vec<Vec3Fix> = self.bodies.iter().map(|b| b.position).collect();
271 generate_stress_heatmap(&positions, &self.contact_forces(dt), config)
272 }
273}
274
275#[cfg(test)]
280mod tests {
281 use super::*;
282
283 fn default_config() -> HeatmapConfig {
284 HeatmapConfig {
285 resolution_x: 4,
286 resolution_y: 4,
287 slice_axis: SliceAxis::Z,
288 slice_offset: Fix128::ZERO,
289 bounds_min: Vec3Fix::from_int(-5, -5, -5),
290 bounds_max: Vec3Fix::from_int(5, 5, 5),
291 }
292 }
293
294 #[test]
295 fn test_stress_heatmap_no_contacts() {
296 let config = default_config();
297 let heatmap = generate_stress_heatmap(&[], &[], &config);
298 assert_eq!(heatmap.width, 4);
299 assert_eq!(heatmap.height, 4);
300 assert_eq!(heatmap.data.len(), 16);
301 for &v in &heatmap.data {
303 assert!(v.is_zero());
304 }
305 }
306
307 #[test]
308 fn test_stress_heatmap_with_contact() {
309 let config = default_config();
310 let contacts = vec![(Vec3Fix::ZERO, Vec3Fix::UNIT_Y, Fix128::from_int(100))];
311 let heatmap = generate_stress_heatmap(&[], &contacts, &config);
312 assert!(heatmap.max_value > Fix128::ZERO);
313 }
314
315 #[test]
316 fn test_temperature_heatmap_uniform() {
317 let config = default_config();
318 let temp = Fix128::from_int(300); let heatmap = generate_temperature_heatmap(|_| temp, &config);
320 assert_eq!(heatmap.min_value, temp);
321 assert_eq!(heatmap.max_value, temp);
322 }
323
324 #[test]
325 fn test_temperature_heatmap_gradient() {
326 let config = HeatmapConfig {
327 resolution_x: 10,
328 resolution_y: 1,
329 slice_axis: SliceAxis::Z,
330 slice_offset: Fix128::ZERO,
331 bounds_min: Vec3Fix::from_int(0, 0, 0),
332 bounds_max: Vec3Fix::from_int(10, 10, 10),
333 };
334 let heatmap = generate_temperature_heatmap(|p| p.x, &config);
336 assert!(heatmap.max_value > heatmap.min_value);
337 }
338
339 #[test]
340 fn test_heatmap_to_rgba_length() {
341 let config = default_config();
342 let heatmap = generate_temperature_heatmap(|_| Fix128::ONE, &config);
343 let rgba = heatmap_to_rgba(&heatmap);
344 assert_eq!(rgba.len(), 16); }
346
347 #[test]
348 fn test_heatmap_to_rgba_alpha_255() {
349 let config = default_config();
350 let heatmap = generate_temperature_heatmap(|p| p.x, &config);
351 let rgba = heatmap_to_rgba(&heatmap);
352 for pixel in &rgba {
353 assert_eq!(pixel[3], 255);
354 }
355 }
356
357 #[test]
358 fn test_viridis_endpoints() {
359 let dark = viridis_color(0.0);
360 let bright = viridis_color(1.0);
361 assert!(dark[0] < 100);
363 assert!(bright[0] > 200);
365 assert!(bright[1] > 200);
366 }
367
368 #[test]
369 fn test_slice_axis_x() {
370 let config = HeatmapConfig {
371 resolution_x: 2,
372 resolution_y: 2,
373 slice_axis: SliceAxis::X,
374 slice_offset: Fix128::from_int(5),
375 bounds_min: Vec3Fix::from_int(0, 0, 0),
376 bounds_max: Vec3Fix::from_int(10, 10, 10),
377 };
378 let heatmap = generate_temperature_heatmap(|p| p.x, &config);
380 for &v in &heatmap.data {
382 assert_eq!(v.hi, 5);
383 }
384 }
385
386 #[test]
387 fn test_slice_axis_y() {
388 let config = HeatmapConfig {
389 resolution_x: 2,
390 resolution_y: 2,
391 slice_axis: SliceAxis::Y,
392 slice_offset: Fix128::from_int(3),
393 bounds_min: Vec3Fix::from_int(0, 0, 0),
394 bounds_max: Vec3Fix::from_int(10, 10, 10),
395 };
396 let heatmap = generate_temperature_heatmap(|p| p.y, &config);
397 for &v in &heatmap.data {
398 assert_eq!(v.hi, 3);
399 }
400 }
401
402 #[test]
403 fn test_heatmap_dimensions() {
404 let config = HeatmapConfig {
405 resolution_x: 7,
406 resolution_y: 3,
407 slice_axis: SliceAxis::Z,
408 slice_offset: Fix128::ZERO,
409 bounds_min: Vec3Fix::from_int(-1, -1, -1),
410 bounds_max: Vec3Fix::from_int(1, 1, 1),
411 };
412 let heatmap = generate_temperature_heatmap(|_| Fix128::ZERO, &config);
413 assert_eq!(heatmap.width, 7);
414 assert_eq!(heatmap.height, 3);
415 assert_eq!(heatmap.data.len(), 21);
416 }
417}