use crate::core::{Affine2D, Bbox, Path, color::Rgba};
use crate::render::{GraphicsContext, Renderer};
use crate::artist::Artist;
#[derive(Debug, Clone, PartialEq)]
pub struct QuadMesh {
nrows: usize,
ncols: usize,
coordinates: Vec<[f64; 2]>,
facecolors: Vec<Rgba>,
edgecolor: Option<Rgba>,
linewidth: f64,
zorder: f64,
visible: bool,
}
impl QuadMesh {
#[must_use]
pub fn new(
nrows: usize,
ncols: usize,
coordinates: Vec<[f64; 2]>,
facecolors: Vec<Rgba>,
) -> Self {
let expected_coords = (nrows + 1) * (ncols + 1);
assert_eq!(
coordinates.len(),
expected_coords,
"QuadMesh: coordinates length {} must equal (nrows + 1) * (ncols + 1) = {}",
coordinates.len(),
expected_coords
);
assert_eq!(
facecolors.len(),
nrows * ncols,
"QuadMesh: facecolors length {} must equal nrows * ncols = {}",
facecolors.len(),
nrows * ncols
);
Self {
nrows,
ncols,
coordinates,
facecolors,
edgecolor: None,
linewidth: 1.0,
zorder: 0.0,
visible: true,
}
}
#[must_use]
pub fn with_edgecolor(mut self, edgecolor: Option<Rgba>) -> Self {
self.edgecolor = edgecolor;
self
}
#[must_use]
pub fn with_linewidth(mut self, linewidth: f64) -> Self {
self.linewidth = linewidth;
self
}
#[must_use]
pub fn with_zorder(mut self, zorder: f64) -> Self {
self.zorder = zorder;
self
}
#[must_use]
pub fn with_visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
fn corner(&self, r: usize, c: usize) -> [f64; 2] {
self.coordinates[r * (self.ncols + 1) + c]
}
fn facecolor(&self, r: usize, c: usize) -> Rgba {
self.facecolors[r * self.ncols + c]
}
}
impl Artist for QuadMesh {
fn draw(&self, renderer: &mut dyn Renderer, transform: &Affine2D) {
if !self.visible {
return;
}
let gc = GraphicsContext {
line_width: self.linewidth,
stroke: self.edgecolor,
..GraphicsContext::new()
};
for r in 0..self.nrows {
for c in 0..self.ncols {
let quad = Path::from_polyline(&[
self.corner(r, c),
self.corner(r, c + 1),
self.corner(r + 1, c + 1),
self.corner(r + 1, c),
self.corner(r, c),
]);
renderer.draw_path(&gc, &quad, transform, Some(self.facecolor(r, c)));
}
}
}
fn zorder(&self) -> f64 {
self.zorder
}
fn visible(&self) -> bool {
self.visible
}
fn data_extents(&self) -> Option<Bbox> {
let mut xmin = f64::INFINITY;
let mut ymin = f64::INFINITY;
let mut xmax = f64::NEG_INFINITY;
let mut ymax = f64::NEG_INFINITY;
let mut any = false;
for &[x, y] in &self.coordinates {
if !x.is_finite() || !y.is_finite() {
continue;
}
xmin = xmin.min(x);
ymin = ymin.min(y);
xmax = xmax.max(x);
ymax = ymax.max(y);
any = true;
}
if any {
Some(Bbox::from_extents(xmin, ymin, xmax, ymax))
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct MockRenderer {
calls: Vec<Call>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Call {
vertices: usize,
fill: Option<Rgba>,
stroke: Option<Rgba>,
}
impl Renderer for MockRenderer {
fn draw_path(
&mut self,
gc: &GraphicsContext,
path: &Path,
_transform: &Affine2D,
fill: Option<Rgba>,
) {
self.calls.push(Call {
vertices: path.vertices().len(),
fill,
stroke: gc.stroke,
});
}
fn canvas_size(&self) -> (f64, f64) {
(100.0, 100.0)
}
}
fn unit_grid(nrows: usize, ncols: usize) -> Vec<[f64; 2]> {
let mut coords = Vec::with_capacity((nrows + 1) * (ncols + 1));
for r in 0..=nrows {
for c in 0..=ncols {
coords.push([c as f64, r as f64]);
}
}
coords
}
#[test]
fn draws_one_quad_per_cell() {
let (nrows, ncols) = (3, 4);
let mesh = QuadMesh::new(
nrows,
ncols,
unit_grid(nrows, ncols),
vec![Rgba::RED; nrows * ncols],
);
let mut r = MockRenderer::default();
mesh.draw(&mut r, &Affine2D::identity());
assert_eq!(r.calls.len(), nrows * ncols);
for call in &r.calls {
assert_eq!(call.vertices, 5);
assert_eq!(call.fill, Some(Rgba::RED));
}
}
#[test]
fn per_cell_facecolors_apply_in_row_major_order() {
let mesh = QuadMesh::new(1, 2, unit_grid(1, 2), vec![Rgba::RED, Rgba::GREEN]);
let mut r = MockRenderer::default();
mesh.draw(&mut r, &Affine2D::identity());
let fills: Vec<_> = r.calls.iter().map(|c| c.fill).collect();
assert_eq!(fills, vec![Some(Rgba::RED), Some(Rgba::GREEN)]);
}
#[test]
fn no_edgecolor_means_no_stroke() {
let mesh = QuadMesh::new(1, 1, unit_grid(1, 1), vec![Rgba::RED]);
let mut r = MockRenderer::default();
mesh.draw(&mut r, &Affine2D::identity());
assert_eq!(r.calls[0].stroke, None);
}
#[test]
fn edgecolor_drives_the_stroke() {
let mesh =
QuadMesh::new(1, 1, unit_grid(1, 1), vec![Rgba::RED]).with_edgecolor(Some(Rgba::BLACK));
let mut r = MockRenderer::default();
mesh.draw(&mut r, &Affine2D::identity());
assert_eq!(r.calls[0].stroke, Some(Rgba::BLACK));
}
#[test]
fn data_extents_match_the_grid() {
let (nrows, ncols) = (2, 3);
let mesh = QuadMesh::new(
nrows,
ncols,
unit_grid(nrows, ncols),
vec![Rgba::RED; nrows * ncols],
);
let e = mesh.data_extents().expect("non-empty grid");
assert_eq!((e.xmin(), e.xmax()), (0.0, ncols as f64));
assert_eq!((e.ymin(), e.ymax()), (0.0, nrows as f64));
}
#[test]
fn invisible_mesh_draws_nothing() {
let mesh = QuadMesh::new(1, 1, unit_grid(1, 1), vec![Rgba::RED]).with_visible(false);
let mut r = MockRenderer::default();
mesh.draw(&mut r, &Affine2D::identity());
assert!(r.calls.is_empty());
}
#[test]
fn default_zorder_is_zero() {
let mesh = QuadMesh::new(1, 1, unit_grid(1, 1), vec![Rgba::RED]);
assert_eq!(Artist::zorder(&mesh), 0.0);
}
#[test]
#[should_panic(expected = "coordinates length")]
fn wrong_coordinate_count_panics() {
let _ = QuadMesh::new(1, 1, vec![[0.0, 0.0]], vec![Rgba::RED]);
}
#[test]
#[should_panic(expected = "facecolors length")]
fn wrong_facecolor_count_panics() {
let _ = QuadMesh::new(1, 1, unit_grid(1, 1), vec![Rgba::RED, Rgba::GREEN]);
}
}