FEMM/crates/femm-doc-heat/src/mesh.rs

162 lines
5.9 KiB
Rust

//! readers for Triangle's .node and .ele output files.
use std::path::Path;
use thiserror::Error;
/// 2D triangular mesh consumed by the canvas overlay and by the post-processor.
#[derive(Debug, Default, Clone)]
pub struct Mesh {
pub nodes: Vec<MeshNode>,
pub elements: Vec<MeshElement>,
}
/// position and optional boundary marker for a single mesh node.
#[derive(Debug, Clone, Copy)]
pub struct MeshNode {
pub x: f64,
pub y: f64,
pub marker: i32,
}
/// triangle element with three zero-based node indices and an optional region attribute.
#[derive(Debug, Clone, Copy)]
pub struct MeshElement {
pub v0: u32,
pub v1: u32,
pub v2: u32,
pub attribute: i32,
}
#[derive(Debug, Error)]
pub enum MeshLoadError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("malformed header in {file}: {reason}")]
BadHeader { file: &'static str, reason: String },
#[error("malformed row in {file}: {reason}")]
BadRow { file: &'static str, reason: String },
}
impl Mesh {
/// loads .node and .ele next to the given path stem.
pub fn load(stem: impl AsRef<Path>) -> Result<Self, MeshLoadError> {
let stem = stem.as_ref();
let node_path = with_extension(stem, "node");
let ele_path = with_extension(stem, "ele");
let nodes = parse_node(&std::fs::read_to_string(&node_path)?)?;
let elements = parse_ele(&std::fs::read_to_string(&ele_path)?)?;
Ok(Mesh { nodes, elements })
}
}
/// parses a .node file: header `count 2 0 nbm`, then count rows of `idx x y [marker]`.
pub fn parse_node(src: &str) -> Result<Vec<MeshNode>, MeshLoadError> {
let mut lines = src.lines().filter(|l| !l.trim_start().starts_with('#') && !l.trim().is_empty());
let header = lines.next().ok_or(MeshLoadError::BadHeader {
file: ".node",
reason: "empty file".into(),
})?;
let mut hparts = header.split_whitespace();
let count: usize = hparts.next()
.and_then(|s| s.parse().ok())
.ok_or(MeshLoadError::BadHeader { file: ".node", reason: format!("expected count, got {header:?}") })?;
let _dim = hparts.next();
let _attrs = hparts.next();
let has_marker = hparts.next().map(|s| s.trim() == "1").unwrap_or(false);
let mut nodes = Vec::with_capacity(count);
for line in lines.take(count) {
let mut parts = line.split_whitespace();
let _idx: u32 = parts.next().and_then(|s| s.parse().ok())
.ok_or(MeshLoadError::BadRow { file: ".node", reason: line.into() })?;
let x: f64 = parts.next().and_then(|s| s.parse().ok())
.ok_or(MeshLoadError::BadRow { file: ".node", reason: line.into() })?;
let y: f64 = parts.next().and_then(|s| s.parse().ok())
.ok_or(MeshLoadError::BadRow { file: ".node", reason: line.into() })?;
let marker = if has_marker {
parts.next().and_then(|s| s.parse().ok()).unwrap_or(0)
} else {
0
};
nodes.push(MeshNode { x, y, marker });
}
Ok(nodes)
}
/// parses a .ele file: header `count 3 natt`, then count rows of `idx v0 v1 v2 [attr0 ...]`.
pub fn parse_ele(src: &str) -> Result<Vec<MeshElement>, MeshLoadError> {
let mut lines = src.lines().filter(|l| !l.trim_start().starts_with('#') && !l.trim().is_empty());
let header = lines.next().ok_or(MeshLoadError::BadHeader {
file: ".ele",
reason: "empty file".into(),
})?;
let mut hparts = header.split_whitespace();
let count: usize = hparts.next()
.and_then(|s| s.parse().ok())
.ok_or(MeshLoadError::BadHeader { file: ".ele", reason: format!("expected count, got {header:?}") })?;
let _nodes_per = hparts.next();
let natt: usize = hparts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
let mut elements = Vec::with_capacity(count);
for line in lines.take(count) {
let mut parts = line.split_whitespace();
let _idx: u32 = parts.next().and_then(|s| s.parse().ok())
.ok_or(MeshLoadError::BadRow { file: ".ele", reason: line.into() })?;
let v0: u32 = parts.next().and_then(|s| s.parse().ok())
.ok_or(MeshLoadError::BadRow { file: ".ele", reason: line.into() })?;
let v1: u32 = parts.next().and_then(|s| s.parse().ok())
.ok_or(MeshLoadError::BadRow { file: ".ele", reason: line.into() })?;
let v2: u32 = parts.next().and_then(|s| s.parse().ok())
.ok_or(MeshLoadError::BadRow { file: ".ele", reason: line.into() })?;
let attribute = if natt > 0 {
parts.next().and_then(|s| s.parse::<f64>().ok()).map(|f| f as i32).unwrap_or(0)
} else {
0
};
elements.push(MeshElement { v0, v1, v2, attribute });
}
Ok(elements)
}
fn with_extension(stem: &Path, ext: &str) -> std::path::PathBuf {
let mut s = stem.to_path_buf();
s.set_extension(ext);
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_node_with_markers() {
// 3 nodes, 2D, no attributes, with markers; comment line at the start is ignored.
let src = "# triangle .node\n3 2 0 1\n0 0.0 0.0 2\n1 1.0 0.0 0\n2 0.0 1.0 0\n";
let nodes = parse_node(src).unwrap();
assert_eq!(nodes.len(), 3);
assert_eq!(nodes[0].marker, 2);
assert!((nodes[2].y - 1.0).abs() < 1e-12);
}
#[test]
fn parses_ele_with_region_attribute() {
// 2 triangles, 3 nodes per element, 1 region attribute column.
let src = "2 3 1\n0 0 1 2 1.0\n1 1 2 3 2.0\n";
let els = parse_ele(src).unwrap();
assert_eq!(els.len(), 2);
assert_eq!(els[0].v0, 0);
assert_eq!(els[0].v1, 1);
assert_eq!(els[0].v2, 2);
assert_eq!(els[0].attribute, 1);
assert_eq!(els[1].attribute, 2);
}
#[test]
fn parses_node_without_marker_column() {
let src = "2 2 0 0\n0 0.0 0.0\n1 1.0 0.0\n";
let nodes = parse_node(src).unwrap();
assert_eq!(nodes.len(), 2);
assert_eq!(nodes[0].marker, 0);
}
}