#[cfg(test)]
use crate::core::Mesh;
use crate::core::{CellKind, Dataset, Error, Field, FieldLocation, Result};
use std::io::Write;
pub(super) fn vtk_type(kind: CellKind) -> u8 {
match kind {
CellKind::Line2 => 3,
CellKind::Triangle3 => 5,
CellKind::Quad4 => 9,
CellKind::Tet4 => 10,
CellKind::Hex8 => 12,
CellKind::Wedge6 => 13,
CellKind::Pyramid5 => 14,
}
}
#[cfg(test)]
pub fn write(mesh: &Mesh, writer: impl Write) -> Result<()> {
write_data(
&Dataset {
mesh: mesh.clone(),
fields: Vec::new(),
},
writer,
)
}
#[allow(clippy::too_many_lines)] pub fn write_data(dataset: &Dataset, mut writer: impl Write) -> Result<()> {
dataset.validate()?;
dataset.mesh.require_no_sets("E_VTU")?;
let mesh = &dataset.mesh;
for field in &dataset.fields {
if !valid_xml_attribute(&field.name)
|| field
.components
.iter()
.any(|name| !valid_xml_attribute(name))
{
return Err(Error::new(
"E_VTU",
"field name contains an invalid XML character",
));
}
if field.name == "nastran_node_id"
|| field.name == "nastran_element_id"
|| field.name == "nastran_property_id"
{
return Err(Error::new(
"E_VTU",
"field name conflicts with reserved ID array",
));
}
}
let mut entries = 0usize;
for cell in &mesh.cells {
entries = entries
.checked_add(cell.connectivity.len())
.ok_or_else(|| Error::new("E_LIMIT", "connectivity size overflows usize"))?;
}
i64::try_from(entries).map_err(|_| Error::new("E_LIMIT", "VTU offsets exceed Int64"))?;
writeln!(writer, "<?xml version=\"1.0\"?>")?;
writeln!(
writer,
"<VTKFile type=\"UnstructuredGrid\" version=\"0.1\" byte_order=\"LittleEndian\">"
)?;
writeln!(
writer,
"<!-- caexfer: geometry and numeric fields; units unspecified -->"
)?;
writeln!(
writer,
"<UnstructuredGrid><Piece NumberOfPoints=\"{}\" NumberOfCells=\"{}\">",
mesh.points.len(),
mesh.cells.len()
)?;
writeln!(
writer,
"<PointData><DataArray type=\"UInt64\" Name=\"nastran_node_id\" format=\"ascii\">"
)?;
for point in &mesh.points {
writeln!(writer, "{}", point.id)?;
}
writeln!(writer, "</DataArray>")?;
for field in dataset
.fields
.iter()
.filter(|f| f.location == FieldLocation::Point)
{
write_field(field, &mut writer)?;
}
writeln!(writer, "</PointData><CellData>")?;
writeln!(
writer,
"<DataArray type=\"UInt64\" Name=\"nastran_element_id\" format=\"ascii\">"
)?;
for cell in &mesh.cells {
writeln!(writer, "{}", cell.id)?;
}
writeln!(
writer,
"</DataArray><DataArray type=\"UInt64\" Name=\"nastran_property_id\" format=\"ascii\">"
)?;
for cell in &mesh.cells {
writeln!(writer, "{}", cell.property_id.unwrap_or(0))?;
}
writeln!(writer, "</DataArray>")?;
for field in dataset
.fields
.iter()
.filter(|f| f.location == FieldLocation::Cell)
{
write_field(field, &mut writer)?;
}
writeln!(
writer,
"</CellData><Points><DataArray type=\"Float64\" NumberOfComponents=\"3\" format=\"ascii\">"
)?;
for point in &mesh.points {
writeln!(
writer,
"{} {} {}",
point.position[0], point.position[1], point.position[2]
)?;
}
writeln!(
writer,
"</DataArray></Points><Cells><DataArray type=\"Int64\" Name=\"connectivity\" format=\"ascii\">"
)?;
for cell in &mesh.cells {
for index in &cell.connectivity {
write!(writer, "{index} ")?;
}
writeln!(writer)?;
}
writeln!(
writer,
"</DataArray><DataArray type=\"Int64\" Name=\"offsets\" format=\"ascii\">"
)?;
let mut offset = 0;
for cell in &mesh.cells {
offset += cell.connectivity.len();
writeln!(writer, "{offset}")?;
}
writeln!(
writer,
"</DataArray><DataArray type=\"UInt8\" Name=\"types\" format=\"ascii\">"
)?;
for cell in &mesh.cells {
writeln!(writer, "{}", vtk_type(cell.kind))?;
}
writeln!(
writer,
"</DataArray></Cells></Piece></UnstructuredGrid></VTKFile>"
)?;
Ok(())
}
pub(super) fn valid_xml_text(value: &str) -> bool {
value.chars().all(|character| {
matches!(character, '\t' | '\n' | '\r')
|| matches!(character as u32, 0x20..=0xD7FF | 0xE000..=0xFFFD | 0x0001_0000..=0x0010_FFFF)
})
}
fn valid_xml_attribute(value: &str) -> bool {
valid_xml_text(value) && !value.contains(['\t', '\n', '\r'])
}
fn escape(value: &str) -> String {
value
.replace('&', "&")
.replace('"', """)
.replace('<', "<")
.replace('>', ">")
}
fn write_field(field: &Field, writer: &mut impl Write) -> Result<()> {
write!(
writer,
"<DataArray type=\"Float64\" Name=\"{}\" NumberOfComponents=\"{}\" format=\"ascii\"",
escape(&field.name),
field.components.len()
)?;
if let Some(step) = field.step {
write!(writer, " caexfer_step=\"{step}\"")?;
}
if let Some(time) = field.time {
write!(writer, " caexfer_time=\"{time}\"")?;
}
for (i, component) in field.components.iter().enumerate() {
write!(writer, " ComponentName{i}=\"{}\"", escape(component))?;
}
writeln!(writer, ">")?;
for chunk in field.values.chunks(field.components.len()) {
for value in chunk {
write!(writer, "{value} ")?;
}
writeln!(writer)?;
}
writeln!(writer, "</DataArray>")?;
Ok(())
}