1use crate::context::shared_wgpu_context;
6use crate::core::{
7 vertex_utils, AlphaMode, BoundingBox, DrawCall, GpuPackContext, GpuVertexBuffer, Material,
8 PipelineType, RenderData, Vertex,
9};
10use crate::gpu::line::LineGpuInputs;
11use crate::gpu::util::readback_scalar_buffer_f64;
12use crate::plots::scatter::MarkerStyle as ScatterMarkerStyle;
13use glam::{Vec3, Vec4};
14use log::{trace, warn};
15
16#[derive(Debug, Clone)]
18pub struct LinePlot {
19 pub x_data: Vec<f64>,
21 pub y_data: Vec<f64>,
22
23 pub color: Vec4,
25 pub line_width: f32,
26 pub line_style: LineStyle,
27 pub line_join: LineJoin,
28 pub line_cap: LineCap,
29 pub marker: Option<LineMarkerAppearance>,
30
31 pub label: Option<String>,
33 pub visible: bool,
34
35 vertices: Option<Vec<Vertex>>,
37 bounds: Option<BoundingBox>,
38 dirty: bool,
39 gpu_vertices: Option<GpuVertexBuffer>,
40 gpu_vertex_count: Option<usize>,
41 gpu_line_inputs: Option<LineGpuInputs>,
42 marker_vertices: Option<Vec<Vertex>>,
43 marker_gpu_vertices: Option<GpuVertexBuffer>,
44 marker_dirty: bool,
45 gpu_topology: Option<PipelineType>,
46 gpu_pack_viewport_px: Option<(u32, u32)>,
47 gpu_pack_view_bounds: Option<(f32, f32, f32, f32)>,
48}
49
50#[derive(Debug, Clone)]
51pub struct LineMarkerAppearance {
52 pub kind: ScatterMarkerStyle,
53 pub size: f32,
54 pub edge_color: Vec4,
55 pub face_color: Vec4,
56 pub filled: bool,
57}
58
59#[derive(Debug, Clone)]
60pub struct LineGpuStyle {
61 pub color: Vec4,
62 pub line_width: f32,
63 pub line_style: LineStyle,
64 pub marker: Option<LineMarkerAppearance>,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum LineStyle {
70 Solid,
71 Dashed,
72 Dotted,
73 DashDot,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum LineJoin {
79 Miter,
80 Bevel,
81 Round,
82}
83
84impl Default for LineJoin {
85 fn default() -> Self {
86 Self::Miter
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum LineCap {
93 Butt,
94 Square,
95 Round,
96}
97
98impl Default for LineCap {
99 fn default() -> Self {
100 Self::Butt
101 }
102}
103
104impl Default for LineStyle {
105 fn default() -> Self {
106 Self::Solid
107 }
108}
109
110impl LinePlot {
111 pub(crate) fn has_gpu_line_inputs(&self) -> bool {
112 self.gpu_line_inputs.is_some()
113 }
114
115 pub(crate) fn has_gpu_vertices(&self) -> bool {
116 self.gpu_vertices.is_some()
117 }
118
119 pub async fn export_scene_xy_data(&self) -> Result<(Vec<f64>, Vec<f64>), String> {
120 if !self.x_data.is_empty() && self.x_data.len() == self.y_data.len() {
121 return Ok((self.x_data.clone(), self.y_data.clone()));
122 }
123 if !self.x_data.is_empty() || !self.y_data.is_empty() {
124 return Err(format!(
125 "line plot has partial CPU source data: x has {} values, y has {} values",
126 self.x_data.len(),
127 self.y_data.len()
128 ));
129 }
130
131 if let Some(inputs) = &self.gpu_line_inputs {
132 let context = shared_wgpu_context().ok_or_else(|| {
133 "line plot has GPU source data but no shared WGPU context is installed".to_string()
134 })?;
135 let len = inputs.len as usize;
136 let x = readback_scalar_buffer_f64(
137 &context.device,
138 &context.queue,
139 &inputs.x_buffer,
140 len,
141 inputs.scalar,
142 )
143 .await?;
144 let y = readback_scalar_buffer_f64(
145 &context.device,
146 &context.queue,
147 &inputs.y_buffer,
148 len,
149 inputs.scalar,
150 )
151 .await?;
152 return Ok((x, y));
153 }
154
155 if self.gpu_vertices.is_some() {
156 return Err(
157 "line plot has GPU render vertices but no exportable source data".to_string(),
158 );
159 }
160
161 Ok((Vec::new(), Vec::new()))
162 }
163
164 pub fn new(x_data: Vec<f64>, y_data: Vec<f64>) -> Result<Self, String> {
166 if x_data.len() != y_data.len() {
167 return Err(format!(
168 "Data length mismatch: x_data has {} points, y_data has {} points",
169 x_data.len(),
170 y_data.len()
171 ));
172 }
173
174 Ok(Self {
175 x_data,
176 y_data,
177 color: Vec4::new(0.0, 0.5, 1.0, 1.0), line_width: 1.0,
179 line_style: LineStyle::default(),
180 line_join: LineJoin::default(),
181 line_cap: LineCap::default(),
182 marker: None,
183 label: None,
184 visible: true,
185 vertices: None,
186 bounds: None,
187 dirty: true,
188 gpu_vertices: None,
189 gpu_vertex_count: None,
190 gpu_line_inputs: None,
191 marker_vertices: None,
192 marker_gpu_vertices: None,
193 marker_dirty: true,
194 gpu_topology: None,
195 gpu_pack_viewport_px: None,
196 gpu_pack_view_bounds: None,
197 })
198 }
199
200 pub fn from_gpu_buffer(
202 buffer: GpuVertexBuffer,
203 vertex_count: usize,
204 style: LineGpuStyle,
205 bounds: BoundingBox,
206 pipeline: PipelineType,
207 marker_buffer: Option<GpuVertexBuffer>,
208 ) -> Self {
209 Self {
210 x_data: Vec::new(),
211 y_data: Vec::new(),
212 color: style.color,
213 line_width: style.line_width,
214 line_style: style.line_style,
215 line_join: LineJoin::Miter,
216 line_cap: LineCap::Butt,
217 marker: style.marker,
218 label: None,
219 visible: true,
220 vertices: None,
221 bounds: Some(bounds),
222 dirty: false,
223 gpu_vertices: Some(buffer),
224 gpu_vertex_count: Some(vertex_count),
225 gpu_line_inputs: None,
226 marker_vertices: None,
227 marker_gpu_vertices: marker_buffer,
228 marker_dirty: true,
229 gpu_topology: Some(pipeline),
230 gpu_pack_viewport_px: None,
231 gpu_pack_view_bounds: None,
232 }
233 }
234
235 pub fn from_gpu_xy(
240 inputs: LineGpuInputs,
241 style: LineGpuStyle,
242 bounds: BoundingBox,
243 marker_buffer: Option<GpuVertexBuffer>,
244 ) -> Self {
245 Self {
246 x_data: Vec::new(),
247 y_data: Vec::new(),
248 color: style.color,
249 line_width: style.line_width,
250 line_style: style.line_style,
251 line_join: LineJoin::Miter,
252 line_cap: LineCap::Butt,
253 marker: style.marker,
254 label: None,
255 visible: true,
256 vertices: None,
257 bounds: Some(bounds),
258 dirty: false,
259 gpu_vertices: None,
260 gpu_vertex_count: None,
261 gpu_line_inputs: Some(inputs),
262 marker_vertices: None,
263 marker_gpu_vertices: marker_buffer,
264 marker_dirty: true,
265 gpu_topology: None,
266 gpu_pack_viewport_px: None,
267 gpu_pack_view_bounds: None,
268 }
269 }
270
271 fn invalidate_gpu_render_cache(&mut self) {
272 self.gpu_vertices = None;
273 self.gpu_vertex_count = None;
274 self.marker_gpu_vertices = None;
275 self.marker_dirty = true;
276 self.gpu_topology = None;
277 self.gpu_pack_viewport_px = None;
278 self.gpu_pack_view_bounds = None;
279 }
280
281 fn clear_gpu_source_inputs(&mut self) {
282 self.gpu_line_inputs = None;
283 }
284
285 fn invalidate_marker_data(&mut self) {
286 self.marker_vertices = None;
287 self.marker_dirty = true;
288 if self.gpu_vertices.is_none() {
289 self.marker_gpu_vertices = None;
290 }
291 }
292
293 pub fn with_style(mut self, color: Vec4, line_width: f32, line_style: LineStyle) -> Self {
295 self.color = color;
296 self.line_width = line_width;
297 self.line_style = line_style;
298 self.dirty = true;
299 self.invalidate_gpu_render_cache();
300 self
301 }
302
303 pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
305 self.label = Some(label.into());
306 self
307 }
308
309 pub fn update_data(&mut self, x_data: Vec<f64>, y_data: Vec<f64>) -> Result<(), String> {
311 if x_data.len() != y_data.len() {
312 return Err(format!(
313 "Data length mismatch: x_data has {} points, y_data has {} points",
314 x_data.len(),
315 y_data.len()
316 ));
317 }
318
319 self.x_data = x_data;
320 self.y_data = y_data;
321 self.dirty = true;
322 self.bounds = None;
323 self.invalidate_gpu_render_cache();
324 self.clear_gpu_source_inputs();
325 self.invalidate_marker_data();
326 Ok(())
327 }
328
329 pub fn set_color(&mut self, color: Vec4) {
331 self.color = color;
332 self.dirty = true;
333 self.invalidate_gpu_render_cache();
334 self.invalidate_marker_data();
335 }
336
337 pub fn set_line_width(&mut self, width: f32) {
339 self.line_width = width.max(0.1); self.dirty = true;
341 self.invalidate_gpu_render_cache();
342 }
343
344 pub fn set_line_style(&mut self, style: LineStyle) {
346 self.line_style = style;
347 self.dirty = true;
348 self.invalidate_gpu_render_cache();
349 }
350
351 pub fn set_marker(&mut self, marker: Option<LineMarkerAppearance>) {
353 self.marker = marker;
354 self.invalidate_marker_data();
355 }
356
357 pub fn set_line_join(&mut self, join: LineJoin) {
359 self.line_join = join;
360 self.dirty = true;
361 self.invalidate_gpu_render_cache();
362 }
363
364 pub fn set_line_cap(&mut self, cap: LineCap) {
366 self.line_cap = cap;
367 self.dirty = true;
368 self.invalidate_gpu_render_cache();
369 }
370
371 pub fn set_visible(&mut self, visible: bool) {
373 self.visible = visible;
374 }
375
376 pub fn len(&self) -> usize {
378 if !self.x_data.is_empty() {
379 self.x_data.len()
380 } else {
381 self.gpu_vertex_count.unwrap_or(0)
382 }
383 }
384
385 pub fn is_empty(&self) -> bool {
387 self.len() == 0
388 }
389
390 pub fn generate_vertices(&mut self) -> &Vec<Vertex> {
392 if self.gpu_vertices.is_some() {
393 if self.vertices.is_none() {
394 self.vertices = Some(Vec::new());
395 }
396 return self.vertices.as_ref().unwrap();
397 }
398 if self.dirty || self.vertices.is_none() {
399 if self.line_width > 1.0 {
400 let base_tris = match self.line_cap {
402 LineCap::Butt => vertex_utils::create_thick_polyline_with_join(
403 &self.x_data,
404 &self.y_data,
405 self.color,
406 self.line_width,
407 self.line_join,
408 ),
409 LineCap::Square => vertex_utils::create_thick_polyline_square_caps(
410 &self.x_data,
411 &self.y_data,
412 self.color,
413 self.line_width,
414 ),
415 LineCap::Round => vertex_utils::create_thick_polyline_round_caps(
416 &self.x_data,
417 &self.y_data,
418 self.color,
419 self.line_width,
420 12,
421 ),
422 };
423 let tris = match self.line_style {
424 LineStyle::Solid => base_tris,
425 LineStyle::Dashed | LineStyle::DashDot | LineStyle::Dotted => {
426 vertex_utils::create_thick_polyline_dashed(
427 &self.x_data,
428 &self.y_data,
429 self.color,
430 self.line_width,
431 self.line_style,
432 )
433 }
434 };
435 self.vertices = Some(tris);
436 } else {
437 let verts = match self.line_style {
438 LineStyle::Solid => {
439 vertex_utils::create_line_plot(&self.x_data, &self.y_data, self.color)
440 }
441 LineStyle::Dashed | LineStyle::DashDot => {
442 vertex_utils::create_line_plot_dashed(
443 &self.x_data,
444 &self.y_data,
445 self.color,
446 self.line_style,
447 )
448 }
449 LineStyle::Dotted => {
450 vertex_utils::create_line_plot_dashed(
452 &self.x_data,
453 &self.y_data,
454 self.color,
455 LineStyle::Dashed,
456 )
457 }
458 };
459 self.vertices = Some(verts);
460 }
461 self.dirty = false;
462 }
463 self.vertices.as_ref().unwrap()
464 }
465
466 fn generate_thin_line_vertices(&self) -> Vec<Vertex> {
467 match self.line_style {
468 LineStyle::Solid => {
469 vertex_utils::create_line_plot(&self.x_data, &self.y_data, self.color)
470 }
471 LineStyle::Dashed | LineStyle::DashDot => vertex_utils::create_line_plot_dashed(
472 &self.x_data,
473 &self.y_data,
474 self.color,
475 self.line_style,
476 ),
477 LineStyle::Dotted => vertex_utils::create_line_plot_dashed(
478 &self.x_data,
479 &self.y_data,
480 self.color,
481 LineStyle::Dashed,
482 ),
483 }
484 }
485
486 pub fn bounds(&mut self) -> BoundingBox {
488 if self.bounds.is_some() && self.x_data.is_empty() && self.y_data.is_empty() {
489 return self.bounds.unwrap_or_default();
490 }
491 if self.x_data.is_empty() && self.y_data.is_empty() {
492 let bounds = BoundingBox::new(Vec3::ZERO, Vec3::ZERO);
493 self.bounds = Some(bounds);
494 return bounds;
495 }
496 if self.dirty || self.bounds.is_none() {
497 let points: Vec<Vec3> = self
498 .x_data
499 .iter()
500 .zip(self.y_data.iter())
501 .map(|(&x, &y)| Vec3::new(x as f32, y as f32, 0.0))
502 .collect();
503 self.bounds = Some(BoundingBox::from_points(&points));
504 }
505 self.bounds.unwrap()
506 }
507
508 fn pack_gpu_vertices_if_needed(
509 &mut self,
510 gpu: &GpuPackContext<'_>,
511 viewport_px: (u32, u32),
512 view_bounds: Option<(f64, f64, f64, f64)>,
513 ) -> Result<(), String> {
514 let bounds = self
515 .bounds
516 .as_ref()
517 .ok_or_else(|| "missing line bounds".to_string())?;
518 let stroke_bounds = Self::stroke_bounds_from_view_bounds(*bounds, view_bounds);
519 let pack_bounds_key = (
520 stroke_bounds.min.x,
521 stroke_bounds.max.x,
522 stroke_bounds.min.y,
523 stroke_bounds.max.y,
524 );
525 if self.gpu_vertices.is_some() {
526 if self.gpu_pack_viewport_px == Some(viewport_px)
527 && self.gpu_pack_view_bounds == Some(pack_bounds_key)
528 {
529 return Ok(());
530 }
531 self.gpu_vertices = None;
532 self.gpu_vertex_count = None;
533 self.gpu_topology = None;
534 }
535 let Some(inputs) = self.gpu_line_inputs.as_ref() else {
536 return Ok(());
537 };
538
539 let stroke_width_px = self.line_width.max(1.0);
540 let x_span = (stroke_bounds.max.x - stroke_bounds.min.x).abs().max(1e-12);
541 let y_span = (stroke_bounds.max.y - stroke_bounds.min.y).abs().max(1e-12);
542 trace!(
543 target: "runmat_plot",
544 "line-pack: begin len={} line_width_px={} stroke_width_px={} viewport_px={:?} bounds=({:?}..{:?}) stroke_bounds=({:?}..{:?})",
545 inputs.len,
546 self.line_width,
547 stroke_width_px,
548 viewport_px,
549 bounds.min,
550 bounds.max,
551 stroke_bounds.min,
552 stroke_bounds.max
553 );
554
555 let params = crate::gpu::line::LineGpuParams {
556 color: self.color,
557 half_width_px: stroke_width_px * 0.5,
558 viewport_width_px: viewport_px.0 as f32,
559 viewport_height_px: viewport_px.1 as f32,
560 x_min: stroke_bounds.min.x,
561 x_span,
562 y_min: stroke_bounds.min.y,
563 y_span,
564 line_style: self.line_style,
565 marker_size: 1.0,
566 };
567 let packed =
568 crate::gpu::line::pack_vertices_from_xy(gpu.device, gpu.queue, inputs, ¶ms)
569 .map_err(|e| format!("gpu line packing failed: {e}"))?;
570 trace!(
571 target: "runmat_plot",
572 "line-pack: complete max_vertices={} indirect_present={}",
573 packed.vertex_count,
574 packed.indirect.is_some()
575 );
576
577 self.gpu_vertices = Some(packed);
578 self.gpu_vertex_count = Some(self.gpu_vertices.as_ref().unwrap().vertex_count);
579 self.gpu_topology = Some(PipelineType::Triangles);
580 self.gpu_pack_viewport_px = Some(viewport_px);
581 self.gpu_pack_view_bounds = Some(pack_bounds_key);
582 Ok(())
583 }
584
585 pub fn render_data_with_viewport_gpu(
586 &mut self,
587 viewport_px: Option<(u32, u32)>,
588 view_bounds: Option<(f64, f64, f64, f64)>,
589 gpu: Option<&GpuPackContext<'_>>,
590 ) -> RenderData {
591 trace!(
592 target: "runmat_plot",
593 "line: render_data_with_viewport_gpu viewport_px={:?} view_bounds={:?} gpu_ctx_present={} gpu_line_inputs_present={} gpu_vertices_present={}",
594 viewport_px,
595 view_bounds,
596 gpu.is_some(),
597 self.gpu_line_inputs.is_some(),
598 self.gpu_vertices.is_some()
599 );
600 if self.gpu_line_inputs.is_some() {
601 if let (Some(gpu), Some(vp)) = (gpu, viewport_px) {
602 if let Err(err) = self.pack_gpu_vertices_if_needed(gpu, vp, view_bounds) {
603 warn!("line gpu pack failed: {err}");
604 }
605 }
606 }
607 self.render_data_with_viewport_and_view_bounds(viewport_px, view_bounds)
608 }
609
610 pub fn render_data(&mut self) -> RenderData {
612 let using_gpu = self.gpu_vertices.is_some();
613 let gpu_vertices = self.gpu_vertices.clone();
614 let (vertices, vertex_count) = if using_gpu {
615 (Vec::new(), self.gpu_vertex_count.unwrap_or(0))
616 } else if self.line_width > 1.0 {
617 let verts = self.generate_thin_line_vertices();
621 let count = verts.len();
622 (verts, count)
623 } else {
624 let verts = self.generate_vertices().clone();
625 let count = verts.len();
626 (verts, count)
627 };
628
629 let style_code = match self.line_style {
635 LineStyle::Solid => 0.0,
636 LineStyle::Dashed => 1.0,
637 LineStyle::Dotted => 2.0,
638 LineStyle::DashDot => 3.0,
639 };
640 let cap_code = match self.line_cap {
641 LineCap::Butt => 0.0,
642 LineCap::Square => 1.0,
643 LineCap::Round => 2.0,
644 };
645 let join_code = match self.line_join {
646 LineJoin::Miter => 0.0,
647 LineJoin::Bevel => 1.0,
648 LineJoin::Round => 2.0,
649 };
650 let mut material = Material {
651 albedo: self.color,
652 ..Default::default()
653 };
654 material.roughness = self.line_width.max(0.0);
655 material.metallic = style_code;
656 material.emissive = Vec4::new(cap_code, join_code, -1.0, 0.0);
657
658 let draw_call = DrawCall {
659 vertex_offset: 0,
660 vertex_count,
661 index_offset: None,
662 index_count: None,
663 instance_count: 1,
664 };
665
666 let pipeline = if using_gpu {
668 self.gpu_topology.unwrap_or(if self.line_width > 1.0 {
669 PipelineType::Triangles
670 } else {
671 PipelineType::Lines
672 })
673 } else {
674 PipelineType::Lines
675 };
676 RenderData {
677 pipeline_type: pipeline,
678 vertices,
679 indices: None,
680 gpu_vertices,
681 bounds: Some(self.bounds()),
682 material,
683 draw_calls: vec![draw_call],
684 image: None,
685 }
686 }
687
688 pub fn render_data_with_viewport(&mut self, viewport_px: Option<(u32, u32)>) -> RenderData {
695 self.render_data_with_viewport_and_view_bounds(viewport_px, None)
696 }
697
698 pub fn render_data_with_viewport_and_view_bounds(
699 &mut self,
700 viewport_px: Option<(u32, u32)>,
701 view_bounds: Option<(f64, f64, f64, f64)>,
702 ) -> RenderData {
703 if self.gpu_vertices.is_some() {
704 return self.render_data();
706 }
707
708 let Some(viewport_px) = viewport_px else {
709 return self.render_data();
710 };
711 let bounds = self.bounds();
712 let stroke_bounds = Self::stroke_bounds_from_view_bounds(bounds, view_bounds);
713 let stroke_width_px = self.line_width.max(1.0);
714 let tris = self.build_viewport_stroke_vertices(stroke_bounds, viewport_px, stroke_width_px);
715 let vertex_count = tris.len();
716
717 let style_code = match self.line_style {
718 LineStyle::Solid => 0.0,
719 LineStyle::Dashed => 1.0,
720 LineStyle::Dotted => 2.0,
721 LineStyle::DashDot => 3.0,
722 };
723 let cap_code = match self.line_cap {
724 LineCap::Butt => 0.0,
725 LineCap::Square => 1.0,
726 LineCap::Round => 2.0,
727 };
728 let join_code = match self.line_join {
729 LineJoin::Miter => 0.0,
730 LineJoin::Bevel => 1.0,
731 LineJoin::Round => 2.0,
732 };
733 let mut material = Material {
734 albedo: self.color,
735 ..Default::default()
736 };
737 material.roughness = self.line_width.max(0.0);
739 material.metallic = style_code;
740 material.emissive = Vec4::new(cap_code, join_code, -1.0, 0.0);
741
742 let draw_call = DrawCall {
743 vertex_offset: 0,
744 vertex_count,
745 index_offset: None,
746 index_count: None,
747 instance_count: 1,
748 };
749
750 RenderData {
751 pipeline_type: PipelineType::Triangles,
752 vertices: tris,
753 indices: None,
754 gpu_vertices: None,
755 bounds: Some(bounds),
756 material,
757 draw_calls: vec![draw_call],
758 image: None,
759 }
760 }
761
762 fn stroke_bounds_from_view_bounds(
763 data_bounds: BoundingBox,
764 view_bounds: Option<(f64, f64, f64, f64)>,
765 ) -> BoundingBox {
766 let Some((left, right, bottom, top)) = view_bounds else {
767 return data_bounds;
768 };
769 if !(left.is_finite() && right.is_finite() && bottom.is_finite() && top.is_finite()) {
770 return data_bounds;
771 }
772 let (min_x, max_x) = if left <= right {
773 (left as f32, right as f32)
774 } else {
775 (right as f32, left as f32)
776 };
777 let (min_y, max_y) = if bottom <= top {
778 (bottom as f32, top as f32)
779 } else {
780 (top as f32, bottom as f32)
781 };
782 if !(min_x.is_finite() && max_x.is_finite() && min_y.is_finite() && max_y.is_finite())
783 || (max_x - min_x).abs() < 1e-12
784 || (max_y - min_y).abs() < 1e-12
785 {
786 return data_bounds;
787 }
788 BoundingBox {
789 min: Vec3::new(min_x, min_y, data_bounds.min.z),
790 max: Vec3::new(max_x, max_y, data_bounds.max.z),
791 }
792 }
793
794 fn build_viewport_stroke_vertices(
795 &self,
796 bounds: BoundingBox,
797 viewport_px: (u32, u32),
798 stroke_width_px: f32,
799 ) -> Vec<Vertex> {
800 let x_span = (bounds.max.x - bounds.min.x).abs().max(1e-12);
801 let y_span = (bounds.max.y - bounds.min.y).abs().max(1e-12);
802 let vw = (viewport_px.0 as f32).max(1.0);
803 let vh = (viewport_px.1 as f32).max(1.0);
804 let sx = vw / x_span;
805 let sy = vh / y_span;
806
807 let x_px: Vec<f64> = self
808 .x_data
809 .iter()
810 .map(|&x| ((x as f32 - bounds.min.x) * sx) as f64)
811 .collect();
812 let y_px: Vec<f64> = self
813 .y_data
814 .iter()
815 .map(|&y| ((y as f32 - bounds.min.y) * sy) as f64)
816 .collect();
817
818 let base_tris = match self.line_cap {
819 LineCap::Butt => vertex_utils::create_thick_polyline_with_join(
820 &x_px,
821 &y_px,
822 self.color,
823 stroke_width_px,
824 self.line_join,
825 ),
826 LineCap::Square => vertex_utils::create_thick_polyline_square_caps(
827 &x_px,
828 &y_px,
829 self.color,
830 stroke_width_px,
831 ),
832 LineCap::Round => vertex_utils::create_thick_polyline_round_caps(
833 &x_px,
834 &y_px,
835 self.color,
836 stroke_width_px,
837 12,
838 ),
839 };
840 let mut tris = match self.line_style {
841 LineStyle::Solid => base_tris,
842 LineStyle::Dashed | LineStyle::DashDot | LineStyle::Dotted => {
843 vertex_utils::create_thick_polyline_dashed(
844 &x_px,
845 &y_px,
846 self.color,
847 stroke_width_px,
848 self.line_style,
849 )
850 }
851 };
852
853 let inv_sx = x_span / vw;
854 let inv_sy = y_span / vh;
855 for v in &mut tris {
856 let px = v.position[0];
857 let py = v.position[1];
858 v.position[0] = bounds.min.x + px * inv_sx;
859 v.position[1] = bounds.min.y + py * inv_sy;
860 }
861 tris
862 }
863
864 pub fn marker_render_data(&mut self) -> Option<RenderData> {
866 let marker = self.marker.clone()?;
867 let material = Self::build_marker_material(&marker);
868
869 if let Some(gpu_vertices) = self.marker_gpu_vertices.clone() {
870 let vertex_count = gpu_vertices.vertex_count;
871 if vertex_count == 0 {
872 return None;
873 }
874 let draw_call = DrawCall {
875 vertex_offset: 0,
876 vertex_count,
877 index_offset: None,
878 index_count: None,
879 instance_count: 1,
880 };
881 return Some(RenderData {
882 pipeline_type: PipelineType::Points,
883 vertices: Vec::new(),
884 indices: None,
885 gpu_vertices: Some(gpu_vertices),
886 bounds: Some(self.bounds()),
887 material,
888 draw_calls: vec![draw_call],
889 image: None,
890 });
891 }
892
893 let vertices = self.marker_vertices_slice(&marker)?;
894 if vertices.is_empty() {
895 return None;
896 }
897 let draw_call = DrawCall {
898 vertex_offset: 0,
899 vertex_count: vertices.len(),
900 index_offset: None,
901 index_count: None,
902 instance_count: 1,
903 };
904
905 Some(RenderData {
906 pipeline_type: PipelineType::Points,
907 vertices: vertices.to_vec(),
908 indices: None,
909 gpu_vertices: None,
910 bounds: Some(self.bounds()),
911 material,
912 draw_calls: vec![draw_call],
913 image: None,
914 })
915 }
916
917 fn build_marker_material(marker: &LineMarkerAppearance) -> Material {
918 let mut material = Material {
919 albedo: marker.face_color,
920 ..Default::default()
921 };
922 if !marker.filled {
923 material.albedo.w = 0.0;
924 }
925 material.emissive = marker.edge_color;
926 material.roughness = 1.0;
927 material.metallic = marker_style_code(marker.kind);
928 material.alpha_mode = AlphaMode::Blend;
929 material
930 }
931
932 fn marker_vertices_slice(&mut self, marker: &LineMarkerAppearance) -> Option<&[Vertex]> {
933 if self.x_data.len() != self.y_data.len() || self.x_data.is_empty() {
934 return None;
935 }
936
937 if self.marker_vertices.is_none() || self.marker_dirty {
938 let mut verts = Vec::with_capacity(self.x_data.len());
939 for (&x, &y) in self.x_data.iter().zip(self.y_data.iter()) {
940 let mut vertex = Vertex::new(Vec3::new(x as f32, y as f32, 0.0), marker.face_color);
941 vertex.normal[2] = marker.size.max(1.0);
942 verts.push(vertex);
943 }
944 self.marker_vertices = Some(verts);
945 self.marker_dirty = false;
946 }
947 self.marker_vertices.as_deref()
948 }
949
950 pub fn statistics(&self) -> PlotStatistics {
952 let (min_x, max_x) = self
953 .x_data
954 .iter()
955 .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), &x| {
956 (min.min(x), max.max(x))
957 });
958 let (min_y, max_y) = self
959 .y_data
960 .iter()
961 .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), &y| {
962 (min.min(y), max.max(y))
963 });
964
965 PlotStatistics {
966 point_count: self.x_data.len(),
967 x_range: (min_x, max_x),
968 y_range: (min_y, max_y),
969 memory_usage: self.estimated_memory_usage(),
970 }
971 }
972
973 pub fn estimated_memory_usage(&self) -> usize {
975 std::mem::size_of::<f64>() * (self.x_data.len() + self.y_data.len())
976 + self
977 .vertices
978 .as_ref()
979 .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>())
980 + self.gpu_vertex_count.unwrap_or(0) * std::mem::size_of::<Vertex>()
981 }
982}
983
984fn marker_style_code(kind: ScatterMarkerStyle) -> f32 {
985 match kind {
986 ScatterMarkerStyle::Circle => 0.0,
987 ScatterMarkerStyle::Square => 1.0,
988 ScatterMarkerStyle::Triangle => 2.0,
989 ScatterMarkerStyle::Diamond => 3.0,
990 ScatterMarkerStyle::Plus => 4.0,
991 ScatterMarkerStyle::Cross => 5.0,
992 ScatterMarkerStyle::Star => 6.0,
993 ScatterMarkerStyle::Hexagon => 7.0,
994 }
995}
996
997#[derive(Debug, Clone)]
999pub struct PlotStatistics {
1000 pub point_count: usize,
1001 pub x_range: (f64, f64),
1002 pub y_range: (f64, f64),
1003 pub memory_usage: usize,
1004}
1005
1006pub mod matlab_compat {
1008 use super::*;
1009
1010 pub fn plot(x: Vec<f64>, y: Vec<f64>) -> Result<LinePlot, String> {
1012 LinePlot::new(x, y)
1013 }
1014
1015 pub fn plot_with_color(x: Vec<f64>, y: Vec<f64>, color: &str) -> Result<LinePlot, String> {
1017 let color_vec = parse_matlab_color(color)?;
1018 Ok(LinePlot::new(x, y)?.with_style(color_vec, 1.0, LineStyle::Solid))
1019 }
1020
1021 fn parse_matlab_color(color: &str) -> Result<Vec4, String> {
1023 match color {
1024 "r" | "red" => Ok(Vec4::new(1.0, 0.0, 0.0, 1.0)),
1025 "g" | "green" => Ok(Vec4::new(0.0, 1.0, 0.0, 1.0)),
1026 "b" | "blue" => Ok(Vec4::new(0.0, 0.0, 1.0, 1.0)),
1027 "c" | "cyan" => Ok(Vec4::new(0.0, 1.0, 1.0, 1.0)),
1028 "m" | "magenta" => Ok(Vec4::new(1.0, 0.0, 1.0, 1.0)),
1029 "y" | "yellow" => Ok(Vec4::new(1.0, 1.0, 0.0, 1.0)),
1030 "k" | "black" => Ok(Vec4::new(0.0, 0.0, 0.0, 1.0)),
1031 "w" | "white" => Ok(Vec4::new(1.0, 1.0, 1.0, 1.0)),
1032 _ => Err(format!("Unknown color: {color}")),
1033 }
1034 }
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039 use super::*;
1040
1041 #[test]
1042 fn test_line_plot_creation() {
1043 let x = vec![0.0, 1.0, 2.0, 3.0];
1044 let y = vec![0.0, 1.0, 0.0, 1.0];
1045
1046 let plot = LinePlot::new(x.clone(), y.clone()).unwrap();
1047
1048 assert_eq!(plot.x_data, x);
1049 assert_eq!(plot.y_data, y);
1050 assert_eq!(plot.len(), 4);
1051 assert!(!plot.is_empty());
1052 assert!(plot.visible);
1053 }
1054
1055 #[test]
1056 fn test_line_plot_data_validation() {
1057 let x = vec![0.0, 1.0, 2.0];
1059 let y = vec![0.0, 1.0];
1060 assert!(LinePlot::new(x, y).is_err());
1061
1062 let empty_x: Vec<f64> = vec![];
1064 let empty_y: Vec<f64> = vec![];
1065 let empty = LinePlot::new(empty_x, empty_y).unwrap();
1066 assert!(empty.is_empty());
1067 }
1068
1069 #[test]
1070 fn test_line_plot_update_data_to_empty_invalidates_render_data() {
1071 let mut plot = LinePlot::new(vec![0.0, 1.0], vec![2.0, 3.0]).unwrap();
1072 assert!(!plot.render_data().vertices.is_empty());
1073
1074 plot.update_data(Vec::new(), Vec::new()).unwrap();
1075 assert!(plot.is_empty());
1076 assert_eq!(plot.render_data().vertices.len(), 0);
1077 assert_eq!(plot.bounds().min, Vec3::ZERO);
1078 assert_eq!(plot.bounds().max, Vec3::ZERO);
1079 }
1080
1081 #[test]
1082 fn test_line_plot_styling() {
1083 let x = vec![0.0, 1.0, 2.0];
1084 let y = vec![1.0, 2.0, 1.5];
1085 let color = Vec4::new(1.0, 0.0, 0.0, 1.0);
1086
1087 let plot = LinePlot::new(x, y)
1088 .unwrap()
1089 .with_style(color, 2.0, LineStyle::Dashed)
1090 .with_label("Test Line");
1091
1092 assert_eq!(plot.color, color);
1093 assert_eq!(plot.line_width, 2.0);
1094 assert_eq!(plot.line_style, LineStyle::Dashed);
1095 assert_eq!(plot.label, Some("Test Line".to_string()));
1096 }
1097
1098 #[test]
1099 fn test_line_plot_data_update() {
1100 let mut plot = LinePlot::new(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
1101
1102 let new_x = vec![0.0, 0.5, 1.0, 1.5];
1103 let new_y = vec![0.0, 0.25, 1.0, 2.25];
1104
1105 plot.update_data(new_x.clone(), new_y.clone()).unwrap();
1106
1107 assert_eq!(plot.x_data, new_x);
1108 assert_eq!(plot.y_data, new_y);
1109 assert_eq!(plot.len(), 4);
1110 }
1111
1112 #[test]
1113 fn test_line_plot_bounds() {
1114 let x = vec![-1.0, 0.0, 1.0, 2.0];
1115 let y = vec![-2.0, 0.0, 1.0, 3.0];
1116
1117 let mut plot = LinePlot::new(x, y).unwrap();
1118 let bounds = plot.bounds();
1119
1120 assert_eq!(bounds.min.x, -1.0);
1121 assert_eq!(bounds.max.x, 2.0);
1122 assert_eq!(bounds.min.y, -2.0);
1123 assert_eq!(bounds.max.y, 3.0);
1124 }
1125
1126 #[test]
1127 fn style_invalidation_preserves_gpu_source_bounds() {
1128 let expected = BoundingBox::new(Vec3::new(-2.0, -1.0, 0.0), Vec3::new(3.0, 4.0, 0.0));
1129 let mut plot = LinePlot::new(Vec::new(), Vec::new()).unwrap();
1130 plot.bounds = Some(expected);
1131 plot.dirty = false;
1132
1133 plot.set_line_width(3.0);
1134
1135 let bounds = plot.bounds();
1136 assert_eq!(bounds.min, expected.min);
1137 assert_eq!(bounds.max, expected.max);
1138 }
1139
1140 #[test]
1141 fn test_line_plot_vertex_generation() {
1142 let x = vec![0.0, 1.0, 2.0];
1143 let y = vec![0.0, 1.0, 0.0];
1144
1145 let mut plot = LinePlot::new(x, y).unwrap();
1146 let vertices = plot.generate_vertices();
1147
1148 assert_eq!(vertices.len(), 4);
1150
1151 assert_eq!(vertices[0].position, [0.0, 0.0, 0.0]);
1153 assert_eq!(vertices[1].position, [1.0, 1.0, 0.0]);
1154 }
1155
1156 #[test]
1157 fn test_line_plot_render_data() {
1158 let x = vec![0.0, 1.0, 2.0];
1159 let y = vec![1.0, 2.0, 1.0];
1160
1161 let mut plot = LinePlot::new(x, y).unwrap();
1162 let render_data = plot.render_data();
1163
1164 assert_eq!(render_data.pipeline_type, PipelineType::Lines);
1165 assert_eq!(render_data.vertices.len(), 4); assert!(render_data.indices.is_none());
1167 assert_eq!(render_data.draw_calls.len(), 1);
1168 }
1169
1170 #[test]
1171 fn test_line_plot_statistics() {
1172 let x = vec![0.0, 1.0, 2.0, 3.0];
1173 let y = vec![-1.0, 0.0, 1.0, 2.0];
1174
1175 let plot = LinePlot::new(x, y).unwrap();
1176 let stats = plot.statistics();
1177
1178 assert_eq!(stats.point_count, 4);
1179 assert_eq!(stats.x_range, (0.0, 3.0));
1180 assert_eq!(stats.y_range, (-1.0, 2.0));
1181 assert!(stats.memory_usage > 0);
1182 }
1183
1184 #[test]
1185 fn test_matlab_compat_colors() {
1186 use super::matlab_compat::*;
1187
1188 let x = vec![0.0, 1.0];
1189 let y = vec![0.0, 1.0];
1190
1191 let red_plot = plot_with_color(x.clone(), y.clone(), "r").unwrap();
1192 assert_eq!(red_plot.color, Vec4::new(1.0, 0.0, 0.0, 1.0));
1193
1194 let blue_plot = plot_with_color(x.clone(), y.clone(), "blue").unwrap();
1195 assert_eq!(blue_plot.color, Vec4::new(0.0, 0.0, 1.0, 1.0));
1196
1197 assert!(plot_with_color(x, y, "invalid").is_err());
1199 }
1200
1201 #[test]
1202 fn marker_render_data_produces_point_draw_call() {
1203 let mut plot = LinePlot::new(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
1204 plot.set_marker(Some(LineMarkerAppearance {
1205 kind: ScatterMarkerStyle::Circle,
1206 size: 8.0,
1207 edge_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
1208 face_color: Vec4::new(1.0, 0.0, 0.0, 1.0),
1209 filled: true,
1210 }));
1211 let marker_data = plot.marker_render_data().expect("marker render data");
1212 assert_eq!(marker_data.pipeline_type, PipelineType::Points);
1213 assert_eq!(marker_data.draw_calls[0].vertex_count, 2);
1214 }
1215
1216 #[test]
1217 fn line_plot_handles_large_trace() {
1218 let n = 50_000;
1219 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
1220 let y: Vec<f64> = (0..n).map(|i| (i as f64 * 0.001).sin()).collect();
1221 let mut plot = LinePlot::new(x, y).unwrap();
1222 let render_data = plot.render_data();
1223 assert_eq!(render_data.vertices.len(), (n - 1) * 2);
1224 }
1225
1226 #[test]
1227 fn thin_line_with_viewport_uses_triangle_stroke_geometry() {
1228 let x = vec![0.0, 1.0, 2.0];
1229 let y = vec![0.0, 1.0, 0.0];
1230 let mut plot = LinePlot::new(x, y).unwrap();
1231 plot.set_line_width(1.0);
1232 let render_data = plot.render_data_with_viewport(Some((800, 600)));
1233 assert_eq!(render_data.pipeline_type, PipelineType::Triangles);
1234 assert!(render_data.vertices.len() >= 12); assert_eq!(render_data.vertices.len() % 3, 0);
1236 }
1237
1238 #[test]
1239 fn thin_line_without_viewport_keeps_legacy_line_path() {
1240 let x = vec![0.0, 1.0, 2.0];
1241 let y = vec![0.0, 1.0, 0.0];
1242 let mut plot = LinePlot::new(x, y).unwrap();
1243 plot.set_line_width(1.0);
1244 let render_data = plot.render_data_with_viewport(None);
1245 assert_eq!(render_data.pipeline_type, PipelineType::Lines);
1246 assert_eq!(render_data.vertices.len(), 4); }
1248
1249 #[test]
1250 fn thick_line_without_viewport_keeps_legacy_line_path() {
1251 let x = vec![0.0, 1.0, 2.0];
1252 let y = vec![0.0, 1.0, 0.0];
1253 let mut plot = LinePlot::new(x, y).unwrap();
1254 plot.set_line_width(2.0);
1255 let render_data = plot.render_data_with_viewport(None);
1256 assert_eq!(render_data.pipeline_type, PipelineType::Lines);
1257 assert_eq!(render_data.vertices.len(), 4); }
1259
1260 #[test]
1261 fn viewport_stroke_width_is_pixel_stable_across_anisotropic_axes() {
1262 let x = vec![-100.0, 0.0];
1263 let y = vec![10000.0, 0.0];
1264 let mut plot = LinePlot::new(x, y).unwrap();
1265 plot.set_line_width(1.0);
1266 let viewport = (1400, 1000);
1267 let render_data = plot.render_data_with_viewport(Some(viewport));
1268 assert_eq!(render_data.pipeline_type, PipelineType::Triangles);
1269 assert!(render_data.vertices.len() >= 6);
1270
1271 let bounds = render_data.bounds.expect("bounds");
1272 let v0 = render_data.vertices[0].position;
1273 let v1 = render_data.vertices[1].position;
1274 let px_per_x = viewport.0 as f32 / (bounds.max.x - bounds.min.x).abs().max(1e-12);
1275 let px_per_y = viewport.1 as f32 / (bounds.max.y - bounds.min.y).abs().max(1e-12);
1276 let dx_px = (v0[0] - v1[0]) * px_per_x;
1277 let dy_px = (v0[1] - v1[1]) * px_per_y;
1278 let width_px = (dx_px * dx_px + dy_px * dy_px).sqrt();
1279 assert!(
1280 (width_px - 1.0).abs() < 0.05,
1281 "expected ~1px stroke, got {width_px}"
1282 );
1283 }
1284
1285 #[test]
1286 fn viewport_stroke_width_uses_visible_view_bounds_when_zoomed() {
1287 let x = vec![0.0, 500.0];
1288 let y = vec![0.0, 0.0];
1289 let mut plot = LinePlot::new(x, y).unwrap();
1290 plot.set_line_width(2.0);
1291 let viewport = (1000, 500);
1292 let view_bounds = (0.0, 30.0, -1.0, 1.0);
1293
1294 let render_data =
1295 plot.render_data_with_viewport_and_view_bounds(Some(viewport), Some(view_bounds));
1296
1297 assert_eq!(render_data.pipeline_type, PipelineType::Triangles);
1298 let v0 = render_data.vertices[0].position;
1299 let v1 = render_data.vertices[1].position;
1300 let px_per_y = viewport.1 as f32 / (view_bounds.3 - view_bounds.2) as f32;
1301 let width_px = (v0[1] - v1[1]).abs() * px_per_y;
1302 assert!(
1303 (width_px - 2.0).abs() < 0.05,
1304 "expected zoomed stroke to remain ~2px, got {width_px}"
1305 );
1306 }
1307}