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