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