gioser

tawasuyu/brahman

Rust 226TOML 67JSON 14HTML 12860 Definitions

⎇ 1fdbd358c8cd914fb767cbee1483d79935aa1fec crates/modules/pineal/stream/src/element.rs Rust · 208 lines · 6.4 KB History Who touched what raw

refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG 550c98f27 · sergio ·

1//! `LapalomaStreamElement` — Element GPUI para visualización de
2//! telemetría con RingBuffer.
3//!
4//! Modo **sweep** (canónico del osciloscopio):
5//! - Los slots `[0, capacity)` tienen `x_norm` fijo precomputado.
6//! - El `head` marca el slot donde se va a escribir el próximo
7//!   sample → visualmente, el "cursor" que separa la traza vieja
8//!   (a la derecha del head) de la nueva (a la izquierda).
9//! - Render en **dos segmentos** split-at-head para evitar la
10//!   línea horizontal del wraparound. Antes del fill (count <
11//!   capacity), sólo se pinta `[0, head)`.
12
13use std::panic;
14
15use gpui::{
16    App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
17    Pixels, Style, Window,
18};
19
20use pineal_core::ring::RingBuffer;
21use pineal_render::{Canvas, Color, Rect, StrokeStyle, WindowCanvas};
22
23/// Element que pinta un `RingBuffer` en modo sweep.
24pub struct LapalomaStreamElement {
25    pub buffer: RingBuffer,
26    pub stroke: StrokeStyle,
27    pub background: Option<Color>,
28    /// Rango Y para proyectar los samples a píxeles. Default `-1..1`.
29    pub y_min: f32,
30    pub y_max: f32,
31    pub padding: f32,
32    /// Scratch reusable entre los dos segmentos del frame.
33    scratch: Vec<f32>,
34}
35
36impl LapalomaStreamElement {
37    pub fn new(buffer: RingBuffer, stroke: StrokeStyle) -> Self {
38        Self {
39            buffer,
40            stroke,
41            background: None,
42            y_min: -1.0,
43            y_max: 1.0,
44            padding: 8.0,
45            scratch: Vec::new(),
46        }
47    }
48
49    pub fn background(mut self, color: Color) -> Self {
50        self.background = Some(color);
51        self
52    }
53
54    pub fn y_range(mut self, min: f32, max: f32) -> Self {
55        debug_assert!(max > min);
56        self.y_min = min;
57        self.y_max = max;
58        self
59    }
60
61    pub fn padding(mut self, px: f32) -> Self {
62        self.padding = px;
63        self
64    }
65
66    fn plot_rect(&self, bounds: Rect) -> Rect {
67        Rect::new(
68            bounds.x + self.padding,
69            bounds.y + self.padding,
70            (bounds.w - self.padding * 2.0).max(1.0),
71            (bounds.h - self.padding * 2.0).max(1.0),
72        )
73    }
74
75}
76
77/// Proyecta una slice de coords `[x_norm, y_value, …]` del
78/// RingBuffer al sistema de píxeles del plot. `out` se extiende
79/// (no se clearea acá; el caller decide).
80fn project_segment(segment: &[f32], plot: Rect, y_min: f32, y_max: f32, out: &mut Vec<f32>) {
81    let y_span = y_max - y_min;
82    if y_span.abs() < 1e-9 {
83        return;
84    }
85    let inv_y_span = 1.0 / y_span;
86    for chunk in segment.chunks_exact(2) {
87        let xn = chunk[0];
88        let yv = chunk[1];
89        let py_norm = (yv - y_min) * inv_y_span;
90        let px = plot.x + xn * plot.w;
91        let py = plot.bottom() - py_norm * plot.h;
92        out.push(px);
93        out.push(py);
94    }
95}
96
97impl IntoElement for LapalomaStreamElement {
98    type Element = Self;
99    fn into_element(self) -> Self::Element {
100        self
101    }
102}
103
104impl Element for LapalomaStreamElement {
105    type RequestLayoutState = ();
106    type PrepaintState = ();
107
108    fn id(&self) -> Option<ElementId> {
109        None
110    }
111
112    fn source_location(&self) -> Option<&'static panic::Location<'static>> {
113        None
114    }
115
116    fn request_layout(
117        &mut self,
118        _id: Option<&GlobalElementId>,
119        _inspector_id: Option<&InspectorElementId>,
120        window: &mut Window,
121        cx: &mut App,
122    ) -> (LayoutId, Self::RequestLayoutState) {
123        let mut style = Style::default();
124        style.size.width = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
125        style.size.height = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
126        let id = window.request_layout(style, [], cx);
127        (id, ())
128    }
129
130    fn prepaint(
131        &mut self,
132        _id: Option<&GlobalElementId>,
133        _inspector_id: Option<&InspectorElementId>,
134        _bounds: Bounds<Pixels>,
135        _request_layout: &mut Self::RequestLayoutState,
136        _window: &mut Window,
137        _cx: &mut App,
138    ) -> Self::PrepaintState {
139    }
140
141    fn paint(
142        &mut self,
143        _id: Option<&GlobalElementId>,
144        _inspector_id: Option<&InspectorElementId>,
145        bounds: Bounds<Pixels>,
146        _request_layout: &mut Self::RequestLayoutState,
147        _prepaint: &mut Self::PrepaintState,
148        window: &mut Window,
149        _cx: &mut App,
150    ) {
151        let ox: f32 = bounds.origin.x.into();
152        let oy: f32 = bounds.origin.y.into();
153        let w: f32 = bounds.size.width.into();
154        let h: f32 = bounds.size.height.into();
155        let outer = Rect::new(ox, oy, w, h);
156        let plot = self.plot_rect(outer);
157
158        let mut canvas = WindowCanvas::new(window);
159
160        if let Some(bg) = self.background {
161            canvas.fill_rect(outer, bg);
162        }
163
164        let coords = self.buffer.coords();
165        let head = self.buffer.head();
166        let cap = self.buffer.capacity();
167
168        if !self.buffer.is_full() {
169            // Pre-fill: sólo [0, head). Evita la línea plana del
170            // 1.0.2 fix del Flutter doc.
171            let filled = head;
172            if filled >= 2 {
173                let slice = &coords[..filled * 2];
174                self.scratch.clear();
175                project_segment(slice, plot, self.y_min, self.y_max, &mut self.scratch);
176                canvas.stroke_polyline(&self.scratch, self.stroke);
177            }
178            return;
179        }
180
181        // Ya filled — dos segmentos split-at-head.
182        let split = head * 2;
183        // Segmento "viejo": [head*2 .. cap*2) — temporalmente más antiguo.
184        if split < cap * 2 {
185            let seg1 = &coords[split..];
186            if seg1.len() >= 4 {
187                self.scratch.clear();
188                project_segment(seg1, plot, self.y_min, self.y_max, &mut self.scratch);
189                canvas.stroke_polyline(&self.scratch, self.stroke);
190            }
191        }
192        // Segmento "nuevo": [0 .. head*2) — más reciente, dibujado a
193        // la izquierda del cursor.
194        if split > 0 {
195            let seg2 = &coords[..split];
196            if seg2.len() >= 4 {
197                self.scratch.clear();
198                project_segment(seg2, plot, self.y_min, self.y_max, &mut self.scratch);
199                canvas.stroke_polyline(&self.scratch, self.stroke);
200            }
201        }
202    }
203}
204
205/// Helper builder-style.
206pub fn pineal_stream(buffer: RingBuffer, stroke: StrokeStyle) -> LapalomaStreamElement {
207    LapalomaStreamElement::new(buffer, stroke)
208}