use crate::core::{Cell, CellKind, Error, Mesh, Point, Result};
use std::collections::{BTreeMap, BTreeSet};
use std::io::Write;
fn err(message: impl Into<String>) -> Error {
Error::new("E_INP", message)
}
fn kind(name: &str) -> Result<CellKind> {
match name {
"B31" | "T3D2" => Ok(CellKind::Line2),
"S3" | "CPS3" | "CPE3" => Ok(CellKind::Triangle3),
"S4" | "CPS4" | "CPE4" => Ok(CellKind::Quad4),
"C3D4" => Ok(CellKind::Tet4),
"C3D5" => Ok(CellKind::Pyramid5),
"C3D6" => Ok(CellKind::Wedge6),
"C3D8" => Ok(CellKind::Hex8),
_ => Err(err(format!("unsupported element type {name}"))),
}
}
fn name(kind: CellKind) -> &'static str {
match kind {
CellKind::Line2 => "B31",
CellKind::Triangle3 => "S3",
CellKind::Quad4 => "S4",
CellKind::Tet4 => "C3D4",
CellKind::Pyramid5 => "C3D5",
CellKind::Wedge6 => "C3D6",
CellKind::Hex8 => "C3D8",
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Inspection {
pub mesh: Mesh,
pub omitted_keywords: BTreeSet<String>,
}
#[allow(clippy::too_many_lines)] pub fn read(source: &str) -> Result<Inspection> {
enum Mode {
None,
Node,
Element(CellKind),
}
let mut mode = Mode::None;
let mut mesh = Mesh::default();
let mut omitted = BTreeSet::new();
let mut node_ids = BTreeMap::new();
let mut pending = Vec::<(u64, CellKind, Vec<u64>)>::new();
let mut seen_node = false;
let mut seen_element = false;
for (line_no, line) in source.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with("**") {
continue;
}
if let Some(stripped) = line.strip_prefix('*') {
let mut parts = stripped.split(',').map(str::trim);
let keyword = parts.next().unwrap_or("").to_ascii_uppercase();
if matches!(
keyword.as_str(),
"INCLUDE"
| "PART"
| "END PART"
| "ASSEMBLY"
| "INSTANCE"
| "END INSTANCE"
| "SYSTEM"
| "NGEN"
| "ELGEN"
) {
return Err(
err(format!("{keyword} needs scoped/expanded geometry")).at(line_no + 1)
);
}
mode = match keyword.as_str() {
"NODE" => {
if parts.next().is_some() {
return Err(err("NODE options are unsupported").at(line_no + 1));
}
seen_node = true;
Mode::Node
}
"ELEMENT" => {
seen_element = true;
let type_value = parts
.next()
.and_then(|part| {
part.split_once('=')
.filter(|(key, _)| key.trim().eq_ignore_ascii_case("TYPE"))
.map(|(_, v)| v.trim().to_ascii_uppercase())
})
.ok_or_else(|| err("ELEMENT requires TYPE").at(line_no + 1))?;
if parts.next().is_some() {
return Err(
err("ELEMENT options beyond TYPE are unsupported").at(line_no + 1)
);
}
Mode::Element(kind(&type_value).map_err(|e| e.at(line_no + 1))?)
}
_ => {
omitted.insert(keyword);
Mode::None
}
};
continue;
}
let values: Vec<&str> = line.split(',').map(str::trim).collect();
match mode {
Mode::Node => {
if values.len() != 4 {
return Err(err("NODE needs ID,X,Y,Z").at(line_no + 1));
}
let id = values[0]
.parse::<u64>()
.map_err(|_| err("invalid node ID").at(line_no + 1))?;
let mut position = [0.; 3];
for i in 0..3 {
position[i] = values[i + 1]
.parse()
.map_err(|_| err("invalid coordinate").at(line_no + 1))?;
}
if node_ids.insert(id, mesh.points.len()).is_some() {
return Err(err("duplicate node ID").at(line_no + 1));
}
mesh.points.push(Point { id, position });
}
Mode::Element(cell_kind) => {
if values.len() != cell_kind.node_count() + 1 {
return Err(
err("element node count mismatch or unsupported continuation")
.at(line_no + 1),
);
}
let id = values[0]
.parse()
.map_err(|_| err("invalid element ID").at(line_no + 1))?;
let nodes = values[1..]
.iter()
.map(|v| {
v.parse::<u64>()
.map_err(|_| err("invalid element node").at(line_no + 1))
})
.collect::<Result<Vec<_>>>()?;
pending.push((id, cell_kind, nodes));
}
Mode::None => {}
}
}
if !seen_node || !seen_element {
return Err(err("both NODE and ELEMENT blocks are required"));
}
for (id, kind, nodes) in pending {
let connectivity = nodes
.into_iter()
.map(|node| {
node_ids
.get(&node)
.copied()
.ok_or_else(|| err(format!("unknown node {node}")))
})
.collect::<Result<Vec<_>>>()?;
mesh.cells.push(Cell {
id,
kind,
connectivity,
property_id: None,
});
}
mesh.validate()?;
Ok(Inspection {
mesh,
omitted_keywords: omitted,
})
}
pub fn write(mesh: &Mesh, mut writer: impl Write) -> Result<()> {
mesh.validate()?;
mesh.require_no_sets("E_INP")?;
if mesh.cells.iter().any(|c| c.property_id.is_some()) {
return Err(err(
"property IDs have no lossless INP mapping in this writer",
));
}
writeln!(
writer,
"** caexfer geometry projection; no sections, materials, loads, or units\n*NODE"
)?;
for p in &mesh.points {
writeln!(
writer,
"{}, {}, {}, {}",
p.id, p.position[0], p.position[1], p.position[2]
)?;
}
let mut groups: BTreeMap<&str, Vec<&Cell>> = BTreeMap::new();
for c in &mesh.cells {
groups.entry(name(c.kind)).or_default().push(c);
}
for (element_type, cells) in groups {
writeln!(writer, "*ELEMENT, TYPE={element_type}")?;
for c in cells {
write!(writer, "{}", c.id)?;
for &idx in &c.connectivity {
write!(writer, ", {}", mesh.points[idx].id)?;
}
writeln!(writer)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_deferred_element_nodes_by_original_id() {
let source = "*ELEMENT, TYPE=B31\n10, 20, 10\n*NODE\n10, 0, 0, 0\n20, 1, 0, 0\n";
let inspection = read(source).unwrap();
assert_eq!(inspection.mesh.cells[0].id, 10);
assert_eq!(inspection.mesh.cells[0].connectivity, [1, 0]);
assert_eq!(inspection.mesh.points[0].id, 10);
}
/// Incomplete or ambiguous flat geometry fails with a source line when known.
#[test]
fn malformed_flat_geometry_is_rejected() {
let cases = [
(
"*NODE\n1, 0, 0\n*ELEMENT, TYPE=B31\n",
"NODE needs",
Some(2),
),
(
"*NODE\n1, 0, , 0, 0\n*ELEMENT, TYPE=B31\n",
"NODE needs",
Some(2),
),
(
"*NODE\nx, 0, 0, 0\n*ELEMENT, TYPE=B31\n",
"invalid node ID",
Some(2),
),
(
"*NODE\n1, x, 0, 0\n*ELEMENT, TYPE=B31\n",
"invalid coordinate",
Some(2),
),
(
"*NODE\n1, 0, 0, 0\n1, 1, 0, 0\n*ELEMENT, TYPE=B31\n",
"duplicate node ID",
Some(3),
),
(
"*NODE\n1, 0, 0, 0\n*ELEMENT, TYPE=B31\n10, 1\n",
"element node count",
Some(4),
),
(
"*NODE\n1, 0, 0, 0\n2, 1, 0, 0\n*ELEMENT, TYPE=B31\n10, 1, , 2\n",
"element node count",
Some(5),
),
(
"*NODE\n1, 0, 0, 0\n*ELEMENT, TYPE=B31\nx, 1, 1\n",
"invalid element ID",
Some(4),
),
(
"*NODE\n1, 0, 0, 0\n*ELEMENT, TYPE=B31\n10, 1, x\n",
"invalid element node",
Some(4),
),
(
"*NODE\n1, 0, 0, 0\n*ELEMENT, TYPE=B31\n10, 1, 2\n",
"unknown node 2",
None,
),
("*NODE\n1, 0, 0, 0\n*ELEMENT\n", "requires TYPE", Some(3)),
(
"*NODE\n1, 0, 0, 0\n*ELEMENT, TYPE=C3D10\n",
"unsupported element type",
Some(3),
),
(
"*NODE, INPUT=nodes.inp\n*ELEMENT, TYPE=B31\n",
"NODE options are unsupported",
Some(1),
),
(
"*NODE\n1, 0, 0, 0\n*ELEMENT, TYPE=B31, INPUT=elements.inp\n",
"ELEMENT options beyond TYPE are unsupported",
Some(3),
),
(
"*NODE, NSET=LEFT\n*ELEMENT, TYPE=B31\n",
"NODE options are unsupported",
Some(1),
),
(
"*NODE\n1, 0, 0, 0\n*ELEMENT, TYPE=B31, ELSET=BEAMS\n",
"ELEMENT options beyond TYPE are unsupported",
Some(3),
),
];
for (source, message, line) in cases {
let error = read(source).unwrap_err();
assert_eq!(error.code, "E_INP");
assert!(error.message.contains(message), "{source:?}: {error}");
assert_eq!(error.line, line, "{source:?}");
}
}
}