This commit is contained in:
sergio
2026-05-13 02:17:40 +00:00
parent 52acaabcf4
commit 88051d220a
37 changed files with 1664 additions and 0 deletions
@@ -0,0 +1,13 @@
[package]
name = "lapaloma-cartesian"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
publish = { workspace = true }
description = "Lapaloma — gráficos cartesianos: LineSeries / BarSeries / AreaSeries, viewport con pan/zoom, picture cache, ejes con decimación, tooltips."
[dependencies]
lapaloma-core = { path = "../../libs/lapaloma-core" }
lapaloma-render = { path = "../lapaloma-render" }
gpui = { workspace = true }
@@ -0,0 +1,31 @@
//! `lapaloma-cartesian` — gráficos cartesianos.
//!
//! Este crate trae:
//!
//! - **`viewport`** — `ChartViewport` con `(x_min, x_max, y_min, y_max)`
//! y helpers de pan/zoom anchor-preserving.
//! - **`coord_system`** — proyecta valores de dominio → pixeles del
//! plot usando las escalas de `lapaloma-core::scale`.
//! - **`series`** — trait `Series` + impls `LineSeries`, `BarSeries`,
//! `AreaSeries`. Cada serie decide LTTB vs raw según densidad.
//! - **`axis`** — ejes con nice-ticks (Wilkinson) y decimación de
//! etiquetas que no overlappean.
//! - **`picture_cache`** — translate-only pan-blit con hash de
//! invalidación. Clipea el outer canvas antes del translate
//! (bug 0.3.0 del Flutter).
//! - **`element`** — el `Element` GPUI que envuelve todo lo de
//! arriba y se inserta en un layout yahweh.
//!
//! Hoy todos los módulos están como placeholders; la primera
//! impl real va a ser `LineSeries` + `element` end-to-end para
//! validar la cadena `core → render → cartesian → gpui`.
#![forbid(unsafe_code)]
#![allow(dead_code)]
pub mod viewport {}
pub mod coord_system {}
pub mod series {}
pub mod axis {}
pub mod picture_cache {}
pub mod element {}
@@ -0,0 +1,12 @@
[package]
name = "lapaloma-export"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
publish = { workspace = true }
description = "Lapaloma — exporters. SVG primero, PDF después. Decimación contextual por DPI: target = width_inches × dpi × vertices_per_pixel."
[dependencies]
lapaloma-core = { path = "../../libs/lapaloma-core" }
lapaloma-render = { path = "../lapaloma-render" }
@@ -0,0 +1,23 @@
//! `lapaloma-export` — exporters.
//!
//! Estrategia: implementar `lapaloma_render::Canvas` con un
//! adapter que emite elementos SVG (o instrucciones PDF). El mismo
//! painter que dibuja en pantalla escribe en el exporter — un sólo
//! camino de código.
//!
//! Decimación contextual:
//! ```text
//! target = width_inches × dpi × vertices_per_pixel
//! ```
//! Print (300 dpi) saca ~3× más vértices que screen (96 dpi) del
//! mismo source data (sección 3.10).
//!
//! - **`svg`** — exporter SVG.
//! - **`pdf`** — placeholder; cuando se implemente, vía `printpdf`
//! sobre el mismo `RenderPlan` que el SVG.
#![forbid(unsafe_code)]
#![allow(dead_code)]
pub mod svg {}
pub mod pdf {}
@@ -0,0 +1,14 @@
[package]
name = "lapaloma-financial"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
publish = { workspace = true }
description = "Lapaloma — gráficos financieros. OHLC / candlesticks con agregación que preserva volatilidad (no LTTB, time-bucketing con max/min de wicks)."
[dependencies]
lapaloma-core = { path = "../../libs/lapaloma-core" }
lapaloma-render = { path = "../lapaloma-render" }
lapaloma-cartesian = { path = "../lapaloma-cartesian" }
gpui = { workspace = true }
@@ -0,0 +1,16 @@
//! `lapaloma-financial` — OHLC y candlesticks.
//!
//! Buffer: 6 floats por bar `[t, o, h, l, c, v]`. Agregación
//! preserva volatilidad (max(h)/min(l), no LTTB — ver sección 3.2):
//! time-bucketing con fallback a index-bucketing cuando todos los
//! timestamps colapsan.
//!
//! Re-usa `lapaloma-cartesian` para viewport, ejes y gestures;
//! sólo aporta el `CandlestickSeries` y la lógica de aggregación.
#![forbid(unsafe_code)]
#![allow(dead_code)]
pub mod ohlc_buffer {}
pub mod aggregate {}
pub mod candlestick {}
@@ -0,0 +1,13 @@
[package]
name = "lapaloma-flow"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
publish = { workspace = true }
description = "Lapaloma — diagramas de flujo Sankey: columnas topológicas + barycenter ordering + ribbons como triangle strips de béziers."
[dependencies]
lapaloma-core = { path = "../../libs/lapaloma-core" }
lapaloma-render = { path = "../lapaloma-render" }
gpui = { workspace = true }
@@ -0,0 +1,16 @@
//! `lapaloma-flow` — diagramas Sankey.
//!
//! Pipeline (sección 3.7 del ARCHITECTURE.md):
//! 1. Columnas via longest-path en el DAG (back-edges drop).
//! 2. Flow por nodo = max(in_value, out_value).
//! 3. Barycenter ordering con inversion-count crossings.
//! 4. Stripes por edge dentro de cada lado del nodo.
//! 5. Ribbons como triangle-strip de béziers, un draw call por
//! ribbon, color por vértice.
#![forbid(unsafe_code)]
#![allow(dead_code)]
pub mod layout {}
pub mod ribbon {}
pub mod element {}
@@ -0,0 +1,13 @@
[package]
name = "lapaloma-heatmap"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
publish = { workspace = true }
description = "Lapaloma — heatmap. Matriz [width × height] de f32 → imagen pre-encodeada que se rendea con un sólo drawImageRect."
[dependencies]
lapaloma-core = { path = "../../libs/lapaloma-core" }
lapaloma-render = { path = "../lapaloma-render" }
gpui = { workspace = true }
@@ -0,0 +1,21 @@
//! `lapaloma-heatmap` — matriz `[width × height]` de `f32` → imagen.
//!
//! Para matrices grandes (4096² = 67 MB de pixels), encodear la
//! imagen una vez al cambiar la data y renderear con un solo
//! `drawImageRect` (o equivalente GPUI). Eso convierte el coste
//! de cada frame en "blit de una textura", sub-millisecond.
//!
//! - **`matrix`** — `HeatmapMatrix { data: Vec<f32>, width, height,
//! revision }`.
//! - **`palette`** — color ramps (viridis, plasma, gray…).
//! - **`encoder`** — convierte la matrix a un buffer ARGB para
//! subir como textura.
//! - **`element`** — `Element` GPUI.
#![forbid(unsafe_code)]
#![allow(dead_code)]
pub mod matrix {}
pub mod palette {}
pub mod encoder {}
pub mod element {}
@@ -0,0 +1,13 @@
[package]
name = "lapaloma-mesh"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
publish = { workspace = true }
description = "Lapaloma — grafos. NodeBuffer / EdgeBuffer + layouts (force-directed con Barnes-Hut, Sugiyama-lite jerárquico, subtree-width)."
[dependencies]
lapaloma-core = { path = "../../libs/lapaloma-core" }
lapaloma-render = { path = "../lapaloma-render" }
gpui = { workspace = true }
@@ -0,0 +1,28 @@
//! `lapaloma-mesh` — visualización de grafos.
//!
//! Módulos:
//! - **`node_buffer`** / **`edge_buffer`** — `Vec<f32>` planos con
//! stride fijo (3 floats por nodo: `[x, y, radius]`).
//! - **`spatial_hash`** — uniform grid para hit-test de nodos
//! móviles (sección 5.1).
//! - **`force_directed`** — layout con Barnes-Hut delegado a
//! `lapaloma_core::barnes_hut` (cuando se implemente).
//! - **`hierarchical`** — Sugiyama-lite, delegado a
//! `lapaloma_core::sugiyama`.
//! - **`tree`** — subtree-width layout, delegado a
//! `lapaloma_core::tree_layout`.
//! - **`camera`** — pan/zoom con anchor-preserving zoom de la
//! sección 5.3.
//! - **`element`** — `Element` GPUI.
#![forbid(unsafe_code)]
#![allow(dead_code)]
pub mod node_buffer {}
pub mod edge_buffer {}
pub mod spatial_hash {}
pub mod force_directed {}
pub mod hierarchical {}
pub mod tree {}
pub mod camera {}
pub mod element {}
@@ -0,0 +1,14 @@
[package]
name = "lapaloma-phosphor"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
publish = { workspace = true }
description = "Lapaloma — decoración CRT sobre lapaloma-stream: trail con alpha decay por edad, ghost, anotaciones magnéticas ancladas a sample index."
[dependencies]
lapaloma-core = { path = "../../libs/lapaloma-core" }
lapaloma-render = { path = "../lapaloma-render" }
lapaloma-stream = { path = "../lapaloma-stream" }
gpui = { workspace = true }
@@ -0,0 +1,15 @@
//! `lapaloma-phosphor` — decoración CRT sobre `lapaloma-stream`.
//!
//! Three pieces (sección 4.3):
//! - **`trail`** — cada sample como 2 vértices ±half_width, triangle
//! strip con color por vértice; alpha = 1 - age/trail_samples.
//! - **`ghost`** — render con offset/blur del trail anterior.
//! - **`magnetic_anchor`** — anotaciones ancladas a sample index
//! absoluto, no a screen pos (sección 5.5).
#![forbid(unsafe_code)]
#![allow(dead_code)]
pub mod trail {}
pub mod ghost {}
pub mod magnetic_anchor {}
@@ -0,0 +1,13 @@
[package]
name = "lapaloma-polar"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
publish = { workspace = true }
description = "Lapaloma — gráficos polares: pie chart, donut, radar."
[dependencies]
lapaloma-core = { path = "../../libs/lapaloma-core" }
lapaloma-render = { path = "../lapaloma-render" }
gpui = { workspace = true }
@@ -0,0 +1,16 @@
//! `lapaloma-polar` — gráficos en coordenadas polares.
//!
//! - **`pie`** — pie / donut chart.
//! - **`radar`** — radar (spider) chart.
//! - **`element`** — `Element` GPUI.
//!
//! No comparte mucho con cartesian; viewport y gestures van
//! ad-hoc. El picture-cache de cartesian no aplica acá (las
//! rotaciones lo invalidan).
#![forbid(unsafe_code)]
#![allow(dead_code)]
pub mod pie {}
pub mod radar {}
pub mod element {}
@@ -0,0 +1,11 @@
[package]
name = "lapaloma-render"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
publish = { workspace = true }
description = "Lapaloma — abstracción de painter: trait Canvas + RenderPlan + color helpers. Habilita backend CPU (gpui hoy) y GPU (wgpu mañana) sin tocar a los painters."
[dependencies]
lapaloma-core = { path = "../../libs/lapaloma-core" }
@@ -0,0 +1,53 @@
//! El trait `Canvas` que todos los painters consumen.
//!
//! Mantenemos el set mínimo: line / polyline / rect (fill+stroke) /
//! triangle strip. Cualquier visualización compleja (curvas
//! bezier, gradients) se descompone en estos primitivos por el
//! painter — el backend no necesita entender la semántica.
//!
//! Convención: coordenadas en píxeles del viewport, origen
//! arriba-izquierda, +Y hacia abajo. La proyección de datos→pixel
//! la hace el painter via las escalas de `lapaloma-core`.
use crate::{Color, Point, Rect};
#[derive(Debug, Clone, Copy)]
pub struct StrokeStyle {
pub width: f32,
pub color: Color,
}
impl StrokeStyle {
pub const fn new(width: f32, color: Color) -> Self {
Self { width, color }
}
}
pub trait Canvas {
/// Clip subsiguiente al rect dado. Stack-discipline:
/// `push_clip` + draw + `pop_clip`.
fn push_clip(&mut self, rect: Rect);
fn pop_clip(&mut self);
/// Rectángulo relleno (sin stroke).
fn fill_rect(&mut self, rect: Rect, color: Color);
/// Rectángulo sólo stroke (sin fill).
fn stroke_rect(&mut self, rect: Rect, stroke: StrokeStyle);
/// Línea de a→b.
fn stroke_line(&mut self, a: Point, b: Point, stroke: StrokeStyle);
/// Polilínea sobre coords interleaved `[x0,y0,x1,y1,…]`.
/// El backend la rendea como un solo draw call cuando puede.
fn stroke_polyline(&mut self, coords: &[f32], stroke: StrokeStyle);
/// Triangle strip rellenado, con un color por vértice
/// (longitudes deben coincidir: `coords.len()/2 == colors.len()`).
/// Es lo que usa el phosphor trail y los ribbons Sankey.
fn fill_triangle_strip(&mut self, coords: &[f32], colors: &[Color]);
/// Glyph de texto sencillo. El layout va a un text-cache
/// dentro del backend; por ahora un trazo simple.
fn draw_text(&mut self, p: Point, text: &str, color: Color, size_px: f32);
}
@@ -0,0 +1,35 @@
//! Color RGBA en f32, agnóstico de backend.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Color {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Color {
pub const TRANSPARENT: Self = Self::rgba(0.0, 0.0, 0.0, 0.0);
pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
Self { r, g, b, a: 1.0 }
}
pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
}
/// Construye desde 0xRRGGBB hex literal.
pub fn from_hex(rgb: u32) -> Self {
let r = ((rgb >> 16) & 0xff) as f32 / 255.0;
let g = ((rgb >> 8) & 0xff) as f32 / 255.0;
let b = (rgb & 0xff) as f32 / 255.0;
Self::rgb(r, g, b)
}
/// Multiplica el canal alpha — útil para fade del phosphor trail.
pub fn with_alpha(self, a: f32) -> Self {
Self { a, ..self }
}
}
@@ -0,0 +1,36 @@
//! Tipos geométricos mínimos en `f32`.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub x: f32,
pub y: f32,
}
impl Point {
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rect {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
impl Rect {
pub const fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Self { x, y, w, h }
}
pub fn right(&self) -> f32 {
self.x + self.w
}
pub fn bottom(&self) -> f32 {
self.y + self.h
}
pub fn contains(&self, p: Point) -> bool {
p.x >= self.x && p.x <= self.right() && p.y >= self.y && p.y <= self.bottom()
}
}
@@ -0,0 +1,30 @@
//! `lapaloma-render` — abstracción de painter.
//!
//! Los crates de visualización (cartesian, mesh, polar…) no
//! conocen `gpui` ni `wgpu`. Hablan contra el trait [`Canvas`]
//! definido acá. Eso permite:
//!
//! - **Backend CPU sobre gpui** — implementación por defecto;
//! sirve para series de hasta ~50 k vértices a 60 FPS sin
//! sudar.
//! - **Backend GPU sobre wgpu** — placeholder hoy; cuando un
//! módulo le pegue al wall (millones de puntos, force-sim
//! pesada), se enchufa sin tocar la lógica de los painters.
//! - **Backend SVG** — `lapaloma-export` implementa el mismo
//! trait emitiendo elementos `<path>`, `<polyline>`, etc.
//!
//! Tipos primitivos (`Color`, `Point`, `Rect`) viven acá para
//! no atarlos a `gpui::Rgba`/`gpui::Point` — los backends
//! traducen al tipo nativo del runtime que les toca.
#![forbid(unsafe_code)]
pub mod color;
pub mod geom;
pub mod canvas;
pub mod plan;
pub use color::Color;
pub use geom::{Point, Rect};
pub use canvas::{Canvas, StrokeStyle};
pub use plan::{RenderCmd, RenderPlan};
@@ -0,0 +1,35 @@
//! `RenderPlan` — comandos materializados para backends que no
//! reciben llamadas en vivo (SVG export, snapshot testing).
//!
//! Un painter que escribe contra [`crate::Canvas`] puede ser
//! capturado en un `RenderPlan` usando un `Canvas` adapter que
//! empuja `RenderCmd`s en lugar de dibujar. El exporter consume
//! el plan y emite `<polyline>` / `<rect>` / etc.
use crate::{Color, Point, Rect, StrokeStyle};
#[derive(Debug, Clone)]
pub enum RenderCmd {
PushClip(Rect),
PopClip,
FillRect { rect: Rect, color: Color },
StrokeRect { rect: Rect, stroke: StrokeStyle },
StrokeLine { a: Point, b: Point, stroke: StrokeStyle },
StrokePolyline { coords: Vec<f32>, stroke: StrokeStyle },
FillTriangleStrip { coords: Vec<f32>, colors: Vec<Color> },
DrawText { p: Point, text: String, color: Color, size_px: f32 },
}
#[derive(Debug, Clone, Default)]
pub struct RenderPlan {
pub cmds: Vec<RenderCmd>,
}
impl RenderPlan {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, cmd: RenderCmd) {
self.cmds.push(cmd);
}
}
@@ -0,0 +1,13 @@
[package]
name = "lapaloma-stream"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
publish = { workspace = true }
description = "Lapaloma — widget de telemetría tipo osciloscopio. Ring buffer + envelope min/max por columna + render en dos segmentos (split at head)."
[dependencies]
lapaloma-core = { path = "../../libs/lapaloma-core" }
lapaloma-render = { path = "../lapaloma-render" }
gpui = { workspace = true }
@@ -0,0 +1,19 @@
//! `lapaloma-stream` — telemetría streaming tipo osciloscopio.
//!
//! Núcleo: `lapaloma_core::ring::RingBuffer` + render en dos
//! segmentos split-at-head (sweep) o con translate por frame
//! (scroll).
//!
//! Módulos:
//! - **`envelope`** — downsample min/max por columna de pixel.
//! Incremental para sweep, single bounded pass para scroll
//! (ver sección 3.3 del ARCHITECTURE.md).
//! - **`element`** — `Element` GPUI con `Model<RingBuffer>`
//! observable. El push viene de otro thread; el Element se
//! redibuja sólo cuando `revision` cambió.
#![forbid(unsafe_code)]
#![allow(dead_code)]
pub mod envelope {}
pub mod element {}
@@ -0,0 +1,13 @@
[package]
name = "lapaloma-treemap"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
publish = { workspace = true }
description = "Lapaloma — treemap con algoritmo squarified (Bruls / d3-hierarchy formulation)."
[dependencies]
lapaloma-core = { path = "../../libs/lapaloma-core" }
lapaloma-render = { path = "../lapaloma-render" }
gpui = { workspace = true }
@@ -0,0 +1,12 @@
//! `lapaloma-treemap` — treemap squarified.
//!
//! Algoritmo en `lapaloma_core::squarify` (placeholder); el `Element`
//! sólo se encarga de iterar las tiles resultantes y dibujarlas.
//! Pre-scaling de valores al area total del rect es clave para
//! estabilidad numérica con rangos amplios.
#![forbid(unsafe_code)]
#![allow(dead_code)]
pub mod tile {}
pub mod element {}
@@ -0,0 +1,41 @@
[package]
name = "lapaloma"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
publish = { workspace = true }
description = "Lapaloma — paraguas: re-exporta los módulos para prototipos. En producción importar los crates hoja directamente para que tree-shaking descarte lo no usado."
[dependencies]
lapaloma-core = { path = "../../libs/lapaloma-core", optional = true }
lapaloma-render = { path = "../lapaloma-render", optional = true }
lapaloma-cartesian = { path = "../lapaloma-cartesian", optional = true }
lapaloma-stream = { path = "../lapaloma-stream", optional = true }
lapaloma-mesh = { path = "../lapaloma-mesh", optional = true }
lapaloma-financial = { path = "../lapaloma-financial", optional = true }
lapaloma-polar = { path = "../lapaloma-polar", optional = true }
lapaloma-heatmap = { path = "../lapaloma-heatmap", optional = true }
lapaloma-treemap = { path = "../lapaloma-treemap", optional = true }
lapaloma-flow = { path = "../lapaloma-flow", optional = true }
lapaloma-phosphor = { path = "../lapaloma-phosphor", optional = true }
lapaloma-export = { path = "../lapaloma-export", optional = true }
[features]
default = ["full"]
full = [
"core", "render", "cartesian", "stream", "mesh", "financial",
"polar", "heatmap", "treemap", "flow", "phosphor", "export",
]
core = ["dep:lapaloma-core"]
render = ["dep:lapaloma-render", "core"]
cartesian = ["dep:lapaloma-cartesian", "render"]
stream = ["dep:lapaloma-stream", "render"]
mesh = ["dep:lapaloma-mesh", "render"]
financial = ["dep:lapaloma-financial", "cartesian"]
polar = ["dep:lapaloma-polar", "render"]
heatmap = ["dep:lapaloma-heatmap", "render"]
treemap = ["dep:lapaloma-treemap", "render"]
flow = ["dep:lapaloma-flow", "render"]
phosphor = ["dep:lapaloma-phosphor", "stream"]
export = ["dep:lapaloma-export", "render"]
@@ -0,0 +1,53 @@
//! `lapaloma` — paraguas re-export.
//!
//! Para **prototipos** que quieren probar varios módulos a la vez
//! sin agregar 8 dependencias a `Cargo.toml`. En producción
//! preferir importar directamente los crates hoja (`lapaloma-core`,
//! `lapaloma-cartesian`, …) para que el linker descarte lo no
//! usado y los tiempos de compilación bajen.
//!
//! Las features mapean 1:1 a cada sub-crate:
//!
//! ```toml
//! [dependencies]
//! lapaloma = { workspace = true, default-features = false,
//! features = ["cartesian", "stream"] }
//! ```
#![forbid(unsafe_code)]
#[cfg(feature = "core")]
pub use lapaloma_core as core;
#[cfg(feature = "render")]
pub use lapaloma_render as render;
#[cfg(feature = "cartesian")]
pub use lapaloma_cartesian as cartesian;
#[cfg(feature = "stream")]
pub use lapaloma_stream as stream;
#[cfg(feature = "mesh")]
pub use lapaloma_mesh as mesh;
#[cfg(feature = "financial")]
pub use lapaloma_financial as financial;
#[cfg(feature = "polar")]
pub use lapaloma_polar as polar;
#[cfg(feature = "heatmap")]
pub use lapaloma_heatmap as heatmap;
#[cfg(feature = "treemap")]
pub use lapaloma_treemap as treemap;
#[cfg(feature = "flow")]
pub use lapaloma_flow as flow;
#[cfg(feature = "phosphor")]
pub use lapaloma_phosphor as phosphor;
#[cfg(feature = "export")]
pub use lapaloma_export as export;