415 lines
18 KiB
Rust
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>")
|
|
}
|