- duplicate_segment_rejected

- segment_passing_through_existing_node_splits
- crossing_second_segment_splits_at_intersection
This commit is contained in:
jess 2026-05-12 19:43:03 -07:00
parent 0199f0de06
commit cce57cce6b
1 changed files with 155 additions and 7 deletions

View File

@ -1,7 +1,15 @@
//! geometry editing primitives on [`FemmDoc`]: add, delete, closest-point queries. //! geometry editing primitives on [`FemmDoc`]: add, delete, closest-point queries.
use crate::geom_math::{
line_arc_intersection, line_line_intersection, shortest_distance_from_segment,
};
use crate::{ArcSegment, BlockLabel, FemmDoc, Node, Segment}; use crate::{ArcSegment, BlockLabel, FemmDoc, Node, Segment};
/// fraction of the node-bbox diagonal used as auto-tolerance for intersection-node coalescing.
const BBOX_TOLERANCE_FRAC: f64 = 1.0e-6;
/// fraction of a segment's length within which an off-endpoint node triggers a recursive split.
const ON_LINE_FRAC: f64 = 1.0e-5;
impl FemmDoc { impl FemmDoc {
/// adds a node at (x, y), returning the index of an existing node within `tol` distance when present. /// adds a node at (x, y), returning the index of an existing node within `tol` distance when present.
pub fn add_node(&mut self, x: f64, y: f64, tol: f64) -> usize { pub fn add_node(&mut self, x: f64, y: f64, tol: f64) -> usize {
@ -50,23 +58,103 @@ impl FemmDoc {
self.block_labels.len() - 1 self.block_labels.len() - 1
} }
/// adds a segment between two node indices, or returns the index of an existing duplicate. /// adds a segment between two node indices, splitting at every crossing and through any on-line node.
pub fn add_segment(&mut self, n0: i32, n1: i32) -> Option<usize> { pub fn add_segment(&mut self, n0: i32, n1: i32) -> bool {
if n0 == n1 { return None; } self.add_segment_with_marker(n0, n1, "")
}
/// PSLG-aware variant propagating a boundary-marker name onto every resulting segment piece.
pub fn add_segment_with_marker(&mut self, n0: i32, n1: i32, marker: &str) -> bool {
if n0 == n1 { return false; }
let nn = self.nodes.len() as i32;
if n0 < 0 || n1 < 0 || n0 >= nn || n1 >= nn { return false; }
// duplicate either-orientation.
let (a, b) = if n0 < n1 { (n0, n1) } else { (n1, n0) }; let (a, b) = if n0 < n1 { (n0, n1) } else { (n1, n0) };
for (i, s) in self.segments.iter().enumerate() { for s in &self.segments {
let (sa, sb) = if s.n0 < s.n1 { (s.n0, s.n1) } else { (s.n1, s.n0) }; let (sa, sb) = if s.n0 < s.n1 { (s.n0, s.n1) } else { (s.n1, s.n0) };
if sa == a && sb == b { return Some(i); } if sa == a && sb == b { 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);
if let Some(hit) = line_line_intersection(n0p, n1p, sp0, sp1) {
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 line_arc_intersection(n0p, n1p, 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);
}
self.segments.push(Segment { self.segments.push(Segment {
n0, n1, n0, n1,
max_side_length: -1.0, max_side_length: -1.0,
boundary_marker: String::new(), boundary_marker: marker.to_string(),
hidden: false, hidden: false,
in_group: 0, in_group: 0,
selected: false, selected: false,
}); });
Some(self.segments.len() - 1)
// first non-endpoint node on the new segment's interior, if any.
let length = ((n1p.0 - n0p.0).powi(2) + (n1p.1 - n0p.1).powi(2)).sqrt();
let dmin = length * 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_segment(np, n0p, n1p);
if d < dmin {
split_at = Some(idx);
break;
}
}
if let Some(mid) = split_at {
self.segments.pop();
let a = self.add_segment_with_marker(n0, mid, marker);
let b = self.add_segment_with_marker(mid, n1, marker);
a || b
} else {
true
}
}
/// 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;
}
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);
}
let dx = xmax - xmin;
let dy = ymax - ymin;
(dx * dx + dy * dy).sqrt() * BBOX_TOLERANCE_FRAC
} }
/// adds an arc segment between two node indices, sweeping `arc_length_deg` degrees. /// adds an arc segment between two node indices, sweeping `arc_length_deg` degrees.
@ -210,3 +298,63 @@ fn point_to_segment_distance(px: f64, py: f64, ax: f64, ay: f64, bx: f64, by: f6
let cy = ay + t * dy; let cy = ay + t * dy;
(px - cx).hypot(py - cy) (px - cx).hypot(py - cy)
} }
#[cfg(test)]
mod tests {
use super::*;
fn doc_with_corners() -> FemmDoc {
let mut d = FemmDoc::default();
d.add_node(-1.0, 0.0, 0.0);
d.add_node( 1.0, 0.0, 0.0);
d.add_node( 0.0,-1.0, 0.0);
d.add_node( 0.0, 1.0, 0.0);
d
}
#[test]
fn duplicate_segment_rejected() {
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(0, 1));
assert!(!d.add_segment(0, 1));
assert!(!d.add_segment(1, 0));
assert_eq!(d.segments.len(), 1);
}
#[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.
let mut d = FemmDoc::default();
d.add_node(-1.0, 0.0, 0.0);
d.add_node( 1.0, 0.0, 0.0);
d.add_node( 0.0, 0.0, 0.0);
assert!(d.add_segment(0, 1));
assert_eq!(d.segments.len(), 2);
let touches_mid = d.segments.iter().filter(|s| s.n0 == 2 || s.n1 == 2).count();
assert_eq!(touches_mid, 2);
}
#[test]
fn crossing_second_segment_splits_at_intersection() {
// horizontal (-1,0)->(1,0) added first stays whole.
// the vertical (0,-1)->(0,1) is added second; it intersects at origin,
// so a new node lands at (0,0) and the vertical splits into two pieces.
let mut d = doc_with_corners();
assert!(d.add_segment(0, 1));
assert!(d.add_segment(2, 3));
assert_eq!(d.nodes.len(), 5, "intersection node added");
let new_idx = 4;
assert!((d.nodes[new_idx].x).abs() < 1e-9);
assert!((d.nodes[new_idx].y).abs() < 1e-9);
// exactly one piece touches node 0 (-1,0) and node 4 (0,0): the original horizontal stays whole here.
// expected pieces: 0->1 (horizontal whole), 2->4, 4->3 (vertical halves).
assert_eq!(d.segments.len(), 3);
let touches_new = d.segments.iter().filter(|s| s.n0 == new_idx as i32 || s.n1 == new_idx as i32).count();
assert_eq!(touches_new, 2);
}
}