162 lines
5.9 KiB
Rust
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);
|
|
}
|
|
}
|