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 Some(viewport_px) = viewport_px else {
220                return self.render_data();
221            };
222            let data_per_px = crate::core::data_units_per_px(&bounds, viewport_px);
223            let width_data = self.line_width.max(0.1) * data_per_px;
224            let verts = self.generate_vertices().clone();
225            let mut thick = Vec::new();
226            for segment in verts.chunks_exact(2) {
227                let x = [segment[0].position[0] as f64, segment[1].position[0] as f64];
228                let y = [segment[0].position[1] as f64, segment[1].position[1] as f64];
229                let color = Vec4::from_array(segment[0].color);
230                thick.extend(vertex_utils::create_thick_polyline(
231                    &x, &y, color, width_data,
232                ));
233            }
234            let count = thick.len();
235            (thick, count, PipelineType::Triangles)
236        } else {
237            let verts = self.generate_vertices().clone();
238            let count = verts.len();
239            (verts, count, PipelineType::Lines)
240        };
241        let material = Material {
242            albedo: self.color,
243            roughness: self.line_width.max(0.0),
244            ..Default::default()
245        };
246        let draw_call = DrawCall {
247            vertex_offset: 0,
248            vertex_count,
249            index_offset: None,
250            index_count: None,
251            instance_count: 1,
252        };
253        RenderData {
254            pipeline_type,
255            vertices,
256            indices: None,
257            gpu_vertices: None,
258            bounds: Some(bounds),
259            material,
260            draw_calls: vec![draw_call],
261            image: None,
262        }
263    }
264
265    pub fn marker_render_data(&mut self) -> Option<RenderData> {
266        let marker = self.marker.clone()?;
267        if let Some(gpu_vertices) = self.marker_gpu_vertices.clone() {
268            let vertex_count = gpu_vertices.vertex_count;
269            if vertex_count == 0 {
270                return None;
271            }
272            let draw_call = DrawCall {
273                vertex_offset: 0,
274                vertex_count,
275                index_offset: None,
276                index_count: None,
277                instance_count: 1,
278            };
279            let material = Self::marker_material(&marker);
280            return Some(RenderData {
281                pipeline_type: PipelineType::Points,
282                vertices: Vec::new(),
283                indices: None,
284                gpu_vertices: Some(gpu_vertices),
285                bounds: None,
286                material,
287                draw_calls: vec![draw_call],
288                image: None,
289            });
290        }
291
292        let vertices = self.marker_vertices_slice(&marker)?;
293        if vertices.is_empty() {
294            return None;
295        }
296        let draw_call = DrawCall {
297            vertex_offset: 0,
298            vertex_count: vertices.len(),
299            index_offset: None,
300            index_count: None,
301            instance_count: 1,
302        };
303        let material = Self::marker_material(&marker);
304        Some(RenderData {
305            pipeline_type: PipelineType::Points,
306            vertices: vertices.to_vec(),
307            indices: None,
308            gpu_vertices: None,
309            bounds: None,
310            material,
311            draw_calls: vec![draw_call],
312            image: None,
313        })
314    }
315
316    fn marker_material(marker: &LineMarkerAppearance) -> Material {
317        let mut material = Material {
318            albedo: marker.face_color,
319            ..Default::default()
320        };
321        if !marker.filled {
322            material.albedo.w = 0.0;
323        }
324        material.emissive = marker.edge_color;
325        material.roughness = 1.0;
326        material.metallic = 0.0;
327        material.alpha_mode = AlphaMode::Blend;
328        material
329    }
330
331    fn marker_vertices_slice(&mut self, marker: &LineMarkerAppearance) -> Option<&[Vertex]> {
332        if self.x.len() != self.y.len() || self.x.is_empty() {
333            return None;
334        }
335        if self.marker_vertices.is_none() || self.marker_dirty {
336            let mut verts = Vec::with_capacity(self.x.len());
337            for (&x, &y) in self.x.iter().zip(self.y.iter()) {
338                let mut vertex = Vertex::new(Vec3::new(x as f32, y as f32, 0.0), marker.face_color);
339                vertex.normal[2] = marker.size.max(1.0);
340                verts.push(vertex);
341            }
342            self.marker_vertices = Some(verts);
343            self.marker_dirty = false;
344        }
345        self.marker_vertices.as_deref()
346    }
347    pub fn estimated_memory_usage(&self) -> usize {
348        self.vertices
349            .as_ref()
350            .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>())
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    #[test]
359    fn thick_stairs_use_viewport_aware_triangles() {
360        let mut plot = StairsPlot::new(vec![0.0, 1.0, 2.0], vec![1.0, 2.0, 1.5])
361            .unwrap()
362            .with_style(Vec4::ONE, 2.0);
363        let render = plot.render_data_with_viewport(Some((600, 400)));
364        assert_eq!(render.pipeline_type, PipelineType::Triangles);
365        assert!(!render.vertices.is_empty());
366    }
367}