use std::collections::BTreeMap;
use std::io::Write;
use std::path::Path;
use crate::error::VtkError;
use crate::field::Field;
use crate::topology::Topology;
use crate::tree::Coord;
const VOXEL: u8 = 11;
type Point = [i64; 3];
pub trait VtkValue: sealed::VtkWrite {}
mod sealed {
use std::io::Write;
use crate::field::Stored;
pub trait VtkWrite: Stored {
const TYPE_NAME: &'static str;
const COMPONENTS: u32;
fn readable(self) -> bool;
fn write_ascii(self, w: &mut dyn Write) -> std::io::Result<()>;
}
}
impl VtkValue for f32 {}
impl VtkValue for f64 {}
impl VtkValue for u8 {}
impl VtkValue for u32 {}
impl VtkValue for [f32; 3] {}
impl VtkValue for [f64; 3] {}
impl sealed::VtkWrite for f32 {
const TYPE_NAME: &'static str = "Float32";
const COMPONENTS: u32 = 1;
fn readable(self) -> bool {
!self.is_infinite()
}
fn write_ascii(self, w: &mut dyn Write) -> std::io::Result<()> {
if self.is_nan() { w.write_all(b"NaN") } else { write!(w, "{self}") }
}
}
impl sealed::VtkWrite for f64 {
const TYPE_NAME: &'static str = "Float64";
const COMPONENTS: u32 = 1;
fn readable(self) -> bool {
!self.is_infinite()
}
fn write_ascii(self, w: &mut dyn Write) -> std::io::Result<()> {
if self.is_nan() { w.write_all(b"NaN") } else { write!(w, "{self}") }
}
}
impl sealed::VtkWrite for u8 {
const TYPE_NAME: &'static str = "UInt8";
const COMPONENTS: u32 = 1;
fn readable(self) -> bool {
true
}
fn write_ascii(self, w: &mut dyn Write) -> std::io::Result<()> {
write!(w, "{self}")
}
}
impl sealed::VtkWrite for u32 {
const TYPE_NAME: &'static str = "UInt32";
const COMPONENTS: u32 = 1;
fn readable(self) -> bool {
true
}
fn write_ascii(self, w: &mut dyn Write) -> std::io::Result<()> {
write!(w, "{self}")
}
}
fn write_components<S: VtkValue>(c: [S; 3], w: &mut dyn Write) -> std::io::Result<()> {
c[0].write_ascii(w)?;
for x in &c[1..] {
w.write_all(b" ")?;
x.write_ascii(w)?;
}
Ok(())
}
impl sealed::VtkWrite for [f32; 3] {
const TYPE_NAME: &'static str = "Float32";
const COMPONENTS: u32 = 3;
fn readable(self) -> bool {
self.iter().all(|x| x.readable())
}
fn write_ascii(self, w: &mut dyn Write) -> std::io::Result<()> {
write_components(self, w)
}
}
impl sealed::VtkWrite for [f64; 3] {
const TYPE_NAME: &'static str = "Float64";
const COMPONENTS: u32 = 3;
fn readable(self) -> bool {
self.iter().all(|x| x.readable())
}
fn write_ascii(self, w: &mut dyn Write) -> std::io::Result<()> {
write_components(self, w)
}
}
struct Cell<T> {
origin: Coord,
edge: i64,
value: T,
}
impl<T> Cell<T> {
fn corners(&self) -> impl Iterator<Item = Point> + use<'_, T> {
let o = self.origin.map(i64::from);
(0..8_i64).map(move |k| {
[
o[0] + (k & 1) * self.edge,
o[1] + (k >> 1 & 1) * self.edge,
o[2] + (k >> 2) * self.edge,
]
})
}
}
pub fn to_bytes<T: VtkValue>(t: &Topology, f: &Field<T>, name: &str) -> Result<Vec<u8>, VtkError> {
let cells = checked(t, f, name)?;
let mut out = Vec::new();
emit(&cells, name, &mut out)?;
Ok(out)
}
pub fn write<T: VtkValue>(
path: &Path,
t: &Topology,
f: &Field<T>,
name: &str,
) -> Result<(), VtkError> {
let cells = checked(t, f, name)?;
crate::cache::replace(path, |out| emit(&cells, name, out))
}
fn checked<T: VtkValue>(t: &Topology, f: &Field<T>, name: &str) -> Result<Vec<Cell<T>>, VtkError> {
check_name(name)?;
cells(t, f)
}
fn emit<T: VtkValue>(cells: &[Cell<T>], name: &str, w: &mut impl Write) -> Result<(), VtkError> {
let (ids, points) = share_points(cells);
writeln!(w, r#"<?xml version="1.0"?>"#)?;
writeln!(w, r#"<VTKFile type="UnstructuredGrid" version="0.1" byte_order="LittleEndian">"#)?;
writeln!(w, "<UnstructuredGrid>")?;
writeln!(w, r#"<Piece NumberOfPoints="{}" NumberOfCells="{}">"#, points.len(), cells.len())?;
let (role, components) = match T::COMPONENTS {
1 => ("Scalars", String::new()),
n => ("Vectors", format!(r#" NumberOfComponents="{n}""#)),
};
writeln!(w, r#"<CellData {role}="{name}">"#)?;
writeln!(w, r#"<DataArray type="{}" Name="{name}"{components} format="ascii">"#, T::TYPE_NAME)?;
for c in cells {
c.value.write_ascii(w)?;
writeln!(w)?;
}
writeln!(w, "</DataArray>\n</CellData>\n<Points>")?;
writeln!(w, r#"<DataArray type="Float64" NumberOfComponents="3" format="ascii">"#)?;
for p in &points {
writeln!(w, "{} {} {}", p[0], p[1], p[2])?;
}
writeln!(w, "</DataArray>\n</Points>\n<Cells>")?;
write_cells(w, cells, &ids)?;
writeln!(w, "</Cells>\n</Piece>\n</UnstructuredGrid>\n</VTKFile>")?;
Ok(())
}
fn check_name(name: &str) -> Result<(), VtkError> {
let fits = |c: char| (c.is_ascii_graphic() || c == ' ') && !"<>&\"'".contains(c);
if name.is_empty() || !name.chars().all(fits) {
return Err(VtkError::Name(name.to_owned()));
}
Ok(())
}
fn cells<T: VtkValue>(t: &Topology, f: &Field<T>) -> Result<Vec<Cell<T>>, VtkError> {
f.belongs_to(t)?;
let mut out = Vec::new();
for n in t.leaf_numbers() {
for (origin, value) in t.leaf_voxels(n)?.zip(f.leaf(t, n)?) {
out.push(Cell { origin, edge: 1, value: *value });
}
}
for n in t.tile_numbers() {
let (level, origin) = t.tile(n)?;
out.push(Cell { origin, edge: i64::from(level.edge()), value: f.tile(t, n)? });
}
match out.iter().find(|c| !c.value.readable()) {
Some(c) => Err(VtkError::Infinite { origin: c.origin }),
None => Ok(out),
}
}
fn share_points<T>(cells: &[Cell<T>]) -> (BTreeMap<Point, u64>, Vec<Point>) {
let mut ids = BTreeMap::new();
let mut points = Vec::new();
for p in cells.iter().flat_map(Cell::corners) {
ids.entry(p).or_insert_with(|| {
points.push(p);
points.len() as u64 - 1
});
}
(ids, points)
}
fn write_cells<T>(
w: &mut impl Write,
cells: &[Cell<T>],
ids: &BTreeMap<Point, u64>,
) -> Result<(), VtkError> {
writeln!(w, r#"<DataArray type="Int64" Name="connectivity" format="ascii">"#)?;
for c in cells {
let corners: Vec<String> = c
.corners()
.map(|p| ids.get(&p).map(u64::to_string).ok_or(VtkError::Inconsistent))
.collect::<Result<_, _>>()?;
writeln!(w, "{}", corners.join(" "))?;
}
writeln!(w, "</DataArray>")?;
writeln!(w, r#"<DataArray type="Int64" Name="offsets" format="ascii">"#)?;
for k in 1..=cells.len() {
writeln!(w, "{}", k * 8)?;
}
writeln!(w, "</DataArray>")?;
writeln!(w, r#"<DataArray type="UInt8" Name="types" format="ascii">"#)?;
for _ in cells {
writeln!(w, "{VOXEL}")?;
}
writeln!(w, "</DataArray>")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_field_of_another_topology_is_refused_on_an_empty_one() {
let empty = Topology::new();
let mut e = empty.edit();
e.activate([0, 0, 0]).unwrap();
let other = e.commit().unwrap().0;
let f = Field::new(&other, 0.0_f32);
let got = to_bytes(&empty, &f, "f");
assert!(
matches!(got, Err(VtkError::Field(crate::error::FieldError::StaleField))),
"{got:?}"
);
}
use crate::tree::TileLevel;
fn one_tile<T: VtkValue + Default>(origin: Coord, value: T) -> (Topology, Field<T>) {
let empty = Topology::new();
let mut e = empty.edit();
e.add_tile(TileLevel::Lower, origin).unwrap();
let (t, _) = e.commit().unwrap();
let mut f = Field::new(&t, T::default());
let n = t.tile_numbers().next().unwrap();
f.set_tile(&t, n, value).unwrap();
(t, f)
}
fn text<T: VtkValue>(t: &Topology, f: &Field<T>) -> String {
String::from_utf8(to_bytes(t, f, "d").unwrap()).unwrap()
}
#[test]
fn a_tile_is_one_voxel_cell_of_its_size() {
let (t, f) = one_tile([8, -16, 0], 2.5_f32);
let want = r#"<?xml version="1.0"?>
<VTKFile type="UnstructuredGrid" version="0.1" byte_order="LittleEndian">
<UnstructuredGrid>
<Piece NumberOfPoints="8" NumberOfCells="1">
<CellData Scalars="d">
<DataArray type="Float32" Name="d" format="ascii">
2.5
</DataArray>
</CellData>
<Points>
<DataArray type="Float64" NumberOfComponents="3" format="ascii">
8 -16 0
16 -16 0
8 -8 0
16 -8 0
8 -16 8
16 -16 8
8 -8 8
16 -8 8
</DataArray>
</Points>
<Cells>
<DataArray type="Int64" Name="connectivity" format="ascii">
0 1 2 3 4 5 6 7
</DataArray>
<DataArray type="Int64" Name="offsets" format="ascii">
8
</DataArray>
<DataArray type="UInt8" Name="types" format="ascii">
11
</DataArray>
</Cells>
</Piece>
</UnstructuredGrid>
</VTKFile>
"#;
assert_eq!(text(&t, &f), want);
}
#[test]
fn a_vector_is_three_components_named_as_vectors() {
let (t, f) = one_tile([8, -16, 0], [0.5_f64, -0.0025, f64::NAN]);
let s = text(&t, &f);
let want = r#"<CellData Vectors="d">
<DataArray type="Float64" Name="d" NumberOfComponents="3" format="ascii">
0.5 -0.0025 NaN
</DataArray>
</CellData>"#;
assert!(s.contains(want), "{s}");
let (t, f) = one_tile([0, 0, 0], [1.25_f32, -2.0, 3.5]);
assert!(text(&t, &f).contains("format=\"ascii\">\n1.25 -2 3.5\n</DataArray>"));
for v in [[0.0, f32::NEG_INFINITY, 0.0], [0.0, 0.0, f32::INFINITY]] {
let (t, f) = one_tile([0, 8, 0], v);
let got = to_bytes(&t, &f, "d");
assert!(matches!(got, Err(VtkError::Infinite { origin: [0, 8, 0] })), "{got:?}");
}
let (t, f) = one_tile([0, 8, 0], [1.0_f64, f64::INFINITY, 0.0]);
let got = to_bytes(&t, &f, "d");
assert!(matches!(got, Err(VtkError::Infinite { origin: [0, 8, 0] })), "{got:?}");
}
#[test]
fn integers_are_written_as_they_are() {
let (t, f) = one_tile([0, 0, 0], 200_u8);
assert!(text(&t, &f).contains("format=\"ascii\">\n200\n</DataArray>"));
let (t, f) = one_tile([0, 0, 0], 4_000_000_000_u32);
assert!(text(&t, &f).contains("format=\"ascii\">\n4000000000\n</DataArray>"));
}
#[test]
fn two_tiles_side_by_side_share_four_points() {
let empty = Topology::new();
let mut e = empty.edit();
e.add_tile(TileLevel::Lower, [0, 0, 0]).unwrap();
e.add_tile(TileLevel::Lower, [8, 0, 0]).unwrap();
let (t, _) = e.commit().unwrap();
let s = text(&t, &Field::new(&t, 1_u8));
assert!(s.contains(r#"<Piece NumberOfPoints="12" NumberOfCells="2">"#), "{s}");
assert!(s.contains(r#"<DataArray type="UInt8" Name="d" format="ascii">"#), "{s}");
}
#[test]
fn an_empty_grid_has_no_cell() {
let t = Topology::new();
let s = text(&t, &Field::new(&t, 0.0_f64));
assert!(s.contains(r#"<Piece NumberOfPoints="0" NumberOfCells="0">"#), "{s}");
}
#[test]
fn a_nan_is_written_as_nan() {
let (t, f) = one_tile([0, 0, 0], -f64::NAN);
assert!(text(&t, &f).contains("format=\"ascii\">\nNaN\n</DataArray>"));
}
#[test]
fn an_infinity_is_refused_before_anything_is_written() {
for value in [f32::INFINITY, f32::NEG_INFINITY] {
let (t, f) = one_tile([0, 8, 0], value);
let got = to_bytes(&t, &f, "d");
assert!(matches!(got, Err(VtkError::Infinite { origin: [0, 8, 0] })), "{got:?}");
}
let empty = Topology::new();
let mut e = empty.edit();
e.activate([0, 0, 0]).unwrap();
let (t, _) = e.commit().unwrap();
let mut f = Field::new(&t, 0.0_f64);
let n = t.leaf_numbers().next().unwrap();
f.leaf_mut(&t, n).unwrap()[8 + 64] = f64::INFINITY;
let got = to_bytes(&t, &f, "d");
assert!(matches!(got, Err(VtkError::Infinite { origin: [0, 1, 1] })), "{got:?}");
}
#[test]
fn a_name_needing_escapes_or_empty_is_refused() {
let (t, f) = one_tile([0, 0, 0], 1_u32);
for name in ["", "a<b", "a&b", "a\"b", "a'b", "tab\t", "é"] {
let got = to_bytes(&t, &f, name);
assert!(matches!(got, Err(VtkError::Name(ref n)) if n == name), "{name:?}: {got:?}");
}
assert!(to_bytes(&t, &f, "signed distance_2").is_ok());
}
}