Skip to main content

runmat_plot/plots/
line3.rs

1use crate::core::{
2    BoundingBox, DrawCall, GpuPackContext, GpuVertexBuffer, Material, PipelineType, RenderData,
3    Vertex,
4};
5use crate::geometry::stroke3d::{
6    create_line_vertices_dashed, tessellate_polyline_tube, StrokeCap3D, StrokeStyle3D,
7};
8use crate::gpu::line3::{Line3GpuInputs, Line3GpuParams};
9use glam::{Vec3, Vec4};
10use log::warn;
11
12const POINTS_TO_PX: f32 = 96.0 / 72.0;
13const TUBE_RADIAL_SEGMENTS: usize = 8;
14
15#[derive(Debug, Clone)]
16pub struct Line3Plot {
17    pub x_data: Vec<f64>,
18    pub y_data: Vec<f64>,
19    pub z_data: Vec<f64>,
20    pub color: Vec4,
21    pub line_width: f32,
22    pub line_style: crate::plots::line::LineStyle,
23    pub label: Option<String>,
24    pub visible: bool,
25    vertices: Option<Vec<Vertex>>,
26    bounds: Option<BoundingBox>,
27    dirty: bool,
28    pub gpu_vertices: Option<GpuVertexBuffer>,
29    pub gpu_vertex_count: Option<usize>,
30    gpu_line_inputs: Option<Line3GpuInputs>,
31}
32
33impl Line3Plot {
34    #[inline]
35    fn line_width_px(&self) -> f32 {
36        (self.line_width.max(0.1)) * POINTS_TO_PX
37    }
38
39    pub fn new(x_data: Vec<f64>, y_data: Vec<f64>, z_data: Vec<f64>) -> Result<Self, String> {
40        if x_data.len() != y_data.len() || x_data.len() != z_data.len() {
41            return Err("Data length mismatch for plot3".to_string());
42        }
43        if x_data.is_empty() {
44            return Err("plot3 requires at least one point".to_string());
45        }
46        Ok(Self {
47            x_data,
48            y_data,
49            z_data,
50            color: Vec4::new(0.0, 0.5, 1.0, 1.0),
51            line_width: 1.0,
52            line_style: crate::plots::line::LineStyle::Solid,
53            label: None,
54            visible: true,
55            vertices: None,
56            bounds: None,
57            dirty: true,
58            gpu_vertices: None,
59            gpu_vertex_count: None,
60            gpu_line_inputs: None,
61        })
62    }
63
64    pub fn from_gpu_buffer(
65        buffer: GpuVertexBuffer,
66        vertex_count: usize,
67        color: Vec4,
68        line_width: f32,
69        line_style: crate::plots::line::LineStyle,
70        bounds: BoundingBox,
71    ) -> Self {
72        Self {
73            x_data: Vec::new(),
74            y_data: Vec::new(),
75            z_data: Vec::new(),
76            color,
77            line_width,
78            line_style,
79            label: None,
80            visible: true,
81            vertices: None,
82            bounds: Some(bounds),
83            dirty: false,
84            gpu_vertices: Some(buffer),
85            gpu_vertex_count: Some(vertex_count),
86            gpu_line_inputs: None,
87        }
88    }
89
90    pub fn from_gpu_xyz(
91        inputs: Line3GpuInputs,
92        color: Vec4,
93        line_width: f32,
94        line_style: crate::plots::line::LineStyle,
95        bounds: BoundingBox,
96    ) -> Self {
97        Self {
98            x_data: Vec::new(),
99            y_data: Vec::new(),
100            z_data: Vec::new(),
101            color,
102            line_width,
103            line_style,
104            label: None,
105            visible: true,
106            vertices: None,
107            bounds: Some(bounds),
108            dirty: false,
109            gpu_vertices: None,
110            gpu_vertex_count: None,
111            gpu_line_inputs: Some(inputs),
112        }
113    }
114
115    pub fn with_gpu_xyz_inputs(mut self, inputs: Line3GpuInputs, bounds: BoundingBox) -> Self {
116        self.gpu_line_inputs = Some(inputs);
117        self.bounds = Some(bounds);
118        self
119    }
120
121    pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
122        self.label = Some(label.into());
123        self
124    }
125
126    pub fn with_style(
127        mut self,
128        color: Vec4,
129        line_width: f32,
130        line_style: crate::plots::line::LineStyle,
131    ) -> Self {
132        self.color = color;
133        self.line_width = line_width;
134        self.line_style = line_style;
135        self.dirty = true;
136        self.gpu_vertices = None;
137        self.gpu_vertex_count = None;
138        self
139    }
140
141    pub fn set_visible(&mut self, visible: bool) {
142        self.visible = visible;
143    }
144
145    fn generate_vertices(&mut self) -> &Vec<Vertex> {
146        if self.gpu_vertices.is_some() {
147            if self.vertices.is_none() {
148                self.vertices = Some(Vec::new());
149            }
150            return self.vertices.as_ref().unwrap();
151        }
152        if self.dirty || self.vertices.is_none() {
153            let points: Vec<Vec3> = self
154                .x_data
155                .iter()
156                .zip(self.y_data.iter())
157                .zip(self.z_data.iter())
158                .map(|((&x, &y), &z)| Vec3::new(x as f32, y as f32, z as f32))
159                .collect();
160            let vertices = if points.len() == 1 {
161                let mut vertex = Vertex::new(points[0], self.color);
162                vertex.normal[2] = (self.line_width_px().max(1.0) * 4.0).max(6.0);
163                vec![vertex]
164            } else if self.line_width_px() > 1.0 {
165                // No viewport hint: interpret width in data units for legacy/non-viewport paths.
166                let fallback_half_width_data = self.line_width_px() * 0.5;
167                tessellate_polyline_tube(
168                    &points,
169                    self.color,
170                    StrokeStyle3D::new(
171                        fallback_half_width_data,
172                        self.line_style,
173                        StrokeCap3D::Square,
174                    ),
175                    TUBE_RADIAL_SEGMENTS,
176                )
177            } else {
178                create_line_vertices_dashed(&points, self.color, self.line_style)
179            };
180            self.vertices = Some(vertices);
181            self.dirty = false;
182        }
183        self.vertices.as_ref().unwrap()
184    }
185
186    pub fn bounds(&mut self) -> BoundingBox {
187        if self.bounds.is_some() && self.x_data.is_empty() {
188            return self.bounds.unwrap();
189        }
190        if self.bounds.is_none() || self.dirty {
191            let points: Vec<Vec3> = self
192                .x_data
193                .iter()
194                .zip(self.y_data.iter())
195                .zip(self.z_data.iter())
196                .map(|((&x, &y), &z)| Vec3::new(x as f32, y as f32, z as f32))
197                .collect();
198            self.bounds = Some(BoundingBox::from_points(&points));
199        }
200        self.bounds.unwrap()
201    }
202
203    pub fn render_data(&mut self) -> RenderData {
204        let single_point = self.x_data.len() == 1 || self.gpu_vertex_count == Some(1);
205        let vertex_count = self
206            .gpu_vertex_count
207            .unwrap_or_else(|| self.generate_vertices().len());
208        let width_px = self.line_width_px();
209        let thick = width_px > 1.0 && !single_point;
210        let indices = if self.gpu_vertices.is_none() && thick {
211            Some((0..vertex_count as u32).collect::<Vec<u32>>())
212        } else {
213            None
214        };
215        RenderData {
216            pipeline_type: if single_point {
217                PipelineType::Scatter3
218            } else if thick {
219                PipelineType::Triangles
220            } else {
221                PipelineType::Lines
222            },
223            vertices: if self.gpu_vertices.is_some() {
224                Vec::new()
225            } else {
226                self.generate_vertices().clone()
227            },
228            indices,
229            gpu_vertices: self.gpu_vertices.clone(),
230            bounds: Some(self.bounds()),
231            material: Material {
232                albedo: self.color,
233                roughness: width_px.max(0.5),
234                ..Default::default()
235            },
236            draw_calls: vec![DrawCall {
237                vertex_offset: 0,
238                vertex_count,
239                index_offset: None,
240                index_count: None,
241                instance_count: 1,
242            }],
243            image: None,
244        }
245    }
246
247    fn pack_gpu_vertices_if_needed(
248        &mut self,
249        gpu: &GpuPackContext<'_>,
250        viewport_px: (u32, u32),
251    ) -> Result<(), String> {
252        if self.gpu_vertices.is_some() {
253            return Ok(());
254        }
255        let Some(inputs) = self.gpu_line_inputs.as_ref() else {
256            return Ok(());
257        };
258        let bounds = self
259            .bounds
260            .as_ref()
261            .ok_or_else(|| "plot3: missing bounds for GPU packing".to_string())?;
262        let width_px = self.line_width_px();
263        let thick_px = width_px > 1.0;
264        let data_per_px = crate::core::data_units_per_px_3d(bounds, viewport_px);
265        let half_width_data = if thick_px {
266            (width_px * 0.5) * data_per_px
267        } else {
268            0.0
269        };
270        let packed = crate::gpu::line3::pack_vertices_from_xyz(
271            gpu.device,
272            gpu.queue,
273            inputs,
274            &Line3GpuParams {
275                color: self.color,
276                half_width_data,
277                thick: thick_px,
278                line_style: self.line_style,
279            },
280        )?;
281        self.gpu_vertex_count =
282            Some((inputs.len.saturating_sub(1) as usize) * if thick_px { 6 } else { 2 });
283        self.gpu_vertices = Some(packed);
284        Ok(())
285    }
286
287    pub fn render_data_with_viewport_gpu(
288        &mut self,
289        viewport_px: Option<(u32, u32)>,
290        view_angles_deg: Option<(f32, f32)>,
291        gpu: Option<&GpuPackContext<'_>>,
292    ) -> RenderData {
293        let can_gpu_pack = self.line_width_px() <= 1.0;
294        if can_gpu_pack && self.gpu_line_inputs.is_some() && self.gpu_vertices.is_none() {
295            if let (Some(gpu), Some(vp)) = (gpu, viewport_px) {
296                if let Err(err) = self.pack_gpu_vertices_if_needed(gpu, vp) {
297                    warn!("plot3 gpu pack failed: {err}");
298                }
299            }
300        }
301        self.render_data_with_viewport_and_view(viewport_px, view_angles_deg)
302    }
303
304    pub fn render_data_with_viewport(&mut self, viewport_px: Option<(u32, u32)>) -> RenderData {
305        self.render_data_with_viewport_and_view(viewport_px, None)
306    }
307
308    pub fn render_data_with_viewport_and_view(
309        &mut self,
310        viewport_px: Option<(u32, u32)>,
311        view_angles_deg: Option<(f32, f32)>,
312    ) -> RenderData {
313        if self.gpu_vertices.is_some() {
314            return self.render_data();
315        }
316
317        let single_point = self.x_data.len() == 1;
318        let width_px = self.line_width_px();
319        let (vertices, vertex_count, pipeline) = if !single_point && width_px > 1.0 {
320            let Some(vp) = viewport_px else {
321                return self.render_data();
322            };
323            let points: Vec<Vec3> = self
324                .x_data
325                .iter()
326                .zip(self.y_data.iter())
327                .zip(self.z_data.iter())
328                .map(|((&x, &y), &z)| Vec3::new(x as f32, y as f32, z as f32))
329                .collect();
330            let bounds = self.bounds();
331            let data_per_px =
332                crate::core::data_units_per_px_3d_camera(&bounds, vp, view_angles_deg);
333            let half_width_data = (width_px * 0.5) * data_per_px;
334            let tris = tessellate_polyline_tube(
335                &points,
336                self.color,
337                StrokeStyle3D::new(half_width_data, self.line_style, StrokeCap3D::Square),
338                TUBE_RADIAL_SEGMENTS,
339            );
340            let count = tris.len();
341            (tris, count, PipelineType::Triangles)
342        } else {
343            let verts = self.generate_vertices().clone();
344            let count = verts.len();
345            let pipeline = if single_point {
346                PipelineType::Scatter3
347            } else {
348                PipelineType::Lines
349            };
350            (verts, count, pipeline)
351        };
352
353        let indices = if pipeline == PipelineType::Triangles {
354            Some((0..vertex_count as u32).collect::<Vec<u32>>())
355        } else {
356            None
357        };
358
359        RenderData {
360            pipeline_type: pipeline,
361            vertices,
362            indices,
363            gpu_vertices: None,
364            bounds: Some(self.bounds()),
365            material: Material {
366                albedo: self.color,
367                roughness: width_px.max(0.5),
368                ..Default::default()
369            },
370            draw_calls: vec![DrawCall {
371                vertex_offset: 0,
372                vertex_count,
373                index_offset: None,
374                index_count: None,
375                instance_count: 1,
376            }],
377            image: None,
378        }
379    }
380
381    pub fn estimated_memory_usage(&self) -> usize {
382        self.vertices
383            .as_ref()
384            .map(|v| v.len() * std::mem::size_of::<Vertex>())
385            .unwrap_or(0)
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn line3_creation_and_bounds() {
395        let mut plot = Line3Plot::new(vec![0.0, 1.0], vec![1.0, 2.0], vec![2.0, 3.0]).unwrap();
396        let bounds = plot.bounds();
397        assert_eq!(bounds.min, Vec3::new(0.0, 1.0, 2.0));
398        assert_eq!(bounds.max, Vec3::new(1.0, 2.0, 3.0));
399    }
400
401    #[test]
402    fn line3_dashed_and_thick_generate_geometry() {
403        let mut plot = Line3Plot::new(
404            vec![0.0, 1.0, 2.0],
405            vec![0.0, 1.0, 0.0],
406            vec![0.0, 0.0, 1.0],
407        )
408        .unwrap()
409        .with_style(Vec4::ONE, 3.0, crate::plots::line::LineStyle::Dashed);
410        let render = plot.render_data();
411        assert_eq!(render.pipeline_type, PipelineType::Triangles);
412        assert!(!render.vertices.is_empty());
413        assert!(render.draw_calls[0].vertex_count >= 2);
414    }
415
416    #[test]
417    fn line3_single_point_uses_scatter_pipeline() {
418        let mut plot = Line3Plot::new(vec![1.0], vec![2.0], vec![3.0])
419            .unwrap()
420            .with_style(Vec4::ONE, 2.0, crate::plots::line::LineStyle::Solid);
421        let render = plot.render_data();
422        assert_eq!(render.pipeline_type, PipelineType::Scatter3);
423        assert_eq!(render.vertices.len(), 1);
424        assert!(render.vertices[0].normal[2] >= 6.0);
425    }
426}