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