refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG
Reorganización física de crates/: - core/ (mezclaba 6 propósitos) se divide en protocol/, init/, runtime/, compat/ - shared/ (3 crates) se redistribuye en protocol/ e init/ - lapaloma (sub-módulo de ui_engine) se promueve a modules/pineal/ Renames de proyectos: - shipote → shuma (runtime de sandboxes) - nouser → akasha (explorador de Mónadas) - yahweh → nahual (motor GPUI, antes ui_engine/) - lapaloma → pineal (data-viz agnóstica) Fraccionamiento UI → core agnóstico: - vista-core (DeckState + snap, 175 LOC, 5 tests verdes) - barra-core (Task + render_html + sanitize, 90 LOC, 5 tests verdes) - vista-web y barra-web ahora son thin DOM bindings Documentación nueva: - 16 SDDs por subdirectorio (≤80 LOC c/u): protocol/init/runtime/compat + 10 módulos + apps/ - docs/STATUS.md con cifras reales por proyecto - docs/ROADMAP.md con plan a finalización (6 hitos, ~6-8 semanas) - CHANGELOG.md particionado en docs/changelog/<proyecto>.md (7 buckets) Automatización: - scripts/reorg.py — script idempotente que: git mv directorios, renombra package names, recomputa path = refs, reescribe imports rust, actualiza workspace Cargo.toml. Soporta --dry-run. - scripts/split-changelog.py — particiona CHANGELOG por componente. Validación: - cargo check --workspace pasa (124 crates + 2 nuevos cores). - 10 tests adicionales (5 en vista-core + 5 en barra-core) verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "pineal-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]
|
||||
pineal-core = { path = "../core" }
|
||||
gpui = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
gpui = ["dep:gpui"]
|
||||
@@ -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 `pineal-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,191 @@
|
||||
//! Backend CPU del trait [`crate::Canvas`] sobre `gpui::Window`.
|
||||
//!
|
||||
//! Bajo el feature `gpui`. Traduce los primitivos de Lapaloma a las
|
||||
//! llamadas nativas de GPUI 0.2 (`paint_quad`, `paint_path`). No
|
||||
//! introduce dependencia transitiva a gpui en los crates de
|
||||
//! visualización — éstos siguen hablando contra el trait abstracto;
|
||||
//! sólo el `Element` GPUI de cada widget importa este módulo.
|
||||
//!
|
||||
//! Limitaciones de la implementación CPU:
|
||||
//! - `push_clip` / `pop_clip` quedan como no-op por ahora — GPUI
|
||||
//! maneja content mask via builders de alto nivel; el chart se
|
||||
//! apoya en el bounds del Element para no pintar fuera.
|
||||
//! - `fill_triangle_strip` no implementado (lo necesitan phosphor
|
||||
//! y Sankey, que aún no están).
|
||||
//! - `draw_text` no implementado (axis labels lo necesitan; va con
|
||||
//! `WindowTextSystem` en una fase próxima).
|
||||
|
||||
use crate::{Canvas, Color, Point, Rect, StrokeStyle};
|
||||
use gpui::{
|
||||
fill, font, hsla, point as gpui_point, px, size as gpui_size, Bounds, Hsla, PathBuilder,
|
||||
SharedString, TextRun, Window,
|
||||
};
|
||||
|
||||
/// Adapter que pinta sobre un `&mut Window` de GPUI.
|
||||
///
|
||||
/// Vida útil del borrow del window iguala la de la pintura. Construir
|
||||
/// uno nuevo en cada `paint()` del Element.
|
||||
pub struct WindowCanvas<'a> {
|
||||
window: &'a mut Window,
|
||||
}
|
||||
|
||||
impl<'a> WindowCanvas<'a> {
|
||||
pub fn new(window: &'a mut Window) -> Self {
|
||||
Self { window }
|
||||
}
|
||||
}
|
||||
|
||||
/// Conversión RGB(a) → HSL(a). GPUI consume `Hsla` para casi todo
|
||||
/// el path. Linear, sin gamma — coincide con la convención del
|
||||
/// resto del codebase nahual.
|
||||
pub(crate) fn color_to_hsla(c: Color) -> Hsla {
|
||||
let (r, g, b, a) = (c.r, c.g, c.b, c.a);
|
||||
let max = r.max(g).max(b);
|
||||
let min = r.min(g).min(b);
|
||||
let l = (max + min) * 0.5;
|
||||
let delta = max - min;
|
||||
if delta.abs() < 1e-6 {
|
||||
return hsla(0.0, 0.0, l, a);
|
||||
}
|
||||
let s = if l < 0.5 { delta / (max + min) } else { delta / (2.0 - max - min) };
|
||||
let h = if max == r {
|
||||
((g - b) / delta).rem_euclid(6.0)
|
||||
} else if max == g {
|
||||
(b - r) / delta + 2.0
|
||||
} else {
|
||||
(r - g) / delta + 4.0
|
||||
};
|
||||
hsla(h / 6.0, s, l, a)
|
||||
}
|
||||
|
||||
fn to_bounds(r: Rect) -> Bounds<gpui::Pixels> {
|
||||
Bounds {
|
||||
origin: gpui_point(px(r.x), px(r.y)),
|
||||
size: gpui_size(px(r.w), px(r.h)),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Canvas for WindowCanvas<'a> {
|
||||
fn push_clip(&mut self, _rect: Rect) {
|
||||
// Sin clip explícito por ahora. El Element pinta dentro
|
||||
// de sus bounds y los painters de pineal respetan el
|
||||
// plot_rect en sus proyecciones.
|
||||
}
|
||||
fn pop_clip(&mut self) {}
|
||||
|
||||
fn fill_rect(&mut self, rect: Rect, color: Color) {
|
||||
let hsla = color_to_hsla(color);
|
||||
self.window.paint_quad(fill(to_bounds(rect), hsla));
|
||||
}
|
||||
|
||||
fn stroke_rect(&mut self, rect: Rect, stroke: StrokeStyle) {
|
||||
// 4 line segments con PathBuilder en stroke mode.
|
||||
let mut pb = PathBuilder::stroke(px(stroke.width));
|
||||
pb.move_to(gpui_point(px(rect.x), px(rect.y)));
|
||||
pb.line_to(gpui_point(px(rect.right()), px(rect.y)));
|
||||
pb.line_to(gpui_point(px(rect.right()), px(rect.bottom())));
|
||||
pb.line_to(gpui_point(px(rect.x), px(rect.bottom())));
|
||||
pb.close();
|
||||
if let Ok(path) = pb.build() {
|
||||
self.window.paint_path(path, color_to_hsla(stroke.color));
|
||||
}
|
||||
}
|
||||
|
||||
fn stroke_line(&mut self, a: Point, b: Point, stroke: StrokeStyle) {
|
||||
let mut pb = PathBuilder::stroke(px(stroke.width));
|
||||
pb.move_to(gpui_point(px(a.x), px(a.y)));
|
||||
pb.line_to(gpui_point(px(b.x), px(b.y)));
|
||||
if let Ok(path) = pb.build() {
|
||||
self.window.paint_path(path, color_to_hsla(stroke.color));
|
||||
}
|
||||
}
|
||||
|
||||
fn stroke_polyline(&mut self, coords: &[f32], stroke: StrokeStyle) {
|
||||
if coords.len() < 4 {
|
||||
return; // <2 puntos → no hay segmento
|
||||
}
|
||||
let mut pb = PathBuilder::stroke(px(stroke.width));
|
||||
pb.move_to(gpui_point(px(coords[0]), px(coords[1])));
|
||||
let mut i = 2;
|
||||
while i + 1 < coords.len() {
|
||||
pb.line_to(gpui_point(px(coords[i]), px(coords[i + 1])));
|
||||
i += 2;
|
||||
}
|
||||
if let Ok(path) = pb.build() {
|
||||
self.window.paint_path(path, color_to_hsla(stroke.color));
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_triangle_strip(&mut self, _coords: &[f32], _colors: &[Color]) {
|
||||
// TODO: cuando phosphor / Sankey lo necesiten. GPUI no
|
||||
// tiene API directa para triangle strips con per-vertex
|
||||
// color — habrá que descomponer en quads o subir un
|
||||
// vertex buffer wgpu.
|
||||
}
|
||||
|
||||
fn draw_text(&mut self, p: Point, text: &str, color: Color, size_px: f32) {
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
let hsla = color_to_hsla(color);
|
||||
let font_size = px(size_px);
|
||||
let text_str: SharedString = text.to_string().into();
|
||||
let runs = [TextRun {
|
||||
len: text.len(),
|
||||
font: font("Monospace"),
|
||||
color: hsla,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}];
|
||||
|
||||
let shaped = self
|
||||
.window
|
||||
.text_system()
|
||||
.shape_line(text_str, font_size, &runs, None);
|
||||
|
||||
// Iteramos glyphs vía `paint_glyph` para evitar la
|
||||
// dependencia con `&mut App` que pide `ShapedLine::paint`.
|
||||
// Eso encaja con el contrato actual del Canvas trait que
|
||||
// sólo expone `&mut Window`.
|
||||
let origin_x = px(p.x);
|
||||
let origin_y = px(p.y);
|
||||
for run in shaped.runs.iter() {
|
||||
for glyph in run.glyphs.iter() {
|
||||
let gx = origin_x + glyph.position.x;
|
||||
let gy = origin_y + glyph.position.y;
|
||||
let _ = self
|
||||
.window
|
||||
.paint_glyph(gpui_point(gx, gy), run.font_id, glyph.id, font_size, hsla);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rgb_a_hsla_grises() {
|
||||
// (0.5, 0.5, 0.5) → h=0, s=0, l=0.5
|
||||
let h = color_to_hsla(Color::rgb(0.5, 0.5, 0.5));
|
||||
assert!((h.s - 0.0).abs() < 1e-6);
|
||||
assert!((h.l - 0.5).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rgb_a_hsla_rojo_puro() {
|
||||
let h = color_to_hsla(Color::rgb(1.0, 0.0, 0.0));
|
||||
// Rojo: h=0, s=1, l=0.5
|
||||
assert!((h.h - 0.0).abs() < 1e-6);
|
||||
assert!((h.s - 1.0).abs() < 1e-6);
|
||||
assert!((h.l - 0.5).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rgb_a_hsla_alpha_pasa_directo() {
|
||||
let h = color_to_hsla(Color::rgba(0.0, 0.0, 1.0, 0.3));
|
||||
assert!((h.a - 0.3).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//! `pineal-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** — `pineal-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;
|
||||
|
||||
#[cfg(feature = "gpui")]
|
||||
pub mod gpui_backend;
|
||||
|
||||
pub use color::Color;
|
||||
pub use geom::{Point, Rect};
|
||||
pub use canvas::{Canvas, StrokeStyle};
|
||||
pub use plan::{RenderCmd, RenderPlan};
|
||||
|
||||
#[cfg(feature = "gpui")]
|
||||
pub use gpui_backend::WindowCanvas;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user