use crate::context::shared_wgpu_context;
use crate::core::{
BoundingBox, DrawCall, GpuVertexBuffer, Material, PipelineType, RenderData, Vertex,
};
use crate::gpu::axis::OwnedAxisData;
use crate::gpu::{util::readback_scalar_buffer_f64, ScalarType};
use glam::{Vec3, Vec4};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct AreaPlot {
pub x: Vec<f64>,
pub y: Vec<f64>,
pub baseline: f64,
pub lower_y: Option<Vec<f64>>,
pub color: Vec4,
pub label: Option<String>,
pub visible: bool,
vertices: Option<Vec<Vertex>>,
indices: Option<Vec<u32>>,
bounds: Option<BoundingBox>,
dirty: bool,
gpu_vertices: Option<GpuVertexBuffer>,
gpu_vertex_count: Option<usize>,
gpu_bounds: Option<BoundingBox>,
gpu_source: Option<AreaGpuSource>,
}
#[derive(Clone, Debug)]
pub struct AreaGpuSource {
pub x_axis: OwnedAxisData,
pub y_buffer: Arc<wgpu::Buffer>,
pub rows: usize,
pub cols: usize,
pub target_col: usize,
pub scalar: ScalarType,
}
impl AreaPlot {
pub async fn export_scene_xy_data(&self) -> Result<(Vec<f64>, Vec<f64>), String> {
if !self.x.is_empty() && self.x.len() == self.y.len() {
return Ok((self.x.clone(), self.y.clone()));
}
if !self.x.is_empty() || !self.y.is_empty() {
return Err(format!(
"area plot has partial CPU source data: x has {} values, y has {} values",
self.x.len(),
self.y.len()
));
}
if let Some(source) = &self.gpu_source {
let context = shared_wgpu_context().ok_or_else(|| {
"area plot has GPU source data but no shared WGPU context is installed".to_string()
})?;
let x = source
.x_axis
.export_f64(&context.device, &context.queue, source.rows, source.scalar)
.await?;
let all_y = readback_scalar_buffer_f64(
&context.device,
&context.queue,
&source.y_buffer,
source.rows * source.cols,
source.scalar,
)
.await?;
let offset = source.target_col * source.rows;
let y = all_y
.get(offset..offset + source.rows)
.ok_or_else(|| "area plot GPU source column is out of range".to_string())?
.to_vec();
return Ok((x, y));
}
if self.gpu_vertices.is_some() {
return Err(
"area plot has GPU render vertices but no exportable source data".to_string(),
);
}
Ok((Vec::new(), Vec::new()))
}
pub fn new(x: Vec<f64>, y: Vec<f64>) -> Result<Self, String> {
if x.len() != y.len() || x.is_empty() {
return Err("area: X and Y must be same non-zero length".to_string());
}
Ok(Self {
x,
y,
baseline: 0.0,
lower_y: None,
color: Vec4::new(0.0, 0.5, 1.0, 0.4),
label: None,
visible: true,
vertices: None,
indices: None,
bounds: None,
dirty: true,
gpu_vertices: None,
gpu_vertex_count: None,
gpu_bounds: None,
gpu_source: None,
})
}
pub fn from_gpu_buffer(
color: Vec4,
baseline: f64,
lower_y: Option<Vec<f64>>,
buffer: GpuVertexBuffer,
vertex_count: usize,
bounds: BoundingBox,
) -> Self {
Self {
x: Vec::new(),
y: Vec::new(),
baseline,
lower_y,
color,
label: None,
visible: true,
vertices: None,
indices: None,
bounds: Some(bounds),
dirty: false,
gpu_vertices: Some(buffer),
gpu_vertex_count: Some(vertex_count),
gpu_bounds: Some(bounds),
gpu_source: None,
}
}
pub fn with_gpu_source(mut self, source: AreaGpuSource) -> Self {
self.gpu_source = Some(source);
self
}
pub fn with_style(mut self, color: Vec4, baseline: f64) -> Self {
self.color = color;
self.baseline = baseline;
self.dirty = true;
self
}
pub fn with_lower_curve(mut self, lower_y: Vec<f64>) -> Self {
self.lower_y = Some(lower_y);
self.gpu_source = None;
self.dirty = true;
self
}
pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
self.label = Some(label.into());
self
}
pub fn set_visible(&mut self, v: bool) {
self.visible = v;
}
pub fn generate_vertices(&mut self) -> (&Vec<Vertex>, &Vec<u32>) {
if self.dirty || self.vertices.is_none() {
let mut verts = Vec::new();
let mut inds = Vec::new();
for i in 0..self.x.len() {
let xi = self.x[i] as f32;
let yi = self.y[i] as f32;
let b = self
.lower_y
.as_ref()
.and_then(|vals| vals.get(i).copied())
.unwrap_or(self.baseline) as f32;
if !xi.is_finite() || !yi.is_finite() {
continue;
}
verts.push(Vertex::new(Vec3::new(xi, b, 0.0), self.color));
verts.push(Vertex::new(Vec3::new(xi, yi, 0.0), self.color));
}
for i in 0..(verts.len() / 2 - 1) {
let base = (i * 2) as u32;
inds.extend_from_slice(&[base, base + 1, base + 3, base, base + 3, base + 2]);
}
self.vertices = Some(verts);
self.indices = Some(inds);
self.dirty = false;
}
(
self.vertices.as_ref().unwrap(),
self.indices.as_ref().unwrap(),
)
}
pub fn bounds(&mut self) -> BoundingBox {
if let Some(bounds) = self.gpu_bounds {
return bounds;
}
if self.dirty || self.bounds.is_none() {
let mut min = Vec3::new(f32::INFINITY, f32::INFINITY, 0.0);
let mut max = Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, 0.0);
for (idx, (&x, &y)) in self.x.iter().zip(self.y.iter()).enumerate() {
let (x, y) = (x as f32, y as f32);
let lower = self
.lower_y
.as_ref()
.and_then(|vals| vals.get(idx).copied())
.unwrap_or(self.baseline) as f32;
if !x.is_finite() || !y.is_finite() {
continue;
}
min.x = min.x.min(x);
max.x = max.x.max(x);
min.y = min.y.min(y.min(lower));
max.y = max.y.max(y.max(lower));
}
if !min.x.is_finite() {
min = Vec3::ZERO;
max = Vec3::ZERO;
}
self.bounds = Some(BoundingBox::new(min, max));
}
self.bounds.unwrap()
}
pub fn render_data(&mut self) -> RenderData {
let using_gpu = self.gpu_vertices.is_some();
let bounds = self.bounds();
let (vertices, indices) = if using_gpu {
(Vec::new(), Vec::new())
} else {
let (v, i) = self.generate_vertices();
(v.clone(), i.clone())
};
let material = Material {
albedo: self.color,
..Default::default()
};
let draw_call = DrawCall {
vertex_offset: 0,
vertex_count: self.gpu_vertex_count.unwrap_or(vertices.len()),
index_offset: if using_gpu { None } else { Some(0) },
index_count: if using_gpu { None } else { Some(indices.len()) },
instance_count: 1,
};
RenderData {
pipeline_type: PipelineType::Triangles,
vertices,
indices: if using_gpu { None } else { Some(indices) },
material,
draw_calls: vec![draw_call],
gpu_vertices: self.gpu_vertices.clone(),
bounds: Some(bounds),
image: None,
}
}
pub fn estimated_memory_usage(&self) -> usize {
self.vertices
.as_ref()
.map_or(0, |v| v.len() * std::mem::size_of::<Vertex>())
+ self
.indices
.as_ref()
.map_or(0, |i| i.len() * std::mem::size_of::<u32>())
}
}