//! .feh text parser: `[Section] = value` scalars and `...` 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 { 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("") { point = PointProp::default(); point.name = String::from("New Point Property"); } else if tok.eq_ignore_ascii_case("") { point.name = unquote(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { point.tp = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { point.qp = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { doc.points.push(std::mem::take(&mut point)); // boundary property stanza } else if tok.eq_ignore_ascii_case("") { bdry = BoundaryProp::default(); bdry.name = String::from("New Boundary"); } else if tok.eq_ignore_ascii_case("") { bdry.name = unquote(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.format = parse_i32(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.tset = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.qs = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.beta = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.h = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.tinf = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { doc.boundaries.push(std::mem::take(&mut bdry)); // material property stanza } else if tok.eq_ignore_ascii_case("") { 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("") { mat.name = unquote(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.kx = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.ky = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.kt = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.qv = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { 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: "", })?; let (t, rest) = take_f64(line); let (k, _) = take_f64(rest); mat.tk_curve.push((t, k)); } } } else if tok.eq_ignore_ascii_case("") { doc.materials.push(std::mem::take(&mut mat)); // conductor stanza } else if tok.eq_ignore_ascii_case("") { cond = ConductorProp::default(); cond.name = String::from("New Conductor"); } else if tok.eq_ignore_ascii_case("") { cond.name = unquote(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { cond.tc = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { cond.qc = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { cond.conductor_type = parse_i32(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { 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(""), in_conductor: String::from(""), 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("") } 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(""), 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) -> Result { 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::().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::().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::().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::().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>(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 "". fn resolve_conductor<'a, I: Iterator>(idx: i32, names: I) -> String { if idx <= 0 { return String::from(""); } let want = idx as usize; for (i, name) in names.enumerate() { if i + 1 == want { return name.to_string(); } } String::from("") }