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

415 lines
18 KiB
Rust

//! .feh text parser: `[Section] = value` scalars and `<beginX>...<endX>` property stanzas.
use crate::{
ArcSegment, BlockLabel, BoundaryProp, ConductorProp, Coords, FemmDoc, LengthUnit,
MaterialProp, Node, PointProp, ProblemType, Segment,
};
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ParseError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("legacy 3.2-format .feh files are not supported")]
LegacyFormat,
#[error("unexpected end of file while reading {context}")]
UnexpectedEof { context: &'static str },
#[error("malformed value in section {section}: {reason}")]
MalformedValue { section: &'static str, reason: String },
}
impl FemmDoc {
/// parses a .feh text buffer into a document.
pub fn parse(src: &str) -> Result<Self, ParseError> {
let mut doc = FemmDoc::default();
doc.depth = -1.0;
doc.smart_mesh = true;
doc.format = 1.0;
if src
.lines()
.next()
.map(|l| l.trim_start().starts_with("Frequency"))
.unwrap_or(false)
{
return Err(ParseError::LegacyFormat);
}
let mut lines = src.lines();
let mut point = PointProp::default();
let mut bdry = BoundaryProp::default();
let mut mat = MaterialProp::default();
let mut cond = ConductorProp::default();
while let Some(raw) = lines.next() {
let tok = first_token(raw);
if tok.is_empty() { continue; }
// problem attributes
if tok.eq_ignore_ascii_case("[format]") {
doc.format = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[depth]") {
doc.depth = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[precision]") {
doc.precision = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[minangle]") {
doc.min_angle = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[dosmartmesh]") {
doc.smart_mesh = parse_i32(strip_key(raw)) != 0;
} else if tok.eq_ignore_ascii_case("[lengthunits]") {
doc.length_units = parse_length_units(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[problemtype]") {
doc.problem_type = parse_problem_type(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[coordinates]") {
doc.coords = parse_coords(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[comment]") {
doc.comment = unescape_comment(unquote(strip_key(raw)));
} else if tok.eq_ignore_ascii_case("[prevsoln]") {
doc.prev_soln = unquote(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[dt]") {
doc.dt = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[extzo]") {
doc.ext_zo = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[extro]") {
doc.ext_ro = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[extri]") {
doc.ext_ri = parse_f64(strip_key(raw));
// point property stanza
} else if tok.eq_ignore_ascii_case("<beginpoint>") {
point = PointProp::default();
point.name = String::from("New Point Property");
} else if tok.eq_ignore_ascii_case("<pointname>") {
point.name = unquote(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<tp>") {
point.tp = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<qp>") {
point.qp = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<endpoint>") {
doc.points.push(std::mem::take(&mut point));
// boundary property stanza
} else if tok.eq_ignore_ascii_case("<beginbdry>") {
bdry = BoundaryProp::default();
bdry.name = String::from("New Boundary");
} else if tok.eq_ignore_ascii_case("<bdryname>") {
bdry.name = unquote(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<bdrytype>") {
bdry.format = parse_i32(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<tset>") {
bdry.tset = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<qs>") {
bdry.qs = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<beta>") {
bdry.beta = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<h>") {
bdry.h = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<tinf>") {
bdry.tinf = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<endbdry>") {
doc.boundaries.push(std::mem::take(&mut bdry));
// material property stanza
} else if tok.eq_ignore_ascii_case("<beginblock>") {
mat = MaterialProp::default();
mat.name = String::from("New Material");
mat.kx = 1.0;
mat.ky = 1.0;
mat.kt = 3.0;
mat.qv = 0.0;
} else if tok.eq_ignore_ascii_case("<blockname>") {
mat.name = unquote(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<kx>") {
mat.kx = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<ky>") {
mat.ky = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<kt>") {
mat.kt = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<qv>") {
mat.qv = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<tkpoints>") {
let count = parse_i32(strip_key(raw));
if count > 0 {
mat.tk_curve.clear();
mat.tk_curve.reserve(count as usize);
for _ in 0..count {
let line = lines.next().ok_or(ParseError::UnexpectedEof {
context: "<TKPoints>",
})?;
let (t, rest) = take_f64(line);
let (k, _) = take_f64(rest);
mat.tk_curve.push((t, k));
}
}
} else if tok.eq_ignore_ascii_case("<endblock>") {
doc.materials.push(std::mem::take(&mut mat));
// conductor stanza
} else if tok.eq_ignore_ascii_case("<beginconductor>") {
cond = ConductorProp::default();
cond.name = String::from("New Conductor");
} else if tok.eq_ignore_ascii_case("<conductorname>") {
cond.name = unquote(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<tc>") {
cond.tc = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<qc>") {
cond.qc = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<conductortype>") {
cond.conductor_type = parse_i32(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<endconductor>") {
doc.conductors.push(std::mem::take(&mut cond));
// counted geometry sections
} else if tok.eq_ignore_ascii_case("[numpoints]") {
let n = parse_i32(strip_key(raw));
for _ in 0..n {
let line = lines.next().ok_or(ParseError::UnexpectedEof { context: "[NumPoints]" })?;
let (x, rest) = take_f64(line);
let (y, rest) = take_f64(rest);
let (bi, rest) = take_i32(rest);
let (group, rest) = take_i32(rest);
let (ci, _) = take_i32(rest);
let bm = resolve_name(bi, doc.points.iter().map(|p| p.name.as_str()));
let ic = resolve_conductor(ci, doc.conductors.iter().map(|c| c.name.as_str()));
doc.nodes.push(Node {
x, y,
boundary_marker: bm,
in_conductor: ic,
in_group: group,
selected: false,
});
}
} else if tok.eq_ignore_ascii_case("[numsegments]") {
let n = parse_i32(strip_key(raw));
for _ in 0..n {
let line = lines.next().ok_or(ParseError::UnexpectedEof { context: "[NumSegments]" })?;
let (n0, rest) = take_i32(line);
let (n1, rest) = take_i32(rest);
let (msl, rest) = take_f64(rest);
let (bi, rest) = take_i32(rest);
let (hidden, rest) = take_i32(rest);
let (group, rest) = take_i32(rest);
let (ci, _) = take_i32(rest);
let bm = resolve_name(bi, doc.boundaries.iter().map(|b| b.name.as_str()));
let ic = resolve_conductor(ci, doc.conductors.iter().map(|c| c.name.as_str()));
doc.segments.push(Segment {
n0, n1,
max_side_length: msl,
boundary_marker: bm,
in_conductor: ic,
hidden: hidden != 0,
in_group: group,
selected: false,
});
}
} else if tok.eq_ignore_ascii_case("[numarcsegments]") {
let n = parse_i32(strip_key(raw));
for _ in 0..n {
let line = lines.next().ok_or(ParseError::UnexpectedEof { context: "[NumArcSegments]" })?;
let (n0, rest) = take_i32(line);
let (n1, rest) = take_i32(rest);
let (al, rest) = take_f64(rest);
let (msl, rest) = take_f64(rest);
let (bi, rest) = take_i32(rest);
let (hidden, rest) = take_i32(rest);
let (group, rest) = take_i32(rest);
let (ci, _) = take_i32(rest);
let bm = resolve_name(bi, doc.boundaries.iter().map(|b| b.name.as_str()));
let ic = resolve_conductor(ci, doc.conductors.iter().map(|c| c.name.as_str()));
doc.arcs.push(ArcSegment {
n0, n1,
arc_length: al,
max_side_length: msl,
boundary_marker: bm,
in_conductor: ic,
hidden: hidden != 0,
in_group: group,
normal_direction: true,
selected: false,
});
}
} else if tok.eq_ignore_ascii_case("[numholes]") {
let n = parse_i32(strip_key(raw));
for _ in 0..n {
let line = lines.next().ok_or(ParseError::UnexpectedEof { context: "[NumHoles]" })?;
let (x, rest) = take_f64(line);
let (y, rest) = take_f64(rest);
let (group, _) = take_i32(rest);
doc.block_labels.push(BlockLabel {
x, y,
max_area: 0.0,
block_type: String::from("<No Mesh>"),
in_conductor: String::from("<None>"),
in_group: group,
is_external: false,
is_default: false,
selected: false,
});
}
} else if tok.eq_ignore_ascii_case("[numblocklabels]") {
let n = parse_i32(strip_key(raw));
for _ in 0..n {
let line = lines.next().ok_or(ParseError::UnexpectedEof { context: "[NumBlockLabels]" })?;
let (x, rest) = take_f64(line);
let (y, rest) = take_f64(rest);
let (mi, rest) = take_i32(rest);
let (ma_diam, rest) = take_f64(rest);
let (group, rest) = take_i32(rest);
let (ext_flags, _) = take_i32(rest);
let block_type = if mi == 0 {
String::from("<None>")
} else {
resolve_name(mi, doc.materials.iter().map(|m| m.name.as_str()))
};
let max_area = if ma_diam < 0.0 {
0.0
} else {
std::f64::consts::PI * ma_diam * ma_diam / 4.0
};
doc.block_labels.push(BlockLabel {
x, y,
max_area,
block_type,
in_conductor: String::from("<None>"),
in_group: group,
is_external: ext_flags & 1 != 0,
is_default: ext_flags & 2 != 0,
selected: false,
});
}
}
// unknown tokens are skipped silently, matching the original parser.
}
// 3.2-era files omitted [Depth]; fill the default in the current length unit.
if doc.depth == -1.0 {
doc.depth = match doc.length_units {
LengthUnit::Millimeters => 1000.0,
LengthUnit::Centimeters => 100.0,
LengthUnit::Meters => 1.0,
LengthUnit::Mils => 1000.0 / 0.0254,
LengthUnit::Microns => 1.0e6,
LengthUnit::Inches => 1.0 / 0.0254,
};
}
Ok(doc)
}
/// loads a .feh file by path.
pub fn open(path: impl AsRef<Path>) -> Result<Self, ParseError> {
let text = std::fs::read_to_string(path)?;
Self::parse(&text)
}
}
fn first_token(line: &str) -> &str {
line.trim_start().split_whitespace().next().unwrap_or("")
}
fn strip_key(line: &str) -> &str {
match line.find('=') {
Some(i) => &line[i + 1..],
None => "",
}
}
fn unquote(s: &str) -> String {
let s = s.trim();
let first = s.find('"');
let last = s.rfind('"');
match (first, last) {
(Some(a), Some(b)) if a < b => s[a + 1..b].to_string(),
_ => s.to_string(),
}
}
fn unescape_comment(s: String) -> String {
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\\' && i + 1 < bytes.len() && bytes[i + 1] == b'n' {
out.push('\r');
out.push('\n');
i += 2;
} else {
out.push(bytes[i] as char);
i += 1;
}
}
out
}
fn parse_f64(s: &str) -> f64 {
let s = s.trim().trim_matches(|c: char| c == ',' || c.is_whitespace());
let end = s.find(|c: char| c.is_whitespace() || c == ',').unwrap_or(s.len());
s[..end].parse::<f64>().unwrap_or(0.0)
}
fn parse_i32(s: &str) -> i32 {
let s = s.trim().trim_matches(|c: char| c == ',' || c.is_whitespace());
let end = s.find(|c: char| c.is_whitespace() || c == ',').unwrap_or(s.len());
s[..end].parse::<i32>().unwrap_or(0)
}
/// peels one f64 off the head of a whitespace/comma-separated row, returning the remainder.
fn take_f64(s: &str) -> (f64, &str) {
let s = s.trim_start_matches(|c: char| c.is_whitespace() || c == ',');
let end = s.find(|c: char| c.is_whitespace() || c == ',').unwrap_or(s.len());
let (head, tail) = s.split_at(end);
(head.parse::<f64>().unwrap_or(0.0), tail)
}
/// peels one i32 off the head of a whitespace/comma-separated row, returning the remainder.
fn take_i32(s: &str) -> (i32, &str) {
let s = s.trim_start_matches(|c: char| c.is_whitespace() || c == ',');
let end = s.find(|c: char| c.is_whitespace() || c == ',').unwrap_or(s.len());
let (head, tail) = s.split_at(end);
(head.parse::<i32>().unwrap_or(0), tail)
}
fn parse_length_units(s: &str) -> LengthUnit {
let t = first_token(s).to_ascii_lowercase();
if t.starts_with("millimeters") { LengthUnit::Millimeters }
else if t.starts_with("c") { LengthUnit::Centimeters }
else if t.starts_with("meters") { LengthUnit::Meters }
else if t.starts_with("mils") { LengthUnit::Mils }
else if t.starts_with("microns") { LengthUnit::Microns }
else { LengthUnit::Inches }
}
fn parse_problem_type(s: &str) -> ProblemType {
let t = first_token(s).to_ascii_lowercase();
if t.starts_with("axi") { ProblemType::Axisymmetric } else { ProblemType::Planar }
}
fn parse_coords(s: &str) -> Coords {
let t = first_token(s).to_ascii_lowercase();
if t.starts_with("polar") { Coords::Polar } else { Coords::Cartesian }
}
/// resolves a 1-based property index into the matching property name, falling back to empty.
fn resolve_name<'a, I: Iterator<Item = &'a str>>(idx: i32, names: I) -> String {
if idx <= 0 { return String::new(); }
let want = idx as usize;
for (i, name) in names.enumerate() {
if i + 1 == want { return name.to_string(); }
}
String::new()
}
/// resolves a 1-based conductor index into a conductor name, falling back to "<None>".
fn resolve_conductor<'a, I: Iterator<Item = &'a str>>(idx: i32, names: I) -> String {
if idx <= 0 { return String::from("<None>"); }
let want = idx as usize;
for (i, name) in names.enumerate() {
if i + 1 == want { return name.to_string(); }
}
String::from("<None>")
}