tawasuyu

tawasuyu/brahman

Rust 326TOML 123JSON 17Markdown 144606 Definitions

⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc crates/modules/ui_engine/widgets/lapaloma-cartesian/src/viewport.rs Rust · 147 lines · 5.3 KB History Who touched what raw

feat(lapaloma-demo): pan + zoom + double-click reset interactivos 138307c2b · sergio ·

1//! `ChartViewport` — ventana visible en el dominio de datos.
2//!
3//! El viewport NO conoce pixeles. Sólo describe qué rango de
4//! valores X/Y es visible. La proyección a píxeles la hace
5//! [`crate::coord_system::CoordinateSystem`] cuando le pasás
6//! el `plot_rect`.
7//!
8//! Pan y zoom mutan el viewport, no los datos. Esto preserva el
9//! P2 zero-alloc: los buffers de DataBuffer / RingBuffer se quedan
10//! quietos; sólo cambian cuatro `f64` en el viewport.
11
12use lapaloma_render::Rect;
13
14/// Rango visible en coordenadas de dominio. `f64` porque ejes
15/// temporales con epoch ms se desbordan en `f32`.
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct ChartViewport {
18    pub x_min: f64,
19    pub x_max: f64,
20    pub y_min: f64,
21    pub y_max: f64,
22}
23
24impl ChartViewport {
25    pub fn new(x_min: f64, x_max: f64, y_min: f64, y_max: f64) -> Self {
26        debug_assert!(x_max > x_min && y_max > y_min);
27        Self { x_min, x_max, y_min, y_max }
28    }
29
30    pub fn x_span(&self) -> f64 {
31        self.x_max - self.x_min
32    }
33    pub fn y_span(&self) -> f64 {
34        self.y_max - self.y_min
35    }
36
37    /// Pan en unidades de **dominio**. Suma dx y dy a ambos
38    /// extremos del rango respectivo.
39    pub fn pan(&mut self, dx: f64, dy: f64) {
40        self.x_min += dx;
41        self.x_max += dx;
42        self.y_min += dy;
43        self.y_max += dy;
44    }
45
46    /// Pan en **píxeles** dado el `plot_rect`. Convierte dx_px →
47    /// unidades de dominio usando el span actual / ancho del plot.
48    ///
49    /// Convención de signos: `dx_px > 0` significa "el mouse se
50    /// movió a la derecha", que arrastra el viewport a la
51    /// **izquierda** (los datos parecen ir hacia la derecha).
52    pub fn pan_pixels(&mut self, dx_px: f32, dy_px: f32, plot: Rect) {
53        let dx = -(dx_px as f64) * self.x_span() / plot.w as f64;
54        // En la convención canvas (+Y hacia abajo) pero queremos
55        // que arrastrar para arriba muestre valores más altos,
56        // así que también invertimos Y.
57        let dy = (dy_px as f64) * self.y_span() / plot.h as f64;
58        self.pan(dx, dy);
59    }
60
61    /// Pan en **fracción del viewport**. `fx = 0.5` arrastra medio
62    /// span hacia la izquierda. Útil cuando el caller no conoce el
63    /// `plot_rect` exacto y trabaja con coords normalizadas
64    /// (drag dividido por el ancho de la window).
65    pub fn pan_fraction(&mut self, fx: f64, fy: f64) {
66        self.pan(-fx * self.x_span(), fy * self.y_span());
67    }
68
69    /// Zoom anchor-preserving (sección 5.3 del ARCHITECTURE.md).
70    /// `anchor_norm` es la posición del ancla **normalizada al
71    /// viewport** en `[0, 1]` por eje (típicamente: la posición
72    /// del mouse dentro del plot_rect, normalizada).
73    ///
74    /// `factor > 1` aleja (zoom out), `< 1` acerca (zoom in).
75    pub fn zoom_at(&mut self, factor_x: f64, factor_y: f64, anchor_norm: (f64, f64)) {
76        let (ax, ay) = anchor_norm;
77        let anchor_x = self.x_min + ax * self.x_span();
78        let anchor_y = self.y_min + ay * self.y_span();
79        let new_xspan = self.x_span() * factor_x;
80        let new_yspan = self.y_span() * factor_y;
81        self.x_min = anchor_x - ax * new_xspan;
82        self.x_max = self.x_min + new_xspan;
83        self.y_min = anchor_y - ay * new_yspan;
84        self.y_max = self.y_min + new_yspan;
85    }
86
87    /// Zoom uniforme con el mismo factor en X e Y.
88    pub fn zoom_uniform(&mut self, factor: f64, anchor_norm: (f64, f64)) {
89        self.zoom_at(factor, factor, anchor_norm);
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn pan_no_cambia_span() {
99        let mut v = ChartViewport::new(0.0, 10.0, -1.0, 1.0);
100        v.pan(2.0, 0.5);
101        assert!((v.x_min - 2.0).abs() < 1e-9);
102        assert!((v.x_max - 12.0).abs() < 1e-9);
103        assert!((v.x_span() - 10.0).abs() < 1e-9);
104    }
105
106    #[test]
107    fn zoom_in_preserva_anchor() {
108        // Zoom in 2× con anchor en el centro: el valor que estaba
109        // en el centro sigue en el centro.
110        let mut v = ChartViewport::new(0.0, 10.0, 0.0, 10.0);
111        v.zoom_uniform(0.5, (0.5, 0.5));
112        let new_center_x = v.x_min + v.x_span() * 0.5;
113        let new_center_y = v.y_min + v.y_span() * 0.5;
114        assert!((new_center_x - 5.0).abs() < 1e-9);
115        assert!((new_center_y - 5.0).abs() < 1e-9);
116        assert!((v.x_span() - 5.0).abs() < 1e-9);
117    }
118
119    #[test]
120    fn zoom_anchor_esquina() {
121        // Anchor en (0,0): la esquina inferior-izquierda no se mueve.
122        let mut v = ChartViewport::new(0.0, 10.0, 0.0, 10.0);
123        v.zoom_uniform(0.5, (0.0, 0.0));
124        assert!((v.x_min - 0.0).abs() < 1e-9);
125        assert!((v.y_min - 0.0).abs() < 1e-9);
126        assert!((v.x_span() - 5.0).abs() < 1e-9);
127    }
128
129    #[test]
130    fn pan_pixels_invertido() {
131        // Plot de 100px ancho, span de dominio 10. Arrastrar 50px
132        // a la derecha = pan dominio -5.
133        let mut v = ChartViewport::new(0.0, 10.0, 0.0, 10.0);
134        v.pan_pixels(50.0, 0.0, Rect::new(0.0, 0.0, 100.0, 100.0));
135        assert!((v.x_min - (-5.0)).abs() < 1e-9);
136        assert!((v.x_max - 5.0).abs() < 1e-9);
137    }
138
139    #[test]
140    fn pan_fraction_es_independiente_de_plot() {
141        let mut v = ChartViewport::new(0.0, 10.0, 0.0, 10.0);
142        // 50% del span hacia la derecha = viewport se mueve -5 en X.
143        v.pan_fraction(0.5, 0.0);
144        assert!((v.x_min - (-5.0)).abs() < 1e-9);
145        assert!((v.x_max - 5.0).abs() < 1e-9);
146    }
147}