Skip to main content

runmat_plot/plots/
area.rs

1//! Area plot implementation (filled area under curve)
2
3use crate::context::shared_wgpu_context;
4use crate::core::{
5    BoundingBox, DrawCall, GpuVertexBuffer, Material, PipelineType, RenderData, Vertex,
6};
7use crate::gpu::axis::OwnedAxisData;
8use crate::gpu::{util::readback_scalar_buffer_f64, ScalarType};
9use glam::{Vec3, Vec4};
10use std::sync::Arc;
11
12#[derive(Debug, Clone)]
13pub struct AreaPlot {
14    pub x: Vec<f64>,
15    pub y: Vec<f64>,
16    pub baseline: f64,
17    pub lower_y: Option<Vec<f64>>,
18    pub color: Vec4,
19    pub label: Option<String>,
20    pub visible: bool,
21    vertices: Option<Vec<Vertex>>,
22    indices: Option<Vec<u32>>,
23    bounds: Option<BoundingBox>,
24    dirty: bool,
25    gpu_vertices: Option<GpuVertexBuffer>,
26    gpu_vertex_count: Option<usize>,
27    gpu_bounds: Option<BoundingBox>,
28    gpu_source: Option<AreaGpuSource>,
29}
30
31#[derive(Clone, Debug)]
32pub struct AreaGpuSource {
33    pub x_axis: OwnedAxisData,
34    pub y_buffer: Arc<wgpu::Buffer>,
35    pub rows: usize,
36    pub cols: usize,
37    pub target_col: usize,
38    pub scalar: ScalarType,
39}
40
41impl AreaPlot {
42    pub async fn export_scene_xy_data(&self) -> Result<(Vec<f64>, Vec<f64>), String> {
43        if !self.x.is_empty() && self.x.len() == self.y.len() {
44            return Ok((self.x.clone(), self.y.clone()));
45        }
46        if !self.x.is_empty() || !self.y.is_empty() {
47            return Err(format!(
48                "area plot has partial CPU source data: x has {} values, y has {} values",
49                self.x.len(),
50                self.y.len()
51            ));
52        }
53
54        if let Some(source) = &self.gpu_source {
55            let context = shared_wgpu_context().ok_or_else(|| {
56                "area plot has GPU source data but no shared WGPU context is installed".to_string()
57            })?;
58            let x = source
59                .x_axis
60                .export_f64(&context.device, &context.queue, source.rows, source.scalar)
61                .await?;
62            let all_y = readback_scalar_buffer_f64(
63                &context.device,
64                &context.queue,
65                &source.y_buffer,
66                source.rows * source.cols,
67                source.scalar,
68            )
69            .await?;
70            let offset = source.target_col * source.rows;
71            let y = all_y
72                .get(offset..offset + source.rows)
73                .ok_or_else(|| "area plot GPU source column is out of range".to_string())?
74                .to_vec();
75            return Ok((x, y));
76        }
77
78        if self.gpu_vertices.is_some() {
79            return Err(
80                "area plot has GPU render vertices but no exportable source data".to_string(),
81            );
82        }
83
84        Ok((Vec::new(), Vec::new()))
85    }
86
87    pub fn new(x: Vec<f64>, y: Vec<f64>) -> Result<Self, String> {
88        if x.len() != y.len() || x.is_empty() {
89            return Err("area: X and Y must be same non-zero length".to_string());
90        }
91        Ok(Self {
92            x,
93            y,
94            baseline: 0.0,
95            lower_y: None,
96            color: Vec4::new(0.0, 0.5, 1.0, 0.4),
97            label: None,
98            visible: true,
99            vertices: None,
100            indices: None,
101            bounds: None,
102            dirty: true,
103            gpu_vertices: None,
104            gpu_vertex_count: None,
105            gpu_bounds: None,
106            gpu_source: None,
107        })
108    }
109    pub fn from_gpu_buffer(
110        color: Vec4,
111        baseline: f64,
112        lower_y: Option<Vec<f64>>,
113        buffer: GpuVertexBuffer,
114        vertex_count: usize,
115        bounds: BoundingBox,
116    ) -> Self {
117        Self {
118            x: Vec::new(),
119            y: Vec::new(),
120            baseline,
121            lower_y,
122            color,
123            label: None,
124            visible: true,
125            vertices: None,
126            indices: None,
127            bounds: Some(bounds),
128            dirty: false,
129            gpu_vertices: Some(buffer),
130            gpu_vertex_count: Some(vertex_count),
131            gpu_bounds: Some(bounds),
132            gpu_source: None,
133        }
134    }
135    pub fn with_gpu_source(mut self, source: AreaGpuSource) -> Self {
136        self.gpu_source = Some(source);
137        self
138    }
139    pub fn with_style(mut self, color: Vec4, baseline: f64) -> Self {
140        self.color = color;
141        self.baseline = baseline;
142        self.dirty = true;
143        self
144    }
145    pub fn with_lower_curve(mut self, lower_y: Vec<f64>) -> Self {
146        self.lower_y = Some(lower_y);
147        self.gpu_source = None;
148        self.dirty = true;
149        self
150    }
151    pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
152        self.label = Some(label.into());
153        self
154    }
155    pub fn set_visible(&mut self, v: bool) {
156        self.visible = v;
157    }
158    pub fn generate_vertices(&mut self) -> (&Vec<Vertex>, &Vec<u32>) {
159        if self.dirty || self.vertices.is_none() {
160            let mut verts = Vec::new();
161            let mut inds = Vec::new();
162            // Build a triangle strip-like mesh: baseline to curve segments
163            for i in 0..self.x.len() {
164                let xi = self.x[i] as f32;
165                let yi = self.y[i] as f32;
166                let b = self
167                    .lower_y
168                    .as_ref()
169                    .and_then(|vals| vals.get(i).copied())
170                    .unwrap_or(self.baseline) as f32;
171                if !xi.is_finite() || !yi.is_finite() {
172                    continue;
173                }
174                verts.push(Vertex::new(Vec3::new(xi, b, 0.0), self.color));
175                verts.push(Vertex::new(Vec3::new(xi, yi, 0.0), self.color));
176            }
177            // Triangles between successive pairs
178            for i in 0..(verts.len() / 2 - 1) {
179                let base = (i * 2) as u32;
180                inds.extend_from_slice(&[base, base + 1, base + 3, base, base + 3, base + 2]);
181            }
182            self.vertices = Some(verts);
183            self.indices = Some(inds);
184            self.dirty = false;
185        }
186        (
187            self.vertices.as_ref().unwrap(),
188            self.indices.as_ref().unwrap(),
189        )
190    }
191    pub fn bounds(&mut self) -> BoundingBox {
192        if let Some(bounds) = self.gpu_bounds {
193            return bounds;
194        }
195        if self.dirty || self.bounds.is_none() {
196            let mut min = Vec3::new(f32::INFINITY, f32::INFINITY, 0.0);
197            let mut max = Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, 0.0);
198            for (idx, (&x, &y)) in self.x.iter().zip(self.y.iter()).enumerate() {
199                let (x, y) = (x as f32, y as f32);
200                let lower = self
201                    .lower_y
202                    .as_ref()
203                    .and_then(|vals| vals.get(idx).copied())
204                    .unwrap_or(self.baseline) as f32;
205                if !x.is_finite() || !y.is_finite() {
206                    continue;
207                }
208                min.x = min.x.min(x);
209                max.x = max.x.max(x);
210                min.y = min.y.min(y.min(lower));
211                max.y = max.y.max(y.max(lower));
212            }
213            if !min.x.is_finite() {
214                min = Vec3::ZERO;
215                max = Vec3::ZERO;
216            }
217            self.bounds = Some(BoundingBox::new(min, max));
218        }
219        self.bounds.unwrap()
220    }
221    pub fn render_data(&mut self) -> RenderData {
222        let using_gpu = self.gpu_vertices.is_some();
223        let bounds = self.bounds();
224        let (vertices, indices) = if using_gpu {
225            (Vec::new(), Vec::new())
226        } else {
227            let (v, i) = self.generate_vertices();
228            (v.clone(), i.clone())
229        };
230        let material = Material {
231            albedo: self.color,
232            ..Default::default()
233        };
234        let draw_call = DrawCall {
235            vertex_offset: 0,
236            vertex_count: self.gpu_vertex_count.unwrap_or(vertices.len()),
237            index_offset: if using_gpu { None } else { Some(0) },
238            index_count: if using_gpu { None } else { Some(indices.len()) },
239            instance_count: 1,
240        };
241        RenderData {
242            pipeline_type: PipelineType::Triangles,
243            vertices,
244            indices: if using_gpu { None } else { Some(indices) },
245            material,
246            draw_calls: vec![draw_call],
247            gpu_vertices: self.gpu_vertices.clone(),
248            bounds: Some(bounds),
249            image: None,
250        }
251    }
252    pub fn estimated_memory_usage(&self) -> usize {
253        self.vertices
254            .as_ref()
255            .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>())
256            + self
257                .indices
258                .as_ref()
259                .map_or(0, |i| i.len() * std::mem::size_of::<u32>())
260    }
261}