1use 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 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 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 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}