Skip to main content

petaplot_render/
camera.rs

1use bytemuck::{Pod, Zeroable};
2
3/// Uniform Buffer conteniendo la matriz de transformación del Viewport y el color de renderizado.
4/// Tamaño total: 96 bytes (alineado a 16 bytes para std140 / WGSL).
5#[repr(C)]
6#[derive(Debug, Clone, Copy, Pod, Zeroable)]
7pub struct CameraUniform {
8    pub transform_matrix: [[f32; 4]; 4],
9    pub color: [f32; 4],
10    pub line_width: f32,
11    pub _padding: [f32; 3],
12}
13
14impl Default for CameraUniform {
15    fn default() -> Self {
16        Self {
17            transform_matrix: [
18                [1.0, 0.0, 0.0, 0.0],
19                [0.0, 1.0, 0.0, 0.0],
20                [0.0, 0.0, 1.0, 0.0],
21                [0.0, 0.0, 0.0, 1.0],
22            ],
23            color: [0.2, 0.6, 1.0, 1.0], // Azul neón por defecto
24            line_width: 1.5,
25            _padding: [0.0; 3],
26        }
27    }
28}
29
30/// Cámara 2D para controlar el Zoom y Pan en el gráfico con $0\text{ ms}$ de latencia en CPU.
31pub struct ViewportCamera {
32    pub x_min: f64,
33    pub x_max: f64,
34    pub y_min: f32,
35    pub y_max: f32,
36    pub color: [f32; 4],
37    pub line_width: f32,
38}
39
40impl Default for ViewportCamera {
41    fn default() -> Self {
42        Self {
43            x_min: 0.0,
44            x_max: 100.0,
45            y_min: -1.0,
46            y_max: 1.0,
47            color: [0.2, 0.7, 1.0, 1.0],
48            line_width: 1.5,
49        }
50    }
51}
52
53impl ViewportCamera {
54    pub fn new(x_min: f64, x_max: f64, y_min: f32, y_max: f32) -> Self {
55        Self {
56            x_min,
57            x_max,
58            y_min,
59            y_max,
60            ..Default::default()
61        }
62    }
63
64    /// Desplaza la cámara en el eje X (Pan horizontal).
65    pub fn pan_x(&mut self, delta_x: f64) {
66        self.x_min += delta_x;
67        self.x_max += delta_x;
68    }
69
70    /// Aplica Zoom centrado en una coordenada $X$.
71    pub fn zoom_x(&mut self, center_x: f64, factor: f64) {
72        let span = self.x_max - self.x_min;
73        let new_span = (span * factor).max(1e-6);
74
75        let ratio = if span > 0.0 {
76            (center_x - self.x_min) / span
77        } else {
78            0.5
79        };
80
81        self.x_min = center_x - ratio * new_span;
82        self.x_max = self.x_min + new_span;
83    }
84
85    /// Genera la matriz de transformación ortográfica 4x4 mapeando las coordenadas del dataset a NDC (Normalized Device Coordinates: $[-1, 1]$).
86    pub fn build_uniform(&self) -> CameraUniform {
87        let span_x = (self.x_max - self.x_min) as f32;
88        let span_y = self.y_max - self.y_min;
89
90        let scale_x = if span_x > 0.0 { 2.0 / span_x } else { 1.0 };
91        let scale_y = if span_y > 0.0 { 2.0 / span_y } else { 1.0 };
92
93        let offset_x = -1.0 - (self.x_min as f32) * scale_x;
94        let offset_y = -1.0 - self.y_min * scale_y;
95
96        let transform_matrix = [
97            [scale_x, 0.0, 0.0, 0.0],
98            [0.0, scale_y, 0.0, 0.0],
99            [0.0, 0.0, 1.0, 0.0],
100            [offset_x, offset_y, 0.0, 1.0],
101        ];
102
103        CameraUniform {
104            transform_matrix,
105            color: self.color,
106            line_width: self.line_width,
107            _padding: [0.0; 3],
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_camera_uniform_transformation() {
118        let camera = ViewportCamera::new(0.0, 100.0, -1.0, 1.0);
119        let uniform = camera.build_uniform();
120
121        // En x = 50.0, la posición transformada en pantalla debe ser NDC x = 0.0
122        let scale_x = uniform.transform_matrix[0][0];
123        let offset_x = uniform.transform_matrix[3][0];
124        let ndc_x = 50.0 * scale_x + offset_x;
125
126        assert!((ndc_x - 0.0).abs() < 1e-5);
127    }
128}