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}