tawasuyu

tawasuyu/brahman

Rust 326TOML 123JSON 17Markdown 144606 Definitions

⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc crates/modules/ui_engine/widgets/lapaloma-cartesian/src/coord_system.rs Rust · 133 lines · 4.5 KB History Who touched what raw

feat(lapaloma-cartesian): viewport + coord_system + LineSeries con LTTB 791ca18d8 · sergio ·

1//! `CoordinateSystem` — proyección dominio ↔ pixel.
2//!
3//! Compone `ChartViewport` (qué se ve) + `plot_rect` (dónde, en
4//! píxeles) en una transformación afín. La invocación es
5//! pointwise; no toca los buffers de datos.
6//!
7//! Convención Y: +Y de pantalla apunta abajo; +Y de datos arriba.
8//! La proyección invierte Y para que un valor alto quede arriba.
9
10use crate::viewport::ChartViewport;
11use lapaloma_render::{Point, Rect};
12
13#[derive(Debug, Clone, Copy)]
14pub struct CoordinateSystem {
15    pub viewport: ChartViewport,
16    pub plot: Rect,
17}
18
19impl CoordinateSystem {
20    pub fn new(viewport: ChartViewport, plot: Rect) -> Self {
21        Self { viewport, plot }
22    }
23
24    /// `(value_x, value_y)` → `(pixel_x, pixel_y)`.
25    pub fn data_to_pixel(&self, x: f64, y: f64) -> Point {
26        let nx = (x - self.viewport.x_min) / self.viewport.x_span();
27        let ny = (y - self.viewport.y_min) / self.viewport.y_span();
28        let px = self.plot.x + nx as f32 * self.plot.w;
29        // +Y de datos = arriba → restar de bottom.
30        let py = self.plot.bottom() - ny as f32 * self.plot.h;
31        Point::new(px, py)
32    }
33
34    /// `(pixel_x, pixel_y)` → `(value_x, value_y)`.
35    /// Usado para hit-test y tooltip-on-hover.
36    pub fn pixel_to_data(&self, p: Point) -> (f64, f64) {
37        let nx = ((p.x - self.plot.x) / self.plot.w) as f64;
38        let ny = ((self.plot.bottom() - p.y) / self.plot.h) as f64;
39        let x = self.viewport.x_min + nx * self.viewport.x_span();
40        let y = self.viewport.y_min + ny * self.viewport.y_span();
41        (x, y)
42    }
43
44    /// Proyecta un buffer entero de coords interleaved
45    /// `[x, y, x, y, …]` (en dominio) a `[px, py, px, py, …]`
46    /// (en píxeles), escribiendo a `out` sin allocar.
47    ///
48    /// El caller debe hacer `out.clear()` previo si quiere reuso
49    /// del buffer; este método sólo extiende.
50    pub fn project_buffer(&self, data: &[f32], out: &mut Vec<f32>) {
51        debug_assert!(data.len() % 2 == 0);
52        // Factorizamos para evitar la división por iteración.
53        let sx = self.plot.w / self.viewport.x_span() as f32;
54        let sy = self.plot.h / self.viewport.y_span() as f32;
55        let tx = self.plot.x - self.viewport.x_min as f32 * sx;
56        let ty = self.plot.bottom() + self.viewport.y_min as f32 * sy;
57
58        out.reserve(data.len());
59        let mut i = 0;
60        while i < data.len() {
61            let px = data[i] * sx + tx;
62            let py = ty - data[i + 1] * sy;
63            out.push(px);
64            out.push(py);
65            i += 2;
66        }
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    fn fixture() -> CoordinateSystem {
75        // Viewport [0..10, 0..10] sobre plot de 100×100 en (0, 0).
76        CoordinateSystem::new(
77            ChartViewport::new(0.0, 10.0, 0.0, 10.0),
78            Rect::new(0.0, 0.0, 100.0, 100.0),
79        )
80    }
81
82    #[test]
83    fn data_to_pixel_origen() {
84        let cs = fixture();
85        // (0,0) data → (0, 100) pixel (bottom-left del plot).
86        let p = cs.data_to_pixel(0.0, 0.0);
87        assert!((p.x - 0.0).abs() < 1e-6);
88        assert!((p.y - 100.0).abs() < 1e-6);
89    }
90
91    #[test]
92    fn data_to_pixel_centro() {
93        let cs = fixture();
94        // (5, 5) → (50, 50)
95        let p = cs.data_to_pixel(5.0, 5.0);
96        assert!((p.x - 50.0).abs() < 1e-6);
97        assert!((p.y - 50.0).abs() < 1e-6);
98    }
99
100    #[test]
101    fn data_to_pixel_top_right() {
102        let cs = fixture();
103        // (10, 10) → (100, 0)
104        let p = cs.data_to_pixel(10.0, 10.0);
105        assert!((p.x - 100.0).abs() < 1e-6);
106        assert!((p.y - 0.0).abs() < 1e-6);
107    }
108
109    #[test]
110    fn pixel_to_data_roundtrip() {
111        let cs = fixture();
112        for (x, y) in [(2.5_f64, 7.5), (0.0, 0.0), (10.0, 10.0), (3.14, 1.59)] {
113            let p = cs.data_to_pixel(x, y);
114            let (x2, y2) = cs.pixel_to_data(p);
115            assert!((x - x2).abs() < 1e-4, "x roundtrip: {} vs {}", x, x2);
116            assert!((y - y2).abs() < 1e-4, "y roundtrip: {} vs {}", y, y2);
117        }
118    }
119
120    #[test]
121    fn project_buffer_consistente_con_pointwise() {
122        let cs = fixture();
123        let data: Vec<f32> = vec![0.0, 0.0, 5.0, 5.0, 10.0, 10.0];
124        let mut out = Vec::new();
125        cs.project_buffer(&data, &mut out);
126        assert_eq!(out.len(), data.len());
127        for i in 0..3 {
128            let expected = cs.data_to_pixel(data[i * 2] as f64, data[i * 2 + 1] as f64);
129            assert!((out[i * 2] - expected.x).abs() < 1e-4);
130            assert!((out[i * 2 + 1] - expected.y).abs() < 1e-4);
131        }
132    }
133}