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