Files
brahman/crates/modules/pineal/phosphor/src/element.rs
T
sergio 550c98f275 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>
2026-05-19 14:48:34 +00:00

280 lines
9.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `LapalomaPhosphorElement` — Element GPUI con trail CRT.
//!
//! El render pinta el RingBuffer como N segmentos polilíneas con
//! alpha decreciente del más nuevo al más viejo. Wraparound se
//! parte en dos sub-polilíneas para no introducir la línea
//! horizontal "del slot cap-1 al slot 0".
use std::panic;
use gpui::{
App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
Pixels, Style, Window,
};
use pineal_core::ring::RingBuffer;
use pineal_render::{Canvas, Color, Rect, StrokeStyle, WindowCanvas};
/// Cantidad de tramos del trail. Más tramos = gradiente más suave,
/// más draw calls. 16 cubre la mayoría de los casos sin ser caro.
const DEFAULT_TRAIL_SEGMENTS: usize = 16;
pub struct LapalomaPhosphorElement {
pub buffer: RingBuffer,
/// Color y ancho base. El alpha se modula por tramo;
/// `base_stroke.color.a` es el alpha máximo (cabeza del trail).
pub base_stroke: StrokeStyle,
pub background: Option<Color>,
pub y_min: f32,
pub y_max: f32,
pub padding: f32,
pub trail_segments: usize,
/// Si > 0, se aplica una pasada adicional con `width × glow_width_mult`
/// y `alpha × glow_alpha` debajo del trazo principal — efecto halo CRT.
pub glow_width_mult: f32,
pub glow_alpha: f32,
scratch: Vec<f32>,
}
impl LapalomaPhosphorElement {
pub fn new(buffer: RingBuffer, base_stroke: StrokeStyle) -> Self {
Self {
buffer,
base_stroke,
background: None,
y_min: -1.0,
y_max: 1.0,
padding: 8.0,
trail_segments: DEFAULT_TRAIL_SEGMENTS,
glow_width_mult: 3.0,
glow_alpha: 0.25,
scratch: Vec::new(),
}
}
pub fn background(mut self, color: Color) -> Self {
self.background = Some(color);
self
}
pub fn y_range(mut self, min: f32, max: f32) -> Self {
debug_assert!(max > min);
self.y_min = min;
self.y_max = max;
self
}
pub fn trail_segments(mut self, n: usize) -> Self {
self.trail_segments = n.max(2);
self
}
pub fn glow(mut self, width_mult: f32, alpha: f32) -> Self {
self.glow_width_mult = width_mult;
self.glow_alpha = alpha;
self
}
pub fn no_glow(mut self) -> Self {
self.glow_width_mult = 0.0;
self.glow_alpha = 0.0;
self
}
fn plot_rect(&self, bounds: Rect) -> Rect {
Rect::new(
bounds.x + self.padding,
bounds.y + self.padding,
(bounds.w - self.padding * 2.0).max(1.0),
(bounds.h - self.padding * 2.0).max(1.0),
)
}
}
impl IntoElement for LapalomaPhosphorElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for LapalomaPhosphorElement {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.size.width = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
style.size.height = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
let id = window.request_layout(style, [], cx);
(id, ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_window: &mut Window,
_cx: &mut App,
) -> Self::PrepaintState {
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
window: &mut Window,
_cx: &mut App,
) {
let ox: f32 = bounds.origin.x.into();
let oy: f32 = bounds.origin.y.into();
let w: f32 = bounds.size.width.into();
let h: f32 = bounds.size.height.into();
let outer = Rect::new(ox, oy, w, h);
let plot = self.plot_rect(outer);
let mut canvas = WindowCanvas::new(window);
if let Some(bg) = self.background {
canvas.fill_rect(outer, bg);
}
let filled = self.buffer.filled_len();
if filled < 2 {
return;
}
let cap = self.buffer.capacity();
let head = self.buffer.head();
let coords = self.buffer.coords();
// El slot del sample temporalmente más viejo:
// - is_full → `head` (el siguiente a sobrescribirse).
// - !is_full → 0.
let start_slot = if self.buffer.is_full() { head } else { 0 };
let n_segs = self.trail_segments.min(filled / 2).max(2);
let base_per_seg = filled / n_segs;
let glow_enabled = self.glow_alpha > 0.0 && self.glow_width_mult > 1.0;
for k in 0..n_segs {
// Rango temporal del segmento. El segmento `k` cubre los
// samples [k*base_per_seg, (k+1)*base_per_seg). El último
// incluye el remainder.
let t_lo = k * base_per_seg;
let t_hi = if k == n_segs - 1 {
filled
} else {
// +1 incluye el primer sample del siguiente segmento
// para que las polilíneas se "toquen" sin gap visual.
((k + 1) * base_per_seg) + 1
};
if t_hi <= t_lo + 1 {
continue;
}
// Alpha decrece linealmente del más nuevo al más viejo.
// k = n_segs - 1 → 1.0; k = 0 → 1/n_segs.
let life = (k as f32 + 1.0) / n_segs as f32;
let alpha = self.base_stroke.color.a * life;
let mut color = self.base_stroke.color;
color.a = alpha;
let stroke = StrokeStyle::new(self.base_stroke.width, color);
// Glow underneath, mismo path con más ancho y menos alpha.
let glow_stroke = if glow_enabled {
let mut gc = color;
gc.a *= self.glow_alpha;
Some(StrokeStyle::new(
self.base_stroke.width * self.glow_width_mult,
gc,
))
} else {
None
};
// Proyectar el rango temporal a slots físicos, partiendo
// si cruzamos el final del buffer.
let seg_len = t_hi - t_lo;
let abs_start = (start_slot + t_lo) % cap;
let contiguous_len = cap - abs_start;
if seg_len <= contiguous_len {
let slice = &coords[abs_start * 2..(abs_start + seg_len) * 2];
self.scratch.clear();
project_segment(slice, plot, self.y_min, self.y_max, &mut self.scratch);
if self.scratch.len() >= 4 {
if let Some(gs) = glow_stroke {
canvas.stroke_polyline(&self.scratch, gs);
}
canvas.stroke_polyline(&self.scratch, stroke);
}
} else {
// Wraparound: dos sub-polilíneas separadas.
let slice_a = &coords[abs_start * 2..];
self.scratch.clear();
project_segment(slice_a, plot, self.y_min, self.y_max, &mut self.scratch);
if self.scratch.len() >= 4 {
if let Some(gs) = glow_stroke {
canvas.stroke_polyline(&self.scratch, gs);
}
canvas.stroke_polyline(&self.scratch, stroke);
}
let remaining = seg_len - contiguous_len;
let slice_b = &coords[..remaining * 2];
self.scratch.clear();
project_segment(slice_b, plot, self.y_min, self.y_max, &mut self.scratch);
if self.scratch.len() >= 4 {
if let Some(gs) = glow_stroke {
canvas.stroke_polyline(&self.scratch, gs);
}
canvas.stroke_polyline(&self.scratch, stroke);
}
}
}
}
}
/// Helper builder.
pub fn pineal_phosphor(
buffer: RingBuffer,
base_stroke: StrokeStyle,
) -> LapalomaPhosphorElement {
LapalomaPhosphorElement::new(buffer, base_stroke)
}
/// Proyecta `[x_norm, y_value, …]` del ring a píxeles del plot.
fn project_segment(segment: &[f32], plot: Rect, y_min: f32, y_max: f32, out: &mut Vec<f32>) {
let y_span = y_max - y_min;
if y_span.abs() < 1e-9 {
return;
}
let inv = 1.0 / y_span;
for chunk in segment.chunks_exact(2) {
let xn = chunk[0];
let yv = chunk[1];
let py_norm = (yv - y_min) * inv;
out.push(plot.x + xn * plot.w);
out.push(plot.bottom() - py_norm * plot.h);
}
}