parser and pre-proc

This commit is contained in:
jess 2026-05-12 21:47:37 -07:00
parent 896ad29936
commit c3495eee92
13 changed files with 162 additions and 22 deletions

View File

@ -8,8 +8,8 @@ rust-version = "1.85"
publish = false
[workspace.dependencies]
femm-sys = { path = "crates/femm-sys" }
femm-doc = { path = "crates/femm-doc" }
femm-sys = { path = "crates/femm-sys" }
femm-doc-mag = { path = "crates/femm-doc-mag" }
[profile.release]
opt-level = 3

View File

@ -11,7 +11,7 @@ name = "femm"
path = "src/main.rs"
[dependencies]
femm-sys = { workspace = true }
femm-doc = { workspace = true }
femm-sys = { workspace = true }
femm-doc-mag = { workspace = true }
iced = { version = "0.14", features = ["canvas"] }
rfd = "0.17"

View File

@ -1,6 +1,6 @@
//! draws a FemmDoc on an iced canvas: nodes, segments, arcs, block labels, with pan/zoom and click-to-add.
use femm_doc::FemmDoc;
use femm_doc_mag::FemmDoc;
use iced::widget::canvas::{
self, Action, Canvas, Event, Frame, Geometry, Path, Stroke, Text, path::Builder,
};

View File

@ -3,7 +3,7 @@
mod doc_canvas;
use doc_canvas::{CanvasMessage, Tool};
use femm_doc::FemmDoc;
use femm_doc_mag::FemmDoc;
use iced::widget::{button, column, container, row, text};
use iced::{Element, Length, Task};

View File

@ -1,5 +1,5 @@
[package]
name = "femm-doc"
name = "femm-doc-mag"
version = "0.0.1"
edition.workspace = true
rust-version.workspace = true

View File

