feat(lapaloma-cartesian): picture cache pan-blit
- ChartCache + ChartCacheHandle (Arc<Mutex<...>>) cacheable entre frames. El Render host crea uno con chart_cache() y lo pasa al Element con .with_cache(handle). Sin handle, cada frame rebuild completo (correcto pero sin la optimización). - Hash estructural: plot rect + viewport.span (no x_min/y_min) + per-series (data.revision + data.len + stroke). 5 tests cubren estabilidad, pan no invalida, zoom invalida, data revision invalida, plot rect invalida. - En paint: si el hash matches, pan-blit = copia las coords cacheadas con offset (dx_px, dy_px) calculado del diff entre viewport.x_min cached vs actual. Salteamos LTTB + projection. - LineSeries::compute_projected expone el pipe LTTB + project_buffer como método público para que el Element pueda cachear sin pasar por paint(). - Demo multi-series usa el cache; header muestra "cache: N pan-blits / M rebuilds" en vivo para que se vea la métrica al draguear (pan-blits crece) y al zoomear (rebuilds crece). Limitación v0.1 anotada en código: el doc canónico (sección 4.4) usa una textura offscreen blitable; GPUI 0.2 no expone esa primitiva directa. La impl actual cachea coords proyectadas y emite las polilíneas con offset — mismo ahorro de CPU (saltea LTTB) sin GPU texture cache. 51 tests verdes (28 cartesian incluyendo 5 nuevos del structural_hash, 20 core, 3 render). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ use gpui::{
|
||||
MouseMoveEvent, MouseUpEvent, Point, Render, ScrollDelta, ScrollWheelEvent, Window,
|
||||
};
|
||||
|
||||
use lapaloma_cartesian::{ChartViewport, LapalomaChartElement};
|
||||
use lapaloma_cartesian::{chart_cache, ChartCacheHandle, ChartViewport, LapalomaChartElement};
|
||||
use lapaloma_core::buffer::DataBuffer;
|
||||
use lapaloma_render::{Color, StrokeStyle};
|
||||
use yahweh_launcher::launch_app;
|
||||
@@ -38,6 +38,7 @@ struct Demo {
|
||||
viewport: ChartViewport,
|
||||
initial_viewport: ChartViewport,
|
||||
drag: Option<DragAnchor>,
|
||||
chart_cache: ChartCacheHandle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -65,6 +66,7 @@ impl Demo {
|
||||
viewport,
|
||||
initial_viewport: viewport,
|
||||
drag: None,
|
||||
chart_cache: chart_cache(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +147,7 @@ impl Render for Demo {
|
||||
let plot_bg = Color::rgba(0.10, 0.12, 0.16, 1.0);
|
||||
let chart = LapalomaChartElement::new(self.viewport)
|
||||
.background(plot_bg)
|
||||
.with_cache(self.chart_cache.clone())
|
||||
.add_series_named(
|
||||
self.series_sin.clone(),
|
||||
StrokeStyle::new(2.0, Color::from_hex(COLOR_SIN)),
|
||||
@@ -162,6 +165,10 @@ impl Render for Demo {
|
||||
);
|
||||
|
||||
let drag_active = self.drag.is_some();
|
||||
let (pan_blits, rebuilds) = {
|
||||
let c = self.chart_cache.lock().unwrap();
|
||||
(c.pan_blits(), c.rebuilds())
|
||||
};
|
||||
|
||||
div()
|
||||
.id("lapaloma-demo-root")
|
||||
@@ -200,7 +207,12 @@ impl Render for Demo {
|
||||
.child(
|
||||
div()
|
||||
.text_color(theme.fg_muted)
|
||||
.child(if drag_active { "· dragging" } else { "" }),
|
||||
.child(format!(
|
||||
"· cache: {} pan-blits / {} rebuilds {}",
|
||||
pan_blits,
|
||||
rebuilds,
|
||||
if drag_active { "· dragging" } else { "" },
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
//! `LapalomaChartElement` — el `Element` GPUI que envuelve el
|
||||
//! pipeline cartesian.
|
||||
//!
|
||||
//! Mantiene un `Vec<ChartSeriesItem>` con `DataBuffer + StrokeStyle`
|
||||
//! por serie. En `paint()` arma el `WindowCanvas` adapter, pinta el
|
||||
//! background, los ejes con sus labels, y delega a una `LineSeries`
|
||||
//! por cada item. Cada serie emite un solo `paint_path` — N series
|
||||
//! = N draw calls totales (no N × por punto).
|
||||
//! ## Picture cache pan-blit
|
||||
//!
|
||||
//! Todas las series comparten viewport. Para Y axis dual (necesario
|
||||
//! cuando series tienen rangos muy distintos) la API se va a
|
||||
//! extender con `y_axis_id` por serie y un viewport segundario.
|
||||
//! En GPUI cada frame se construye un Element nuevo (el árbol se
|
||||
//! recrea), así que el cache no puede vivir en el Element. El
|
||||
//! caller crea un `ChartCacheHandle` (Arc<Mutex<ChartCache>>) una
|
||||
//! vez y se lo pasa a cada frame.
|
||||
//!
|
||||
//! Algoritmo (sección 4.4 del ARCHITECTURE.md adaptada a GPUI):
|
||||
//! - Hash estructural = plot rect + span (no x_min/y_min) + por
|
||||
//! serie: revision + len + stroke.
|
||||
//! - Si hash igual al cached: **pan puro** → emitimos las coords
|
||||
//! cacheadas con un offset `(dx_px, dy_px)` calculado del
|
||||
//! diff `viewport.x_min - cached.x_min`. Saltea LTTB +
|
||||
//! projection.
|
||||
//! - Si hash distinto: full rebuild. Re-corre LTTB + project,
|
||||
//! pisa el cache, actualiza el snapshot del viewport.
|
||||
//!
|
||||
//! Sin cache, el Element funciona igual: cada frame rebuild
|
||||
//! completo. Útil para tests/smoke.
|
||||
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::panic;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
|
||||
@@ -24,18 +37,58 @@ use lapaloma_render::{Canvas, Color, Point, Rect, StrokeStyle, WindowCanvas};
|
||||
|
||||
use crate::axis::{decimate_labels, format_tick, ticks_nice, AxisStyle};
|
||||
use crate::coord_system::CoordinateSystem;
|
||||
use crate::series::{LineSeries, PaintCtx, RenderMode, Series};
|
||||
use crate::series::LineSeries;
|
||||
use crate::viewport::ChartViewport;
|
||||
|
||||
const TARGET_TICKS_X: usize = 8;
|
||||
const TARGET_TICKS_Y: usize = 6;
|
||||
|
||||
/// Aproximación del ancho de glifo monoespaciado en función del
|
||||
/// font size. Suficiente para alinear / decimar labels.
|
||||
const MONO_GLYPH_RATIO: f32 = 0.55;
|
||||
|
||||
/// Una serie del chart: data + estilo + nombre opcional (para
|
||||
/// futura leyenda visual).
|
||||
/// Cache de coords proyectadas para reuso entre frames. Es lo
|
||||
/// que habilita el pan-blit: el caller lo crea una vez y lo
|
||||
/// pasa por handle.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ChartCache {
|
||||
/// Coords proyectadas por serie. `projected.len()` debe coincidir
|
||||
/// con la cantidad de series del Element.
|
||||
projected: Vec<Vec<f32>>,
|
||||
/// Hash de la geometría + identidades de data. Si cambia,
|
||||
/// invalidamos.
|
||||
structural_hash: u64,
|
||||
/// `viewport.x_min` con el que se proyectaron las coords.
|
||||
cached_x_min: f64,
|
||||
cached_y_min: f64,
|
||||
/// Estadística informativa: cuántos pan-blits desde el último
|
||||
/// rebuild. Útil para debugging y para mostrar en demos.
|
||||
pan_blits: u64,
|
||||
/// Estadística informativa: cuántos rebuilds totales.
|
||||
rebuilds: u64,
|
||||
has_valid_cache: bool,
|
||||
}
|
||||
|
||||
impl ChartCache {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn pan_blits(&self) -> u64 {
|
||||
self.pan_blits
|
||||
}
|
||||
pub fn rebuilds(&self) -> u64 {
|
||||
self.rebuilds
|
||||
}
|
||||
pub fn invalidate(&mut self) {
|
||||
*self = Self::default();
|
||||
}
|
||||
}
|
||||
|
||||
pub type ChartCacheHandle = Arc<Mutex<ChartCache>>;
|
||||
|
||||
/// Atajo para crear un cache compartido. El caller lo guarda en
|
||||
/// su `Render` host y le pasa el clone al Element en cada frame.
|
||||
pub fn chart_cache() -> ChartCacheHandle {
|
||||
Arc::new(Mutex::new(ChartCache::new()))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ChartSeriesItem {
|
||||
pub data: DataBuffer,
|
||||
@@ -47,7 +100,6 @@ impl ChartSeriesItem {
|
||||
pub fn new(data: DataBuffer, stroke: StrokeStyle) -> Self {
|
||||
Self { data, stroke, name: None }
|
||||
}
|
||||
|
||||
pub fn named(data: DataBuffer, stroke: StrokeStyle, name: impl Into<String>) -> Self {
|
||||
Self { data, stroke, name: Some(name.into()) }
|
||||
}
|
||||
@@ -63,13 +115,13 @@ pub struct LapalomaChartElement {
|
||||
pub margin_left: f32,
|
||||
pub margin_top: f32,
|
||||
pub margin_right: f32,
|
||||
/// Scratch reusable entre series y entre frames.
|
||||
/// Cache opcional compartido con el `Render` host. Si está
|
||||
/// presente, habilita pan-blit.
|
||||
pub cache: Option<ChartCacheHandle>,
|
||||
scratch: Vec<f32>,
|
||||
}
|
||||
|
||||
impl LapalomaChartElement {
|
||||
/// Construye un chart vacío con un viewport. Las series se
|
||||
/// agregan con `add_series` / `add_series_named`.
|
||||
pub fn new(viewport: ChartViewport) -> Self {
|
||||
Self {
|
||||
series: Vec::new(),
|
||||
@@ -81,6 +133,7 @@ impl LapalomaChartElement {
|
||||
margin_left: 32.0,
|
||||
margin_top: 8.0,
|
||||
margin_right: 8.0,
|
||||
cache: None,
|
||||
scratch: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -118,6 +171,13 @@ impl LapalomaChartElement {
|
||||
self
|
||||
}
|
||||
|
||||
/// Enchufa un cache compartido. Sin esto, cada frame es rebuild
|
||||
/// completo (correcto pero sin la optimización pan-blit).
|
||||
pub fn with_cache(mut self, cache: ChartCacheHandle) -> Self {
|
||||
self.cache = Some(cache);
|
||||
self
|
||||
}
|
||||
|
||||
fn plot_rect(&self, bounds: Rect) -> Rect {
|
||||
Rect::new(
|
||||
bounds.x + self.margin_left,
|
||||
@@ -145,7 +205,6 @@ impl LapalomaChartElement {
|
||||
axis_stroke,
|
||||
);
|
||||
|
||||
// X axis ticks + labels.
|
||||
let x_ticks = ticks_nice(self.viewport.x_min, self.viewport.x_max, TARGET_TICKS_X);
|
||||
let x_step = nice_step(self.viewport.x_min, self.viewport.x_max, TARGET_TICKS_X);
|
||||
let mut x_pos: Vec<f32> = Vec::with_capacity(x_ticks.len());
|
||||
@@ -180,7 +239,6 @@ impl LapalomaChartElement {
|
||||
);
|
||||
}
|
||||
|
||||
// Y axis ticks + labels con decimación vertical.
|
||||
let y_ticks = ticks_nice(self.viewport.y_min, self.viewport.y_max, TARGET_TICKS_Y);
|
||||
let y_step = nice_step(self.viewport.y_min, self.viewport.y_max, TARGET_TICKS_Y);
|
||||
let y_label_pitch = style.label_size_px + style.label_min_spacing_px;
|
||||
@@ -216,6 +274,73 @@ impl LapalomaChartElement {
|
||||
prev_py = Some(py);
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuild full: LTTB + projection por serie. Pinta directo desde
|
||||
/// el cache si está enchufado (para no copiar dos veces).
|
||||
fn rebuild_and_paint(&mut self, cs: &CoordinateSystem, canvas: &mut dyn Canvas) {
|
||||
if let Some(handle) = self.cache.clone() {
|
||||
let mut cache = handle.lock().unwrap();
|
||||
cache.projected.clear();
|
||||
cache.projected.resize_with(self.series.len(), Vec::new);
|
||||
for (i, item) in self.series.iter().enumerate() {
|
||||
let series = LineSeries::new(&item.data, item.stroke);
|
||||
series.compute_projected(cs, &mut cache.projected[i]);
|
||||
if cache.projected[i].len() >= 4 {
|
||||
canvas.stroke_polyline(&cache.projected[i], item.stroke);
|
||||
}
|
||||
}
|
||||
cache.structural_hash = structural_hash(
|
||||
cs.plot,
|
||||
self.viewport.x_span(),
|
||||
self.viewport.y_span(),
|
||||
&self.series,
|
||||
);
|
||||
cache.cached_x_min = self.viewport.x_min;
|
||||
cache.cached_y_min = self.viewport.y_min;
|
||||
cache.has_valid_cache = true;
|
||||
cache.pan_blits = 0;
|
||||
cache.rebuilds = cache.rebuilds.wrapping_add(1);
|
||||
} else {
|
||||
// Sin cache: usamos el scratch local.
|
||||
for item in &self.series {
|
||||
let series = LineSeries::new(&item.data, item.stroke);
|
||||
series.compute_projected(cs, &mut self.scratch);
|
||||
if self.scratch.len() >= 4 {
|
||||
canvas.stroke_polyline(&self.scratch, item.stroke);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Emite las coords cacheadas con un offset en pixel space.
|
||||
/// Se usa cuando detectamos pan puro (mismo hash estructural).
|
||||
fn pan_blit_paint(&mut self, plot: Rect, canvas: &mut dyn Canvas) {
|
||||
let Some(handle) = self.cache.clone() else {
|
||||
return;
|
||||
};
|
||||
let mut cache = handle.lock().unwrap();
|
||||
let dx_px = ((cache.cached_x_min - self.viewport.x_min) * plot.w as f64
|
||||
/ self.viewport.x_span()) as f32;
|
||||
let dy_px = ((self.viewport.y_min - cache.cached_y_min) * plot.h as f64
|
||||
/ self.viewport.y_span()) as f32;
|
||||
|
||||
for (i, item) in self.series.iter().enumerate() {
|
||||
let cached = &cache.projected[i];
|
||||
if cached.len() < 4 {
|
||||
continue;
|
||||
}
|
||||
self.scratch.clear();
|
||||
self.scratch.reserve(cached.len());
|
||||
let mut k = 0;
|
||||
while k + 1 < cached.len() {
|
||||
self.scratch.push(cached[k] + dx_px);
|
||||
self.scratch.push(cached[k + 1] + dy_px);
|
||||
k += 2;
|
||||
}
|
||||
canvas.stroke_polyline(&self.scratch, item.stroke);
|
||||
}
|
||||
cache.pan_blits = cache.pan_blits.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for LapalomaChartElement {
|
||||
@@ -232,7 +357,6 @@ impl Element for LapalomaChartElement {
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
@@ -288,22 +412,60 @@ impl Element for LapalomaChartElement {
|
||||
|
||||
self.paint_axes(&mut canvas, &cs);
|
||||
|
||||
// Una pasada de paint por serie. Re-usamos el mismo scratch
|
||||
// entre series — cada `LineSeries::paint` hace `clear()`.
|
||||
for item in &self.series {
|
||||
let series = LineSeries::new(&item.data, item.stroke);
|
||||
let mut ctx = PaintCtx {
|
||||
cs,
|
||||
mode: RenderMode::UiRich,
|
||||
scratch: &mut self.scratch,
|
||||
};
|
||||
series.paint(&mut ctx, &mut canvas);
|
||||
// Decide rebuild vs pan-blit.
|
||||
let current_hash = structural_hash(
|
||||
plot,
|
||||
self.viewport.x_span(),
|
||||
self.viewport.y_span(),
|
||||
&self.series,
|
||||
);
|
||||
let pan_only = self
|
||||
.cache
|
||||
.as_ref()
|
||||
.map(|h| {
|
||||
let c = h.lock().unwrap();
|
||||
c.has_valid_cache
|
||||
&& c.structural_hash == current_hash
|
||||
&& c.projected.len() == self.series.len()
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if pan_only {
|
||||
self.pan_blit_paint(plot, &mut canvas);
|
||||
} else {
|
||||
self.rebuild_and_paint(&cs, &mut canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper de una sola línea. Para multi-series usar
|
||||
/// `LapalomaChartElement::new(viewport).add_series(...).add_series(...)`.
|
||||
/// Hash de la geometría + identidades de data. Lo que NO va acá:
|
||||
/// `viewport.x_min` y `y_min` (el pan los mueve sin invalidar).
|
||||
fn structural_hash(
|
||||
plot: Rect,
|
||||
x_span: f64,
|
||||
y_span: f64,
|
||||
series: &[ChartSeriesItem],
|
||||
) -> u64 {
|
||||
let mut h = DefaultHasher::new();
|
||||
plot.x.to_bits().hash(&mut h);
|
||||
plot.y.to_bits().hash(&mut h);
|
||||
plot.w.to_bits().hash(&mut h);
|
||||
plot.h.to_bits().hash(&mut h);
|
||||
x_span.to_bits().hash(&mut h);
|
||||
y_span.to_bits().hash(&mut h);
|
||||
(series.len() as u64).hash(&mut h);
|
||||
for s in series {
|
||||
s.data.revision().hash(&mut h);
|
||||
(s.data.len() as u64).hash(&mut h);
|
||||
s.stroke.width.to_bits().hash(&mut h);
|
||||
s.stroke.color.r.to_bits().hash(&mut h);
|
||||
s.stroke.color.g.to_bits().hash(&mut h);
|
||||
s.stroke.color.b.to_bits().hash(&mut h);
|
||||
s.stroke.color.a.to_bits().hash(&mut h);
|
||||
}
|
||||
h.finish()
|
||||
}
|
||||
|
||||
pub fn lapaloma_chart(
|
||||
data: DataBuffer,
|
||||
viewport: ChartViewport,
|
||||
@@ -311,3 +473,85 @@ pub fn lapaloma_chart(
|
||||
) -> LapalomaChartElement {
|
||||
LapalomaChartElement::new(viewport).add_series(data, stroke)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn structural_hash_estable_para_mismo_estado() {
|
||||
let mut data = DataBuffer::with_capacity(10);
|
||||
for i in 0..10 {
|
||||
data.push(i as f32, (i as f32).sin());
|
||||
}
|
||||
let series = vec![ChartSeriesItem::new(
|
||||
data,
|
||||
StrokeStyle::new(2.0, Color::rgb(1.0, 0.0, 0.0)),
|
||||
)];
|
||||
let plot = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
let a = structural_hash(plot, 10.0, 2.0, &series);
|
||||
let b = structural_hash(plot, 10.0, 2.0, &series);
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structural_hash_pan_no_cambia() {
|
||||
// Mismo span pero distinto x_min/y_min — hash igual.
|
||||
let mut data = DataBuffer::with_capacity(10);
|
||||
for i in 0..10 {
|
||||
data.push(i as f32, (i as f32).sin());
|
||||
}
|
||||
let series = vec![ChartSeriesItem::new(
|
||||
data,
|
||||
StrokeStyle::new(2.0, Color::rgb(1.0, 0.0, 0.0)),
|
||||
)];
|
||||
let plot = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
let a = structural_hash(plot, 10.0, 2.0, &series);
|
||||
// Pan implícito: x_span/y_span no cambiaron → hash igual.
|
||||
let b = structural_hash(plot, 10.0, 2.0, &series);
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structural_hash_zoom_invalida() {
|
||||
let series = vec![ChartSeriesItem::new(
|
||||
DataBuffer::new(),
|
||||
StrokeStyle::new(2.0, Color::WHITE),
|
||||
)];
|
||||
let plot = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
let a = structural_hash(plot, 10.0, 2.0, &series);
|
||||
let b = structural_hash(plot, 5.0, 2.0, &series); // zoom in X
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structural_hash_data_revision_invalida() {
|
||||
let mut data = DataBuffer::with_capacity(2);
|
||||
data.push(0.0, 0.0);
|
||||
let series0 = vec![ChartSeriesItem::new(
|
||||
data.clone(),
|
||||
StrokeStyle::new(2.0, Color::WHITE),
|
||||
)];
|
||||
let plot = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
let a = structural_hash(plot, 1.0, 1.0, &series0);
|
||||
|
||||
data.push(1.0, 1.0); // bump revision
|
||||
let series1 = vec![ChartSeriesItem::new(
|
||||
data,
|
||||
StrokeStyle::new(2.0, Color::WHITE),
|
||||
)];
|
||||
let b = structural_hash(plot, 1.0, 1.0, &series1);
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structural_hash_plot_rect_invalida() {
|
||||
let series = vec![ChartSeriesItem::new(
|
||||
DataBuffer::new(),
|
||||
StrokeStyle::new(2.0, Color::WHITE),
|
||||
)];
|
||||
let a = structural_hash(Rect::new(0.0, 0.0, 100.0, 100.0), 1.0, 1.0, &series);
|
||||
let b = structural_hash(Rect::new(0.0, 0.0, 200.0, 100.0), 1.0, 1.0, &series);
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,7 @@ pub use coord_system::CoordinateSystem;
|
||||
pub use series::{LineSeries, PaintCtx, RenderMode, Series};
|
||||
|
||||
#[cfg(feature = "gpui")]
|
||||
pub use element::{lapaloma_chart, LapalomaChartElement};
|
||||
pub use element::{
|
||||
chart_cache, lapaloma_chart, ChartCache, ChartCacheHandle, ChartSeriesItem,
|
||||
LapalomaChartElement,
|
||||
};
|
||||
|
||||
@@ -58,35 +58,42 @@ impl<'a> LineSeries<'a> {
|
||||
Self { data, stroke, lttb_target: None }
|
||||
}
|
||||
|
||||
fn effective_target(&self, plot_w: f32) -> usize {
|
||||
pub fn effective_target(&self, plot_w: f32) -> usize {
|
||||
self.lttb_target.unwrap_or_else(|| (plot_w as usize).saturating_mul(3))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Series for LineSeries<'a> {
|
||||
fn paint(&self, ctx: &mut PaintCtx<'_>, canvas: &mut dyn Canvas) {
|
||||
/// Materializa las coords proyectadas a pixel space en `out`,
|
||||
/// aplicando LTTB cuando densidad > target. `out` se clearea.
|
||||
///
|
||||
/// Útil para callers que necesitan cachear el resultado
|
||||
/// (picture cache pan-blit) sin pasar por `paint()`.
|
||||
pub fn compute_projected(&self, cs: &CoordinateSystem, out: &mut Vec<f32>) {
|
||||
out.clear();
|
||||
if self.data.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let target = self.effective_target(ctx.cs.plot.w);
|
||||
|
||||
ctx.scratch.clear();
|
||||
|
||||
let target = self.effective_target(cs.plot.w);
|
||||
if self.data.len() > target {
|
||||
// Decimar primero (en coords de dominio), proyectar después.
|
||||
let mut idx = Vec::with_capacity(target);
|
||||
let mut idx: Vec<usize> = Vec::with_capacity(target);
|
||||
lttb::lttb_indices(self.data.coords(), target, &mut idx);
|
||||
let mut decimated: Vec<f32> = Vec::with_capacity(idx.len() * 2);
|
||||
for i in idx {
|
||||
decimated.push(self.data.coords()[i * 2]);
|
||||
decimated.push(self.data.coords()[i * 2 + 1]);
|
||||
}
|
||||
ctx.cs.project_buffer(&decimated, ctx.scratch);
|
||||
cs.project_buffer(&decimated, out);
|
||||
} else {
|
||||
ctx.cs.project_buffer(self.data.coords(), ctx.scratch);
|
||||
cs.project_buffer(self.data.coords(), out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Series for LineSeries<'a> {
|
||||
fn paint(&self, ctx: &mut PaintCtx<'_>, canvas: &mut dyn Canvas) {
|
||||
self.compute_projected(&ctx.cs, ctx.scratch);
|
||||
if ctx.scratch.len() < 4 {
|
||||
return;
|
||||
}
|
||||
canvas.stroke_polyline(ctx.scratch, self.stroke);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user