use std::io::Write;
use std::path::Path;
use crate::accessor::ValueAccessor;
use crate::error::SliceError;
use crate::field::{Field, Value};
use crate::topology::{Location, Topology};
pub use crate::tree::Axis;
use crate::tree::Coord;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Slice {
pub axis: Axis,
pub position: i32,
pub min: [i32; 2],
pub max: [i32; 2],
}
impl Slice {
pub fn new(axis: Axis, position: i32, min: [i32; 2], max: [i32; 2]) -> Slice {
Slice { axis, position, min, max }
}
fn voxel(&self, u: i32, v: i32) -> Coord {
match self.axis {
Axis::X => [self.position, u, v],
Axis::Y => [u, self.position, v],
Axis::Z => [u, v, self.position],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum ColorMap {
Diverging {
limit: f64,
},
Sequential {
min: f64,
max: f64,
},
}
impl ColorMap {
fn check(self) -> Result<(), SliceError> {
let usable = match self {
ColorMap::Diverging { limit } => limit.is_finite() && limit > 0.0,
ColorMap::Sequential { min, max } => min.is_finite() && max.is_finite() && min < max,
};
if usable { Ok(()) } else { Err(SliceError::BadColorMap(self)) }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Image {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
impl Image {
pub fn new(width: u32, height: u32, rgba: Vec<u8>) -> Image {
Image { width, height, rgba }
}
}
const BACKGROUND: [u8; 3] = [128, 128, 128];
const NOT_A_NUMBER: [u8; 3] = [255, 0, 255];
const CONTOUR: [u8; 3] = [0, 0, 0];
const BLUE: [f64; 3] = [59.0, 76.0, 192.0];
const WHITE: [f64; 3] = [247.0, 247.0, 247.0];
const RED: [f64; 3] = [180.0, 4.0, 38.0];
const DARK: [f64; 3] = [68.0, 1.0, 84.0];
const MIDDLE: [f64; 3] = [33.0, 145.0, 140.0];
const LIGHT: [f64; 3] = [253.0, 231.0, 37.0];
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "rounded within 0..=255"
)]
fn lerp(a: [f64; 3], b: [f64; 3], t: f64) -> [u8; 3] {
[0, 1, 2].map(|k| (a[k] + (b[k] - a[k]) * t).round().clamp(0.0, 255.0) as u8)
}
fn color(map: ColorMap, v: f64) -> [u8; 3] {
if v.is_nan() {
return NOT_A_NUMBER;
}
match map {
ColorMap::Diverging { limit } => {
let t = (v / limit).clamp(-1.0, 1.0);
if t < 0.0 { lerp(WHITE, BLUE, -t) } else { lerp(WHITE, RED, t) }
}
ColorMap::Sequential { min, max } => {
let t = ((v - min) / (max - min)).clamp(0.0, 1.0);
if t < 0.5 { lerp(DARK, MIDDLE, 2.0 * t) } else { lerp(MIDDLE, LIGHT, 2.0 * t - 1.0) }
}
}
}
#[derive(Debug, Clone, Copy)]
enum Pixel {
Value(f64),
Tile(f64),
Background,
}
pub fn render<T: Value + Into<f64>>(
t: &Topology,
f: &Field<T>,
slice: Slice,
map: ColorMap,
) -> Result<Image, SliceError> {
map.check()?;
let side = |k: usize| {
let n = i64::from(slice.max[k]) - i64::from(slice.min[k]) + 1;
if n <= 0 {
return Err(SliceError::EmptyRange);
}
u32::try_from(n).map_err(|_| SliceError::TooLarge)
};
let (width, height) = (side(0)?, side(1)?);
let mut acc = ValueAccessor::new(t, f)?;
let count =
usize::try_from(u64::from(width) * u64::from(height)).map_err(|_| SliceError::TooLarge)?;
let mut pixels = Vec::new();
pixels.try_reserve_exact(count).map_err(|_| SliceError::TooLarge)?;
for v in (slice.min[1]..=slice.max[1]).rev() {
for u in slice.min[0]..=slice.max[0] {
let voxel = slice.voxel(u, v);
pixels.push(match acc.locate(voxel) {
Location::Leaf(_) => Pixel::Value(acc.get(voxel).into()),
Location::Tile(..) => Pixel::Tile(acc.get(voxel).into()),
Location::Background => Pixel::Background,
});
}
}
let w = width as usize;
let mut rgba = Vec::new();
let bytes = pixels.len().checked_mul(4).ok_or(SliceError::TooLarge)?;
rgba.try_reserve_exact(bytes).map_err(|_| SliceError::TooLarge)?;
for (i, p) in pixels.iter().enumerate() {
let (row, col) = (i / w, i % w);
let rgb = match *p {
Pixel::Background => BACKGROUND,
Pixel::Value(_) | Pixel::Tile(_) if on_contour(map, &pixels, i, w) => CONTOUR,
Pixel::Value(x) => color(map, x),
Pixel::Tile(x) if (row + col).is_multiple_of(4) => color(map, x).map(|c| c / 2 + c / 8),
Pixel::Tile(x) => color(map, x),
};
rgba.extend_from_slice(&rgb);
rgba.push(255);
}
Ok(Image { width, height, rgba })
}
fn on_contour(map: ColorMap, pixels: &[Pixel], i: usize, w: usize) -> bool {
let (ColorMap::Diverging { .. }, Some(Pixel::Value(x) | Pixel::Tile(x))) = (map, pixels.get(i))
else {
return false;
};
let left =
if i.is_multiple_of(w) { None } else { i.checked_sub(1).and_then(|j| pixels.get(j)) };
let right = if (i + 1).is_multiple_of(w) { None } else { pixels.get(i + 1) };
let up = i.checked_sub(w).and_then(|j| pixels.get(j));
[left, right, up, pixels.get(i + w)].into_iter().flatten().any(|n| match *n {
Pixel::Value(y) | Pixel::Tile(y) => (*x < 0.0) != (y < 0.0) && x.abs() <= y.abs(),
Pixel::Background => false,
})
}
pub fn to_bytes(image: &Image) -> Result<Vec<u8>, SliceError> {
let mut out = Vec::new();
let mut encoder = png::Encoder::new(&mut out, image.width, image.height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
encoder.set_compression(png::Compression::Balanced);
encoder.set_filter(png::Filter::Adaptive);
let refused = |e: png::EncodingError| SliceError::Encode(std::io::Error::other(e));
let mut writer = encoder.write_header().map_err(refused)?;
writer.write_image_data(&image.rgba).map_err(refused)?;
writer.finish().map_err(refused)?;
Ok(out)
}
pub fn write(path: &Path, image: &Image) -> Result<(), SliceError> {
let bytes = to_bytes(image)?;
crate::cache::replace(path, |out| out.write_all(&bytes).map_err(SliceError::Io))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tree::TileLevel;
fn draw(map: ColorMap) -> Result<Image, SliceError> {
let empty = Topology::new();
let mut e = empty.edit();
e.activate([0, 0, 0]).unwrap();
let t = e.commit().unwrap().0;
let f = Field::new(&t, 1.0_f32);
render(&t, &f, Slice::new(Axis::Z, 0, [0, 0], [7, 7]), map)
}
#[test]
fn a_colour_map_with_no_usable_range_is_refused() {
for map in [
ColorMap::Diverging { limit: 0.0 },
ColorMap::Diverging { limit: -1.0 },
ColorMap::Diverging { limit: f64::NAN },
ColorMap::Diverging { limit: f64::INFINITY },
ColorMap::Sequential { min: 1.0, max: 1.0 },
ColorMap::Sequential { min: 2.0, max: 1.0 },
ColorMap::Sequential { min: f64::NAN, max: 1.0 },
ColorMap::Sequential { min: 0.0, max: f64::INFINITY },
] {
let refused = match draw(map) {
Err(SliceError::BadColorMap(m)) => format!("{m:?}"),
other => format!("{other:?}"),
};
assert_eq!(refused, format!("{map:?}"));
}
}
#[test]
fn a_colour_map_with_a_range_draws() {
for map in
[ColorMap::Diverging { limit: 3.0 }, ColorMap::Sequential { min: -1.0, max: 1.0 }]
{
assert_eq!(draw(map).unwrap().rgba.len(), 8 * 8 * 4, "{map:?}");
}
}
#[test]
fn a_range_too_large_to_hold_is_refused() {
let t = Topology::new();
let f = Field::new(&t, 1.0_f32);
let map = ColorMap::Diverging { limit: 3.0 };
let ranges = [
([i32::MIN, 0], [i32::MAX, 0]),
([0, 0], [i32::MAX - 2; 2]),
([0, 0], [(1 << 24) - 1; 2]),
];
for (min, max) in ranges {
let got = render(&t, &f, Slice { axis: Axis::Z, position: 0, min, max }, map);
assert!(matches!(got, Err(SliceError::TooLarge)), "{max:?}: {got:?}");
}
}
#[test]
fn a_range_empty_along_one_axis_is_refused() {
let t = Topology::new();
let f = Field::new(&t, 1.0_f32);
let map = ColorMap::Diverging { limit: 3.0 };
for (min, max) in [([0, 0], [-1, 7]), ([0, 0], [7, -1])] {
let got = render(&t, &f, Slice { axis: Axis::Z, position: 0, min, max }, map);
assert!(matches!(got, Err(SliceError::EmptyRange)), "{max:?}: {got:?}");
}
}
#[test]
fn equal_and_opposite_neighbours_are_both_on_the_contour() {
let empty = Topology::new();
let mut e = empty.edit();
e.activate([0, 0, 0]).unwrap();
let t = e.commit().unwrap().0;
let mut f = Field::new(&t, 2.0_f32);
let n = t.leaf_numbers().next().unwrap();
let leaf = f.leaf_mut(&t, n).unwrap();
leaf.fill(2.0);
(leaf[0], leaf[1]) = (0.5, -0.5);
let slice = Slice::new(Axis::Z, 0, [0, 0], [7, 7]);
let image = render(&t, &f, slice, ColorMap::Diverging { limit: 3.0 }).unwrap();
let rgb = |x: usize| &image.rgba[(7 * 8 + x) * 4..(7 * 8 + x) * 4 + 3];
assert_eq!((rgb(0), rgb(1)), (&CONTOUR[..], &CONTOUR[..]));
assert_ne!(rgb(2), &CONTOUR[..]);
}
#[test]
fn a_tile_nearer_zero_than_its_leaf_neighbour_is_on_the_contour() {
let empty = Topology::new();
let mut e = empty.edit();
e.activate([0, 0, 0]).unwrap();
e.add_tile(TileLevel::Lower, [8, 0, 0]).unwrap();
let t = e.commit().unwrap().0;
let mut f = Field::new(&t, 5.0_f32);
let tile = t.tile_numbers().next().unwrap();
f.set_tile(&t, tile, -0.1).unwrap();
let slice = Slice::new(Axis::Z, 0, [0, 0], [15, 7]);
let image = render(&t, &f, slice, ColorMap::Diverging { limit: 6.0 }).unwrap();
let rgb = |x: usize| &image.rgba[(7 * 16 + x) * 4..(7 * 16 + x) * 4 + 3];
assert_ne!(rgb(7), &CONTOUR[..]);
assert_eq!(rgb(8), &CONTOUR[..]);
assert_ne!(rgb(9), &CONTOUR[..]);
}
#[test]
fn a_sequential_map_spans_its_range() {
let map = ColorMap::Sequential { min: 10.0, max: 30.0 };
let at = |c: [f64; 3]| lerp(c, c, 0.0);
let dark = at(DARK);
assert_eq!(color(map, 10.0), dark);
assert_eq!(color(map, 5.0), dark);
assert_eq!(color(map, 20.0), at(MIDDLE));
assert_eq!(color(map, 30.0), at(LIGHT));
assert_eq!(color(map, 25.0), lerp(MIDDLE, LIGHT, 0.5));
}
#[test]
fn zeros_among_positives_are_off_the_contour() {
let empty = Topology::new();
let mut e = empty.edit();
e.activate([0, 0, 0]).unwrap();
let t = e.commit().unwrap().0;
let mut f = Field::new(&t, 2.0_f32);
let n = t.leaf_numbers().next().unwrap();
let leaf = f.leaf_mut(&t, n).unwrap();
leaf.fill(2.0);
(leaf[0], leaf[1]) = (0.0, 0.0);
let slice = Slice::new(Axis::Z, 0, [0, 0], [7, 7]);
let image = render(&t, &f, slice, ColorMap::Diverging { limit: 3.0 }).unwrap();
for x in [0, 1] {
let at = (7 * 8 + x) * 4;
assert_eq!(&image.rgba[at..at + 3], &lerp(WHITE, WHITE, 0.0)[..], "pixel {x}");
}
}
}