@ -1,9 +1,11 @@
//! geometry editing primitives on [`FemmDoc`]: add, delete, closest-point queries.
use crate::geom_math::{
line_arc_intersection, line_line_intersection, shortest_distance_from_segment,
arc_arc_intersection, circle_from_arc, line_arc_intersection, line_line_intersection,
shortest_distance_from_arc, shortest_distance_from_segment,
};
use crate::{ArcSegment, BlockLabel, FemmDoc, Node, Segment};
use num_complex::Complex64;
/// fraction of the node-bbox diagonal used as auto-tolerance for intersection-node coalescing.
const BBOX_TOLERANCE_FRAC: f64 = 1.0e-6;
@ -190,10 +192,9 @@ impl FemmDoc {
self.block_labels = old_block_labels;
}
/// adds an arc segment between two node indices, sweeping `arc_length_deg` degrees.
pub fn add_arc_segment(&mut self, n0: i32, n1: i32, arc_length_deg: f64) -> Option<usize> {
if n0 == n1 { return None; }
self.arcs.push(ArcSegment {
/// adds an arc between two node indices, splitting at every crossing and through any on-arc node.
pub fn add_arc_segment(&mut self, n0: i32, n1: i32, arc_length_deg: f64) -> bool {
let template = ArcSegment {
n0, n1,
arc_length: arc_length_deg,
max_side_length: 10.0,
@ -202,8 +203,97 @@ impl FemmDoc {
in_group: 0,
normal_direction: true,
selected: false,
});
Some(self.arcs.len() - 1)
};
self.add_arc_segment_with_template(n0, n1, arc_length_deg, &template)
}
/// PSLG-aware variant propagating boundary marker and side-length metadata onto every arc piece.
pub fn add_arc_segment_with_template(
&mut self,
n0: i32,
n1: i32,
arc_length_deg: f64,
template: &ArcSegment,
) -> bool {
if n0 == n1 { return false; }
let nn = self.nodes.len() as i32;
if n0 < 0 || n1 < 0 || n0 >= nn || n1 >= nn { return false; }
// same directed endpoints with similar sweep counts as duplicate.
for a in &self.arcs {
if a.n0 == n0 && a.n1 == n1 && (a.arc_length - arc_length_deg).abs() < 1.0e-2 {
return false;
}
}
let n0p = (self.nodes[n0 as usize].x, self.nodes[n0 as usize].y);
let n1p = (self.nodes[n1 as usize].x, self.nodes[n1 as usize].y);
// crossings with existing segments and arcs.
let mut new_points: Vec<(f64, f64)> = Vec::new();
for s in &self.segments {
let sp0 = (self.nodes[s.n0 as usize].x, self.nodes[s.n0 as usize].y);
let sp1 = (self.nodes[s.n1 as usize].x, self.nodes[s.n1 as usize].y);
for hit in line_arc_intersection(sp0, sp1, n0p, n1p, arc_length_deg) {
new_points.push(hit);
}
}
for arc in &self.arcs {
let ap0 = (self.nodes[arc.n0 as usize].x, self.nodes[arc.n0 as usize].y);
let ap1 = (self.nodes[arc.n1 as usize].x, self.nodes[arc.n1 as usize].y);
for hit in arc_arc_intersection(n0p, n1p, arc_length_deg, ap0, ap1, arc.arc_length) {
new_points.push(hit);
}
}
let tol = self.bbox_tolerance();
for (x, y) in new_points {
self.add_node(x, y, tol);
}
let new_arc = ArcSegment {
n0, n1,
arc_length: arc_length_deg,
..template.clone()
};
self.arcs.push(new_arc);
// first non-endpoint node on the new arc's sweep range, if any.
let (cx, cy, radius) = circle_from_arc(n0p, n1p, arc_length_deg);
let sweep_rad = arc_length_deg.to_radians();
let arc_length_world = radius * sweep_rad.abs();
let dmin = arc_length_world * ON_LINE_FRAC;
let mut split_at: Option<i32> = None;
for (i, node) in self.nodes.iter().enumerate() {
let idx = i as i32;
if idx == n0 || idx == n1 { continue; }
let np = (node.x, node.y);
let de0 = ((np.0 - n0p.0).powi(2) + (np.1 - n0p.1).powi(2)).sqrt();
let de1 = ((np.0 - n1p.0).powi(2) + (np.1 - n1p.1).powi(2)).sqrt();
if de0 < dmin || de1 < dmin { continue; }
let d = shortest_distance_from_arc(np, n0p, n1p, arc_length_deg);
if d < dmin {
split_at = Some(idx);
break;
}
}
if let Some(mid) = split_at {
self.arcs.pop();
let mid_pos = (self.nodes[mid as usize].x, self.nodes[mid as usize].y);
let c = Complex64::new(cx, cy);
let a0 = Complex64::new(n0p.0, n0p.1);
let a1 = Complex64::new(n1p.0, n1p.1);
let a2 = Complex64::new(mid_pos.0, mid_pos.1);
let sweep_to_mid = ((a2 - c) / (a0 - c)).arg().to_degrees();
let sweep_from_mid = ((a1 - c) / (a2 - c)).arg().to_degrees();
let a = self.add_arc_segment_with_template(n0, mid, sweep_to_mid, template);
let b = self.add_arc_segment_with_template(mid, n1, sweep_from_mid, template);
a || b
} else {
true
}
}
/// removes selected nodes and rewrites segment/arc endpoint indices to drop references.
@ -376,8 +466,8 @@ mod tests {
#[test]
fn segment_passing_through_existing_node_splits() {
// nodes at (-1, 0), (1, 0), and (0, 0); adding a segment between the outer two
// must split at the midpoint node, yielding two segments.
// three colinear nodes (-1,0), (1,0), (0,0). adding the outer-to-outer segment
// splits at the midpoint node, producing two pieces meeting there.
let mut d = FemmDoc::default();
d.add_node(-1.0, 0.0, 0.0);
d.add_node( 1.0, 0.0, 0.0);
@ -390,18 +480,17 @@ mod tests {
#[test]
fn enforce_pslg_splits_first_segment_at_late_intersection() {
// horizontal added first stays whole; vertical second, its split inserts node 4 at origin.
// enforce_pslg now re-adds both segments fresh; with node 4 already in place, both
// get split through it, ending at 4 segments touching the origin node.
// incremental edit splits only the second of two crossing segments. enforce_pslg
// back-splits the first segment through the intersection node.
let mut d = doc_with_corners();
assert!(d.add_segment(0, 1));
assert!(d.add_segment(2, 3));
assert_eq!(d.segments.len(), 3, "before enforce: one whole, one split");
assert_eq!(d.segments.len(), 3, "pre-enforce: one whole, one split");
d.enforce_pslg();
assert_eq!(d.nodes.len(), 5, "no node added or merged by enforce");
assert_eq!(d.segments.len(), 4, "both originals now split at the origin");
assert_eq!(d.segments.len(), 4, "both segments split at the origin");
let touches_origin = d.segments.iter().filter(|s| s.n0 == 4 || s.n1 == 4).count();
assert_eq!(touches_origin, 4);
}
@ -419,6 +508,57 @@ mod tests {
assert_eq!(d.segments[0].boundary_marker, "outer");
}
#[test]
fn duplicate_arc_rejected() {
let mut d = FemmDoc::default();
d.add_node(1.0, 0.0, 0.0);
d.add_node(0.0, 1.0, 0.0);
assert!(d.add_arc_segment(0, 1, 90.0));
assert!(!d.add_arc_segment(0, 1, 90.0));
// reversed endpoints define a distinct arc.
assert!(d.add_arc_segment(1, 0, 90.0));
assert_eq!(d.arcs.len(), 2);
}
#[test]
fn add_segment_crossing_unit_quarter_arc_splits_segment() {
// unit quarter arc from (1,0) to (0,1); horizontal line at y=0.5 crosses it once.
// expect a new intersection node near (0.866, 0.5) and the segment split into two.
let mut d = FemmDoc::default();
d.add_node( 1.0, 0.0, 0.0);
d.add_node( 0.0, 1.0, 0.0);
d.add_node(-2.0, 0.5, 0.0);
d.add_node( 2.0, 0.5, 0.0);
assert!(d.add_arc_segment(0, 1, 90.0));
assert!(d.add_segment(2, 3));
assert_eq!(d.nodes.len(), 5);
let n4 = &d.nodes[4];
assert!((n4.x - 0.75_f64.sqrt()).abs() < 1e-6);
assert!((n4.y - 0.5).abs() < 1e-9);
assert_eq!(d.segments.len(), 2);
assert_eq!(d.arcs.len(), 1);
}
#[test]
fn add_arc_passing_through_existing_node_splits_into_two_arcs() {
// pre-existing node at (cos45, sin45) lies on the unit quarter arc from (1,0) to (0,1).
// adding the arc should detect the on-arc node and emit two sub-arcs of 45 degrees each.
let mut d = FemmDoc::default();
d.add_node(1.0, 0.0, 0.0);
d.add_node(0.0, 1.0, 0.0);
let mid_x = std::f64::consts::FRAC_1_SQRT_2;
d.add_node(mid_x, mid_x, 0.0);
assert!(d.add_arc_segment(0, 1, 90.0));
assert_eq!(d.arcs.len(), 2);
let touches_mid = d.arcs.iter().filter(|a| a.n0 == 2 || a.n1 == 2).count();
assert_eq!(touches_mid, 2);
// split sweeps sum to the parent 90 degrees, within floating slop.
let total: f64 = d.arcs.iter().map(|a| a.arc_length).sum();
assert!((total - 90.0).abs() < 1e-6, "sum of split sweeps = {total}");
}
#[test]
fn crossing_second_segment_splits_at_intersection() {
// horizontal (-1,0)->(1,0) added first stays whole.

View File

@ -1,4 +1,4 @@
use femm_doc::FemmDoc;
use femm_doc_mag::FemmDoc;
const FIXTURE: &str = r#"[Format] = 4.0
[Frequency] = 60