- duplicate_segment_rejected
- segment_passing_through_existing_node_splits - crossing_second_segment_splits_at_intersection
This commit is contained in:
parent
0199f0de06
commit
cce57cce6b
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue