Files
brahman/crates/modules/pineal/heatmap/src/matrix.rs
T
sergio 4528e08e04 feat(pineal): cierra stub heatmap — matrix + viridis + encoder + paint
Fase F: segundo stub de pineal cerrado.

- matrix — HeatmapMatrix densa width×height de f32, con revision para
  invalidación de textura; get/set/min_max/replace_data.
- palette — Ramp::{Viridis, Grayscale}; Viridis por interpolación
  lineal de 5 control points perceptualmente uniformes.
- encoder — encode_argb: normaliza por min/max + rampa + pack 0xAARRGGBB
  para subir como textura (camino de matrices grandes).
- paint — painter agnóstico: un fill_rect por celda contra un Canvas
  (camino de matrices chicas + export SVG).

12 tests verdes. cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 14:13:10 +00:00

94 lines
2.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `HeatmapMatrix` — matriz densa `width × height` de `f32`.
/// Matriz de valores para un heatmap. `revision` se incrementa en cada
/// mutación — los backends lo usan para invalidar la textura cacheada.
#[derive(Debug, Clone)]
pub struct HeatmapMatrix {
data: Vec<f32>,
width: usize,
height: usize,
revision: u64,
}
impl HeatmapMatrix {
/// Matriz de ceros de `width × height`.
pub fn new(width: usize, height: usize) -> Self {
Self { data: vec![0.0; width * height], width, height, revision: 0 }
}
/// Construye desde datos crudos. `None` si `data.len() != width*height`.
pub fn from_data(data: Vec<f32>, width: usize, height: usize) -> Option<Self> {
if data.len() != width * height {
return None;
}
Some(Self { data, width, height, revision: 0 })
}
pub fn width(&self) -> usize { self.width }
pub fn height(&self) -> usize { self.height }
pub fn revision(&self) -> u64 { self.revision }
pub fn data(&self) -> &[f32] { &self.data }
/// Valor en `(x, y)`. `0.0` si está fuera de rango.
pub fn get(&self, x: usize, y: usize) -> f32 {
if x >= self.width || y >= self.height {
return 0.0;
}
self.data[y * self.width + x]
}
/// Fija el valor en `(x, y)` e incrementa `revision`. No-op si está
/// fuera de rango.
pub fn set(&mut self, x: usize, y: usize, v: f32) {
if x >= self.width || y >= self.height {
return;
}
self.data[y * self.width + x] = v;
self.revision += 1;
}
/// Reemplaza todos los datos (mismas dimensiones) e incrementa
/// `revision`. No-op si la longitud no coincide.
pub fn replace_data(&mut self, data: Vec<f32>) {
if data.len() == self.width * self.height {
self.data = data;
self.revision += 1;
}
}
/// `(min, max)` de los valores. `(0.0, 0.0)` si la matriz está vacía.
pub fn min_max(&self) -> (f32, f32) {
let mut it = self.data.iter().copied();
let Some(first) = it.next() else { return (0.0, 0.0) };
it.fold((first, first), |(lo, hi), v| (lo.min(v), hi.max(v)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_data_checks_length() {
assert!(HeatmapMatrix::from_data(vec![1.0; 6], 2, 3).is_some());
assert!(HeatmapMatrix::from_data(vec![1.0; 5], 2, 3).is_none());
}
#[test]
fn get_set_and_revision() {
let mut m = HeatmapMatrix::new(3, 2);
assert_eq!(m.revision(), 0);
m.set(1, 1, 4.5);
assert_eq!(m.get(1, 1), 4.5);
assert_eq!(m.revision(), 1);
m.set(99, 99, 1.0); // fuera de rango → no-op
assert_eq!(m.revision(), 1);
}
#[test]
fn min_max_over_values() {
let m = HeatmapMatrix::from_data(vec![3.0, -1.0, 7.0, 2.0], 2, 2).unwrap();
assert_eq!(m.min_max(), (-1.0, 7.0));
}
}