#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct HeatMapConfig {
pub range_min: f32,
pub range_max: f32,
pub clamp: bool,
}
#[derive(Debug, Clone)]
pub struct HeatMapVertex {
pub value: f32,
}
#[derive(Debug, Clone)]
pub struct HeatMapOverlay {
pub vertices: Vec<HeatMapVertex>,
pub total_bytes: usize,
}
pub fn default_heat_map_config() -> HeatMapConfig {
HeatMapConfig {
range_min: 0.0,
range_max: 1.0,
clamp: true,
}
}
pub fn new_heat_map_overlay() -> HeatMapOverlay {
HeatMapOverlay {
vertices: Vec::new(),
total_bytes: 0,
}
}
pub fn heat_map_set_values(overlay: &mut HeatMapOverlay, values: &[f32]) {
overlay.vertices = values.iter().map(|&v| HeatMapVertex { value: v }).collect();
}
pub fn heat_map_sample_color(
value: f32,
cfg: &HeatMapConfig,
) -> [f32; 4] {
let range = cfg.range_max - cfg.range_min;
let t = if range.abs() < 1e-10 {
0.5_f32
} else {
let raw = (value - cfg.range_min) / range;
if cfg.clamp { raw.clamp(0.0, 1.0) } else { raw.fract().abs() }
};
let (r, g, b) = if t < 0.25 {
let s = t / 0.25;
(0.0_f32, s, 1.0_f32)
} else if t < 0.5 {
let s = (t - 0.25) / 0.25;
(0.0_f32, 1.0_f32, 1.0 - s)
} else if t < 0.75 {
let s = (t - 0.5) / 0.25;
(s, 1.0_f32, 0.0_f32)
} else {
let s = (t - 0.75) / 0.25;
(1.0_f32, 1.0 - s, 0.0_f32)
};
[r, g, b, 1.0]
}
pub fn heat_map_min_value(overlay: &HeatMapOverlay) -> f32 {
overlay
.vertices
.iter()
.map(|v| v.value)
.fold(f32::INFINITY, f32::min)
.max(0.0_f32)
.min(if overlay.vertices.is_empty() { 0.0 } else { f32::INFINITY })
}
pub fn heat_map_max_value(overlay: &HeatMapOverlay) -> f32 {
overlay
.vertices
.iter()
.map(|v| v.value)
.fold(f32::NEG_INFINITY, f32::max)
.min(0.0_f32.max(if overlay.vertices.is_empty() { 0.0 } else { f32::INFINITY }))
}
pub fn heat_map_set_range(cfg: &mut HeatMapConfig, min: f32, max: f32) {
cfg.range_min = min;
cfg.range_max = max;
}
pub fn heat_map_to_json(overlay: &mut HeatMapOverlay, cfg: &HeatMapConfig) -> String {
let mut out = format!(
"{{\"range_min\":{:.6},\"range_max\":{:.6},\"clamp\":{},\"vertex_count\":{}}}",
cfg.range_min,
cfg.range_max,
cfg.clamp,
overlay.vertices.len()
);
overlay.total_bytes = out.len();
let mut arr = String::from("[");
for (i, v) in overlay.vertices.iter().enumerate() {
if i > 0 { arr.push(','); }
arr.push_str(&format!("{:.6}", v.value));
}
arr.push(']');
out = format!(
"{{\"range_min\":{:.6},\"range_max\":{:.6},\"clamp\":{},\"values\":{}}}",
cfg.range_min,
cfg.range_max,
cfg.clamp,
arr
);
overlay.total_bytes = out.len();
out
}
pub fn heat_map_clear(overlay: &mut HeatMapOverlay) {
overlay.vertices.clear();
overlay.total_bytes = 0;
}
pub fn heat_map_vertex_count(overlay: &HeatMapOverlay) -> usize {
overlay.vertices.len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_values() {
let cfg = default_heat_map_config();
assert!((cfg.range_min - 0.0).abs() < 1e-6);
assert!((cfg.range_max - 1.0).abs() < 1e-6);
assert!(cfg.clamp);
}
#[test]
fn new_overlay_is_empty() {
let o = new_heat_map_overlay();
assert_eq!(heat_map_vertex_count(&o), 0);
}
#[test]
fn set_values_updates_vertex_count() {
let mut o = new_heat_map_overlay();
heat_map_set_values(&mut o, &[0.1, 0.5, 0.9]);
assert_eq!(heat_map_vertex_count(&o), 3);
}
#[test]
fn sample_color_zero_is_blue() {
let cfg = default_heat_map_config();
let c = heat_map_sample_color(0.0, &cfg);
assert!(c[0] < 0.01);
assert!(c[2] > 0.99);
}
#[test]
fn sample_color_one_is_red() {
let cfg = default_heat_map_config();
let c = heat_map_sample_color(1.0, &cfg);
assert!(c[0] > 0.99);
assert!(c[1] < 0.01);
assert!(c[2] < 0.01);
}
#[test]
fn sample_color_alpha_is_one() {
let cfg = default_heat_map_config();
let c = heat_map_sample_color(0.5, &cfg);
assert!((c[3] - 1.0).abs() < 1e-6);
}
#[test]
fn set_range_updates_config() {
let mut cfg = default_heat_map_config();
heat_map_set_range(&mut cfg, -10.0, 10.0);
assert!((cfg.range_min - -10.0).abs() < 1e-6);
assert!((cfg.range_max - 10.0).abs() < 1e-6);
}
#[test]
fn json_contains_range_fields() {
let mut o = new_heat_map_overlay();
heat_map_set_values(&mut o, &[0.2, 0.8]);
let cfg = default_heat_map_config();
let json = heat_map_to_json(&mut o, &cfg);
assert!(json.contains("range_min"));
assert!(json.contains("range_max"));
assert!(json.contains("values"));
}
#[test]
fn clear_resets_overlay() {
let mut o = new_heat_map_overlay();
heat_map_set_values(&mut o, &[0.5, 0.5, 0.5]);
heat_map_clear(&mut o);
assert_eq!(heat_map_vertex_count(&o), 0);
assert_eq!(o.total_bytes, 0);
}
#[test]
fn set_values_replaces_previous() {
let mut o = new_heat_map_overlay();
heat_map_set_values(&mut o, &[0.1, 0.2, 0.3, 0.4]);
heat_map_set_values(&mut o, &[1.0]);
assert_eq!(heat_map_vertex_count(&o), 1);
}
#[test]
fn clamped_out_of_range_maps_to_blue_or_red() {
let cfg = default_heat_map_config();
let low = heat_map_sample_color(-5.0, &cfg);
let high = heat_map_sample_color(5.0, &cfg);
assert!(low[2] > 0.99);
assert!(high[0] > 0.99);
}
}