Skip to main content

runmat_plot/plots/
stairs.rs

1//! Stairs (step) plot implementation
2
3use crate::core::{
4    vertex_utils, AlphaMode, BoundingBox, DrawCall, GpuVertexBuffer, Material, PipelineType,
5    RenderData, Vertex,
6};
7use crate::plots::line::LineMarkerAppearance;
8use glam::{Vec3, Vec4};
9
10#[derive(Debug, Clone)]
11pub struct StairsPlot {
12    pub x: Vec<f64>,
13    pub y: Vec<f64>,
14    pub color: Vec4,
15    pub line_width: f32,
16    pub label: Option<String>,
17    pub visible: bool,
18    vertices: Option<Vec<Vertex>>,
19    bounds: Option<BoundingBox>,
20    dirty: bool,
21    gpu_vertices: Option<GpuVertexBuffer>,
22    gpu_vertex_count: Option<usize>,
23    gpu_bounds: Option<BoundingBox>,
24    marker: Option<LineMarkerAppearance>,
25    marker_vertices: Option<Vec<Vertex>>,
26    marker_gpu_vertices: Option<GpuVertexBuffer>,
27    marker_dirty: bool,
28}
29
30impl StairsPlot {
31    pub fn new(x: Vec<f64>, y: Vec<f64>) -> Result<Self, String> {
32        if x.len() != y.len() || x.is_empty() {
33            return Err("stairs: X and Y must be same non-zero length".to_string());
34        }
35        Ok(Self {
36            x,
37            y,
38            color: Vec4::new(0.0, 0.5, 1.0, 1.0),
39            line_width: 1.0,
40            label: None,
41            visible: true,
42            vertices: None,
43            bounds: None,
44            dirty: true,
45            gpu_vertices: None,
46            gpu_vertex_count: None,
47            gpu_bounds: None,
48            marker: None,
49            marker_vertices: None,
50            marker_gpu_vertices: None,
51            marker_dirty: true,
52        })
53    }
54
55    /// Build a stairs plot backed directly by a GPU vertex buffer.
56    pub fn from_gpu_buffer(
57        color: Vec4,
58        buffer: GpuVertexBuffer,
59        vertex_count: usize,
60        bounds: BoundingBox,
61    ) -> Self {
62        Self {
63            x: Vec::new(),
64            y: Vec::new(),
65            color,
66            line_width: 1.0,
67            label: None,
68            visible: true,
69            vertices: None,
70            bounds: None,
71            dirty: false,
72            gpu_vertices: Some(buffer),
73            gpu_vertex_count: Some(vertex_count),
74            gpu_bounds: Some(bounds),
75            marker: None,
76            marker_vertices: None,
77            marker_gpu_vertices: None,
78            marker_dirty: true,
79        }
80    }
81
82    pub fn with_style(mut self, color: Vec4, line_width: f32) -> Self {
83        self.color = color;
84        self.line_width = line_width.max(0.5);
85        self.dirty = true;
86        self.marker_dirty = true;
87        self.drop_gpu();
88        self
89    }
90    pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
91        self.label = Some(label.into());
92        self
93    }
94    pub fn set_visible(&mut self, v: bool) {
95        self.visible = v;
96    }
97
98    pub fn set_marker(&mut self, marker: Option<LineMarkerAppearance>) {
99        self.marker = marker;
100        self.marker_dirty = true;
101        if self.marker.is_none() {
102            self.marker_vertices = None;
103            self.marker_gpu_vertices = None;
104        }
105    }
106
107    pub fn set_marker_gpu_vertices(&mut self, buffer: Option<GpuVertexBuffer>) {
108        let has_gpu = buffer.is_some();
109        self.marker_gpu_vertices = buffer;
110        if has_gpu {
111            self.marker_vertices = None;
112        }
113    }
114
115    fn drop_gpu(&mut self) {
116        self.gpu_vertices = None;
117        self.gpu_vertex_count = None;
118        self.gpu_bounds = None;
119        self.marker_gpu_vertices = None;
120    }
121    pub fn generate_vertices(&mut self) -> &Vec<Vertex> {
122        if self.gpu_vertices.is_some() {
123            if self.vertices.is_none() {
124                self.vertices = Some(Vec::new());
125            }
126            return self.vertices.as_ref().unwrap();
127        }
128        if self.dirty || self.vertices.is_none() {
129            let mut verts = Vec::new();
130            for i in 0..self.x.len().saturating_sub(1) {
131                let x0 = self.x[i] as f32;
132                let y0 = self.y[i] as f32;
133                let x1 = self.x[i + 1] as f32;
134                let y1 = self.y[i + 1] as f32;
135                if !x0.is_finite() || !y0.is_finite() || !x1.is_finite() || !y1.is_finite() {
136                    continue;
137                }
138                // Horizontal segment
139                verts.push(Vertex::new(Vec3::new(x0, y0, 0.0), self.color));
140                verts.push(Vertex::new(Vec3::new(x1, y0, 0.0), self.color));
141                // Vertical jump
142                verts.push(Vertex::new(Vec3::new(x1, y0, 0.0), self.color));
143                verts.push(Vertex::new(Vec3::new(x1, y1, 0.0), self.color));
144            }
145            self.vertices = Some(verts);
146            self.dirty = false;
147        }
148        self.vertices.as_ref().unwrap()
149    }
150    pub fn bounds(&mut self) -> BoundingBox {
151        if let Some(bounds) = self.gpu_bounds {
152            return bounds;
153        }
154        if self.dirty || self.bounds.is_none() {
155            let mut min = Vec3::new(f32::INFINITY, f32::INFINITY, 0.0);
156            let mut max = Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, 0.0);
157            for (&x, &y) in self.x.iter().zip(self.y.iter()) {
158                let (x, y) = (x as f32, y as f32);
159                if !x.is_finite() || !y.is_finite() {
160                    continue;
161                }
162                min.x = min.x.min(x);
163                max.x = max.x.max(x);
164                min.y = min.y.min(y);
165                max.y = max.y.max(y);
166            }
167            if !min.x.is_finite() {
168                min = Vec3::ZERO;
169                max = Vec3::ZERO;
170            }
171            self.bounds = Some(BoundingBox::new(min, max));
172        }
173        self.bounds.unwrap()
174    }
175    pub fn render_data(&mut self) -> RenderData {
176        let using_gpu = self.gpu_vertices.is_some();
177        let bounds = self.bounds();
178        let (vertices, vertex_count, gpu_vertices) = if using_gpu {
179            (
180                Vec::new(),
181                self.gpu_vertex_count.unwrap_or(0),
182                self.gpu_vertices.clone(),
183            )
184        } else {
185            let verts = self.generate_vertices().clone();
186            let count = verts.len();
187            (verts, count, None)
188        };
189        let material = Material {
190            albedo: self.color,
191            ..Default::default()
192        };
193        let draw_call = DrawCall {
194            vertex_offset: 0,
195            vertex_count,
196            index_offset: None,
197            index_count: None,
198            instance_count: 1,
199        };
200        RenderData {
201            pipeline_type: PipelineType::Lines,
202            vertices,
203            indices: None,
204            gpu_vertices,
205            bounds: Some(bounds),
206            material,
207            draw_calls: vec![draw_call],
208            image: None,
209        }
210    }
211
212    pub fn render_data_with_viewport(&mut self, viewport_px: Option<(u32, u32)>) -> RenderData {
213        if self.gpu_vertices.is_some() {
214            return self.render_data();
215        }
216
217        let bounds = self.bounds();
218        let (vertices, vertex_count, pipeline_type) = if self.line_width > 1.0 {
219            let viewport_px = viewport_px.unwrap_or((600, 400));
220            let data_per_px = crate::core::data_units_per_px(&bounds, viewport_px);
221            let width_data = self.line_width.max(0.1) * data_per_px;
222            let verts = self.generate_vertices().clone();
223            let mut thick = Vec::new();
224            for segment in verts.chunks_exact(2) {
225                let x = [segment[0].position[0] as f64, segment[1].position[0] as f64];
226                let y = [segment[0].position[1] as f64, segment[1].position[1] as f64];
227                let color = Vec4::from_array(segment[0].color);
228                thick.extend(vertex_utils::create_thick_polyline(
229                    &x, &y, color, width_data,
230                ));
231            }
232            let count = thick.len();
233            (thick, count, PipelineType::Triangles)
234        } else {
235            let verts = self.generate_vertices().clone();
236            let count = verts.len();
237            (verts, count, PipelineType::Lines)
238        };
239        let material = Material {
240            albedo: self.color,
241            roughness: self.line_width.max(0.0),
242            ..Default::default()
243        };
244        let draw_call = DrawCall {
245            vertex_offset: 0,
246            vertex_count,
247            index_offset: None,
248            index_count: None,
249            instance_count: 1,
250        };
251        RenderData {
252            pipeline_type,
253            vertices,
254            indices: None,
255            gpu_vertices: None,
256            bounds: Some(bounds),
257            material,
258            draw_calls: vec![draw_call],
259            image: None,
260        }
261    }
262
263    pub fn marker_render_data(&mut self) -> Option<RenderData> {
264        let marker = self.marker.clone()?;
265        if let Some(gpu_vertices) = self.marker_gpu_vertices.clone() {
266            let vertex_count = gpu_vertices.vertex_count;
267            if vertex_count == 0 {
268                return None;
269            }
270            let draw_call = DrawCall {
271                vertex_offset: 0,
272                vertex_count,
273                index_offset: None,
274                index_count: None,
275                instance_count: 1,
276            };
277            let material = Self::marker_material(&marker);
278            return Some(RenderData {
279                pipeline_type: PipelineType::Points,
280                vertices: Vec::new(),
281                indices: None,
282                gpu_vertices: Some(gpu_vertices),
283                bounds: None,
284                material,
285                draw_calls: vec![draw_call],
286                image: None,
287            });
288        }
289
290        let vertices = self.marker_vertices_slice(&marker)?;
291        if vertices.is_empty() {
292            return None;
293        }
294        let draw_call = DrawCall {
295            vertex_offset: 0,
296            vertex_count: vertices.len(),
297            index_offset: None,
298            index_count: None,
299            instance_count: 1,
300        };
301        let material = Self::marker_material(&marker);
302        Some(RenderData {
303            pipeline_type: PipelineType::Points,
304            vertices: vertices.to_vec(),
305            indices: None,
306            gpu_vertices: None,
307            bounds: None,
308            material,
309            draw_calls: vec![draw_call],
310            image: None,
311        })
312    }
313
314    fn marker_material(marker: &LineMarkerAppearance) -> Material {
315        let mut material = Material {
316            albedo: marker.face_color,
317            ..Default::default()
318        };
319        if !marker.filled {
320            material.albedo.w = 0.0;
321        }
322        material.emissive = marker.edge_color;
323        material.roughness = 1.0;
324        material.metallic = 0.0;
325        material.alpha_mode = AlphaMode::Blend;
326        material
327    }
328
329    fn marker_vertices_slice(&mut self, marker: &LineMarkerAppearance) -> Option<&[Vertex]> {
330        if self.x.len() != self.y.len() || self.x.is_empty() {
331            return None;
332        }
333        if self.marker_vertices.is_none() || self.marker_dirty {
334            let mut verts = Vec::with_capacity(self.x.len());
335            for (&x, &y) in self.x.iter().zip(self.y.iter()) {
336                let mut vertex = Vertex::new(Vec3::new(x as f32, y as f32, 0.0), marker.face_color);
337                vertex.normal[2] = marker.size.max(1.0);
338                verts.push(vertex);
339            }
340            self.marker_vertices = Some(verts);
341            self.marker_dirty = false;
342        }
343        self.marker_vertices.as_deref()
344    }
345    pub fn estimated_memory_usage(&self) -> usize {
346        self.vertices
347            .as_ref()
348            .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>())
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn thick_stairs_use_viewport_aware_triangles() {
358        let mut plot = StairsPlot::new(vec![0.0, 1.0, 2.0], vec![1.0, 2.0, 1.5])
359            .unwrap()
360            .with_style(Vec4::ONE, 2.0);
361        let render = plot.render_data_with_viewport(Some((600, 400)));
362        assert_eq!(render.pipeline_type, PipelineType::Triangles);
363        assert!(!render.vertices.is_empty());
364    }
365}