add_segment_with_marker using closest_node

This commit is contained in:
jess 2026-05-12 20:06:57 -07:00
parent cce57cce6b
commit 896ad29936
1 changed files with 94 additions and 12 deletions

View File

@ -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.