4528e08e04
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>
62 lines
2.0 KiB
Rust
62 lines
2.0 KiB
Rust
//! Painter agnóstico: dibuja una `HeatmapMatrix` contra un `Canvas`.
|
|
//!
|
|
//! Emite un `fill_rect` por celda. Apto para matrices chicas y para
|
|
//! export SVG. Para matrices grandes el backend GPUI usa
|
|
//! [`crate::encoder`] + textura en vez de este camino.
|
|
|
|
use crate::matrix::HeatmapMatrix;
|
|
use crate::palette::Ramp;
|
|
use pineal_render::{Canvas, Rect};
|
|
|
|
/// Dibuja `matrix` dentro de `area`, una celda = un `fill_rect`.
|
|
/// Los valores se normalizan por min/max de la matriz.
|
|
pub fn paint(matrix: &HeatmapMatrix, ramp: Ramp, area: Rect, canvas: &mut dyn Canvas) {
|
|
let (w, h) = (matrix.width(), matrix.height());
|
|
if w == 0 || h == 0 {
|
|
return;
|
|
}
|
|
let (min, max) = matrix.min_max();
|
|
let span = max - min;
|
|
let cell_w = area.w / w as f32;
|
|
let cell_h = area.h / h as f32;
|
|
|
|
for y in 0..h {
|
|
for x in 0..w {
|
|
let v = matrix.get(x, y);
|
|
let t = if span > 0.0 { (v - min) / span } else { 0.0 };
|
|
let color = ramp.sample(t);
|
|
let rect = Rect::new(
|
|
area.x + x as f32 * cell_w,
|
|
area.y + y as f32 * cell_h,
|
|
cell_w,
|
|
cell_h,
|
|
);
|
|
canvas.fill_rect(rect, color);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use pineal_render::{PlanRecorder, RenderCmd};
|
|
|
|
#[test]
|
|
fn emits_one_fill_rect_per_cell() {
|
|
let m = HeatmapMatrix::from_data(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0], 3, 2).unwrap();
|
|
let mut rec = PlanRecorder::new();
|
|
paint(&m, Ramp::Viridis, Rect::new(0.0, 0.0, 300.0, 200.0), &mut rec);
|
|
let plan = rec.into_plan();
|
|
assert_eq!(plan.cmds.len(), 6);
|
|
assert!(plan.cmds.iter().all(|c| matches!(c, RenderCmd::FillRect { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn empty_matrix_emits_nothing() {
|
|
let m = HeatmapMatrix::new(0, 0);
|
|
let mut rec = PlanRecorder::new();
|
|
paint(&m, Ramp::Viridis, Rect::new(0.0, 0.0, 10.0, 10.0), &mut rec);
|
|
assert!(rec.into_plan().cmds.is_empty());
|
|
}
|
|
}
|