⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc
crates/modules/ui_engine/widgets/lapaloma-cartesian/src/coord_system.rs
History
Who touched what
raw
feat(lapaloma-cartesian): viewport + coord_system + LineSeries con LTTB
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}