runmat-plot 0.4.0

GPU-accelerated and static plotting for RunMat with WGPU and Plotters
Documentation
//! Quiver plot (vector field) implementation

use crate::core::{
    BoundingBox, DrawCall, GpuVertexBuffer, Material, PipelineType, RenderData, Vertex,
};
use glam::{Vec3, Vec4};

#[derive(Debug, Clone)]
pub struct QuiverPlot {
    pub x: Vec<f64>,
    pub y: Vec<f64>,
    pub u: Vec<f64>,
    pub v: Vec<f64>,

    pub color: Vec4,
    pub line_width: f32,
    pub scale: f32,
    pub head_size: f32,

    pub label: Option<String>,
    pub visible: bool,

    vertices: Option<Vec<Vertex>>,
    bounds: Option<BoundingBox>,
    dirty: bool,
    gpu_vertices: Option<GpuVertexBuffer>,
    gpu_vertex_count: Option<usize>,
    gpu_bounds: Option<BoundingBox>,
}

impl QuiverPlot {
    pub fn new(x: Vec<f64>, y: Vec<f64>, u: Vec<f64>, v: Vec<f64>) -> Result<Self, String> {
        let n = x.len();
        if n == 0 || y.len() != n || u.len() != n || v.len() != n {
            return Err("quiver: X,Y,U,V must have same non-zero length".to_string());
        }
        Ok(Self {
            x,
            y,
            u,
            v,
            color: Vec4::new(0.0, 0.0, 0.0, 1.0),
            line_width: 1.0,
            scale: 1.0,
            head_size: 0.1,
            label: None,
            visible: true,
            vertices: None,
            bounds: None,
            dirty: true,
            gpu_vertices: None,
            gpu_vertex_count: None,
            gpu_bounds: None,
        })
    }
    pub fn from_gpu_buffer(
        color: Vec4,
        line_width: f32,
        scale: f32,
        head_size: f32,
        buffer: GpuVertexBuffer,
        vertex_count: usize,
        bounds: BoundingBox,
    ) -> Self {
        Self {
            x: Vec::new(),
            y: Vec::new(),
            u: Vec::new(),
            v: Vec::new(),
            color,
            line_width,
            scale,
            head_size,
            label: None,
            visible: true,
            vertices: None,
            bounds: Some(bounds),
            dirty: false,
            gpu_vertices: Some(buffer),
            gpu_vertex_count: Some(vertex_count),
            gpu_bounds: Some(bounds),
        }
    }
    pub fn with_style(mut self, color: Vec4, line_width: f32, scale: f32, head_size: f32) -> Self {
        self.color = color;
        self.line_width = line_width.max(0.5);
        self.scale = scale.max(0.0);
        self.head_size = head_size.max(0.0);
        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> {
        if self.dirty || self.vertices.is_none() {
            let mut verts = Vec::new();
            for i in 0..self.x.len() {
                let (x, y, u, v) = (
                    self.x[i] as f32,
                    self.y[i] as f32,
                    self.u[i] as f32,
                    self.v[i] as f32,
                );
                if !x.is_finite() || !y.is_finite() || !u.is_finite() || !v.is_finite() {
                    continue;
                }
                let dx = u * self.scale;
                let dy = v * self.scale;
                // Main shaft
                verts.push(Vertex::new(Vec3::new(x, y, 0.0), self.color));
                verts.push(Vertex::new(Vec3::new(x + dx, y + dy, 0.0), self.color));
                // Arrowhead as two short lines forming a V
                let len = (dx * dx + dy * dy).sqrt();
                if len > 0.0 && self.head_size > 0.0 {
                    let hx = dx / len;
                    let hy = dy / len;
                    // Perpendicular
                    let px = -hy;
                    let py = hx;
                    let h = self.head_size.min(len * 0.5);
                    let tipx = x + dx;
                    let tipy = y + dy;
                    let leftx = tipx - h * hx + 0.5 * h * px;
                    let lefty = tipy - h * hy + 0.5 * h * py;
                    let rightx = tipx - h * hx - 0.5 * h * px;
                    let righty = tipy - h * hy - 0.5 * h * py;
                    verts.push(Vertex::new(Vec3::new(tipx, tipy, 0.0), self.color));
                    verts.push(Vertex::new(Vec3::new(leftx, lefty, 0.0), self.color));
                    verts.push(Vertex::new(Vec3::new(tipx, tipy, 0.0), self.color));
                    verts.push(Vertex::new(Vec3::new(rightx, righty, 0.0), self.color));
                }
            }
            self.vertices = Some(verts);
            self.dirty = false;
        }
        self.vertices.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 i in 0..self.x.len() {
                let x = self.x[i] as f32;
                let y = self.y[i] as f32;
                let dx = (self.u[i] as f32) * self.scale;
                let dy = (self.v[i] as f32) * self.scale;
                if !x.is_finite() || !y.is_finite() || !dx.is_finite() || !dy.is_finite() {
                    continue;
                }
                min.x = min.x.min(x.min(x + dx));
                max.x = max.x.max(x.max(x + dx));
                min.y = min.y.min(y.min(y + dy));
                max.y = max.y.max(y.max(y + dy));
            }
            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 = if using_gpu {
            Vec::new()
        } else {
            self.generate_vertices().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: None,
            index_count: None,
            instance_count: 1,
        };
        RenderData {
            pipeline_type: PipelineType::Lines,
            vertices,
            indices: None,
            gpu_vertices: self.gpu_vertices.clone(),
            bounds: Some(bounds),
            material,
            draw_calls: vec![draw_call],
            image: None,
        }
    }

    pub fn estimated_memory_usage(&self) -> usize {
        self.vertices
            .as_ref()
            .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>())
    }
}