Skip to main content

runmat_plot/plots/
line3.rs

1use crate::core::{
2    BoundingBox, DrawCall, GpuVertexBuffer, Material, PipelineType, RenderData, Vertex,
3};
4use glam::{Vec3, Vec4};
5
6#[derive(Debug, Clone)]
7pub struct Line3Plot {
8    pub x_data: Vec<f64>,
9    pub y_data: Vec<f64>,
10    pub z_data: Vec<f64>,
11    pub color: Vec4,
12    pub line_width: f32,
13    pub line_style: crate::plots::line::LineStyle,
14    pub label: Option<String>,
15    pub visible: bool,
16    vertices: Option<Vec<Vertex>>,
17    bounds: Option<BoundingBox>,
18    dirty: bool,
19    pub gpu_vertices: Option<GpuVertexBuffer>,
20    pub gpu_vertex_count: Option<usize>,
21}
22
23impl Line3Plot {
24    pub fn new(x_data: Vec<f64>, y_data: Vec<f64>, z_data: Vec<f64>) -> Result<Self, String> {
25        if x_data.len() != y_data.len() || x_data.len() != z_data.len() {
26            return Err("Data length mismatch for plot3".to_string());
27        }
28        if x_data.is_empty() {
29            return Err("plot3 requires at least one point".to_string());
30        }
31        Ok(Self {
32            x_data,
33            y_data,
34            z_data,
35            color: Vec4::new(0.0, 0.5, 1.0, 1.0),
36            line_width: 1.0,
37            line_style: crate::plots::line::LineStyle::Solid,
38            label: None,
39            visible: true,
40            vertices: None,
41            bounds: None,
42            dirty: true,
43            gpu_vertices: None,
44            gpu_vertex_count: None,
45        })
46    }
47
48    pub fn from_gpu_buffer(
49        buffer: GpuVertexBuffer,
50        vertex_count: usize,
51        color: Vec4,
52        line_width: f32,
53        line_style: crate::plots::line::LineStyle,
54        bounds: BoundingBox,
55    ) -> Self {
56        Self {
57            x_data: Vec::new(),
58            y_data: Vec::new(),
59            z_data: Vec::new(),
60            color,
61            line_width,
62            line_style,
63            label: None,
64            visible: true,
65            vertices: None,
66            bounds: Some(bounds),
67            dirty: false,
68            gpu_vertices: Some(buffer),
69            gpu_vertex_count: Some(vertex_count),
70        }
71    }
72
73    pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
74        self.label = Some(label.into());
75        self
76    }
77
78    pub fn with_style(
79        mut self,
80        color: Vec4,
81        line_width: f32,
82        line_style: crate::plots::line::LineStyle,
83    ) -> Self {
84        self.color = color;
85        self.line_width = line_width;
86        self.line_style = line_style;
87        self.dirty = true;
88        self.gpu_vertices = None;
89        self.gpu_vertex_count = None;
90        self
91    }
92
93    pub fn set_visible(&mut self, visible: bool) {
94        self.visible = visible;
95    }
96
97    fn generate_vertices(&mut self) -> &Vec<Vertex> {
98        if self.gpu_vertices.is_some() {
99            if self.vertices.is_none() {
100                self.vertices = Some(Vec::new());
101            }
102            return self.vertices.as_ref().unwrap();
103        }
104        if self.dirty || self.vertices.is_none() {
105            let points: Vec<Vec3> = self
106                .x_data
107                .iter()
108                .zip(self.y_data.iter())
109                .zip(self.z_data.iter())
110                .map(|((&x, &y), &z)| Vec3::new(x as f32, y as f32, z as f32))
111                .collect();
112            let vertices = if points.len() == 1 {
113                let mut vertex = Vertex::new(points[0], self.color);
114                vertex.normal[2] = (self.line_width.max(1.0) * 4.0).max(6.0);
115                vec![vertex]
116            } else if self.line_width > 1.0 {
117                create_thick_polyline3_dashed(&points, self.color, self.line_width, self.line_style)
118            } else {
119                create_line3_vertices_dashed(&points, self.color, self.line_style)
120            };
121            self.vertices = Some(vertices);
122            self.dirty = false;
123        }
124        self.vertices.as_ref().unwrap()
125    }
126
127    pub fn bounds(&mut self) -> BoundingBox {
128        if self.bounds.is_some() && self.x_data.is_empty() {
129            return self.bounds.unwrap();
130        }
131        if self.bounds.is_none() || self.dirty {
132            let points: Vec<Vec3> = self
133                .x_data
134                .iter()
135                .zip(self.y_data.iter())
136                .zip(self.z_data.iter())
137                .map(|((&x, &y), &z)| Vec3::new(x as f32, y as f32, z as f32))
138                .collect();
139            self.bounds = Some(BoundingBox::from_points(&points));
140        }
141        self.bounds.unwrap()
142    }
143
144    pub fn render_data(&mut self) -> RenderData {
145        let single_point = self.x_data.len() == 1 || self.gpu_vertex_count == Some(1);
146        let vertex_count = self
147            .gpu_vertex_count
148            .unwrap_or_else(|| self.generate_vertices().len());
149        let thick = self.line_width > 1.0 && !single_point;
150        RenderData {
151            pipeline_type: if single_point {
152                PipelineType::Scatter3
153            } else if thick {
154                PipelineType::Triangles
155            } else {
156                PipelineType::Lines
157            },
158            vertices: if self.gpu_vertices.is_some() {
159                Vec::new()
160            } else {
161                self.generate_vertices().clone()
162            },
163            indices: None,
164            gpu_vertices: self.gpu_vertices.clone(),
165            bounds: Some(self.bounds()),
166            material: Material {
167                albedo: self.color,
168                roughness: self.line_width.max(0.5),
169                ..Default::default()
170            },
171            draw_calls: vec![DrawCall {
172                vertex_offset: 0,
173                vertex_count,
174                index_offset: None,
175                index_count: None,
176                instance_count: 1,
177            }],
178            image: None,
179        }
180    }
181
182    pub fn estimated_memory_usage(&self) -> usize {
183        self.vertices
184            .as_ref()
185            .map(|v| v.len() * std::mem::size_of::<Vertex>())
186            .unwrap_or(0)
187    }
188}
189
190fn create_line3_vertices_dashed(
191    points: &[Vec3],
192    color: Vec4,
193    style: crate::plots::line::LineStyle,
194) -> Vec<Vertex> {
195    let mut vertices = Vec::new();
196    for i in 1..points.len() {
197        let include = match style {
198            crate::plots::line::LineStyle::Solid => true,
199            crate::plots::line::LineStyle::Dashed => (i % 4) < 2,
200            crate::plots::line::LineStyle::Dotted => (i % 4) == 0,
201            crate::plots::line::LineStyle::DashDot => {
202                let m = i % 6;
203                m < 2 || m == 3
204            }
205        };
206        if include {
207            vertices.push(Vertex::new(points[i - 1], color));
208            vertices.push(Vertex::new(points[i], color));
209        }
210    }
211    vertices
212}
213
214fn create_thick_polyline3_dashed(
215    points: &[Vec3],
216    color: Vec4,
217    width: f32,
218    style: crate::plots::line::LineStyle,
219) -> Vec<Vertex> {
220    let mut out = Vec::new();
221    for i in 0..points.len().saturating_sub(1) {
222        let include = match style {
223            crate::plots::line::LineStyle::Solid => true,
224            crate::plots::line::LineStyle::Dashed => (i % 4) < 2,
225            crate::plots::line::LineStyle::Dotted => (i % 4) == 0,
226            crate::plots::line::LineStyle::DashDot => {
227                let m = i % 6;
228                m < 2 || m == 3
229            }
230        };
231        if !include {
232            continue;
233        }
234        out.extend(extrude_segment_3d(
235            points[i],
236            points[i + 1],
237            color,
238            width.max(0.5) * 0.01,
239        ));
240    }
241    out
242}
243
244fn extrude_segment_3d(start: Vec3, end: Vec3, color: Vec4, half_width: f32) -> Vec<Vertex> {
245    let dir = (end - start).normalize_or_zero();
246    let mut normal = dir.cross(Vec3::Z);
247    if normal.length_squared() < 1e-6 {
248        normal = dir.cross(Vec3::X);
249    }
250    let normal = normal.normalize_or_zero() * half_width;
251    let v0 = start + normal;
252    let v1 = end + normal;
253    let v2 = end - normal;
254    let v3 = start - normal;
255    vec![
256        Vertex::new(v0, color),
257        Vertex::new(v1, color),
258        Vertex::new(v2, color),
259        Vertex::new(v0, color),
260        Vertex::new(v2, color),
261        Vertex::new(v3, color),
262    ]
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn line3_creation_and_bounds() {
271        let mut plot = Line3Plot::new(vec![0.0, 1.0], vec![1.0, 2.0], vec![2.0, 3.0]).unwrap();
272        let bounds = plot.bounds();
273        assert_eq!(bounds.min, Vec3::new(0.0, 1.0, 2.0));
274        assert_eq!(bounds.max, Vec3::new(1.0, 2.0, 3.0));
275    }
276
277    #[test]
278    fn line3_dashed_and_thick_generate_geometry() {
279        let mut plot = Line3Plot::new(
280            vec![0.0, 1.0, 2.0],
281            vec![0.0, 1.0, 0.0],
282            vec![0.0, 0.0, 1.0],
283        )
284        .unwrap()
285        .with_style(Vec4::ONE, 3.0, crate::plots::line::LineStyle::Dashed);
286        let render = plot.render_data();
287        assert_eq!(render.pipeline_type, PipelineType::Triangles);
288        assert!(!render.vertices.is_empty());
289        assert!(render.draw_calls[0].vertex_count >= 2);
290    }
291
292    #[test]
293    fn line3_single_point_uses_scatter_pipeline() {
294        let mut plot = Line3Plot::new(vec![1.0], vec![2.0], vec![3.0])
295            .unwrap()
296            .with_style(Vec4::ONE, 2.0, crate::plots::line::LineStyle::Solid);
297        let render = plot.render_data();
298        assert_eq!(render.pipeline_type, PipelineType::Scatter3);
299        assert_eq!(render.vertices.len(), 1);
300        assert!(render.vertices[0].normal[2] >= 6.0);
301    }
302}