Skip to main content

runmat_plot/plots/
line3.rs

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