From 88051d220ae611c8eba0f6f3018ea6af7acac62f Mon Sep 17 00:00:00 2001 From: sergio Date: Wed, 13 May 2026 02:17:40 +0000 Subject: [PATCH] creation --- Cargo.lock | 120 ++++++++++ Cargo.toml | 32 +++ .../ui_engine/libs/lapaloma-core/Cargo.toml | 12 + .../libs/lapaloma-core/src/buffer.rs | 128 +++++++++++ .../ui_engine/libs/lapaloma-core/src/lib.rs | 55 +++++ .../ui_engine/libs/lapaloma-core/src/lttb.rs | 177 ++++++++++++++ .../ui_engine/libs/lapaloma-core/src/ring.rs | 216 ++++++++++++++++++ .../ui_engine/libs/lapaloma-core/src/scale.rs | 153 +++++++++++++ .../libs/lapaloma-core/src/spatial.rs | 149 ++++++++++++ .../widgets/lapaloma-cartesian/Cargo.toml | 13 ++ .../widgets/lapaloma-cartesian/src/lib.rs | 31 +++ .../widgets/lapaloma-export/Cargo.toml | 12 + .../widgets/lapaloma-export/src/lib.rs | 23 ++ .../widgets/lapaloma-financial/Cargo.toml | 14 ++ .../widgets/lapaloma-financial/src/lib.rs | 16 ++ .../widgets/lapaloma-flow/Cargo.toml | 13 ++ .../widgets/lapaloma-flow/src/lib.rs | 16 ++ .../widgets/lapaloma-heatmap/Cargo.toml | 13 ++ .../widgets/lapaloma-heatmap/src/lib.rs | 21 ++ .../widgets/lapaloma-mesh/Cargo.toml | 13 ++ .../widgets/lapaloma-mesh/src/lib.rs | 28 +++ .../widgets/lapaloma-phosphor/Cargo.toml | 14 ++ .../widgets/lapaloma-phosphor/src/lib.rs | 15 ++ .../widgets/lapaloma-polar/Cargo.toml | 13 ++ .../widgets/lapaloma-polar/src/lib.rs | 16 ++ .../widgets/lapaloma-render/Cargo.toml | 11 + .../widgets/lapaloma-render/src/canvas.rs | 53 +++++ .../widgets/lapaloma-render/src/color.rs | 35 +++ .../widgets/lapaloma-render/src/geom.rs | 36 +++ .../widgets/lapaloma-render/src/lib.rs | 30 +++ .../widgets/lapaloma-render/src/plan.rs | 35 +++ .../widgets/lapaloma-stream/Cargo.toml | 13 ++ .../widgets/lapaloma-stream/src/lib.rs | 19 ++ .../widgets/lapaloma-treemap/Cargo.toml | 13 ++ .../widgets/lapaloma-treemap/src/lib.rs | 12 + .../ui_engine/widgets/lapaloma/Cargo.toml | 41 ++++ .../ui_engine/widgets/lapaloma/src/lib.rs | 53 +++++ 37 files changed, 1664 insertions(+) create mode 100644 crates/modules/ui_engine/libs/lapaloma-core/Cargo.toml create mode 100644 crates/modules/ui_engine/libs/lapaloma-core/src/buffer.rs create mode 100644 crates/modules/ui_engine/libs/lapaloma-core/src/lib.rs create mode 100644 crates/modules/ui_engine/libs/lapaloma-core/src/lttb.rs create mode 100644 crates/modules/ui_engine/libs/lapaloma-core/src/ring.rs create mode 100644 crates/modules/ui_engine/libs/lapaloma-core/src/scale.rs create mode 100644 crates/modules/ui_engine/libs/lapaloma-core/src/spatial.rs create mode 100644 crates/modules/ui_engine/widgets/lapaloma-cartesian/Cargo.toml create mode 100644 crates/modules/ui_engine/widgets/lapaloma-cartesian/src/lib.rs create mode 100644 crates/modules/ui_engine/widgets/lapaloma-export/Cargo.toml create mode 100644 crates/modules/ui_engine/widgets/lapaloma-export/src/lib.rs create mode 100644 crates/modules/ui_engine/widgets/lapaloma-financial/Cargo.toml create mode 100644 crates/modules/ui_engine/widgets/lapaloma-financial/src/lib.rs create mode 100644 crates/modules/ui_engine/widgets/lapaloma-flow/Cargo.toml create mode 100644 crates/modules/ui_engine/widgets/lapaloma-flow/src/lib.rs create mode 100644 crates/modules/ui_engine/widgets/lapaloma-heatmap/Cargo.toml create mode 100644 crates/modules/ui_engine/widgets/lapaloma-heatmap/src/lib.rs create mode 100644 crates/modules/ui_engine/widgets/lapaloma-mesh/Cargo.toml create mode 100644 crates/modules/ui_engine/widgets/lapaloma-mesh/src/lib.rs create mode 100644 crates/modules/ui_engine/widgets/lapaloma-phosphor/Cargo.toml create mode 100644 crates/modules/ui_engine/widgets/lapaloma-phosphor/src/lib.rs create mode 100644 crates/modules/ui_engine/widgets/lapaloma-polar/Cargo.toml create mode 100644 crates/modules/ui_engine/widgets/lapaloma-polar/src/lib.rs create mode 100644 crates/modules/ui_engine/widgets/lapaloma-render/Cargo.toml create mode 100644 crates/modules/ui_engine/widgets/lapaloma-render/src/canvas.rs create mode 100644 crates/modules/ui_engine/widgets/lapaloma-render/src/color.rs create mode 100644 crates/modules/ui_engine/widgets/lapaloma-render/src/geom.rs create mode 100644 crates/modules/ui_engine/widgets/lapaloma-render/src/lib.rs create mode 100644 crates/modules/ui_engine/widgets/lapaloma-render/src/plan.rs create mode 100644 crates/modules/ui_engine/widgets/lapaloma-stream/Cargo.toml create mode 100644 crates/modules/ui_engine/widgets/lapaloma-stream/src/lib.rs create mode 100644 crates/modules/ui_engine/widgets/lapaloma-treemap/Cargo.toml create mode 100644 crates/modules/ui_engine/widgets/lapaloma-treemap/src/lib.rs create mode 100644 crates/modules/ui_engine/widgets/lapaloma/Cargo.toml create mode 100644 crates/modules/ui_engine/widgets/lapaloma/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4cb9aed..b1adef1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5362,6 +5362,126 @@ dependencies = [ "rustversion", ] +[[package]] +name = "lapaloma" +version = "0.1.0" +dependencies = [ + "lapaloma-cartesian", + "lapaloma-core", + "lapaloma-export", + "lapaloma-financial", + "lapaloma-flow", + "lapaloma-heatmap", + "lapaloma-mesh", + "lapaloma-phosphor", + "lapaloma-polar", + "lapaloma-render", + "lapaloma-stream", + "lapaloma-treemap", +] + +[[package]] +name = "lapaloma-cartesian" +version = "0.1.0" +dependencies = [ + "gpui", + "lapaloma-core", + "lapaloma-render", +] + +[[package]] +name = "lapaloma-core" +version = "0.1.0" + +[[package]] +name = "lapaloma-export" +version = "0.1.0" +dependencies = [ + "lapaloma-core", + "lapaloma-render", +] + +[[package]] +name = "lapaloma-financial" +version = "0.1.0" +dependencies = [ + "gpui", + "lapaloma-cartesian", + "lapaloma-core", + "lapaloma-render", +] + +[[package]] +name = "lapaloma-flow" +version = "0.1.0" +dependencies = [ + "gpui", + "lapaloma-core", + "lapaloma-render", +] + +[[package]] +name = "lapaloma-heatmap" +version = "0.1.0" +dependencies = [ + "gpui", + "lapaloma-core", + "lapaloma-render", +] + +[[package]] +name = "lapaloma-mesh" +version = "0.1.0" +dependencies = [ + "gpui", + "lapaloma-core", + "lapaloma-render", +] + +[[package]] +name = "lapaloma-phosphor" +version = "0.1.0" +dependencies = [ + "gpui", + "lapaloma-core", + "lapaloma-render", + "lapaloma-stream", +] + +[[package]] +name = "lapaloma-polar" +version = "0.1.0" +dependencies = [ + "gpui", + "lapaloma-core", + "lapaloma-render", +] + +[[package]] +name = "lapaloma-render" +version = "0.1.0" +dependencies = [ + "lapaloma-core", +] + +[[package]] +name = "lapaloma-stream" +version = "0.1.0" +dependencies = [ + "gpui", + "lapaloma-core", + "lapaloma-render", +] + +[[package]] +name = "lapaloma-treemap" +version = "0.1.0" +dependencies = [ + "gpui", + "lapaloma-core", + "lapaloma-render", +] + [[package]] name = "lazy_static" version = "1.5.0" diff --git a/Cargo.toml b/Cargo.toml index b85535f..0e444ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,21 @@ members = [ "crates/modules/ui_engine/widgets/app-header", "crates/modules/ui_engine/widgets/theme-switcher", + # --- lapaloma: módulo de gráficos data-viz (ver ARCHITECTURE.md fuente) --- + "crates/modules/ui_engine/libs/lapaloma-core", + "crates/modules/ui_engine/widgets/lapaloma-render", + "crates/modules/ui_engine/widgets/lapaloma-cartesian", + "crates/modules/ui_engine/widgets/lapaloma-stream", + "crates/modules/ui_engine/widgets/lapaloma-mesh", + "crates/modules/ui_engine/widgets/lapaloma-financial", + "crates/modules/ui_engine/widgets/lapaloma-polar", + "crates/modules/ui_engine/widgets/lapaloma-heatmap", + "crates/modules/ui_engine/widgets/lapaloma-treemap", + "crates/modules/ui_engine/widgets/lapaloma-flow", + "crates/modules/ui_engine/widgets/lapaloma-phosphor", + "crates/modules/ui_engine/widgets/lapaloma-export", + "crates/modules/ui_engine/widgets/lapaloma", + # ============================================================ # modules/nakui/ — ERP matemático (nakui absorbido) # ============================================================ @@ -235,6 +250,23 @@ yahweh-database-explorer = { path = "crates/apps/database_explorer" } yahweh-text-viewer = { path = "crates/apps/text_viewer" } yahweh-image-viewer = { path = "crates/apps/image_viewer" } +# ============================================================ +# Intra-workspace deps de lapaloma (módulo de gráficos) +# ============================================================ +lapaloma-core = { path = "crates/modules/ui_engine/libs/lapaloma-core" } +lapaloma-render = { path = "crates/modules/ui_engine/widgets/lapaloma-render" } +lapaloma-cartesian = { path = "crates/modules/ui_engine/widgets/lapaloma-cartesian" } +lapaloma-stream = { path = "crates/modules/ui_engine/widgets/lapaloma-stream" } +lapaloma-mesh = { path = "crates/modules/ui_engine/widgets/lapaloma-mesh" } +lapaloma-financial = { path = "crates/modules/ui_engine/widgets/lapaloma-financial" } +lapaloma-polar = { path = "crates/modules/ui_engine/widgets/lapaloma-polar" } +lapaloma-heatmap = { path = "crates/modules/ui_engine/widgets/lapaloma-heatmap" } +lapaloma-treemap = { path = "crates/modules/ui_engine/widgets/lapaloma-treemap" } +lapaloma-flow = { path = "crates/modules/ui_engine/widgets/lapaloma-flow" } +lapaloma-phosphor = { path = "crates/modules/ui_engine/widgets/lapaloma-phosphor" } +lapaloma-export = { path = "crates/modules/ui_engine/widgets/lapaloma-export" } +lapaloma = { path = "crates/modules/ui_engine/widgets/lapaloma" } + [profile.release] lto = "thin" codegen-units = 1 diff --git a/crates/modules/ui_engine/libs/lapaloma-core/Cargo.toml b/crates/modules/ui_engine/libs/lapaloma-core/Cargo.toml new file mode 100644 index 0000000..2496550 --- /dev/null +++ b/crates/modules/ui_engine/libs/lapaloma-core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "lapaloma-core" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +authors = { workspace = true } +publish = { workspace = true } +description = "Lapaloma — primitivas agnósticas: DataBuffer interleaved, RingBuffer streaming, SpatialIndex, LTTB, escalas. Cero gpui, cero alloc en hot path." + +[dependencies] + +[dev-dependencies] diff --git a/crates/modules/ui_engine/libs/lapaloma-core/src/buffer.rs b/crates/modules/ui_engine/libs/lapaloma-core/src/buffer.rs new file mode 100644 index 0000000..a80469a --- /dev/null +++ b/crates/modules/ui_engine/libs/lapaloma-core/src/buffer.rs @@ -0,0 +1,128 @@ +//! `DataBuffer` — buffer interleaved `[x0, y0, x1, y1, ...]` con +//! revision counter para invalidación de cachés. +//! +//! Es la primitiva universal de Lapaloma: todo serie cartesiana, +//! todo grafo de nodos, todo OHLC vive en uno de estos (o en una +//! variante con stride distinto). El layout `f32` x `f32` es lo +//! que el GPU consume sin transformación. + +/// Buffer de coordenadas planas `[x, y]` empacadas. +/// +/// La longitud lógica (número de puntos) es `coords.len() / 2`. +/// Mutar in-place (`set_xy`, `push`) bumpea `revision` — los +/// painters comparan su `last_seen_revision` para decidir si +/// rebuilear su caché. +#[derive(Debug, Clone, Default)] +pub struct DataBuffer { + coords: Vec, + revision: u64, +} + +impl DataBuffer { + pub fn new() -> Self { + Self::default() + } + + /// Reserva espacio para `n` puntos sin agregarlos. Usalo al + /// montar el widget para que `push` no realloque después. + pub fn with_capacity(n: usize) -> Self { + Self { + coords: Vec::with_capacity(n * 2), + revision: 0, + } + } + + /// Construye a partir de coords interleaved ya armadas. + /// Útil en tests y carga inicial. + pub fn from_interleaved(coords: Vec) -> Self { + assert!(coords.len() % 2 == 0, "interleaved coords deben ser pares"); + Self { + coords, + revision: 0, + } + } + + pub fn push(&mut self, x: f32, y: f32) { + self.coords.push(x); + self.coords.push(y); + self.revision = self.revision.wrapping_add(1); + } + + /// Sobrescribe un punto existente. `i` es el índice de punto + /// (no de float), 0-based. + pub fn set_xy(&mut self, i: usize, x: f32, y: f32) { + self.coords[i * 2] = x; + self.coords[i * 2 + 1] = y; + self.revision = self.revision.wrapping_add(1); + } + + /// Pisa el contenido completo con la nueva slice. + /// Útil para hidratar el buffer en un solo memcpy. + pub fn replace_from(&mut self, src: &[f32]) { + assert!(src.len() % 2 == 0); + self.coords.clear(); + self.coords.extend_from_slice(src); + self.revision = self.revision.wrapping_add(1); + } + + pub fn clear(&mut self) { + self.coords.clear(); + self.revision = self.revision.wrapping_add(1); + } + + pub fn len(&self) -> usize { + self.coords.len() / 2 + } + + pub fn is_empty(&self) -> bool { + self.coords.is_empty() + } + + pub fn xy(&self, i: usize) -> (f32, f32) { + (self.coords[i * 2], self.coords[i * 2 + 1]) + } + + /// Slice plana lista para `drawRawPoints` / `wgpu::Buffer` + /// / ``. No realiza copia. + pub fn coords(&self) -> &[f32] { + &self.coords + } + + pub fn revision(&self) -> u64 { + self.revision + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn push_y_len() { + let mut b = DataBuffer::with_capacity(4); + b.push(0.0, 1.0); + b.push(1.0, 2.0); + assert_eq!(b.len(), 2); + assert_eq!(b.xy(1), (1.0, 2.0)); + } + + #[test] + fn revision_bumps() { + let mut b = DataBuffer::new(); + let r0 = b.revision(); + b.push(0.0, 0.0); + let r1 = b.revision(); + b.set_xy(0, 1.0, 1.0); + let r2 = b.revision(); + assert_ne!(r0, r1); + assert_ne!(r1, r2); + } + + #[test] + fn coords_slice_is_zero_copy() { + let raw = vec![0.0, 0.0, 1.0, 1.0, 2.0, 2.0]; + let b = DataBuffer::from_interleaved(raw); + assert_eq!(b.coords(), &[0.0, 0.0, 1.0, 1.0, 2.0, 2.0]); + assert_eq!(b.len(), 3); + } +} diff --git a/crates/modules/ui_engine/libs/lapaloma-core/src/lib.rs b/crates/modules/ui_engine/libs/lapaloma-core/src/lib.rs new file mode 100644 index 0000000..2607e7a --- /dev/null +++ b/crates/modules/ui_engine/libs/lapaloma-core/src/lib.rs @@ -0,0 +1,55 @@ +//! `lapaloma-core` — primitivas agnósticas de Lapaloma. +//! +//! Cero `gpui`, cero `wgpu`, cero I/O. Todo lo que vive acá puede +//! correr en un test unitario, en un worker thread o en un export +//! a SVG. Las tres reglas del documento de arquitectura aplican: +//! +//! - **P1 Zero boxing.** Los datos viven en `Vec` planos +//! indexados, nunca como `Vec`. Cache L1 caliente y el +//! compilador puede SIMD-loopearlo. +//! - **P2 Zero alloc en hot path.** Buffers se reservan al construir, +//! se mutan in-place para siempre. Helpers escriben a `&mut Vec` +//! provistos por el caller, no devuelven `Vec` nuevos. +//! - **P3 Una draw call por capa.** Acá no se dibuja; pero los +//! tipos exponen slices contiguos listos para mandar al GPU +//! sin copia. +//! +//! Convención de coordenadas: el buffer canónico es interleaved +//! `[x0, y0, x1, y1, ...]`. Esto es el formato que `drawRawPoints`, +//! `Vertices.raw`, `wgpu` vertex buffers y `` SVG +//! consumen sin transformación. + +#![forbid(unsafe_code)] + +pub mod buffer; +pub mod ring; +pub mod spatial; +pub mod lttb; +pub mod scale; + +// Algoritmos de layout — quedan como placeholders hasta que cada +// módulo de visualización (mesh, treemap, flow) los demande. + +/// Barnes-Hut quadtree para layouts force-directed. +/// +/// Cuando se implemente: el quadtree es un `Vec` plano de +/// stride 7 (cm_x, cm_y, mass, half_size, center_x, center_y, +/// child_base), no un árbol de objetos. Rebuild O(n) por frame +/// sin allocations. +pub mod barnes_hut {} + +/// Sugiyama-lite jerárquico: cycle-removal por DFS + Kahn layering +/// + barycenter ordering con inversion-count crossings. +pub mod sugiyama {} + +/// Squarified treemap (Bruls / d3-hierarchy). Worst-aspect formula +/// usa el lado *corto* del rectángulo restante. +pub mod squarify {} + +/// Subtree-width tree layout: BFS spanning + bottom-up width +/// measurement + top-down placement. Simpler que Reingold-Tilford. +pub mod tree_layout {} + +/// Force-Directed Edge Bundling (FDEB-lite, single quadratic-bezier +/// control point por edge). +pub mod fdeb {} diff --git a/crates/modules/ui_engine/libs/lapaloma-core/src/lttb.rs b/crates/modules/ui_engine/libs/lapaloma-core/src/lttb.rs new file mode 100644 index 0000000..31466c4 --- /dev/null +++ b/crates/modules/ui_engine/libs/lapaloma-core/src/lttb.rs @@ -0,0 +1,177 @@ +//! LTTB (Largest-Triangle-Three-Buckets) — downsampling preservador +//! de silueta para series cartesianas. +//! +//! Algoritmo: dividir `n` puntos en `k-2` buckets (los extremos se +//! mantienen siempre). Por cada bucket, elegir el punto que forma +//! el triángulo de área máxima con el último punto elegido y el +//! centroide del bucket siguiente. Costo total O(n). Output ≤ k. +//! +//! Knob práctico: `target ≈ width_px × 3`. Tres vértices por pixel, +//! el anti-aliasing rellena el resto. + +/// Reduce `coords` (interleaved `[x,y,x,y,…]`) a a lo sumo `target` +/// puntos, escribiendo los **índices originales** seleccionados en +/// `out` (sin clearearlo: el caller decide). +/// +/// Si `n <= target` o `target < 3`, devuelve todos los índices +/// `[0..n)`. +pub fn lttb_indices(coords: &[f32], target: usize, out: &mut Vec) { + let n = coords.len() / 2; + if n == 0 { + return; + } + if n <= target || target < 3 { + out.extend(0..n); + return; + } + lttb_in_range_indices(coords, 0, n, target, out); +} + +/// Variante que opera sobre el rango `[start, end)` de un buffer +/// más grande. Los índices devueltos son **absolutos** (relativos +/// al `coords` original), no al sub-rango — esto le ahorra al caller +/// la corrección de offset después de un `SpatialIndex::range`. +pub fn lttb_in_range_indices( + coords: &[f32], + start: usize, + end: usize, + target: usize, + out: &mut Vec, +) { + debug_assert!(coords.len() % 2 == 0); + debug_assert!(start <= end && end <= coords.len() / 2); + + let len = end - start; + if len == 0 { + return; + } + if len <= target || target < 3 { + out.extend(start..end); + return; + } + + // Primero el extremo izquierdo. + out.push(start); + + let bucket_size = (len - 2) as f64 / (target - 2) as f64; + let mut a = start; // último punto elegido + + for i in 0..target - 2 { + // Bucket actual y siguiente, en índices absolutos. + let cur_lo = start + 1 + (i as f64 * bucket_size).floor() as usize; + let cur_hi = start + 1 + ((i + 1) as f64 * bucket_size).floor() as usize; + let next_lo = cur_hi.min(end); + let next_hi = (start + 1 + ((i + 2) as f64 * bucket_size).floor() as usize).min(end); + + // Centroide del bucket siguiente. Si está vacío, fallback + // al último punto. + let (avg_x, avg_y) = if next_hi > next_lo { + let span = (next_hi - next_lo) as f32; + let mut sx = 0.0f32; + let mut sy = 0.0f32; + for j in next_lo..next_hi { + sx += coords[j * 2]; + sy += coords[j * 2 + 1]; + } + (sx / span, sy / span) + } else { + (coords[(end - 1) * 2], coords[(end - 1) * 2 + 1]) + }; + + let ax = coords[a * 2]; + let ay = coords[a * 2 + 1]; + + let mut max_area = -1.0f32; + let mut max_idx = cur_lo; + for j in cur_lo..cur_hi.min(end) { + let bx = coords[j * 2]; + let by = coords[j * 2 + 1]; + // Área del triángulo (sin /2 porque comparamos relativos). + let area = ((ax - avg_x) * (by - ay) - (ax - bx) * (avg_y - ay)).abs(); + if area > max_area { + max_area = area; + max_idx = j; + } + } + out.push(max_idx); + a = max_idx; + } + + // Extremo derecho. + out.push(end - 1); +} + +/// Variante que materializa coords decimadas directamente — útil +/// cuando el painter sólo quiere un slice listo para `drawRawPoints` +/// y no necesita los índices. +pub fn lttb_coords(coords: &[f32], target: usize, out: &mut Vec) { + let mut idx_buf: Vec = Vec::with_capacity(target); + lttb_indices(coords, target, &mut idx_buf); + for i in idx_buf { + out.push(coords[i * 2]); + out.push(coords[i * 2 + 1]); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn no_decimate_si_n_menor_que_target() { + let coords: Vec = (0..5).flat_map(|i| [i as f32, (i * i) as f32]).collect(); + let mut out = Vec::new(); + lttb_indices(&coords, 10, &mut out); + assert_eq!(out, vec![0, 1, 2, 3, 4]); + } + + #[test] + fn extremos_preservados() { + let n = 100; + let coords: Vec = (0..n).flat_map(|i| [i as f32, (i as f32).sin()]).collect(); + let mut out = Vec::new(); + lttb_indices(&coords, 10, &mut out); + assert_eq!(out.first(), Some(&0)); + assert_eq!(out.last(), Some(&(n - 1))); + assert!(out.len() <= 10); + } + + #[test] + fn indices_sorted_y_unicos() { + let coords: Vec = (0..1000) + .flat_map(|i| [i as f32, (i as f32 * 0.01).sin()]) + .collect(); + let mut out = Vec::new(); + lttb_indices(&coords, 50, &mut out); + for w in out.windows(2) { + assert!(w[0] < w[1], "indices deben ser estrictamente crecientes"); + } + } + + #[test] + fn in_range_indices_son_absolutos() { + let n = 100; + let coords: Vec = (0..n).flat_map(|i| [i as f32, i as f32]).collect(); + let mut out = Vec::new(); + lttb_in_range_indices(&coords, 20, 80, 10, &mut out); + assert_eq!(out.first(), Some(&20)); + assert_eq!(out.last(), Some(&79)); + // ningún índice fuera del rango pedido + for &i in &out { + assert!(i >= 20 && i < 80); + } + } + + #[test] + fn preserva_picos_extremos() { + // Señal plana con un pico al medio: LTTB debe agarrar el pico. + let mut coords: Vec = Vec::new(); + for i in 0..200 { + coords.push(i as f32); + coords.push(if i == 100 { 10.0 } else { 0.0 }); + } + let mut out = Vec::new(); + lttb_indices(&coords, 20, &mut out); + assert!(out.contains(&100), "pico debe sobrevivir el downsample"); + } +} diff --git a/crates/modules/ui_engine/libs/lapaloma-core/src/ring.rs b/crates/modules/ui_engine/libs/lapaloma-core/src/ring.rs new file mode 100644 index 0000000..2f5fa55 --- /dev/null +++ b/crates/modules/ui_engine/libs/lapaloma-core/src/ring.rs @@ -0,0 +1,216 @@ +//! `RingBuffer` — buffer circular de samples para streaming tipo +//! osciloscopio. +//! +//! Capacidad fija. `push(v)` hace dos writes (uno a `values`, uno +//! a `coords[head*2+1]`) y un increment de head + revision. El +//! buffer **nunca se reasigna**; el painter consume slices del +//! mismo backing memory frame tras frame. +//! +//! Convención: `x_norm` se pre-computa una vez en construcción +//! (modo sweep). El painter aplica el escalado a píxeles via su +//! propio transform — el buffer no rota X entre frames. +//! +//! ## Trampa del pre-fill (1.0.2 fix del Flutter) +//! +//! Antes que `count >= capacity`, los slots `[head, capacity)` +//! contienen ceros iniciales. Si el painter dibuja toda la +//! ringa, aparece una línea plana sobre la mitad derecha. La +//! API expone [`RingBuffer::filled_len`] que devuelve `head` en +//! ese caso, y `capacity` después — el painter clipea a eso. + +/// Ring buffer en modo sweep (x_norm de cada slot es fijo). +/// +/// Para modo scroll el painter aplica un translate adicional por +/// frame; la estructura de datos es la misma. +#[derive(Debug, Clone)] +pub struct RingBuffer { + /// Sample raw por slot. + values: Vec, + /// `[x_norm, y_value]` por slot. `x_norm = slot / (cap - 1)`, + /// fijo. `y_value` = `values[slot]`. + coords: Vec, + capacity: usize, + /// Próximo slot a escribir. + head: usize, + /// Monotonic, sobrevive wraparound. Útil para anclar + /// anotaciones por sample index absoluto. + count: u64, + revision: u64, +} + +impl RingBuffer { + /// Asume `capacity >= 2` para que `x_norm` no divida por cero. + pub fn new(capacity: usize) -> Self { + assert!(capacity >= 2, "RingBuffer requiere capacity >= 2"); + let mut coords = vec![0.0; capacity * 2]; + let denom = (capacity - 1) as f32; + for slot in 0..capacity { + coords[slot * 2] = slot as f32 / denom; + } + Self { + values: vec![0.0; capacity], + coords, + capacity, + head: 0, + count: 0, + revision: 0, + } + } + + pub fn push(&mut self, v: f32) { + self.values[self.head] = v; + self.coords[self.head * 2 + 1] = v; + self.head = (self.head + 1) % self.capacity; + self.count = self.count.wrapping_add(1); + self.revision = self.revision.wrapping_add(1); + } + + /// Inserción en batch con dos memcpys (cola + wrap-around). + /// Para batches > capacity se queda con los últimos `capacity` + /// samples (los anteriores se sobreescribirían igual). + pub fn push_all(&mut self, batch: &[f32]) { + if batch.is_empty() { + return; + } + + let cap = self.capacity; + let src = if batch.len() > cap { + &batch[batch.len() - cap..] + } else { + batch + }; + + let tail = cap - self.head; + if src.len() <= tail { + self.values[self.head..self.head + src.len()].copy_from_slice(src); + for (i, v) in src.iter().enumerate() { + self.coords[(self.head + i) * 2 + 1] = *v; + } + self.head = (self.head + src.len()) % cap; + } else { + let (a, b) = src.split_at(tail); + self.values[self.head..].copy_from_slice(a); + for (i, v) in a.iter().enumerate() { + self.coords[(self.head + i) * 2 + 1] = *v; + } + self.values[..b.len()].copy_from_slice(b); + for (i, v) in b.iter().enumerate() { + self.coords[i * 2 + 1] = *v; + } + self.head = b.len(); + } + + self.count = self.count.wrapping_add(src.len() as u64); + self.revision = self.revision.wrapping_add(1); + } + + pub fn capacity(&self) -> usize { + self.capacity + } + + pub fn head(&self) -> usize { + self.head + } + + pub fn count(&self) -> u64 { + self.count + } + + pub fn revision(&self) -> u64 { + self.revision + } + + pub fn is_full(&self) -> bool { + self.count >= self.capacity as u64 + } + + /// Cantidad de slots con datos reales. Antes del fill es + /// `head`; después es `capacity`. El painter clipea a este + /// valor para evitar el flicker del pre-fill. + pub fn filled_len(&self) -> usize { + if self.is_full() { + self.capacity + } else { + self.head + } + } + + /// Slice interleaved de `[x_norm, y]`. Para render en dos + /// segmentos: `&coords()[..head*2]` y `&coords()[head*2..]` + /// (cuando is_full). + pub fn coords(&self) -> &[f32] { + &self.coords + } + + /// Slice plana de samples raw — útil para downsample envelope + /// min/max sin pasar por coords. + pub fn values(&self) -> &[f32] { + &self.values + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn x_norm_precomputado() { + let r = RingBuffer::new(4); + // x_norm en slots 0, 1, 2, 3 = 0.0, 1/3, 2/3, 1.0 + assert!((r.coords()[0] - 0.0).abs() < 1e-6); + assert!((r.coords()[2] - 1.0 / 3.0).abs() < 1e-6); + assert!((r.coords()[6] - 1.0).abs() < 1e-6); + } + + #[test] + fn push_actualiza_y_no_x() { + let mut r = RingBuffer::new(4); + r.push(5.0); + r.push(7.0); + // slot 0 → y=5, slot 1 → y=7, x quedó igual + assert_eq!(r.coords()[1], 5.0); + assert_eq!(r.coords()[3], 7.0); + assert!((r.coords()[2] - 1.0 / 3.0).abs() < 1e-6); + assert_eq!(r.head(), 2); + assert_eq!(r.count(), 2); + } + + #[test] + fn filled_len_bloquea_prefill() { + let mut r = RingBuffer::new(4); + assert_eq!(r.filled_len(), 0); + r.push(1.0); + r.push(2.0); + assert_eq!(r.filled_len(), 2); + r.push(3.0); + r.push(4.0); + assert_eq!(r.filled_len(), 4); + r.push(5.0); // wrap + assert_eq!(r.filled_len(), 4); + assert!(r.is_full()); + } + + #[test] + fn push_all_wrap_around() { + let mut r = RingBuffer::new(4); + r.push_all(&[1.0, 2.0, 3.0]); // head=3 + r.push_all(&[4.0, 5.0, 6.0]); // wrap: 4 en slot 3, 5 en slot 0, 6 en slot 1 + assert_eq!(r.values()[3], 4.0); + assert_eq!(r.values()[0], 5.0); + assert_eq!(r.values()[1], 6.0); + assert_eq!(r.head(), 2); + assert_eq!(r.count(), 6); + } + + #[test] + fn push_all_oversized_se_queda_con_la_cola() { + let mut r = RingBuffer::new(4); + r.push_all(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]); + // Sólo los últimos 4 importan: [6,7,8,9] + assert_eq!(r.values()[0], 6.0); + assert_eq!(r.values()[1], 7.0); + assert_eq!(r.values()[2], 8.0); + assert_eq!(r.values()[3], 9.0); + assert!(r.is_full()); + } +} diff --git a/crates/modules/ui_engine/libs/lapaloma-core/src/scale.rs b/crates/modules/ui_engine/libs/lapaloma-core/src/scale.rs new file mode 100644 index 0000000..da18d4f --- /dev/null +++ b/crates/modules/ui_engine/libs/lapaloma-core/src/scale.rs @@ -0,0 +1,153 @@ +//! Escalas value→pixel para series cartesianas. +//! +//! La proyección no se aplica sobre los datos (eso rompería el +//! P2 zero-alloc — habría que reescribir todo el buffer por frame). +//! Las escalas devuelven el `(scale_x, scale_y, translate_x, +//! translate_y)` que el painter mete en un transform GPU. Los +//! datos quedan intactos. + +/// Trait común a Linear / Log / Time. Cada implementación traduce +/// un valor de dominio a posición normalizada `[0, 1]` (que luego +/// el painter mapea al pixel range del plot). +pub trait Scale { + fn to_norm(&self, value: f64) -> f64; + fn from_norm(&self, norm: f64) -> f64; + fn domain(&self) -> (f64, f64); +} + +#[derive(Debug, Clone, Copy)] +pub struct LinearScale { + min: f64, + max: f64, +} + +impl LinearScale { + pub fn new(min: f64, max: f64) -> Self { + debug_assert!(max > min, "LinearScale: max debe ser > min"); + Self { min, max } + } +} + +impl Scale for LinearScale { + fn to_norm(&self, v: f64) -> f64 { + (v - self.min) / (self.max - self.min) + } + fn from_norm(&self, n: f64) -> f64 { + self.min + n * (self.max - self.min) + } + fn domain(&self) -> (f64, f64) { + (self.min, self.max) + } +} + +/// Escala logarítmica base e. `min` y `max` deben ser positivos. +#[derive(Debug, Clone, Copy)] +pub struct LogScale { + log_min: f64, + log_max: f64, + min: f64, + max: f64, +} + +impl LogScale { + pub fn new(min: f64, max: f64) -> Self { + debug_assert!(min > 0.0 && max > min, "LogScale: 0 < min < max"); + Self { + log_min: min.ln(), + log_max: max.ln(), + min, + max, + } + } +} + +impl Scale for LogScale { + fn to_norm(&self, v: f64) -> f64 { + (v.ln() - self.log_min) / (self.log_max - self.log_min) + } + fn from_norm(&self, n: f64) -> f64 { + (self.log_min + n * (self.log_max - self.log_min)).exp() + } + fn domain(&self) -> (f64, f64) { + (self.min, self.max) + } +} + +/// Escala temporal sobre epoch ms. Internamente lineal. +#[derive(Debug, Clone, Copy)] +pub struct TimeScale { + inner: LinearScale, +} + +impl TimeScale { + pub fn new(min_epoch_ms: f64, max_epoch_ms: f64) -> Self { + Self { + inner: LinearScale::new(min_epoch_ms, max_epoch_ms), + } + } +} + +impl Scale for TimeScale { + fn to_norm(&self, v: f64) -> f64 { + self.inner.to_norm(v) + } + fn from_norm(&self, n: f64) -> f64 { + self.inner.from_norm(n) + } + fn domain(&self) -> (f64, f64) { + self.inner.domain() + } +} + +/// Wilkinson "nice numbers" — devuelve el step ideal en `{1, 2, 5} × 10^k` +/// para que un rango `[min, max]` tenga ~`target_ticks` divisiones. +pub fn nice_step(min: f64, max: f64, target_ticks: usize) -> f64 { + debug_assert!(max > min && target_ticks > 0); + let raw = (max - min) / target_ticks as f64; + let mag = 10f64.powf(raw.log10().floor()); + let norm = raw / mag; + let nice = if norm < 1.5 { + 1.0 + } else if norm < 3.0 { + 2.0 + } else if norm < 7.0 { + 5.0 + } else { + 10.0 + }; + nice * mag +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn linear_roundtrip() { + let s = LinearScale::new(10.0, 20.0); + assert!((s.to_norm(15.0) - 0.5).abs() < 1e-9); + assert!((s.from_norm(0.5) - 15.0).abs() < 1e-9); + } + + #[test] + fn log_roundtrip() { + let s = LogScale::new(1.0, 1000.0); + // 10 está a 1/3 del camino en log10. ln(10)/ln(1000) = 1/3. + assert!((s.to_norm(10.0) - 1.0 / 3.0).abs() < 1e-9); + assert!((s.from_norm(2.0 / 3.0) - 100.0).abs() < 1e-9); + } + + #[test] + fn nice_step_es_potencia() { + // 100/5 = 20 — exact match para el branch nice=2.0 · mag=10. + assert!((nice_step(0.0, 100.0, 5) - 20.0).abs() < 1e-9); + // 1.0/10 = 0.1 — branch nice=1.0 · mag=0.1. + assert!((nice_step(0.0, 1.0, 10) - 0.1).abs() < 1e-9); + // 14/5 = 2.8 — branch nice=2.0 (1.5 ≤ norm < 3) · mag=1. + assert!((nice_step(0.0, 14.0, 5) - 2.0).abs() < 1e-9); + // 7/5 = 1.4 — cae bajo 1.5 → snap a 1.0 · mag=1 = 1.0. + assert!((nice_step(0.0, 7.0, 5) - 1.0).abs() < 1e-9); + // 50/5 = 10 — branch nice=10 · mag=1 = 10. (Equivalente a 1·10.) + assert!((nice_step(0.0, 50.0, 5) - 10.0).abs() < 1e-9); + } +} diff --git a/crates/modules/ui_engine/libs/lapaloma-core/src/spatial.rs b/crates/modules/ui_engine/libs/lapaloma-core/src/spatial.rs new file mode 100644 index 0000000..4b1daa3 --- /dev/null +++ b/crates/modules/ui_engine/libs/lapaloma-core/src/spatial.rs @@ -0,0 +1,149 @@ +//! `SpatialIndex` — hit-testing sobre coords interleaved sorted-by-X. +//! +//! Cuando los puntos vienen ordenados por X (caso típico de series +//! temporales) un binary search basta y es O(log n) sin estructuras +//! auxiliares. Para nodos que se mueven cada frame (mesh graph) +//! corresponde un spatial hash uniforme — ese va en `lapaloma-mesh`, +//! no acá. + +/// View sobre un buffer interleaved `[x0,y0,x1,y1,…]` sorted-asc por X. +/// +/// El binary search asume invariante de ordenamiento. Si tu pipeline +/// puede generar coords desordenadas, sortealas antes de construir +/// el índice (no hay debug-assert porque sería O(n) en hot path). +#[derive(Debug, Clone, Copy)] +pub struct SpatialIndex<'a> { + coords: &'a [f32], +} + +impl<'a> SpatialIndex<'a> { + pub fn new(coords: &'a [f32]) -> Self { + debug_assert!(coords.len() % 2 == 0); + Self { coords } + } + + pub fn len(&self) -> usize { + self.coords.len() / 2 + } + + pub fn is_empty(&self) -> bool { + self.coords.is_empty() + } + + /// Índice del punto cuya X está más cerca de `target_x`. + /// `None` si el buffer está vacío. + pub fn nearest(&self, target_x: f32) -> Option { + let n = self.len(); + if n == 0 { + return None; + } + // Binary search sobre la columna X. + let mut lo = 0usize; + let mut hi = n; + while lo < hi { + let mid = (lo + hi) / 2; + if self.coords[mid * 2] < target_x { + lo = mid + 1; + } else { + hi = mid; + } + } + // `lo` es la primera X >= target_x. El más cercano es lo o lo-1. + if lo == 0 { + Some(0) + } else if lo >= n { + Some(n - 1) + } else { + let prev = lo - 1; + let dx_prev = target_x - self.coords[prev * 2]; + let dx_next = self.coords[lo * 2] - target_x; + if dx_prev <= dx_next { + Some(prev) + } else { + Some(lo) + } + } + } + + /// Rango `[start, end)` de puntos con X en `[x_min, x_max]`. + /// Útil para clip-to-viewport antes de LTTB. + pub fn range(&self, x_min: f32, x_max: f32) -> (usize, usize) { + let n = self.len(); + if n == 0 { + return (0, 0); + } + // lower bound: primer i con coords[i*2] >= x_min + let start = { + let mut lo = 0usize; + let mut hi = n; + while lo < hi { + let mid = (lo + hi) / 2; + if self.coords[mid * 2] < x_min { + lo = mid + 1; + } else { + hi = mid; + } + } + lo + }; + // upper bound: primer i con coords[i*2] > x_max + let end = { + let mut lo = start; + let mut hi = n; + while lo < hi { + let mid = (lo + hi) / 2; + if self.coords[mid * 2] <= x_max { + lo = mid + 1; + } else { + hi = mid; + } + } + lo + }; + (start, end) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn fixture() -> Vec { + // x: 0, 1, 3, 5, 8 — y irrelevante. + vec![0.0, 0.0, 1.0, 0.0, 3.0, 0.0, 5.0, 0.0, 8.0, 0.0] + } + + #[test] + fn nearest_dentro() { + let c = fixture(); + let s = SpatialIndex::new(&c); + assert_eq!(s.nearest(0.0), Some(0)); + assert_eq!(s.nearest(2.0), Some(1)); // 1 está más cerca que 3 + assert_eq!(s.nearest(2.5), Some(2)); // 3 está más cerca que 1 + assert_eq!(s.nearest(8.0), Some(4)); + } + + #[test] + fn nearest_fuera_clamp() { + let c = fixture(); + let s = SpatialIndex::new(&c); + assert_eq!(s.nearest(-10.0), Some(0)); + assert_eq!(s.nearest(99.0), Some(4)); + } + + #[test] + fn nearest_empty() { + let empty: [f32; 0] = []; + assert_eq!(SpatialIndex::new(&empty).nearest(0.0), None); + } + + #[test] + fn range_clip() { + let c = fixture(); + let s = SpatialIndex::new(&c); + assert_eq!(s.range(1.0, 5.0), (1, 4)); // incluye x=1,3,5 + assert_eq!(s.range(2.0, 4.0), (2, 3)); // sólo x=3 + assert_eq!(s.range(-1.0, 100.0), (0, 5)); + assert_eq!(s.range(10.0, 20.0), (5, 5)); + } +} diff --git a/crates/modules/ui_engine/widgets/lapaloma-cartesian/Cargo.toml b/crates/modules/ui_engine/widgets/lapaloma-cartesian/Cargo.toml new file mode 100644 index 0000000..09892e9 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-cartesian/Cargo.toml @@ -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 } diff --git a/crates/modules/ui_engine/widgets/lapaloma-cartesian/src/lib.rs b/crates/modules/ui_engine/widgets/lapaloma-cartesian/src/lib.rs new file mode 100644 index 0000000..0f2baa2 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-cartesian/src/lib.rs @@ -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 {} diff --git a/crates/modules/ui_engine/widgets/lapaloma-export/Cargo.toml b/crates/modules/ui_engine/widgets/lapaloma-export/Cargo.toml new file mode 100644 index 0000000..ffd4571 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-export/Cargo.toml @@ -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" } diff --git a/crates/modules/ui_engine/widgets/lapaloma-export/src/lib.rs b/crates/modules/ui_engine/widgets/lapaloma-export/src/lib.rs new file mode 100644 index 0000000..b55dd0d --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-export/src/lib.rs @@ -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 {} diff --git a/crates/modules/ui_engine/widgets/lapaloma-financial/Cargo.toml b/crates/modules/ui_engine/widgets/lapaloma-financial/Cargo.toml new file mode 100644 index 0000000..72029e0 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-financial/Cargo.toml @@ -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 } diff --git a/crates/modules/ui_engine/widgets/lapaloma-financial/src/lib.rs b/crates/modules/ui_engine/widgets/lapaloma-financial/src/lib.rs new file mode 100644 index 0000000..2c1d7d1 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-financial/src/lib.rs @@ -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 {} diff --git a/crates/modules/ui_engine/widgets/lapaloma-flow/Cargo.toml b/crates/modules/ui_engine/widgets/lapaloma-flow/Cargo.toml new file mode 100644 index 0000000..4dd8da4 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-flow/Cargo.toml @@ -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 } diff --git a/crates/modules/ui_engine/widgets/lapaloma-flow/src/lib.rs b/crates/modules/ui_engine/widgets/lapaloma-flow/src/lib.rs new file mode 100644 index 0000000..9e5196e --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-flow/src/lib.rs @@ -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 {} diff --git a/crates/modules/ui_engine/widgets/lapaloma-heatmap/Cargo.toml b/crates/modules/ui_engine/widgets/lapaloma-heatmap/Cargo.toml new file mode 100644 index 0000000..29bc7c9 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-heatmap/Cargo.toml @@ -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 } diff --git a/crates/modules/ui_engine/widgets/lapaloma-heatmap/src/lib.rs b/crates/modules/ui_engine/widgets/lapaloma-heatmap/src/lib.rs new file mode 100644 index 0000000..07d7611 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-heatmap/src/lib.rs @@ -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, 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 {} diff --git a/crates/modules/ui_engine/widgets/lapaloma-mesh/Cargo.toml b/crates/modules/ui_engine/widgets/lapaloma-mesh/Cargo.toml new file mode 100644 index 0000000..80e1644 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-mesh/Cargo.toml @@ -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 } diff --git a/crates/modules/ui_engine/widgets/lapaloma-mesh/src/lib.rs b/crates/modules/ui_engine/widgets/lapaloma-mesh/src/lib.rs new file mode 100644 index 0000000..461d993 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-mesh/src/lib.rs @@ -0,0 +1,28 @@ +//! `lapaloma-mesh` — visualización de grafos. +//! +//! Módulos: +//! - **`node_buffer`** / **`edge_buffer`** — `Vec` 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 {} diff --git a/crates/modules/ui_engine/widgets/lapaloma-phosphor/Cargo.toml b/crates/modules/ui_engine/widgets/lapaloma-phosphor/Cargo.toml new file mode 100644 index 0000000..f29e906 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-phosphor/Cargo.toml @@ -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 } diff --git a/crates/modules/ui_engine/widgets/lapaloma-phosphor/src/lib.rs b/crates/modules/ui_engine/widgets/lapaloma-phosphor/src/lib.rs new file mode 100644 index 0000000..1865107 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-phosphor/src/lib.rs @@ -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 {} diff --git a/crates/modules/ui_engine/widgets/lapaloma-polar/Cargo.toml b/crates/modules/ui_engine/widgets/lapaloma-polar/Cargo.toml new file mode 100644 index 0000000..2205af4 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-polar/Cargo.toml @@ -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 } diff --git a/crates/modules/ui_engine/widgets/lapaloma-polar/src/lib.rs b/crates/modules/ui_engine/widgets/lapaloma-polar/src/lib.rs new file mode 100644 index 0000000..7afc06e --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-polar/src/lib.rs @@ -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 {} diff --git a/crates/modules/ui_engine/widgets/lapaloma-render/Cargo.toml b/crates/modules/ui_engine/widgets/lapaloma-render/Cargo.toml new file mode 100644 index 0000000..4eee5e0 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-render/Cargo.toml @@ -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" } diff --git a/crates/modules/ui_engine/widgets/lapaloma-render/src/canvas.rs b/crates/modules/ui_engine/widgets/lapaloma-render/src/canvas.rs new file mode 100644 index 0000000..e5a494f --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-render/src/canvas.rs @@ -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); +} diff --git a/crates/modules/ui_engine/widgets/lapaloma-render/src/color.rs b/crates/modules/ui_engine/widgets/lapaloma-render/src/color.rs new file mode 100644 index 0000000..255daab --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-render/src/color.rs @@ -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 } + } +} diff --git a/crates/modules/ui_engine/widgets/lapaloma-render/src/geom.rs b/crates/modules/ui_engine/widgets/lapaloma-render/src/geom.rs new file mode 100644 index 0000000..8af2d2b --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-render/src/geom.rs @@ -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() + } +} diff --git a/crates/modules/ui_engine/widgets/lapaloma-render/src/lib.rs b/crates/modules/ui_engine/widgets/lapaloma-render/src/lib.rs new file mode 100644 index 0000000..44f05ae --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-render/src/lib.rs @@ -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 ``, ``, 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}; diff --git a/crates/modules/ui_engine/widgets/lapaloma-render/src/plan.rs b/crates/modules/ui_engine/widgets/lapaloma-render/src/plan.rs new file mode 100644 index 0000000..825fa9e --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-render/src/plan.rs @@ -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 `` / `` / 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, stroke: StrokeStyle }, + FillTriangleStrip { coords: Vec, colors: Vec }, + DrawText { p: Point, text: String, color: Color, size_px: f32 }, +} + +#[derive(Debug, Clone, Default)] +pub struct RenderPlan { + pub cmds: Vec, +} + +impl RenderPlan { + pub fn new() -> Self { + Self::default() + } + pub fn push(&mut self, cmd: RenderCmd) { + self.cmds.push(cmd); + } +} diff --git a/crates/modules/ui_engine/widgets/lapaloma-stream/Cargo.toml b/crates/modules/ui_engine/widgets/lapaloma-stream/Cargo.toml new file mode 100644 index 0000000..706e66d --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-stream/Cargo.toml @@ -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 } diff --git a/crates/modules/ui_engine/widgets/lapaloma-stream/src/lib.rs b/crates/modules/ui_engine/widgets/lapaloma-stream/src/lib.rs new file mode 100644 index 0000000..d6c3dca --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-stream/src/lib.rs @@ -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` +//! 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 {} diff --git a/crates/modules/ui_engine/widgets/lapaloma-treemap/Cargo.toml b/crates/modules/ui_engine/widgets/lapaloma-treemap/Cargo.toml new file mode 100644 index 0000000..3f2eed8 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-treemap/Cargo.toml @@ -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 } diff --git a/crates/modules/ui_engine/widgets/lapaloma-treemap/src/lib.rs b/crates/modules/ui_engine/widgets/lapaloma-treemap/src/lib.rs new file mode 100644 index 0000000..fe460b1 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma-treemap/src/lib.rs @@ -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 {} diff --git a/crates/modules/ui_engine/widgets/lapaloma/Cargo.toml b/crates/modules/ui_engine/widgets/lapaloma/Cargo.toml new file mode 100644 index 0000000..2eefcfd --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma/Cargo.toml @@ -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"] diff --git a/crates/modules/ui_engine/widgets/lapaloma/src/lib.rs b/crates/modules/ui_engine/widgets/lapaloma/src/lib.rs new file mode 100644 index 0000000..5a0a5a9 --- /dev/null +++ b/crates/modules/ui_engine/widgets/lapaloma/src/lib.rs @@ -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;