//! .fem text parser: `[Section] = value` scalars and `...` property stanzas. use crate::{ ACSolver, ArcSegment, BlockLabel, BoundaryProp, CircuitProp, Coords, FemmDoc, LengthUnit, MaterialProp, Node, PointProp, PrevType, ProblemType, Segment, }; use num_complex::Complex64; 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 .fem 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 .fem 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 = 4.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 version: i32 = 0; let mut point = PointProp::default(); let mut bdry = BoundaryProp::default(); let mut mat = MaterialProp::default(); let mut circ = CircuitProp::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]") { let v = strip_key(raw); let f = parse_f64(v); doc.format = f; version = (10.0 * f + 0.5) as i32; } else if tok.eq_ignore_ascii_case("[frequency]") { doc.frequency = 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("[acsolver]") { doc.ac_solver = match parse_i32(strip_key(raw)) { 1 => ACSolver::Newton, _ => ACSolver::SuccessiveApprox, }; } 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)); if doc.prev_soln.is_empty() { doc.prev_type = PrevType::None; } } else if tok.eq_ignore_ascii_case("[prevtype]") { doc.prev_type = match parse_i32(strip_key(raw)) { 1 => PrevType::Incremental, 2 => PrevType::Frozen, _ => PrevType::None, }; } 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.ap.re = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { point.ap.im = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { point.jp.re = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { point.jp.im = 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.mu = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.sig = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.a0 = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.a1 = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.a2 = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.phi = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.c0.re = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.c0.im = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.c1.re = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.c1.im = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.inner_angle = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { bdry.outer_angle = 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.mu_x = 1.0; mat.mu_y = 1.0; mat.lam_fill = 1.0; } else if tok.eq_ignore_ascii_case("") { mat.name = unquote(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.mu_x = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.mu_y = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.h_c = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.theta_m = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.j_src.re = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.j_src.im = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.cduct = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.theta_hn = parse_f64(strip_key(raw)); if version == 30 { mat.theta_hx = mat.theta_hn; mat.theta_hy = mat.theta_hn; } } else if tok.eq_ignore_ascii_case("") { mat.theta_hx = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.theta_hy = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.lam_d = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.lam_fill = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.lam_type = parse_i32(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.n_strands = parse_i32(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { mat.wire_d = parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { let count = parse_i32(strip_key(raw)); if count > 0 { mat.bh_curve.clear(); mat.bh_curve.reserve(count as usize); for _ in 0..count { let line = lines.next().ok_or(ParseError::UnexpectedEof { context: "", })?; let (b, rest) = take_f64(line); let (h, _) = take_f64(rest); mat.bh_curve.push((b, h)); } } } else if tok.eq_ignore_ascii_case("") { doc.materials.push(std::mem::take(&mut mat)); // circuit stanza } else if tok.eq_ignore_ascii_case("") { circ = CircuitProp::default(); circ.name = String::from("New Circuit"); } else if tok.eq_ignore_ascii_case("") { circ.name = unquote(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { circ.amps.re += parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { circ.amps.im += parse_f64(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { circ.circ_type = parse_i32(strip_key(raw)); } else if tok.eq_ignore_ascii_case("") { doc.circuits.push(std::mem::take(&mut circ)); // 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, _) = take_i32(rest); let bm = resolve_name(bi, doc.points.iter().map(|p| p.name.as_str())); doc.nodes.push(Node { x, y, boundary_marker: bm, 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, _) = take_i32(rest); let bm = resolve_name(bi, doc.boundaries.iter().map(|b| b.name.as_str())); doc.segments.push(Segment { n0, n1, max_side_length: msl, boundary_marker: bm, 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, _) = take_i32(rest); let bm = resolve_name(bi, doc.boundaries.iter().map(|b| b.name.as_str())); doc.arcs.push(ArcSegment { n0, n1, arc_length: al, max_side_length: msl, boundary_marker: bm, 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, mag_dir: 0.0, mag_dir_fctn: String::new(), turns: 1, block_type: String::from(""), in_circuit: 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 (ci, rest) = take_i32(rest); let (mag_dir, rest) = take_f64(rest); let (group, rest) = take_i32(rest); let (turns, rest) = take_i32(rest); let (ext_flags, rest) = take_i32(rest); let mdf = take_quoted(rest); let block_type = if mi == 0 { String::from("") } else { resolve_name(mi, doc.materials.iter().map(|m| m.name.as_str())) }; let in_circuit = if ci == 0 { String::from("") } else { resolve_name(ci, doc.circuits.iter().map(|c| c.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, mag_dir, mag_dir_fctn: mdf, turns, block_type, in_circuit, 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 .fem 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) } /// extracts the contents of the first double-quoted span in `s`, or returns empty. fn take_quoted(s: &str) -> String { let first = s.find('"'); if let Some(a) = first { let after = &s[a + 1..]; if let Some(b) = after.find('"') { return after[..b].to_string(); } } String::new() } 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 } // matches original's 1-char check 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() } // silence dead-code warning in lib.rs until the writer module consumes it. #[allow(dead_code)] fn _force_complex_use(_c: Complex64) {}