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