From 896ad29936bf665b0178bf46697d7d71967e086a Mon Sep 17 00:00:00 2001 From: jess Date: Tue, 12 May 2026 20:06:57 -0700 Subject: [PATCH] add_segment_with_marker using closest_node --- crates/femm-doc/src/edit.rs | 106 ++++++++++++++++++++++++++++++++---- 1 file changed, 94 insertions(+), 12 deletions(-) diff --git a/crates/femm-doc/src/edit.rs b/crates/femm-doc/src/edit.rs index a0c00dd..29f05da 100644 --- a/crates/femm-doc/src/edit.rs +++ b/crates/femm-doc/src/edit.rs @@ -141,20 +141,53 @@ impl FemmDoc { /// auto-tolerance for intersection-node coalescing, derived from the node bounding box. fn bbox_tolerance(&self) -> f64 { - if self.nodes.len() < 2 { - return 1.0e-8; + nodes_bbox_tolerance(&self.nodes) + } + + /// rebuilds every list through the PSLG-aware add primitives, catching crossings missed by incremental edits. + pub fn enforce_pslg(&mut self) { + let old_nodes = std::mem::take(&mut self.nodes); + let old_segments = std::mem::take(&mut self.segments); + let old_arcs = std::mem::take(&mut self.arcs); + let old_block_labels = std::mem::take(&mut self.block_labels); + + let tol = nodes_bbox_tolerance(&old_nodes); + + // dedupes by position with metadata preserved. + for n in &old_nodes { + let mut duplicate = false; + for existing in &self.nodes { + if (existing.x - n.x).hypot(existing.y - n.y) < tol { + duplicate = true; + break; + } + } + if !duplicate { + self.nodes.push(n.clone()); + } } - let mut xmin = f64::INFINITY; - let mut xmax = f64::NEG_INFINITY; - let mut ymin = f64::INFINITY; - let mut ymax = f64::NEG_INFINITY; - for n in &self.nodes { - xmin = xmin.min(n.x); xmax = xmax.max(n.x); - ymin = ymin.min(n.y); ymax = ymax.max(n.y); + + for s in old_segments { + let p0 = (old_nodes[s.n0 as usize].x, old_nodes[s.n0 as usize].y); + let p1 = (old_nodes[s.n1 as usize].x, old_nodes[s.n1 as usize].y); + let (Some(n0), Some(n1)) = ( + self.closest_node(p0.0, p0.1), + self.closest_node(p1.0, p1.1), + ) else { continue }; + self.add_segment_with_marker(n0 as i32, n1 as i32, &s.boundary_marker); } - let dx = xmax - xmin; - let dy = ymax - ymin; - (dx * dx + dy * dy).sqrt() * BBOX_TOLERANCE_FRAC + + for a in old_arcs { + let p0 = (old_nodes[a.n0 as usize].x, old_nodes[a.n0 as usize].y); + let p1 = (old_nodes[a.n1 as usize].x, old_nodes[a.n1 as usize].y); + let (Some(n0), Some(n1)) = ( + self.closest_node(p0.0, p0.1), + self.closest_node(p1.0, p1.1), + ) else { continue }; + self.add_arc_segment(n0 as i32, n1 as i32, a.arc_length); + } + + self.block_labels = old_block_labels; } /// adds an arc segment between two node indices, sweeping `arc_length_deg` degrees. @@ -285,6 +318,24 @@ impl FemmDoc { } } +/// auto-tolerance derived from the bounding box of a node slice. +fn nodes_bbox_tolerance(nodes: &[Node]) -> f64 { + if nodes.len() < 2 { + return 1.0e-8; + } + let mut xmin = f64::INFINITY; + let mut xmax = f64::NEG_INFINITY; + let mut ymin = f64::INFINITY; + let mut ymax = f64::NEG_INFINITY; + for n in nodes { + xmin = xmin.min(n.x); xmax = xmax.max(n.x); + ymin = ymin.min(n.y); ymax = ymax.max(n.y); + } + let dx = xmax - xmin; + let dy = ymax - ymin; + (dx * dx + dy * dy).sqrt() * BBOX_TOLERANCE_FRAC +} + /// Euclidean distance from (px, py) to the segment between (ax, ay) and (bx, by). fn point_to_segment_distance(px: f64, py: f64, ax: f64, ay: f64, bx: f64, by: f64) -> f64 { let dx = bx - ax; @@ -337,6 +388,37 @@ mod tests { assert_eq!(touches_mid, 2); } + #[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. + 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"); + + 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"); + let touches_origin = d.segments.iter().filter(|s| s.n0 == 4 || s.n1 == 4).count(); + assert_eq!(touches_origin, 4); + } + + #[test] + fn enforce_pslg_preserves_segment_boundary_marker() { + let mut d = FemmDoc::default(); + d.add_node(0.0, 0.0, 0.0); + d.add_node(1.0, 0.0, 0.0); + assert!(d.add_segment_with_marker(0, 1, "outer")); + + d.enforce_pslg(); + + assert_eq!(d.segments.len(), 1); + assert_eq!(d.segments[0].boundary_marker, "outer"); + } + #[test] fn crossing_second_segment_splits_at_intersection() { // horizontal (-1,0)->(1,0) added first stays whole.