FEMM/crates/femm-doc-heat/src/edit.rs

613 lines
23 KiB
Rust

//! geometry editing primitives on [`FemmDoc`]: add, delete, closest-point queries.
use crate::geom_math::{
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;
/// 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 {
/// 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 {
if tol > 0.0 {
for (i, n) in self.nodes.iter().enumerate() {
let dx = n.x - x;
let dy = n.y - y;
if (dx * dx + dy * dy).sqrt() <= tol {
return i;
}
}
}
self.nodes.push(Node {
x, y,
boundary_marker: String::new(),
in_conductor: String::new(),
in_group: 0,
selected: false,
});
self.nodes.len() - 1
}
/// adds a block label at (x, y), returning the index of an existing label within `tol` when present.
pub fn add_block_label(&mut self, x: f64, y: f64, tol: f64) -> usize {
if tol > 0.0 {
for (i, b) in self.block_labels.iter().enumerate() {
let dx = b.x - x;
let dy = b.y - y;
if (dx * dx + dy * dy).sqrt() <= tol {
return i;
}
}
}
self.block_labels.push(BlockLabel {
x, y,
max_area: 0.0,
block_type: String::from("<None>"),
in_conductor: String::from("<None>"),
in_group: 0,
is_external: false,
is_default: false,
selected: false,
});
self.block_labels.len() - 1
}
/// 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) -> bool {
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) };
for s in &self.segments {
let (sa, sb) = if s.n0 < s.n1 { (s.n0, s.n1) } else { (s.n1, s.n0) };
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 {
n0, n1,
max_side_length: -1.0,
boundary_marker: marker.to_string(),
in_conductor: String::new(),
hidden: false,
in_group: 0,
selected: false,
});
// 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 {
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());
}
}
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);
}
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_with_template(n0 as i32, n1 as i32, a.arc_length, &a);
}
self.block_labels = old_block_labels;
}
/// 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,
boundary_marker: String::new(),
in_conductor: String::new(),
hidden: false,
in_group: 0,
normal_direction: true,
selected: false,
};
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.
pub fn delete_selected_nodes(&mut self) -> usize {
let keep: Vec<bool> = self.nodes.iter().map(|n| !n.selected).collect();
let mut remap: Vec<i32> = Vec::with_capacity(keep.len());
let mut next: i32 = 0;
for &k in &keep {
remap.push(if k { let r = next; next += 1; r } else { -1 });
}
let removed = self.nodes.len() - next as usize;
let mut new_nodes = Vec::with_capacity(next as usize);
for (i, n) in self.nodes.drain(..).enumerate() {
if keep[i] { new_nodes.push(n); }
}
self.nodes = new_nodes;
self.segments.retain(|s| {
let a = s.n0 as usize;
let b = s.n1 as usize;
a < keep.len() && b < keep.len() && keep[a] && keep[b]
});
for s in &mut self.segments {
s.n0 = remap[s.n0 as usize];
s.n1 = remap[s.n1 as usize];
}
self.arcs.retain(|a| {
let i = a.n0 as usize;
let j = a.n1 as usize;
i < keep.len() && j < keep.len() && keep[i] && keep[j]
});
for a in &mut self.arcs {
a.n0 = remap[a.n0 as usize];
a.n1 = remap[a.n1 as usize];
}
removed
}
/// removes selected segments and returns the count.
pub fn delete_selected_segments(&mut self) -> usize {
let before = self.segments.len();
self.segments.retain(|s| !s.selected);
before - self.segments.len()
}
/// removes selected arc segments and returns the count.
pub fn delete_selected_arcs(&mut self) -> usize {
let before = self.arcs.len();
self.arcs.retain(|a| !a.selected);
before - self.arcs.len()
}
/// removes selected block labels and returns the count.
pub fn delete_selected_block_labels(&mut self) -> usize {
let before = self.block_labels.len();
self.block_labels.retain(|b| !b.selected);
before - self.block_labels.len()
}
/// returns the index of the closest node to (x, y), or None when the node list is empty.
pub fn closest_node(&self, x: f64, y: f64) -> Option<usize> {
let mut best: Option<(usize, f64)> = None;
for (i, n) in self.nodes.iter().enumerate() {
let d = (n.x - x).hypot(n.y - y);
match best {
None => best = Some((i, d)),
Some((_, bd)) if d < bd => best = Some((i, d)),
_ => {}
}
}
best.map(|(i, _)| i)
}
/// returns the index of the closest block label to (x, y), or None when none exist.
pub fn closest_block_label(&self, x: f64, y: f64) -> Option<usize> {
let mut best: Option<(usize, f64)> = None;
for (i, b) in self.block_labels.iter().enumerate() {
let d = (b.x - x).hypot(b.y - y);
match best {
None => best = Some((i, d)),
Some((_, bd)) if d < bd => best = Some((i, d)),
_ => {}
}
}
best.map(|(i, _)| i)
}
/// returns the index of the segment whose nearest point is closest to (x, y).
pub fn closest_segment(&self, x: f64, y: f64) -> Option<usize> {
let mut best: Option<(usize, f64)> = None;
for (i, s) in self.segments.iter().enumerate() {
let (Some(p0), Some(p1)) = (self.nodes.get(s.n0 as usize), self.nodes.get(s.n1 as usize)) else { continue };
let d = point_to_segment_distance(x, y, p0.x, p0.y, p1.x, p1.y);
match best {
None => best = Some((i, d)),
Some((_, bd)) if d < bd => best = Some((i, d)),
_ => {}
}
}
best.map(|(i, _)| i)
}
/// clears the selection flag on every geometric entity in the doc.
pub fn clear_selection(&mut self) {
for n in &mut self.nodes { n.selected = false; }
for s in &mut self.segments { s.selected = false; }
for a in &mut self.arcs { a.selected = false; }
for b in &mut self.block_labels { b.selected = false; }
}
}
/// 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;
let dy = by - ay;
let len2 = dx * dx + dy * dy;
if len2 < 1e-18 {
return (px - ax).hypot(py - ay);
}
let t = (((px - ax) * dx + (py - ay) * dy) / len2).clamp(0.0, 1.0);
let cx = ax + t * dx;
let cy = ay + t * dy;
(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() {
// 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);
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 enforce_pslg_splits_first_segment_at_late_intersection() {
// 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, "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 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);
}
#[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 enforce_pslg_preserves_arc_metadata() {
// arc carrying non-default marker, side length, group, and direction.
// enforce_pslg round-trips every field intact.
let mut d = FemmDoc::default();
d.add_node(1.0, 0.0, 0.0);
d.add_node(0.0, 1.0, 0.0);
let template = ArcSegment {
n0: 0,
n1: 1,
arc_length: 90.0,
max_side_length: 5.0,
boundary_marker: String::from("outer"),
in_conductor: String::new(),
hidden: true,
in_group: 7,
normal_direction: false,
selected: false,
};
assert!(d.add_arc_segment_with_template(0, 1, 90.0, &template));
d.enforce_pslg();
assert_eq!(d.arcs.len(), 1);
let a = &d.arcs[0];
assert_eq!(a.boundary_marker, "outer");
assert!((a.max_side_length - 5.0).abs() < 1e-12);
assert!(a.hidden);
assert_eq!(a.in_group, 7);
assert!(!a.normal_direction);
}
#[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 the arc once.
// expect a fresh 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 detects the on-arc node and emits 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() {
// two perpendicular segments through the origin. horizontal first, vertical second.
// the vertical add inserts an origin node and self-splits; the horizontal stays whole.
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);
// expected pieces: 0->1 (horizontal whole), 2->4 and 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);
}
}