feat(lapaloma-stream): osciloscopio CRT con RingBuffer en sweep mode
- lapaloma-stream: feature `gpui` (default). LapalomaStreamElement pinta un RingBuffer en modo sweep — dos polilíneas split-at-head (segmento [head..cap) viejo + [0..head) nuevo) para evitar la línea horizontal del wraparound. Pre-fill (count < cap) sólo pinta [0, head) para evitar el flat-line del 1.0.2 fix. - y_range configurable, background opcional, padding. - crates/apps/lapaloma-stream-demo: osciloscopio sintético con RingBuffer cap=512. Timer en cx.background_executor que hace push(synthesize(t)) + cx.notify() cada 16ms (60 Hz). Señal = suma de dos sinusoides desfasadas + jitter determinístico. Header muestra cap / head / filled% / t / revision en vivo. - Workspace: registrada la app lapaloma-stream-demo. Showcase del P2 zero-alloc: push(v) son 2 writes + 2 increments, zero allocations per frame, ningún Vec se reasigna. 46 tests verdes (sin cambios; el stream se valida en runtime via demo). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "lapaloma-stream-demo"
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
publish = { workspace = true }
|
||||
description = "Lapaloma — demo de streaming: RingBuffer + timer 60 Hz + sweep render. Showcase del zero-alloc en hot path."
|
||||
|
||||
[dependencies]
|
||||
gpui = { workspace = true }
|
||||
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
|
||||
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
|
||||
lapaloma-core = { path = "../../modules/ui_engine/libs/lapaloma-core" }
|
||||
lapaloma-render = { path = "../../modules/ui_engine/widgets/lapaloma-render", features = ["gpui"] }
|
||||
lapaloma-stream = { path = "../../modules/ui_engine/widgets/lapaloma-stream" }
|
||||
@@ -0,0 +1,125 @@
|
||||
//! `lapaloma-stream-demo` — osciloscopio sintético.
|
||||
//!
|
||||
//! Ventana con un `LapalomaStreamElement` montado sobre un
|
||||
//! `RingBuffer` de 512 slots. Un timer en el background executor
|
||||
//! empuja un sample cada **16 ms** (≈ 60 Hz) y dispara
|
||||
//! `cx.notify()`. El sample es la suma de dos sinusoides desfasadas
|
||||
//! más un poquito de ruido determinístico.
|
||||
//!
|
||||
//! El efecto visual: la traza barre la ventana como en un
|
||||
//! osciloscopio CRT — split-at-head deja un "cursor" donde
|
||||
//! arranca la traza fresca, la traza vieja se mantiene a la
|
||||
//! derecha hasta que el cursor la sobrescriba.
|
||||
//!
|
||||
//! Showcase del **P2 zero-alloc en hot path**: el `push(v)` del
|
||||
//! RingBuffer son 2 escrituras + 2 increments. Cero allocations
|
||||
//! por frame, ningún `Vec` se reasigna.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{div, prelude::*, px, Context, IntoElement, Render, Window};
|
||||
|
||||
use lapaloma_core::ring::RingBuffer;
|
||||
use lapaloma_render::{Color, StrokeStyle};
|
||||
use lapaloma_stream::lapaloma_stream;
|
||||
use yahweh_launcher::launch_app;
|
||||
use yahweh_theme::Theme;
|
||||
|
||||
const RING_CAPACITY: usize = 512;
|
||||
const SAMPLE_PERIOD: Duration = Duration::from_millis(16);
|
||||
|
||||
fn main() {
|
||||
launch_app(
|
||||
"Lapaloma — stream (osciloscopio sintético 60 Hz)",
|
||||
(900., 480.),
|
||||
StreamDemo::new,
|
||||
);
|
||||
}
|
||||
|
||||
struct StreamDemo {
|
||||
buffer: RingBuffer,
|
||||
/// Tick count global. Sirve de fase para la señal sintética y
|
||||
/// se muestra en el header para verificar que el timer corre.
|
||||
t: u64,
|
||||
}
|
||||
|
||||
impl StreamDemo {
|
||||
fn new(cx: &mut Context<Self>) -> Self {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let timer = cx.background_executor().clone();
|
||||
loop {
|
||||
timer.timer(SAMPLE_PERIOD).await;
|
||||
let r = this.update(cx, |me, cx| {
|
||||
me.tick();
|
||||
cx.notify();
|
||||
});
|
||||
if r.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
buffer: RingBuffer::new(RING_CAPACITY),
|
||||
t: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn tick(&mut self) {
|
||||
let v = synthesize(self.t);
|
||||
self.buffer.push(v);
|
||||
self.t = self.t.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Señal sintética: suma de dos sinusoides + jitter determinístico.
|
||||
/// El rango efectivo queda en `[-1, 1]` aproximadamente.
|
||||
fn synthesize(t: u64) -> f32 {
|
||||
let phase = t as f32;
|
||||
let signal = (phase * 0.07).sin() * 0.75 + (phase * 0.19).sin() * 0.22;
|
||||
let jitter = ((phase * 37.0).sin() * 1000.0).fract() * 0.04;
|
||||
signal + jitter
|
||||
}
|
||||
|
||||
impl Render for StreamDemo {
|
||||
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let theme = Theme::global(cx).clone();
|
||||
|
||||
let plot_bg = Color::rgba(0.08, 0.10, 0.13, 1.0);
|
||||
let stroke = StrokeStyle::new(1.8, Color::from_hex(0xa3be8c));
|
||||
|
||||
let stream = lapaloma_stream(self.buffer.clone(), stroke)
|
||||
.background(plot_bg)
|
||||
.y_range(-1.2, 1.2);
|
||||
|
||||
let fill_pct = (self.buffer.filled_len() * 100) / RING_CAPACITY;
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.bg(theme.bg_app.clone())
|
||||
.p(px(16.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap(px(10.))
|
||||
.child(
|
||||
div()
|
||||
.text_color(theme.fg_text)
|
||||
.text_size(px(18.))
|
||||
.child("Lapaloma — stream"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap(px(16.))
|
||||
.text_size(px(11.))
|
||||
.text_color(theme.fg_muted)
|
||||
.child(format!("cap = {}", RING_CAPACITY))
|
||||
.child(format!("head = {}", self.buffer.head()))
|
||||
.child(format!("filled = {}%", fill_pct))
|
||||
.child(format!("t = {}", self.t))
|
||||
.child(format!("rev = {}", self.buffer.revision())),
|
||||
)
|
||||
.child(div().w_full().flex_grow().child(stream))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user