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