Files
brahman/crates/modules/pineal/flow/src/ribbon.rs
T
sergio 0042fe3f1f feat(pineal): cierra stub flow — diagrama Sankey
Fase F: quinto stub de pineal cerrado.

- layout — pipeline Sankey: columnas por longest-path en el DAG
  (back-edges detectadas por DFS y descartadas para romper ciclos),
  valor de nodo = max(entrante, saliente), apilado vertical por columna
  escalado a la altura, una pasada de barycenter para reducir cruces,
  anclas de cada banda en los bordes de sus nodos.
- ribbon — teselado de bandas como triangle-strip con curva S
  (x lineal, y por smoothstep → tangentes horizontales). paint_ribbon
  + paint_sankey (ribbons al fondo, nodos encima).

Painters agnósticos (trait Canvas). 6 tests verdes (columnas, ciclos
sin loop infinito, proporcionalidad, conteo de draw calls).

Pineal: 5/6 stubs cerrados. Resta mesh (viz de grafos: force-directed
+ Sugiyama + tree layout — módulo, no stub).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:06:58 +00:00

106 lines
3.6 KiB
Rust

//! Tesela y dibuja las bandas (ribbons) de un Sankey.
//!
//! Cada banda es una franja con curva S: `x` avanza lineal entre nodos y
//! `y` interpola con `smoothstep`, lo que da tangentes horizontales en
//! ambos extremos (el look clásico de Sankey). Se emite como un triangle
//! strip `[top0,bot0,top1,bot1,…]`, un draw call por ribbon.
use crate::layout::{LinkBand, SankeyLayout};
use pineal_render::{Canvas, Color};
/// Segmentos por ribbon — controla la suavidad de la curva.
const RIBBON_SEGMENTS: usize = 24;
fn smoothstep(t: f32) -> f32 {
t * t * (3.0 - 2.0 * t)
}
/// Tesela una banda en coords interleaved `[x,y,…]` de un triangle strip.
pub fn ribbon_strip(band: &LinkBand) -> Vec<f32> {
let mut coords = Vec::with_capacity((RIBBON_SEGMENTS + 1) * 4);
for i in 0..=RIBBON_SEGMENTS {
let t = i as f32 / RIBBON_SEGMENTS as f32;
let e = smoothstep(t);
let x_top = band.src_top.x + (band.dst_top.x - band.src_top.x) * t;
let y_top = band.src_top.y + (band.dst_top.y - band.src_top.y) * e;
let x_bot = band.src_bot.x + (band.dst_bot.x - band.src_bot.x) * t;
let y_bot = band.src_bot.y + (band.dst_bot.y - band.src_bot.y) * e;
coords.push(x_top);
coords.push(y_top);
coords.push(x_bot);
coords.push(y_bot);
}
coords
}
/// Dibuja una sola banda con el color dado.
pub fn paint_ribbon(band: &LinkBand, color: Color, canvas: &mut dyn Canvas) {
let coords = ribbon_strip(band);
let colors = vec![color; coords.len() / 2];
canvas.fill_triangle_strip(&coords, &colors);
}
/// Dibuja un Sankey completo: ribbons primero (al fondo), nodos encima.
pub fn paint_sankey(
layout: &SankeyLayout,
node_color: Color,
link_color: Color,
canvas: &mut dyn Canvas,
) {
for band in &layout.links {
paint_ribbon(band, link_color, canvas);
}
for nb in &layout.nodes {
if nb.rect.w > 0.0 && nb.rect.h > 0.0 {
canvas.fill_rect(nb.rect, node_color);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::{compute_layout, SankeyLink, SankeyNode};
use pineal_render::{PlanRecorder, Rect, RenderCmd};
#[test]
fn ribbon_strip_has_expected_vertex_count() {
let band = LinkBand {
link: 0,
src_top: pineal_render::Point::new(0.0, 0.0),
src_bot: pineal_render::Point::new(0.0, 10.0),
dst_top: pineal_render::Point::new(100.0, 50.0),
dst_bot: pineal_render::Point::new(100.0, 60.0),
};
let coords = ribbon_strip(&band);
assert_eq!(coords.len(), (RIBBON_SEGMENTS + 1) * 4);
}
#[test]
fn paint_sankey_emits_nodes_and_ribbons() {
let nodes = vec![
SankeyNode::new("a"),
SankeyNode::new("b"),
SankeyNode::new("c"),
];
let links = [
SankeyLink { source: 0, target: 1, value: 5.0 },
SankeyLink { source: 1, target: 2, value: 3.0 },
];
let layout = compute_layout(
&nodes,
&links,
Rect::new(0.0, 0.0, 300.0, 150.0),
18.0,
6.0,
);
let mut rec = PlanRecorder::new();
paint_sankey(&layout, Color::from_hex(0x335577), Color::from_hex(0x88aacc), &mut rec);
let cmds = rec.into_plan().cmds;
let rects = cmds.iter().filter(|c| matches!(c, RenderCmd::FillRect { .. })).count();
let strips = cmds.iter().filter(|c| matches!(c, RenderCmd::FillTriangleStrip { .. })).count();
assert_eq!(rects, 3, "un fill_rect por nodo");
assert_eq!(strips, 2, "un triangle strip por link");
}
}