Skip to main content

runmat_plot/plots/
line.rs

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