helion_core/
data.rs

1use bytemuck::{Pod, Zeroable};
2
3#[cfg(feature = "python")]
4use pyo3::prelude::*;
5
6/// 2D point data structure
7#[repr(C)]
8#[derive(Debug, Clone, Copy, Pod, Zeroable)]
9#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
10pub struct Point2D {
11    pub x: f32,
12    pub y: f32,
13}
14
15impl Point2D {
16    pub fn new(x: f32, y: f32) -> Self {
17        Self { x, y }
18    }
19}
20
21/// Python-specific methods
22#[cfg(feature = "python")]
23#[pymethods]
24impl Point2D {
25    #[new]
26    fn py_new(x: f32, y: f32) -> Self {
27        Self::new(x, y)
28    }
29}
30
31/// Color in RGBA format
32#[repr(C)]
33#[derive(Debug, Clone, Copy, Pod, Zeroable)]
34#[cfg_attr(feature = "python", pyo3::pyclass(name = "Color", get_all, set_all))]
35pub struct Color {
36    pub r: f32,
37    pub g: f32,
38    pub b: f32,
39    pub a: f32,
40}
41
42impl Color {
43    pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
44        Self { r, g, b, a }
45    }
46
47    pub fn from_hex(hex: &str) -> Self {
48        let hex = hex.trim_start_matches('#');
49        let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f32 / 255.0;
50        let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f32 / 255.0;
51        let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f32 / 255.0;
52        let a = if hex.len() >= 8 {
53            u8::from_str_radix(&hex[6..8], 16).unwrap_or(255) as f32 / 255.0
54        } else {
55            1.0
56        };
57        Self { r, g, b, a }
58    }
59}
60
61/// Python-specific methods
62#[cfg(feature = "python")]
63#[pymethods]
64impl Color {
65    #[new]
66    #[pyo3(signature = (r, g, b, a=1.0))]
67    fn py_new(r: f32, g: f32, b: f32, a: f32) -> Self {
68        Self::new(r, g, b, a)
69    }
70    
71    /// Create color from hex string (e.g., "#FF5733")
72    #[staticmethod]
73    #[pyo3(name = "from_hex")]
74    fn from_hex_py(hex: &str) -> Self {
75        Self::from_hex(hex)
76    }
77}
78
79impl Default for Color {
80    fn default() -> Self {
81        Self::new(0.0, 0.5, 1.0, 1.0) // Default blue
82    }
83}
84
85/// Vertex data for rendering (position + color + size)
86#[repr(C)]
87#[derive(Debug, Clone, Copy, Pod, Zeroable)]
88pub struct Vertex {
89    pub position: [f32; 2],
90    pub color: [f32; 4],
91    pub size: f32,
92    pub _padding: [f32; 3], // Align to 16 bytes
93}
94
95impl Vertex {
96    pub fn new(position: Point2D, color: Color, size: f32) -> Self {
97        Self {
98            position: [position.x, position.y],
99            color: [color.r, color.g, color.b, color.a],
100            size,
101            _padding: [0.0; 3],
102        }
103    }
104
105    pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
106        wgpu::VertexBufferLayout {
107            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
108            step_mode: wgpu::VertexStepMode::Vertex,
109            attributes: &[
110                // Position
111                wgpu::VertexAttribute {
112                    offset: 0,
113                    shader_location: 0,
114                    format: wgpu::VertexFormat::Float32x2,
115                },
116                // Color
117                wgpu::VertexAttribute {
118                    offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
119                    shader_location: 1,
120                    format: wgpu::VertexFormat::Float32x4,
121                },
122                // Size
123                wgpu::VertexAttribute {
124                    offset: std::mem::size_of::<[f32; 6]>() as wgpu::BufferAddress,
125                    shader_location: 2,
126                    format: wgpu::VertexFormat::Float32,
127                },
128            ],
129        }
130    }
131}
132
133/// Chart data container
134pub struct ChartData {
135    pub vertices: Vec<Vertex>,
136    pub viewport_width: f32,
137    pub viewport_height: f32,
138}
139
140impl ChartData {
141    pub fn new(width: f32, height: f32) -> Self {
142        Self {
143            vertices: Vec::new(),
144            viewport_width: width,
145            viewport_height: height,
146        }
147    }
148
149    /// Add a point to the chart
150    ///
151    /// # Parameters
152    /// * `point` - The 2D coordinates (x, y) of the point to add
153    /// * `color` - The RGBA color for this point (values 0.0-1.0)
154    /// * `size` - The size/radius of the point in pixels
155    pub fn add_point(&mut self, point: Point2D, color: Color, size: f32) {
156        self.vertices.push(Vertex::new(point, color, size));
157    }
158
159    /// Create scatter plot data from raw arrays
160    ///
161    /// Converts raw x and y coordinate arrays into normalized vertex data ready for GPU rendering.
162    /// By default, normalizes coordinates to the [-1, 1] range required by GPU clip space.
163    ///
164    /// # Parameters
165    /// * `x` - Array of x-coordinates for each point
166    /// * `y` - Array of y-coordinates for each point (must be same length as x)
167    /// * `color` - Optional color for all points. If None, uses default blue color
168    /// * `size` - Optional size for all points in pixels. If None, defaults to 2.0
169    /// * `width` - Viewport width in pixels
170    /// * `height` - Viewport height in pixels
171    ///
172    /// # Returns
173    /// A new `ChartData` instance with normalized vertices ready for rendering
174    pub fn from_scatter(
175        x: &[f32],
176        y: &[f32],
177        color: Option<Color>,
178        size: Option<f32>,
179        width: f32,
180        height: f32,
181    ) -> Self {
182        Self::from_scatter_with_range(x, y, color, size, width, height, None, None)
183    }
184
185    /// Create scatter plot data with custom normalization ranges
186    ///
187    /// Converts raw x and y coordinate arrays into normalized vertex data with user-specified
188    /// output ranges. This allows control over the coordinate mapping for custom viewports.
189    ///
190    /// # Parameters
191    /// * `x` - Array of x-coordinates for each point
192    /// * `y` - Array of y-coordinates for each point (must be same length as x)
193    /// * `color` - Optional color for all points. If None, uses default blue color
194    /// * `size` - Optional size for all points in pixels. If None, defaults to 2.0
195    /// * `width` - Viewport width in pixels
196    /// * `height` - Viewport height in pixels
197    /// * `x_range` - Optional custom output range for x (min, max). If None, uses [-1.0, 1.0]
198    /// * `y_range` - Optional custom output range for y (min, max). If None, uses [-1.0, 1.0]
199    ///
200    /// # Returns
201    /// A new `ChartData` instance with normalized vertices
202    ///
203    /// # Example
204    /// ```
205    /// use helion_core::data::ChartData;
206    /// 
207    /// let x_data = vec![1.0, 2.0, 3.0];
208    /// let y_data = vec![4.0, 5.0, 6.0];
209    /// 
210    /// // Map to [0, 1] range instead of [-1, 1]
211    /// let data = ChartData::from_scatter_with_range(
212    ///     &x_data, &y_data, None, None, 800.0, 600.0,
213    ///     Some((0.0, 1.0)),  // x maps to [0, 1]
214    ///     Some((0.0, 1.0)),  // y maps to [0, 1]
215    /// );
216    /// ```
217    pub fn from_scatter_with_range(
218        x: &[f32],
219        y: &[f32],
220        color: Option<Color>,
221        size: Option<f32>,
222        width: f32,
223        height: f32,
224        x_range: Option<(f32, f32)>,
225        y_range: Option<(f32, f32)>,
226    ) -> Self {
227        let mut data = Self::new(width, height);
228        let color = color.unwrap_or_default();
229        let size = size.unwrap_or(2.0);
230
231        // Default to GPU clip space [-1, 1] if not specified
232        let (x_out_min, x_out_max) = x_range.unwrap_or((-1.0, 1.0));
233        let (y_out_min, y_out_max) = y_range.unwrap_or((-1.0, 1.0));
234
235        // Find input data bounds
236        let x_min = x.iter().cloned().fold(f32::INFINITY, f32::min);
237        let x_max = x.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
238        let y_min = y.iter().cloned().fold(f32::INFINITY, f32::min);
239        let y_max = y.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
240
241        let x_in_range = x_max - x_min;
242        let y_in_range = y_max - y_min;
243        let x_out_range = x_out_max - x_out_min;
244        let y_out_range = y_out_max - y_out_min;
245
246        // Normalize coordinates to specified output range
247        for i in 0..x.len().min(y.len()) {
248            let norm_x = ((x[i] - x_min) / x_in_range) * x_out_range + x_out_min;
249            let norm_y = ((y[i] - y_min) / y_in_range) * y_out_range + y_out_min;
250            
251            data.add_point(Point2D::new(norm_x, norm_y), color, size);
252        }
253
254        data
255    }
256}